"""
=========================================
CV6 AI Trading OS — Backtest Reporter
Full analytics engine for completed runs.
Metrics:
  Win Rate, Profit Factor, Sharpe, Sortino,
  Max Drawdown, Recovery Factor, Expectancy,
  Avg Holding Time, Largest Win/Loss,
  Monthly / Weekly / Daily returns,
  Capital Curve, Drawdown Curve,
  Strategy / Sector / AI distribution,
  Learning Engine (best/worst per dimension)
=========================================
"""
from __future__ import annotations

import math
from collections import defaultdict
from datetime import datetime
from typing import List, Dict, Any, Optional


class BacktestReporter:

    # ── Public API ────────────────────────────────────────────────────────────

    def generate(
        self,
        trades: List[Dict],
        equity_curve: List[Dict],   # [{"date": "YYYY-MM-DD", "equity": float}, ...]
        initial_capital: float,
        config: Optional[dict] = None,
    ) -> dict:
        """
        Compute full analytics from completed backtest trades + equity curve.
        Returns dict with every metric required by the dashboard.
        """
        if not trades:
            return self._empty_report(initial_capital, equity_curve)

        # ── Basic counts ──────────────────────────────────────────────────
        wins   = [t for t in trades if t.get("pnl", 0) > 0]
        losses = [t for t in trades if t.get("pnl", 0) <= 0]
        total  = len(trades)
        win_rate = len(wins) / total * 100 if total > 0 else 0.0

        net_pnl      = sum(t.get("pnl", 0) for t in trades)
        gross_profit = sum(t.get("pnl", 0) for t in wins)
        gross_loss   = abs(sum(t.get("pnl", 0) for t in losses))
        total_charges = sum(t.get("charges", 0) for t in trades)

        # ── Profit factor ─────────────────────────────────────────────────
        profit_factor = (gross_profit / gross_loss) if gross_loss > 0 else (
            float("inf") if gross_profit > 0 else 0.0
        )

        # ── Avg win / loss ────────────────────────────────────────────────
        avg_win  = gross_profit / len(wins)   if wins   else 0.0
        avg_loss = gross_loss   / len(losses) if losses else 0.0

        # ── Expectancy = (WinPct × AvgWin) – (LossPct × AvgLoss) ─────────
        win_pct  = len(wins)   / total if total else 0
        loss_pct = len(losses) / total if total else 0
        expectancy = (win_pct * avg_win) - (loss_pct * avg_loss)

        # ── Largest win / loss ────────────────────────────────────────────
        largest_win  = max((t.get("pnl", 0) for t in wins),   default=0.0)
        largest_loss = min((t.get("pnl", 0) for t in losses), default=0.0)

        # ── Average holding time ──────────────────────────────────────────
        avg_hold_hours = self._avg_hold_hours(trades)

        # ── Equity series ─────────────────────────────────────────────────
        equities = [pt["equity"] for pt in equity_curve] if equity_curve else [initial_capital]
        final_capital = equities[-1] if equities else initial_capital

        # ── Daily returns for ratio computation ───────────────────────────
        daily_ret = []
        for i in range(1, len(equities)):
            if equities[i - 1] > 0:
                daily_ret.append((equities[i] - equities[i - 1]) / equities[i - 1])

        rf_daily = 0.06 / 252   # 6% annual risk-free

        # ── Sharpe / Sortino ──────────────────────────────────────────────
        sharpe  = self._sharpe(daily_ret, rf_daily)
        sortino = self._sortino(daily_ret, rf_daily)

        # ── Max drawdown ──────────────────────────────────────────────────
        max_dd_pct, max_dd_amt, max_dd_days = self._max_drawdown(equities)

        # ── Recovery factor = net profit / max drawdown amount ────────────
        recovery = (net_pnl / max_dd_amt) if max_dd_amt > 0 else 0.0

        # ── CAGR ──────────────────────────────────────────────────────────
        n_days = max(len(equities), 1)
        years  = n_days / 252
        cagr = 0.0
        if years > 0 and initial_capital > 0 and final_capital > 0:
            cagr = ((final_capital / initial_capital) ** (1 / years) - 1) * 100

        # ── Return % ──────────────────────────────────────────────────────
        return_pct = (net_pnl / initial_capital * 100) if initial_capital > 0 else 0.0

        # ── Risk score (0–100, lower is safer) ───────────────────────────
        risk_score = self._risk_score(
            max_dd_pct, sharpe, win_rate, profit_factor
        )

        return {
            # ── Basic ──────────────────────────────────────────────────────
            "total_trades":     total,
            "winning_trades":   len(wins),
            "losing_trades":    len(losses),
            "win_rate":         round(win_rate, 2),
            "net_pnl":          round(net_pnl, 2),
            "net_profit":       round(gross_profit, 2),
            "net_loss":         round(-gross_loss, 2),
            "total_charges":    round(total_charges, 2),
            "return_pct":       round(return_pct, 2),
            "initial_capital":  round(initial_capital, 2),
            "final_capital":    round(final_capital, 2),

            # ── Advanced ratios ────────────────────────────────────────────
            "profit_factor":    round(min(profit_factor, 999.0), 3),
            "sharpe_ratio":     round(sharpe, 3),
            "sortino_ratio":    round(sortino, 3),
            "max_drawdown_pct": round(max_dd_pct, 2),
            "max_drawdown_amount": round(max_dd_amt, 2),
            "max_drawdown_duration_days": max_dd_days,
            "recovery_factor":  round(recovery, 3),
            "cagr":             round(cagr, 2),
            "risk_score":       round(risk_score, 1),

            # ── Per-trade stats ────────────────────────────────────────────
            "avg_holding_hours": round(avg_hold_hours, 2),
            "avg_win":          round(avg_win, 2),
            "avg_loss":         round(-avg_loss, 2),
            "expectancy":       round(expectancy, 2),
            "largest_win":      round(largest_win, 2),
            "largest_loss":     round(largest_loss, 2),

            # ── Period returns ─────────────────────────────────────────────
            "monthly_returns":  self._period_returns(equity_curve, "monthly"),
            "weekly_returns":   self._period_returns(equity_curve, "weekly"),
            "daily_returns":    self._period_returns(equity_curve, "daily"),

            # ── Curves ────────────────────────────────────────────────────
            "equity_curve":     equity_curve,
            "drawdown_curve":   self._drawdown_curve(equities, equity_curve),

            # ── Distributions ─────────────────────────────────────────────
            "strategy_distribution": self._distribution(trades, "strategy"),
            "sector_distribution":   self._distribution(trades, "sector"),
            "ai_distribution":       self._distribution(trades, "ai_model"),
            "pnl_histogram":         self._pnl_histogram(trades),
            "trade_duration_hist":   self._duration_histogram(trades),

            # ── Learning engine ────────────────────────────────────────────
            "learning": self._learning_engine(trades, equity_curve),
        }

    # ── Statistical helpers ───────────────────────────────────────────────────

    def _sharpe(self, returns: List[float], rf: float) -> float:
        if len(returns) < 2:
            return 0.0
        excess = [r - rf for r in returns]
        mean   = sum(excess) / len(excess)
        var    = sum((r - mean) ** 2 for r in excess) / len(excess)
        std    = math.sqrt(var) if var > 0 else 0.0
        return (mean / std * math.sqrt(252)) if std > 0 else 0.0

    def _sortino(self, returns: List[float], rf: float) -> float:
        if len(returns) < 2:
            return 0.0
        excess   = [r - rf for r in returns]
        mean     = sum(excess) / len(excess)
        downside = [r for r in excess if r < 0]
        if not downside:
            return 999.0 if mean > 0 else 0.0
        ds_var = sum(r ** 2 for r in downside) / len(downside)
        ds_std = math.sqrt(ds_var) if ds_var > 0 else 0.0
        return (mean / ds_std * math.sqrt(252)) if ds_std > 0 else 0.0

    def _max_drawdown(self, equities: List[float]) -> tuple:
        if not equities:
            return 0.0, 0.0, 0
        peak      = equities[0]
        max_dd_pct = max_dd_amt = 0.0
        max_dd_days = dd_start_i = 0

        for i, val in enumerate(equities):
            if val > peak:
                peak      = val
                dd_start_i = i
            dd_pct = (peak - val) / peak * 100 if peak > 0 else 0.0
            dd_amt = peak - val
            if dd_pct > max_dd_pct:
                max_dd_pct  = dd_pct
                max_dd_amt  = dd_amt
                max_dd_days = i - dd_start_i

        return max_dd_pct, max_dd_amt, max_dd_days

    def _avg_hold_hours(self, trades: List[Dict]) -> float:
        hours = []
        for t in trades:
            try:
                entry = datetime.fromisoformat(str(t.get("entry_time", ""))[:19])
                exit_ = datetime.fromisoformat(str(t.get("exit_time",  ""))[:19])
                hours.append((exit_ - entry).total_seconds() / 3600)
            except Exception:
                pass
        return sum(hours) / len(hours) if hours else 0.0

    def _risk_score(
        self, max_dd: float, sharpe: float, win_rate: float, pf: float
    ) -> float:
        """0 = very safe, 100 = very risky."""
        dd_score   = min(max_dd * 2, 50)           # up to 50 pts for drawdown
        sharpe_pen = max(0, (2 - sharpe) * 10)     # penalty if Sharpe < 2
        wr_pen     = max(0, (50 - win_rate))        # penalty for <50% win rate
        pf_pen     = max(0, (1.5 - pf) * 10)       # penalty for PF < 1.5
        return min(100, dd_score + sharpe_pen + wr_pen * 0.3 + pf_pen)

    # ── Period return aggregation ─────────────────────────────────────────────

    def _period_returns(self, equity_curve: List[Dict], period: str) -> List[Dict]:
        if not equity_curve:
            return []

        grouped: dict = defaultdict(list)
        for pt in equity_curve:
            date_str = str(pt.get("date", ""))[:10]
            if not date_str or date_str == "None":
                continue
            try:
                dt = datetime.strptime(date_str, "%Y-%m-%d")
                if period == "monthly":
                    key = dt.strftime("%Y-%m")
                elif period == "weekly":
                    key = f"{dt.isocalendar()[0]}-W{dt.isocalendar()[1]:02d}"
                else:
                    key = date_str
                grouped[key].append(float(pt["equity"]))
            except Exception:
                pass

        result = []
        prev_eq = float(equity_curve[0]["equity"]) if equity_curve else 0
        for key in sorted(grouped.keys()):
            pts = grouped[key]
            end = pts[-1]
            ret_pct  = ((end - prev_eq) / prev_eq * 100) if prev_eq > 0 else 0.0
            ret_amt  = end - prev_eq
            result.append({
                "period":       key,
                "end_equity":   round(end, 2),
                "return_pct":   round(ret_pct, 2),
                "return_amount":round(ret_amt, 2),
                "positive":     ret_pct >= 0,
            })
            prev_eq = end
        return result

    # ── Curve helpers ─────────────────────────────────────────────────────────

    def _drawdown_curve(
        self, equities: List[float], equity_curve: List[Dict]
    ) -> List[Dict]:
        if not equities or not equity_curve:
            return []
        peak = equities[0]
        result = []
        for eq, pt in zip(equities, equity_curve):
            if eq > peak:
                peak = eq
            dd = (peak - eq) / peak * 100 if peak > 0 else 0.0
            result.append({
                "date":         pt.get("date", ""),
                "drawdown_pct": round(-dd, 2),
                "equity":       round(eq, 2),
            })
        return result

    # ── Distribution helpers ──────────────────────────────────────────────────

    def _distribution(self, trades: List[Dict], key: str) -> List[Dict]:
        bucket: dict = defaultdict(lambda: {"count": 0, "pnl": 0.0, "wins": 0})
        for t in trades:
            k = str(t.get(key) or "Unknown")
            bucket[k]["count"] += 1
            bucket[k]["pnl"]   += t.get("pnl", 0)
            if t.get("pnl", 0) > 0:
                bucket[k]["wins"] += 1
        result = []
        for name, d in bucket.items():
            wr = d["wins"] / d["count"] * 100 if d["count"] > 0 else 0
            result.append({
                "name":     name,
                "trades":   d["count"],
                "wins":     d["wins"],
                "win_rate": round(wr, 1),
                "pnl":      round(d["pnl"], 2),
            })
        return sorted(result, key=lambda x: x["pnl"], reverse=True)

    def _pnl_histogram(self, trades: List[Dict], bins: int = 12) -> List[Dict]:
        if not trades:
            return []
        pnls = [t.get("pnl", 0) for t in trades]
        mn, mx = min(pnls), max(pnls)
        if mn == mx:
            return [{"range": f"₹{mn:.0f}", "count": len(pnls)}]
        bw  = (mx - mn) / bins
        hist: dict = defaultdict(int)
        for p in pnls:
            b = min(int((p - mn) / bw), bins - 1)
            hist[b] += 1
        result = []
        for i in range(bins):
            lo = mn + i * bw
            hi = lo + bw
            result.append({
                "range": f"₹{lo:.0f}–₹{hi:.0f}",
                "count": hist[i],
            })
        return result

    def _duration_histogram(self, trades: List[Dict]) -> List[Dict]:
        """Histogram of trade holding durations in hours."""
        hours = []
        for t in trades:
            try:
                e1 = datetime.fromisoformat(str(t.get("entry_time", ""))[:19])
                e2 = datetime.fromisoformat(str(t.get("exit_time",  ""))[:19])
                hours.append((e2 - e1).total_seconds() / 3600)
            except Exception:
                pass
        if not hours:
            return []
        mn, mx = min(hours), max(hours)
        if mn == mx:
            return [{"range": f"{mn:.1f}h", "count": len(hours)}]
        bins = 10
        bw   = (mx - mn) / bins
        hist: dict = defaultdict(int)
        for h in hours:
            b = min(int((h - mn) / bw), bins - 1)
            hist[b] += 1
        return [
            {"range": f"{mn + i*bw:.1f}–{mn + (i+1)*bw:.1f}h", "count": hist[i]}
            for i in range(bins)
        ]

    # ── Learning engine ───────────────────────────────────────────────────────

    def _learning_engine(self, trades: List[Dict], equity_curve: List[Dict]) -> dict:
        """
        Extract best/worst performance across:
          strategy, sector, AI model, hour-of-day
        """
        if not trades:
            return {}

        def _best_worst(field: str):
            d: dict = defaultdict(lambda: {"pnl": 0.0, "count": 0, "wins": 0})
            for t in trades:
                k = str(t.get(field) or "Unknown")
                d[k]["pnl"]   += t.get("pnl", 0)
                d[k]["count"] += 1
                if t.get("pnl", 0) > 0:
                    d[k]["wins"] += 1
            if not d:
                return None, None, {}, {}
            srt = sorted(d.items(), key=lambda x: x[1]["pnl"], reverse=True)
            best_k, best_v = srt[0]
            worst_k, worst_v = srt[-1]
            def _fmt(k, v):
                wr = v["wins"] / v["count"] * 100 if v["count"] else 0
                return {"name": k, "pnl": round(v["pnl"], 2),
                        "trades": v["count"], "win_rate": round(wr, 1)}
            return best_k, worst_k, _fmt(best_k, best_v), _fmt(worst_k, worst_v)

        bs, ws, bs_d, ws_d = _best_worst("strategy")
        bse, wse, bse_d, wse_d = _best_worst("sector")
        bai, wai, bai_d, wai_d = _best_worst("ai_model")

        # Time-of-day analysis
        time_pnl: dict = defaultdict(float)
        for t in trades:
            try:
                h = datetime.fromisoformat(str(t.get("entry_time", ""))[:19]).hour
                time_pnl[f"{h:02d}:00"] += t.get("pnl", 0)
            except Exception:
                pass

        best_time  = max(time_pnl, key=time_pnl.get) if time_pnl else "N/A"
        worst_time = min(time_pnl, key=time_pnl.get) if time_pnl else "N/A"

        # Market personality analysis
        mkt_pnl: dict = defaultdict(float)
        for t in trades:
            mp = t.get("market_personality", "UNKNOWN")
            mkt_pnl[mp] += t.get("pnl", 0)
        best_market  = max(mkt_pnl, key=mkt_pnl.get) if mkt_pnl else "N/A"
        worst_market = min(mkt_pnl, key=mkt_pnl.get) if mkt_pnl else "N/A"

        return {
            "best_strategy":  bs,
            "worst_strategy": ws,
            "best_strategy_details":  bs_d,
            "worst_strategy_details": ws_d,
            "best_sector":    bse,
            "worst_sector":   wse,
            "best_sector_details":  bse_d,
            "worst_sector_details": wse_d,
            "best_ai":        bai,
            "worst_ai":       wai,
            "best_ai_details":  bai_d,
            "worst_ai_details": wai_d,
            "best_time":      best_time,
            "worst_time":     worst_time,
            "time_pnl_map":   {k: round(v, 2) for k, v in sorted(time_pnl.items())},
            "best_market":    best_market,
            "worst_market":   worst_market,
            "market_pnl_map": {k: round(v, 2) for k, v in sorted(mkt_pnl.items())},
        }

    # ── Empty report template ─────────────────────────────────────────────────

    def _empty_report(self, initial_capital: float, equity_curve: List[Dict]) -> dict:
        final = equity_curve[-1]["equity"] if equity_curve else initial_capital
        return {
            "total_trades": 0, "winning_trades": 0, "losing_trades": 0,
            "win_rate": 0.0, "net_pnl": 0.0, "net_profit": 0.0, "net_loss": 0.0,
            "total_charges": 0.0, "return_pct": 0.0,
            "initial_capital": initial_capital, "final_capital": round(final, 2),
            "profit_factor": 0.0, "sharpe_ratio": 0.0, "sortino_ratio": 0.0,
            "max_drawdown_pct": 0.0, "max_drawdown_amount": 0.0,
            "max_drawdown_duration_days": 0, "recovery_factor": 0.0,
            "cagr": 0.0, "risk_score": 0.0,
            "avg_holding_hours": 0.0, "avg_win": 0.0, "avg_loss": 0.0,
            "expectancy": 0.0, "largest_win": 0.0, "largest_loss": 0.0,
            "monthly_returns": [], "weekly_returns": [], "daily_returns": [],
            "equity_curve": equity_curve, "drawdown_curve": [],
            "strategy_distribution": [], "sector_distribution": [],
            "ai_distribution": [], "pnl_histogram": [],
            "trade_duration_hist": [], "learning": {},
        }


# Singleton
backtest_reporter = BacktestReporter()
