"""Pure-function performance metrics for the backtester.

All functions take lists of domain objects (no pandas).
All ratio helpers return None instead of 0.0 for empty/zero denominators.
"""
from __future__ import annotations

import datetime as dt
import math
import random
import statistics
from collections import defaultdict
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from libs.backtest.domain import DailyPortfolioState, ExitReason, FilledTrade, MetricsBundle


# ---------------------------------------------------------------------------
# Trade metrics (7)
# ---------------------------------------------------------------------------


def compute_win_rate(trades: list[FilledTrade]) -> float | None:
    if not trades:
        return None
    wins = sum(1 for t in trades if t.net_pnl > 0)
    return wins / len(trades)


def compute_avg_win_pct(trades: list[FilledTrade]) -> float | None:
    wins = [t.pnl_pct for t in trades if t.net_pnl > 0]
    if not wins:
        return None
    return statistics.mean(wins)


def compute_avg_loss_pct(trades: list[FilledTrade]) -> float | None:
    losses = [t.pnl_pct for t in trades if t.net_pnl <= 0]
    if not losses:
        return None
    return statistics.mean(losses)


def compute_profit_factor(trades: list[FilledTrade]) -> float | None:
    gross_profit = sum(t.net_pnl for t in trades if t.net_pnl > 0)
    gross_loss = abs(sum(t.net_pnl for t in trades if t.net_pnl < 0))
    if gross_loss == 0:
        return None
    return gross_profit / gross_loss


def compute_expectancy_r(trades: list[FilledTrade]) -> float | None:
    if not trades:
        return None
    return statistics.mean(t.r_multiple for t in trades)


def compute_avg_r_multiple(trades: list[FilledTrade]) -> float | None:
    if not trades:
        return None
    return statistics.mean(t.r_multiple for t in trades)


# ---------------------------------------------------------------------------
# Portfolio metrics (8)
# ---------------------------------------------------------------------------


def compute_total_return_pct(equity_curve: list[DailyPortfolioState]) -> float | None:
    if len(equity_curve) < 2:
        return None
    start = equity_curve[0].equity
    end = equity_curve[-1].equity
    if start == 0:
        return None
    return (end - start) / start * 100.0


def compute_simple_return_pct(trades: list[FilledTrade], initial_equity: float) -> float | None:
    """Sum of all trade net_pnl / initial_equity * 100 (no compounding)."""
    if not trades or initial_equity <= 0:
        return None
    return sum(t.net_pnl for t in trades) / initial_equity * 100.0


def compute_annualized_return_pct(
    equity_curve: list[DailyPortfolioState],
) -> float | None:
    if len(equity_curve) < 2:
        return None
    start = equity_curve[0].equity
    end = equity_curve[-1].equity
    if start <= 0:
        return None
    days = (equity_curve[-1].date - equity_curve[0].date).days
    if days <= 0:
        return None
    years = days / 365.25
    return ((end / start) ** (1.0 / years) - 1.0) * 100.0


def compute_max_drawdown_pct(equity_curve: list[DailyPortfolioState]) -> float | None:
    if not equity_curve:
        return None
    peak = equity_curve[0].equity
    max_dd = 0.0
    for state in equity_curve:
        if state.equity > peak:
            peak = state.equity
        if peak > 0:
            dd = (peak - state.equity) / peak * 100.0
            max_dd = max(max_dd, dd)
    return max_dd


def compute_calmar_ratio(
    annualized_return: float | None,
    max_drawdown: float | None,
) -> float | None:
    if annualized_return is None or max_drawdown is None or max_drawdown == 0:
        return None
    return annualized_return / max_drawdown


def _daily_returns(equity_curve: list[DailyPortfolioState]) -> list[float]:
    returns = []
    for i in range(1, len(equity_curve)):
        prev = equity_curve[i - 1].equity
        curr = equity_curve[i].equity
        if prev > 0:
            returns.append((curr - prev) / prev)
    return returns


def compute_sharpe_ratio(
    equity_curve: list[DailyPortfolioState],
    risk_free_daily: float = 0.0,
) -> float | None:
    returns = _daily_returns(equity_curve)
    if len(returns) < 2:
        return None
    excess = [r - risk_free_daily for r in returns]
    mean = statistics.mean(excess)
    try:
        std = statistics.stdev(excess)
    except statistics.StatisticsError:
        return None
    if std == 0:
        return None
    return (mean / std) * math.sqrt(252)


