"""Performance analytics for backtesting results."""

from __future__ import annotations

import math
from dataclasses import dataclass


@dataclass
class Metrics:
    """Computed performance metrics from a backtest."""

    # Returns
    total_return: float = 0.0
    total_return_pct: float = 0.0
    cagr: float = 0.0

    # Risk-adjusted
    sharpe_ratio: float = 0.0
    sortino_ratio: float = 0.0
    calmar_ratio: float = 0.0

    # Drawdown
    max_drawdown: float = 0.0
    max_drawdown_pct: float = 0.0
    max_drawdown_duration_secs: float = 0.0

    # Trades
    total_trades: int = 0
    win_rate: float = 0.0
    profit_factor: float = 0.0
    expectancy: float = 0.0
    avg_win: float = 0.0
    avg_loss: float = 0.0
    largest_win: float = 0.0
    largest_loss: float = 0.0
    total_fees: float = 0.0

    # Prediction market specific
    brier_score: float | None = None
    avg_edge: float | None = None
    log_loss: float | None = None
    ece: float | None = None  # Expected Calibration Error


def compute_metrics(
    equity_curve: list[tuple[float, float]],
    trades: list,
    initial_capital: float,
    outcomes: dict[str, float] | None = None,
) -> Metrics:
    """Compute all performance metrics from backtest results.

    Args:
        equity_curve: List of (timestamp, equity) tuples.
        trades: List of Fill objects from the engine.
        initial_capital: Starting capital.
        outcomes: Optional dict of market_id -> outcome (1.0 or 0.0) for Brier score.

    Returns:
        Metrics dataclass with all computed values.
    """
    m = Metrics()

    if not equity_curve:
        return m

    # --- Returns ---
    final_equity = equity_curve[-1][1]
    m.total_return = final_equity - initial_capital
    m.total_return_pct = (m.total_return / initial_capital) * 100.0 if initial_capital > 0 else 0.0

    # CAGR - only meaningful for durations >= 1 day
    duration_secs = equity_curve[-1][0] - equity_curve[0][0]
    if duration_secs >= 86400 and initial_capital > 0 and final_equity > 0:
        years = duration_secs / (365.25 * 86400)
        try:
            m.cagr = (final_equity / initial_capital) ** (1.0 / years) - 1.0
        except (OverflowError, ValueError):
            m.cagr = 0.0

    # --- Drawdown ---
    peak = initial_capital
    max_dd = 0.0
    max_dd_pct = 0.0
    dd_start_time = equity_curve[0][0]
    max_dd_duration = 0.0
    in_drawdown = False

    for ts, equity in equity_curve:
        if equity >= peak:
            if in_drawdown:
                dd_dur = ts - dd_start_time
                max_dd_duration = max(max_dd_duration, dd_dur)
            peak = equity
            in_drawdown = False
        else:
            if not in_drawdown:
                dd_start_time = ts
                in_drawdown = True
            dd = peak - equity
            dd_pct = (dd / peak) * 100.0 if peak > 0 else 0.0
            if dd > max_dd:
                max_dd = dd
                max_dd_pct = dd_pct

    # If still in drawdown at end, count that duration too
    if in_drawdown:
        dd_dur = equity_curve[-1][0] - dd_start_time
        max_dd_duration = max(max_dd_duration, dd_dur)

    m.max_drawdown = max_dd
    m.max_drawdown_pct = max_dd_pct
    m.max_drawdown_duration_secs = max_dd_duration

    # --- Sharpe / Sortino (from equity returns) ---
    if len(equity_curve) >= 2:
        returns = []
        for i in range(1, len(equity_curve)):
            prev_eq = equity_curve[i - 1][1]
            curr_eq = equity_curve[i][1]
            if prev_eq > 0:
                returns.append((curr_eq - prev_eq) / prev_eq)

        if returns:
            mean_ret = sum(returns) / len(returns)
            denom = len(returns) - 1 if len(returns) > 1 else 1
            variance = sum((r - mean_ret) ** 2 for r in returns) / denom
            std_ret = math.sqrt(variance)

            # Annualization factor: estimate periods per year from data
            avg_interval = duration_secs / len(returns) if duration_secs > 0 else 1.0
            periods_per_year = (365.25 * 86400) / avg_interval if avg_interval > 0 else 1.0
            ann_factor = math.sqrt(periods_per_year)

            if std_ret > 0:
                m.sharpe_ratio = (mean_ret / std_ret) * ann_factor

            # Sortino: only downside deviation
            downside = [r for r in returns if r < 0]
            if downside:
                down_var = sum(r ** 2 for r in downside) / len(returns)
                down_std = math.sqrt(down_var)
                if down_std > 0:
                    m.sortino_ratio = (mean_ret / down_std) * ann_factor

    # Calmar
    if m.max_drawdown_pct > 0 and m.cagr != 0:
        m.calmar_ratio = m.cagr / (m.max_drawdown_pct / 100.0)

    # --- Trade analytics ---
    trade_pnls = _compute_trade_pnls(trades)
    m.total_trades = len(trade_pnls)

    if trade_pnls:
        wins = [p for p in trade_pnls if p > 0]
        losses = [p for p in trade_pnls if p < 0]

        m.win_rate = len(wins) / len(trade_pnls) * 100.0

        m.avg_win = sum(wins) / len(wins) if wins else 0.0
        m.avg_loss = sum(losses) / len(losses) if losses else 0.0
        m.largest_win = max(wins) if wins else 0.0
        m.largest_loss = min(losses) if losses else 0.0

        gross_profit = sum(wins)
        gross_loss = abs(sum(losses))
        m.profit_factor = gross_profit / gross_loss if gross_loss > 0 else 9999.99 if gross_profit > 0 else 0.0

        m.expectancy = sum(trade_pnls) / len(trade_pnls)

    # Fees
    m.total_fees = sum(getattr(t, "fee", 0.0) or 0.0 for t in trades)

    # --- Prediction market metrics ---
    if outcomes:
        m.brier_score = _compute_brier_score(trades, outcomes)
        m.avg_edge = _compute_avg_edge(trades, outcomes)

        # Use Rust analytics for log-loss and ECE when available
        try:
            from horizon._horizon import calibration_curve as _rust_calibration
            from horizon._horizon import log_loss as _rust_log_loss

            predictions, outcome_vals = _extract_predictions_outcomes(trades, outcomes)
            if predictions:
                m.log_loss = _rust_log_loss(predictions, outcome_vals)
                cal = _rust_calibration(predictions, outcome_vals, 10)
                m.ece = cal.ece
        except (ImportError, Exception):
            pass  # Rust analytics not available, skip

    return m


