"""
Backtest performance metrics (StockLab overhaul, Part 20).

**Status: the computation core here is REAL and tested against hand-computed values — this is
NOT the biggest gap. The biggest gap (see `app/engines/backtesting/replay.py`) is that nothing in
this codebase yet WALKS a portfolio forward through point-in-time-correct historical data to
produce the `period_returns` this module consumes.** This module answers "given a returns series
(and optionally trade-level and benchmark data), what are CAGR/Vol/MaxDD/Sharpe/Sortino/Win Rate/
Avg Winner/Avg Loser/Turnover/Alpha/Beta" — a well-defined, independently-testable numerical
problem, deliberately separated from the (unbuilt) replay orchestration that would produce real
`period_returns` for an actual StockLab strategy.

Dependency-free (numpy + stdlib only), like every other engine module.
"""
from __future__ import annotations

from dataclasses import dataclass
from typing import Optional, Sequence

import numpy as np


@dataclass(frozen=True)
class BacktestPerformanceReport:
    periods: int
    periods_per_year: float
    total_return: Optional[float]
    cagr: Optional[float]
    annualized_volatility: Optional[float]
    max_drawdown: Optional[float]
    sharpe_ratio: Optional[float]
    sortino_ratio: Optional[float]
    win_rate: Optional[float]
    avg_winner: Optional[float]
    avg_loser: Optional[float]
    turnover: Optional[float]
    alpha_annualized: Optional[float]
    beta: Optional[float]


def _total_return(period_returns: Sequence[float]) -> float:
    cumulative = 1.0
    for r in period_returns:
        cumulative *= (1 + r)
    return cumulative - 1.0


def _max_drawdown(period_returns: Sequence[float]) -> float:
    equity = [1.0]
    for r in period_returns:
        equity.append(equity[-1] * (1 + r))
    peak = equity[0]
    max_dd = 0.0
    for v in equity:
        peak = max(peak, v)
        dd = (v - peak) / peak if peak else 0.0
        max_dd = min(max_dd, dd)
    return max_dd  # negative number, e.g. -0.35 for a 35% drawdown


def compute_performance_report(
    period_returns: Sequence[float],
    periods_per_year: float = 12.0,
    risk_free_rate_per_period: float = 0.0,
    trade_returns: Optional[Sequence[float]] = None,
    turnover_per_period: Optional[Sequence[float]] = None,
    benchmark_period_returns: Optional[Sequence[float]] = None,
) -> BacktestPerformanceReport:
    n = len(period_returns)
    if n == 0:
        return BacktestPerformanceReport(
            periods=0, periods_per_year=periods_per_year, total_return=None, cagr=None,
            annualized_volatility=None, max_drawdown=None, sharpe_ratio=None, sortino_ratio=None,
            win_rate=None, avg_winner=None, avg_loser=None, turnover=None,
            alpha_annualized=None, beta=None,
        )

    returns = np.asarray(period_returns, dtype=float)
    total_return = _total_return(returns)
    years = n / periods_per_year
    cagr = (1 + total_return) ** (1 / years) - 1 if years > 0 and (1 + total_return) > 0 else None

    vol = float(np.std(returns, ddof=1)) * np.sqrt(periods_per_year) if n > 1 else None
    max_dd = _max_drawdown(returns)

    excess = returns - risk_free_rate_per_period
    sharpe = None
    if n > 1 and float(np.std(excess, ddof=1)) > 0:
        sharpe = float(np.mean(excess) / np.std(excess, ddof=1)) * np.sqrt(periods_per_year)

    downside = excess[excess < 0]
    sortino = None
    if n > 1 and len(downside) > 1 and float(np.std(downside, ddof=1)) > 0:
        sortino = float(np.mean(excess) / np.std(downside, ddof=1)) * np.sqrt(periods_per_year)
    elif n > 1 and len(downside) == 0:
        sortino = float("inf")  # no downside periods at all — genuinely undefined-large, not 0

    win_rate = avg_winner = avg_loser = None
    if trade_returns:
        trades = np.asarray(trade_returns, dtype=float)
        wins = trades[trades > 0]
        losses = trades[trades < 0]
        win_rate = len(wins) / len(trades) if len(trades) else None
        avg_winner = float(np.mean(wins)) if len(wins) else None
        avg_loser = float(np.mean(losses)) if len(losses) else None

    turnover = float(np.mean(turnover_per_period)) if turnover_per_period else None

    alpha_annualized = beta = None
    if benchmark_period_returns and len(benchmark_period_returns) == n and n > 1:
        bench = np.asarray(benchmark_period_returns, dtype=float)
        bench_var = float(np.var(bench, ddof=1))
        if bench_var > 0:
            cov = float(np.cov(returns, bench, ddof=1)[0, 1])
            beta = cov / bench_var
            alpha_per_period = float(np.mean(returns) - beta * np.mean(bench))
            alpha_annualized = alpha_per_period * periods_per_year

    return BacktestPerformanceReport(
        periods=n, periods_per_year=periods_per_year, total_return=total_return, cagr=cagr,
        annualized_volatility=vol, max_drawdown=max_dd, sharpe_ratio=sharpe, sortino_ratio=sortino,
        win_rate=win_rate, avg_winner=avg_winner, avg_loser=avg_loser, turnover=turnover,
        alpha_annualized=alpha_annualized, beta=beta,
    )