def compute_sortino_ratio(
    equity_curve: list[DailyPortfolioState],
    risk_free_daily: float = 0.0,
) -> float | None:
    returns = _daily_returns(equity_curve)
    if len(returns) < 2:
        return None
    excess = [r - risk_free_daily for r in returns]
    mean = statistics.mean(excess)
    downside = [r for r in excess if r < 0]
    if len(downside) < 2:
        return None
    try:
        downside_std = statistics.stdev(downside)
    except statistics.StatisticsError:
        return None
    if downside_std == 0:
        return None
    return (mean / downside_std) * math.sqrt(252)


def compute_avg_daily_pnl(equity_curve: list[DailyPortfolioState]) -> float | None:
    if len(equity_curve) < 2:
        return None
    daily_pnls = []
    for i in range(1, len(equity_curve)):
        daily_pnls.append(equity_curve[i].equity - equity_curve[i - 1].equity)
    return statistics.mean(daily_pnls)


def compute_avg_positions_held(equity_curve: list[DailyPortfolioState]) -> float | None:
    if not equity_curve:
        return None
    return statistics.mean(len(s.open_positions) for s in equity_curve)


def compute_avg_gross_exposure_pct(
    equity_curve: list[DailyPortfolioState],
) -> float | None:
    if not equity_curve:
        return None
    exposure_pcts = [
        (state.gross_exposure / state.equity) * 100.0
        for state in equity_curve
        if state.equity > 0
    ]
    if not exposure_pcts:
        return None
    return statistics.mean(exposure_pcts)


def compute_avg_net_exposure_pct(
    equity_curve: list[DailyPortfolioState],
) -> float | None:
    if not equity_curve:
        return None
    exposure_pcts = [
        (state.net_exposure / state.equity) * 100.0
        for state in equity_curve
        if state.equity > 0
    ]
    if not exposure_pcts:
        return None
    return statistics.mean(exposure_pcts)


def compute_days_in_market_pct(
    equity_curve: list[DailyPortfolioState],
) -> float | None:
    if not equity_curve:
        return None
    days_in_market = sum(1 for state in equity_curve if state.gross_exposure > 0)
    return days_in_market / len(equity_curve) * 100.0


def compute_avg_idle_fraction_pct(
    equity_curve: list[DailyPortfolioState],
) -> float | None:
    """Mean daily fraction of equity sitting idle (raw cash + parking)."""
    vals = [
        ((s.raw_cash or 0.0) + (s.parking_value or 0.0)) / s.equity * 100.0
        for s in equity_curve
        if s.equity > 0 and s.raw_cash is not None
    ]
    return statistics.mean(vals) if vals else None


def compute_avg_primary_utilization_pct(
    equity_curve: list[DailyPortfolioState],
) -> float | None:
    """Mean daily fraction of equity in primary-engine positions."""
    vals = [
        (s.primary_exposure or 0.0) / s.equity * 100.0
        for s in equity_curve
        if s.equity > 0 and s.primary_exposure is not None
    ]
    return statistics.mean(vals) if vals else None


def compute_avg_ia_utilization_pct(
    equity_curve: list[DailyPortfolioState],
) -> float | None:
    """Mean daily fraction of equity in idle-alpha-sleeve positions."""
    vals = [
        (s.idle_alpha_exposure or 0.0) / s.equity * 100.0
        for s in equity_curve
        if s.equity > 0 and s.idle_alpha_exposure is not None
    ]
    return statistics.mean(vals) if vals else None


# ---------------------------------------------------------------------------
# Stability metrics (4)
# ---------------------------------------------------------------------------


def compute_trade_skewness(trades: list[FilledTrade]) -> float | None:
    if len(trades) < 3:
        return None
    pnls = [t.net_pnl for t in trades]
    mean = statistics.mean(pnls)
    try:
        std = statistics.stdev(pnls)
    except statistics.StatisticsError:
        return None
    if std == 0:
        return None
    n = len(pnls)
    skew = sum(((x - mean) / std) ** 3 for x in pnls) * n / ((n - 1) * (n - 2))
    return skew


