"""BacktestResult container for backtest output."""

from __future__ import annotations

import csv
from dataclasses import dataclass, field

from horizon.analytics import (
    Metrics, RoundTrip, Tearsheet, TradeAnalysis,
    _analyze_trades, compute_metrics, generate_tearsheet,
)


@dataclass
class BacktestResult:
    """Container for backtest results with lazy metric computation."""

    equity_curve: list[tuple[float, float]] = field(default_factory=list)
    trades: list = field(default_factory=list)
    positions: list = field(default_factory=list)
    initial_capital: float = 1000.0
    outcomes: dict[str, float] | None = None
    mid_prices: list[tuple[float, float]] = field(default_factory=list)

    _metrics: Metrics | None = field(default=None, repr=False)
    _analysis: TradeAnalysis | None = field(default=None, repr=False)

    @property
    def metrics(self) -> Metrics:
        """Lazy-compute metrics on first access."""
        if self._metrics is None:
            self._metrics = compute_metrics(
                self.equity_curve, self.trades, self.initial_capital,
                self.outcomes, self.mid_prices or None,
            )
        return self._metrics

    @property
    def analysis(self) -> TradeAnalysis:
        """Lazy-compute trade analysis (round-trips + open positions)."""
        if self._analysis is None:
            self._analysis = _analyze_trades(self.trades)
        return self._analysis

    @property
    def round_trips(self) -> list[RoundTrip]:
        """All matched round-trip trades (long and short)."""
        return self.analysis.round_trips

    def tearsheet(self) -> Tearsheet:
        """Generate a full performance tearsheet."""
        return generate_tearsheet(self.equity_curve, self.trades, self.initial_capital)

    def pnl_by_market(self) -> dict[str, float]:
        """Compute realized PnL per market from round-trip trades."""
        from collections import defaultdict
        pnl: dict[str, float] = defaultdict(float)
        for rt in self.analysis.round_trips:
            pnl[rt.market_id] += rt.pnl
        return dict(pnl)

    def resolution_pnl(self) -> dict[str, float]:
        """Compute PnL from unmatched positions resolved at outcome prices.

        In prediction markets, positions held at expiry resolve at 0 or 1.
        Returns per-market resolution PnL. Requires outcomes to be set.
        """
        if not self.outcomes:
            raise ValueError("outcomes must be set to compute resolution PnL")

        from collections import defaultdict
        pnl: dict[str, float] = defaultdict(float)
        analysis = self.analysis

        for market_id, positions in analysis.open_longs.items():
            if market_id in self.outcomes:
                outcome = self.outcomes[market_id]
                for price, size, fee, _ts in positions:
                    pnl[market_id] += size * (outcome - price) - fee

        for market_id, positions in analysis.open_shorts.items():
            if market_id in self.outcomes:
                outcome = self.outcomes[market_id]
                for price, size, fee, _ts in positions:
                    pnl[market_id] += size * (price - outcome) - fee

        return dict(pnl)

    def summary(self) -> str:
        """Format a human-readable summary of backtest results."""
        m = self.metrics
        lines = [
            "=" * 50,
            "  BACKTEST RESULTS",
            "=" * 50,
            "",
            "--- Returns ---",
            f"  Initial Capital:   ${self.initial_capital:,.2f}",
            f"  Final Equity:      ${self.equity_curve[-1][1]:,.2f}" if self.equity_curve else "  Final Equity:      N/A",
            f"  Total Return:      ${m.total_return:,.2f} ({m.total_return_pct:+.2f}%)",
            f"  CAGR:              {m.cagr * 100:.2f}%",
            "",
            "--- Risk ---",
            f"  Sharpe Ratio:      {m.sharpe_ratio:.3f}",
            f"  Sortino Ratio:     {m.sortino_ratio:.3f}",
            f"  Calmar Ratio:      {m.calmar_ratio:.3f}",
            f"  Max Drawdown:      ${m.max_drawdown:,.2f} ({m.max_drawdown_pct:.2f}%)",
            f"  Max DD Duration:   {m.max_drawdown_duration_secs:.0f}s",
            "",
            "--- Trades ---",
            f"  Total Trades:      {m.total_trades}",
            f"  Win Rate:          {m.win_rate:.1f}%",
            f"  Profit Factor:     {m.profit_factor:.2f}",
            f"  Expectancy:        ${m.expectancy:,.4f}",
            f"  Avg Win:           ${m.avg_win:,.4f}",
            f"  Avg Loss:          ${m.avg_loss:,.4f}",
            f"  Largest Win:       ${m.largest_win:,.4f}",
            f"  Largest Loss:      ${m.largest_loss:,.4f}",
            f"  Total Fees:        ${m.total_fees:,.4f}",
        ]

        if m.brier_score is not None:
            lines += [
                "",
                "--- Prediction Market ---",
                f"  Brier Score:       {m.brier_score:.4f}",
            ]
            if m.avg_edge is not None:
                lines.append(f"  Avg Edge:          {m.avg_edge:.4f}")
            if m.resolution_pnl is not None:
                lines.append(f"  Resolution PnL:    ${m.resolution_pnl:,.4f}")
            if m.edge_capture is not None:
                lines.append(f"  Edge Capture:      {m.edge_capture:.2%}")
            if m.adverse_selection_5 is not None:
                lines.append(f"  Adverse Sel (5t):  {m.adverse_selection_5:.6f}")

        lines += [
            "",
            "=" * 50,
        ]
        return "\n".join(lines)

    def calibration(self, n_bins: int = 10):
        """Compute calibration curve from buy trades + self.outcomes.

        Returns a CalibrationResult with bins, brier_score, log_loss, ece.
        Requires outcomes to be set.
        """
        if not self.outcomes:
            raise ValueError("outcomes must be set to compute calibration")

        from horizon._horizon import calibration_curve
        from horizon.analytics import _extract_predictions_outcomes

        predictions, outcome_vals = _extract_predictions_outcomes(
            self.trades, self.outcomes
        )
        if not predictions:
            raise ValueError("no buy trades with matching outcomes found")

        return calibration_curve(predictions, outcome_vals, n_bins)

    def log_loss_score(self) -> float:
        """Compute log-loss from buy trades + self.outcomes.

        Returns the log-loss value. Requires outcomes to be set.
        """
        if not self.outcomes:
            raise ValueError("outcomes must be set to compute log-loss")

        from horizon._horizon import log_loss
        from horizon.analytics import _extract_predictions_outcomes

        predictions, outcome_vals = _extract_predictions_outcomes(
            self.trades, self.outcomes
        )
        if not predictions:
            raise ValueError("no buy trades with matching outcomes found")

        return log_loss(predictions, outcome_vals)

    def robustness(self, **kwargs):
        """Run statistical robustness tests on this backtest.

        Kwargs are forwarded to robustness_test(). See
        horizon.robustness.robustness_test for full signature.
        """
        from horizon.robustness import robustness_test
        return robustness_test(self, **kwargs)

    def to_csv(self, path: str, what: str = "equity") -> None:
        """Export results to CSV.

        Args:
            path: File path to write.
            what: "equity" for equity curve, "trades" for trade log.
        """
        with open(path, "w", newline="", encoding="utf-8") as f:
            writer = csv.writer(f)
            if what == "equity":
                writer.writerow(["timestamp", "equity"])
                for ts, eq in self.equity_curve:
                    writer.writerow([ts, eq])
            elif what == "trades":
                writer.writerow([
                    "timestamp", "market_id", "order_side", "price", "size", "fee",
                ])
                for fill in self.trades:
                    writer.writerow([
                        getattr(fill, "timestamp", 0.0),
                        getattr(fill, "market_id", ""),
                        str(getattr(fill, "order_side", "")),
                        getattr(fill, "price", 0.0),
                        getattr(fill, "size", 0.0),
                        getattr(fill, "fee", 0.0) or 0.0,
                    ])
