#!/usr/bin/env python3
"""
strategy_feedback.py -- Lightweight Contextual Bandit feedback loop.

Logs every strategy+tactic decision along with its PnL outcome, then
computes correction coefficients per (strategy, regime, adx_bucket, vol_bucket)
so the tactical scheduler weights get reinforced or dampened based on
actual realised edge -- not just theoretical profile scores.
"""

from __future__ import annotations

import json
import math
import time
from collections import defaultdict
from pathlib import Path
from typing import Any

_FEEDBACK_FILE = "data/learning/strategy_feedback.ndjson"


def _feedback_path(project_root: Path) -> Path:
    return project_root / _FEEDBACK_FILE


def log_feedback(
    project_root: Path,
    *,
    strategy_id: str,
    regime: str,
    session: str,
    adx: float,
    vol_ratio: float,
    atr_expansion: float,
    raw_weight: float,
    corrected_weight: float,
    pnl: float,
    is_win: bool,
    side: str,
    fingerprint: dict[str, Any] | None = None,
) -> None:
    entry = {
        "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        "strategy_id": strategy_id,
        "regime": regime,
        "session": session,
        "adx": round(adx, 1),
        "vol_ratio": round(vol_ratio, 3),
        "atr_expansion": round(atr_expansion, 3),
        "raw_weight": round(raw_weight, 4),
        "corrected_weight": round(corrected_weight, 4),
        "pnl": round(pnl, 2),
        "is_win": bool(is_win),
        "side": side,
        "fingerprint": fingerprint or {},
    }
    path = _feedback_path(project_root)
    path.parent.mkdir(parents=True, exist_ok=True)
    try:
        with open(path, "a", encoding="utf-8") as f:
            f.write(json.dumps(entry, ensure_ascii=False) + "\n")
    except OSError:
        pass


def _adx_bucket(adx: float) -> str:
    if adx < 18:
        return "low"
    if adx < 28:
        return "mid"
    return "high"


def _vol_bucket(vol_ratio: float) -> str:
    if vol_ratio < 0.8:
        return "low"
    if vol_ratio < 1.4:
        return "normal"
    return "high"


def _atr_bucket(atr_expansion: float) -> str:
    if atr_expansion < 1.2:
        return "normal"
    if atr_expansion < 1.8:
        return "elevated"
    return "spike"


def _load_feedback(project_root: Path, max_records: int = 2000) -> list[dict[str, Any]]:
    path = _feedback_path(project_root)
    if not path.exists():
        return []
    records: list[dict[str, Any]] = []
    try:
        for line in path.read_text(encoding="utf-8").strip().splitlines():
            line = line.strip()
            if line:
                try:
                    records.append(json.loads(line))
                except json.JSONDecodeError:
                    continue
    except OSError:
        return []
    return records[-max_records:]


def build_correction_coefficients(
    project_root: Path,
    *,
    min_samples: int = 5,
    lookback_records: int = 2000,
) -> dict[str, dict[str, float]]:
    records = _load_feedback(project_root, max_records=lookback_records)
    if len(records) < min_samples:
        return {}

    groups: dict[tuple[str, str, str, str, str], list[float]] = defaultdict(list)
    group_wins: dict[tuple[str, str, str, str, str], int] = defaultdict(int)
    for r in records:
        sid = r.get("strategy_id", "")
        regime = r.get("regime", "UNKNOWN")
        ab = _adx_bucket(float(r.get("adx", 0) or 0))
        vb = _vol_bucket(float(r.get("vol_ratio", 1.0) or 1.0))
        atrb = _atr_bucket(float(r.get("atr_expansion", 1.0) or 1.0))
        key = (sid, regime, ab, vb, atrb)
        pnl = float(r.get("pnl", 0) or 0)
        groups[key].append(pnl)
        if r.get("is_win"):
            group_wins[key] += 1

    group_scores: dict[tuple[str, str, str, str, str], float] = {}
    for key, pnls in groups.items():
        n = len(pnls)
        if n < min_samples:
            continue
        wins = group_wins.get(key, 0)
        wr = wins / n
        avg_pnl = sum(pnls) / n
        score = wr + 0.2 * (avg_pnl / max(1.0, abs(avg_pnl)))
        correction = max(0.5, min(1.5, 0.5 + score))
        group_scores[key] = correction

    if not group_scores:
        return {}

    by_strategy: dict[str, list[tuple[float, int]]] = defaultdict(list)
    for key, corr in group_scores.items():
        sid = key[0]
        n = len(groups[key])
        by_strategy[sid].append((corr, n))

    result: dict[str, dict[str, float]] = {}
    for sid, corr_list in by_strategy.items():
        total_n = sum(n for _, n in corr_list)
        weighted = sum(c * n for c, n in corr_list) / total_n if total_n > 0 else 1.0
        result[sid] = {"correction": round(weighted, 4), "samples": total_n}

    return result


def apply_feedback_correction(
    tactical_weights: dict[str, float],
    coefficients: dict[str, dict[str, float]],
) -> dict[str, float]:
    if not coefficients:
        return tactical_weights

    for sid in list(tactical_weights.keys()):
        info = coefficients.get(sid)
        if info is not None:
            corr = info.get("correction", 1.0)
            tactical_weights[sid] = max(0.001, min(0.50, tactical_weights[sid] * corr))
            tactical_weights[f"_feedback_{sid}"] = corr

    return tactical_weights
