"""Performance analytics — the "scorekeeper" and "report card" layer from
docs/AI_AGENT_ARCHITECTURE.md (Agents 1, 2, 4, 14 foundations).

Each strategy (options momentum, swing trend-pullback) is treated as a
competing "AI agent" and graded on evidence. The composite score follows the
architecture doc's weights, with honest substitutions documented until enough
regime-tagged history exists:

  30% return (expectancy) · 25% win rate (stand-in for regime accuracy until
  regime-tagged samples accumulate) · 20% profit factor (risk-adjusted
  stand-in) · 15% drawdown · 10% confidence calibration error
"""
from datetime import datetime, timedelta

from sqlalchemy.orm import Session

from .. import models
from .market_data import get_provider

MIN_TRADES_FOR_GRADE = 5


def _strategy_of(signal: models.Signal | None) -> str:
    if signal is None:
        return "manual"
    return signal.signal_type or "options"


def _stats_for(rows: list[tuple[models.PaperTrade, models.Signal | None]]) -> dict:
    closed = [(t, s) for t, s in rows if t.status == "closed"]
    if not closed:
        return {"trades": 0, "open_trades": len(rows), "win_rate": None, "total_pnl": 0.0,
                "avg_pnl": None, "expectancy_r": None, "profit_factor": None,
                "max_drawdown": None, "avg_hold_hours": None,
                "avg_mfe_r": None, "avg_mae_r": None, "calibration_error": None}

    wins = [(t, s) for t, s in closed if t.pnl > 0]
    total = round(sum(t.pnl for t, _ in closed), 2)
    gross_w = sum(t.pnl for t, _ in wins)
    gross_l = abs(sum(t.pnl for t, _ in closed if t.pnl < 0))

    # Drawdown on the realized-P&L curve ordered by close time
    ordered = sorted(closed, key=lambda x: x[0].closed_at or datetime.utcnow())
    peak = dd = cum = 0.0
    for t, _ in ordered:
        cum += t.pnl
        peak = max(peak, cum)
        dd = max(dd, peak - cum)

    holds = [((t.closed_at - t.opened_at).total_seconds() / 3600)
             for t, _ in closed if t.closed_at]

    # Expectancy in R using excursion-normalized risk
    r_values = []
    for t, _ in closed:
        stop = t.initial_stop if t.initial_stop is not None else t.stop_loss
        risk = abs(t.entry_price - stop) * t.quantity
        if risk > 0:
            r_values.append(t.pnl / risk)

    # Calibration error: |predicted win probability − actual win rate|
    probs = [s.probability for _, s in closed if s is not None]
    actual_wr = len(wins) / len(closed) * 100
    calib_err = round(abs(sum(probs) / len(probs) - actual_wr), 1) if probs else None

    return {
        "trades": len(closed),
        "open_trades": len(rows) - len(closed),
        "win_rate": round(actual_wr, 1),
        "total_pnl": total,
        "avg_pnl": round(total / len(closed), 2),
        "expectancy_r": round(sum(r_values) / len(r_values), 2) if r_values else None,
        "profit_factor": round(gross_w / gross_l, 2) if gross_l else round(gross_w, 2),
        "max_drawdown": round(dd, 2),
        "avg_hold_hours": round(sum(holds) / len(holds), 1) if holds else None,
        "avg_mfe_r": round(sum(t.mfe_r or 0 for t, _ in closed) / len(closed), 2),
        "avg_mae_r": round(sum(t.mae_r or 0 for t, _ in closed) / len(closed), 2),
        "calibration_error": calib_err,
    }


def _composite(stats: dict) -> float | None:
    """Composite score per the architecture doc's weighting (0-100)."""
    if not stats["trades"] or stats["trades"] < MIN_TRADES_FOR_GRADE:
        return None
    expectancy = max(-1.0, min(2.0, stats["expectancy_r"] or 0))       # -1..2R
    ret_score = (expectancy + 1) / 3 * 100                             # → 0..100
    wr_score = stats["win_rate"] or 0
    pf = min(3.0, stats["profit_factor"] or 0)
    pf_score = pf / 3 * 100
    # Drawdown penalty relative to total gross pnl scale
    dd = stats["max_drawdown"] or 0
    scale = abs(stats["total_pnl"]) + dd or 1
    dd_score = max(0.0, 100 - dd / scale * 100)
    calib = stats["calibration_error"]
    calib_score = max(0.0, 100 - 2 * calib) if calib is not None else 50.0

    return round(ret_score * 0.30 + wr_score * 0.25 + pf_score * 0.20
                 + dd_score * 0.15 + calib_score * 0.10, 1)


def _grade(score: float | None) -> str:
    if score is None:
        return "—"
    for cutoff, grade in ((80, "A"), (70, "B"), (60, "C"), (50, "D")):
        if score >= cutoff:
            return grade
    return "F"


def strategy_performance(db: Session) -> dict:
    rows = (
        db.query(models.PaperTrade, models.Signal)
        .outerjoin(models.Signal, models.PaperTrade.signal_id == models.Signal.id)
        .all()
    )
    by_strategy: dict[str, list] = {}
    for t, s in rows:
        by_strategy.setdefault(_strategy_of(s), []).append((t, s))

    strategies = []
    for name, group in sorted(by_strategy.items()):
        stats = _stats_for(group)
        score = _composite(stats)
        strategies.append({
            "strategy": name,
            **stats,
            "composite_score": score,
            "grade": _grade(score),
            "graded": score is not None,
        })
    strategies.sort(key=lambda s: (s["composite_score"] is not None, s["composite_score"] or 0),
                    reverse=True)
    return {
        "strategies": strategies,
        "min_trades_for_grade": MIN_TRADES_FOR_GRADE,
        "note": ("Strategies are graded as competing agents per docs/AI_AGENT_ARCHITECTURE.md. "
                 "Grades appear after ≥5 closed trades per strategy. Win rate currently stands in "
                 "for regime-conditional accuracy until regime-tagged history accumulates."),
    }


def benchmark(db: Session, days: int = 30) -> dict:
    """Agent 14: compare realized performance against the index (SPY)."""
    provider = get_provider()
    bars = provider.get_history("SPY", days + 10)
    window = [b for b in bars][-max(2, min(len(bars), days)):]
    spy_return = round((window[-1]["close"] / window[0]["close"] - 1) * 100, 2) if len(window) >= 2 else None

    since = datetime.utcnow() - timedelta(days=days)
    closed = (
        db.query(models.PaperTrade)
        .filter(models.PaperTrade.status == "closed", models.PaperTrade.closed_at >= since)
        .all()
    )
    from ..config import settings

    realized = round(sum(t.pnl for t in closed), 2)
    account_return = round(realized / settings.ACCOUNT_EQUITY * 100, 2)

    return {
        "period_days": days,
        "spy_return_pct": spy_return,
        "account_return_pct": account_return,
        "realized_pnl": realized,
        "trades": len(closed),
        "outperforming": (account_return > spy_return) if spy_return is not None else None,
        "note": ("Account return = realized paper P&L / starting equity over the period. "
                 "Compare against buy-and-hold SPY; underperforming the index means the "
                 "strategy isn't yet earning its complexity."),
    }