def compute_trade_kurtosis(trades: list[FilledTrade]) -> float | None:
    if len(trades) < 4:
        return None
    pnls = [t.net_pnl for t in trades]
    mean = statistics.mean(pnls)
    try:
        std = statistics.stdev(pnls)
    except statistics.StatisticsError:
        return None
    if std == 0:
        return None
    n = len(pnls)
    # Excess kurtosis (Fisher's definition)
    kurt = sum(((x - mean) / std) ** 4 for x in pnls) * n * (n + 1) / (
        (n - 1) * (n - 2) * (n - 3)
    ) - 3 * (n - 1) ** 2 / ((n - 2) * (n - 3))
    return kurt


def compute_monthly_win_rate(trades: list[FilledTrade]) -> float | None:
    """Fraction of calendar months with net positive PnL."""
    if not trades:
        return None
    monthly: dict[str, float] = defaultdict(float)
    for t in trades:
        key = t.exit_date.strftime("%Y-%m")
        monthly[key] += t.net_pnl
    if not monthly:
        return None
    wins = sum(1 for v in monthly.values() if v > 0)
    return wins / len(monthly)


def compute_equity_curve_r_squared(equity_curve: list[DailyPortfolioState]) -> float | None:
    """R² of a linear regression fit to the equity curve (higher = smoother growth)."""
    if len(equity_curve) < 3:
        return None
    n = len(equity_curve)
    xs = list(range(n))
    ys = [s.equity for s in equity_curve]
    x_mean = statistics.mean(xs)
    y_mean = statistics.mean(ys)
    ss_xx = sum((x - x_mean) ** 2 for x in xs)
    ss_xy = sum((x - x_mean) * (y - y_mean) for x, y in zip(xs, ys))
    ss_yy = sum((y - y_mean) ** 2 for y in ys)
    if ss_xx == 0 or ss_yy == 0:
        return None
    r = ss_xy / math.sqrt(ss_xx * ss_yy)
    return r ** 2


# ---------------------------------------------------------------------------
# Practicality metrics (4)
# ---------------------------------------------------------------------------


def compute_avg_holding_days(trades: list[FilledTrade]) -> float | None:
    if not trades:
        return None
    return statistics.mean(t.holding_days for t in trades)


def compute_stop_exit_rate(trades: list[FilledTrade]) -> float | None:
    from libs.backtest.domain import ExitReason

    if not trades:
        return None
    stops = sum(1 for t in trades if t.exit_reason in (ExitReason.STOP, ExitReason.TRAILING))
    return stops / len(trades)


def compute_target_exit_rate(trades: list[FilledTrade]) -> float | None:
    from libs.backtest.domain import ExitReason

    if not trades:
        return None
    targets = sum(1 for t in trades if t.exit_reason == ExitReason.TARGET)
    return targets / len(trades)


def compute_no_follow_through_rate(trades: list[FilledTrade]) -> float | None:
    from libs.backtest.domain import ExitReason

    if not trades:
        return None
    nft = sum(1 for t in trades if t.exit_reason == ExitReason.NO_FOLLOW_THROUGH)
    return nft / len(trades)


def compute_score_bucket_hit_rate(trades: list[FilledTrade], candidate_map: dict[str, object]) -> dict[str, float]:
    """Win rate per score_bucket (uses trade_id -> candidate mapping)."""
    bucket_wins: dict[str, int] = defaultdict(int)
    bucket_total: dict[str, int] = defaultdict(int)
    for t in trades:
        cand = candidate_map.get(t.trade_id)
        if cand is None:
            continue
        bucket = getattr(cand, "score_bucket", "unknown")
        bucket_total[bucket] += 1
        if t.net_pnl > 0:
            bucket_wins[bucket] += 1
    return {
        b: bucket_wins[b] / bucket_total[b]
        for b in bucket_total
        if bucket_total[b] > 0
    }


# ---------------------------------------------------------------------------
# Bootstrap confidence intervals
# ---------------------------------------------------------------------------


def bootstrap_ci(
    trades: list[FilledTrade],
    metric_fn: callable,
    n_iterations: int = 1000,
    ci_level: float = 0.95,
    seed: int = 42,
) -> tuple[float, float] | None:
    """Compute bootstrap confidence interval for a trade-level metric.

    Args:
        trades: List of FilledTrade objects.
        metric_fn: Function that takes list[FilledTrade] and returns float | None.
        n_iterations: Number of bootstrap resamples.
        ci_level: Confidence level (default 0.95 for 95% CI).
        seed: Random seed for reproducibility.

    Returns:
        (lower, upper) bounds or None if metric can't be computed.
    """
    if len(trades) < 5:
        return None

    rng = random.Random(seed)
    results = []
    for _ in range(n_iterations):
        sample = rng.choices(trades, k=len(trades))
        val = metric_fn(sample)
        if val is not None:
            results.append(val)

    if len(results) < n_iterations * 0.5:
        return None

    results.sort()
    alpha = (1 - ci_level) / 2
    lo_idx = int(alpha * len(results))
    hi_idx = int((1 - alpha) * len(results)) - 1
    return (results[lo_idx], results[hi_idx])