def _compute_trade_pnls(trades: list) -> list[float]:
    """FIFO buy/sell matching to compute round-trip PnLs.

    Groups trades by market_id, then matches buys and sells in FIFO order.
    Uses deque for O(1) popleft instead of O(N) list.pop(0).
    Tracks buy-side fees for accurate round-trip PnL.
    """
    from collections import defaultdict, deque

    # market -> deque of (price, size, fee)
    buys: dict[str, deque[tuple[float, float, float]]] = defaultdict(deque)
    pnls: list[float] = []

    for fill in trades:
        market_id = getattr(fill, "market_id", "")
        price = getattr(fill, "price", 0.0)
        size = getattr(fill, "size", 0.0)
        side = getattr(fill, "order_side", None)
        fee = getattr(fill, "fee", 0.0) or 0.0

        # Determine if this is an opening or closing trade
        side_str = str(side).lower() if side else ""
        is_buy = "buy" in side_str

        if is_buy:
            buys[market_id].append((price, size, fee))
        else:
            # Match against buys FIFO
            remaining = size
            trade_pnl = 0.0
            buy_fee_total = 0.0
            while remaining > 0 and buys[market_id]:
                buy_price, buy_size, buy_fee = buys[market_id][0]
                matched = min(remaining, buy_size)
                trade_pnl += matched * (price - buy_price)
                # Prorate buy-side fee by matched fraction
                buy_fee_total += buy_fee * (matched / buy_size) if buy_size > 0 else 0.0
                remaining -= matched
                if matched >= buy_size:
                    buys[market_id].popleft()
                else:
                    remaining_buy_fee = buy_fee * ((buy_size - matched) / buy_size) if buy_size > 0 else 0.0
                    buys[market_id][0] = (buy_price, buy_size - matched, remaining_buy_fee)
            pnls.append(trade_pnl - fee - buy_fee_total)

    return pnls


def _compute_brier_score(trades: list, outcomes: dict[str, float]) -> float:
    """Compute Brier score: mean((forecast - outcome)^2).

    Only considers buy trades as forecasts (the price paid = implied probability).
    """
    forecasts = []
    for fill in trades:
        market_id = getattr(fill, "market_id", "")
        if market_id not in outcomes:
            continue
        side_str = str(getattr(fill, "order_side", "")).lower()
        if "buy" not in side_str:
            continue
        price = getattr(fill, "price", 0.0)
        outcome = outcomes[market_id]
        forecasts.append((price - outcome) ** 2)

    return sum(forecasts) / len(forecasts) if forecasts else 0.0


def _compute_avg_edge(trades: list, outcomes: dict[str, float]) -> float:
    """Compute average edge: mean(outcome - price_paid) for buy trades."""
    edges = []
    for fill in trades:
        market_id = getattr(fill, "market_id", "")
        if market_id not in outcomes:
            continue
        side_str = str(getattr(fill, "order_side", "")).lower()
        if "buy" not in side_str:
            continue
        price = getattr(fill, "price", 0.0)
        outcome = outcomes[market_id]
        edges.append(outcome - price)

    return sum(edges) / len(edges) if edges else 0.0


def _extract_predictions_outcomes(
    trades: list, outcomes: dict[str, float]
) -> tuple[list[float], list[float]]:
    """Extract (predictions, outcomes) vectors from buy trades for Rust analytics."""
    predictions = []
    outcome_vals = []
    for fill in trades:
        market_id = getattr(fill, "market_id", "")
        if market_id not in outcomes:
            continue
        side_str = str(getattr(fill, "order_side", "")).lower()
        if "buy" not in side_str:
            continue
        predictions.append(getattr(fill, "price", 0.0))
        outcome_vals.append(outcomes[market_id])
    return predictions, outcome_vals