def compute_bootstrap_cis(
    trades: list[FilledTrade],
    n_iterations: int = 1000,
    seed: int = 42,
) -> dict[str, tuple[float, float] | None]:
    """Compute 95% bootstrap CIs for key trade metrics."""
    metrics_fns = {
        "win_rate": compute_win_rate,
        "avg_win_pct": compute_avg_win_pct,
        "avg_loss_pct": compute_avg_loss_pct,
        "profit_factor": compute_profit_factor,
        "expectancy_r": compute_expectancy_r,
    }
    return {
        f"{name}_ci_95": bootstrap_ci(trades, fn, n_iterations=n_iterations, seed=seed)
        for name, fn in metrics_fns.items()
    }


# ---------------------------------------------------------------------------
# Builder
# ---------------------------------------------------------------------------


def build_metrics_bundle(
    trades: list[FilledTrade],
    equity_curve: list[DailyPortfolioState],
    candidate_map: dict[str, object] | None = None,
) -> MetricsBundle:
    from libs.backtest.domain import MetricsBundle

    initial_equity = equity_curve[0].equity if equity_curve else 0.0
    ann_ret = compute_annualized_return_pct(equity_curve)
    max_dd = compute_max_drawdown_pct(equity_curve)

    # Bootstrap CIs (only when enough trades)
    cis = compute_bootstrap_cis(trades) if len(trades) >= 5 else {}

    return MetricsBundle(
        # Trade
        trade_count=len(trades),
        win_rate=compute_win_rate(trades),
        avg_win_pct=compute_avg_win_pct(trades),
        avg_loss_pct=compute_avg_loss_pct(trades),
        profit_factor=compute_profit_factor(trades),
        expectancy_r=compute_expectancy_r(trades),
        avg_r_multiple=compute_avg_r_multiple(trades),
        # Portfolio
        total_return_pct=compute_total_return_pct(equity_curve),
        annualized_return_pct=ann_ret,
        max_drawdown_pct=max_dd,
        calmar_ratio=compute_calmar_ratio(ann_ret, max_dd),
        sharpe_ratio=compute_sharpe_ratio(equity_curve),
        sortino_ratio=compute_sortino_ratio(equity_curve),
        avg_daily_pnl=compute_avg_daily_pnl(equity_curve),
        avg_positions_held=compute_avg_positions_held(equity_curve),
        avg_gross_exposure_pct=compute_avg_gross_exposure_pct(equity_curve),
        avg_net_exposure_pct=compute_avg_net_exposure_pct(equity_curve),
        days_in_market_pct=compute_days_in_market_pct(equity_curve),
        # Stability
        trade_skewness=compute_trade_skewness(trades),
        trade_kurtosis=compute_trade_kurtosis(trades),
        monthly_win_rate=compute_monthly_win_rate(trades),
        equity_curve_r_squared=compute_equity_curve_r_squared(equity_curve),
        # Practicality
        avg_holding_days=compute_avg_holding_days(trades),
        stop_exit_rate=compute_stop_exit_rate(trades),
        target_exit_rate=compute_target_exit_rate(trades),
        no_follow_through_exit_rate=compute_no_follow_through_rate(trades),
        score_bucket_hit_rate=compute_score_bucket_hit_rate(trades, candidate_map or {}),
        # Simple return (no compounding)
        simple_return_pct=compute_simple_return_pct(trades, initial_equity),
        initial_equity=initial_equity if initial_equity > 0 else None,
        # Bootstrap CIs
        bootstrap_cis=cis,
        # Idle capital decomposition
        avg_idle_fraction_pct=compute_avg_idle_fraction_pct(equity_curve),
        avg_primary_utilization_pct=compute_avg_primary_utilization_pct(equity_curve),
        avg_ia_utilization_pct=compute_avg_ia_utilization_pct(equity_curve),
    )
