"""Performance analyzer for backtest results.

This module provides the PerformanceAnalyzer class for calculating
various trading performance metrics from backtest results.
"""

from dataclasses import dataclass
from datetime import datetime
from typing import Any

import numpy as np
import pandas as pd
from numpy.typing import NDArray


@dataclass
class TradeRecord:
    """Single trade record for analysis.

    Attributes:
        entry_time: Entry timestamp
        exit_time: Exit timestamp
        side: Trade side ("long" or "short")
        entry_price: Entry price
        exit_price: Exit price
        quantity: Number of shares/contracts
        pnl: Profit/loss amount
        is_win: Whether trade was profitable
    """

    entry_time: datetime
    exit_time: datetime
    side: str
    entry_price: float
    exit_price: float
    quantity: float
    pnl: float
    is_win: bool


@dataclass
class BacktestResult:
    """Container for backtest results.

    Attributes:
        initial_capital: Starting capital
        final_capital: Ending capital
        equity_curve: List of equity values over time
        timestamps: List of timestamps corresponding to equity values
        trades: List of completed trades
        start_time: Backtest start time
        end_time: Backtest end time
    """

    initial_capital: float
    final_capital: float
    equity_curve: list[float]
    timestamps: list[datetime]
    trades: list[TradeRecord]
    start_time: datetime
    end_time: datetime


class PerformanceAnalyzer:
    """Analyze trading performance metrics from backtest results.

    This class provides methods for calculating various performance
    metrics including returns, drawdowns, risk-adjusted ratios, and
    trade statistics.

    Example:
        >>> result = BacktestResult(...)
        >>> analyzer = PerformanceAnalyzer()
        >>> report = analyzer.generate_report(result)
        >>> print(f"Sharpe Ratio: {report['sharpe_ratio']:.2f}")
    """

    # Trading days per year for annualization
    TRADING_DAYS_PER_YEAR = 252

    def calculate_returns(self, equity_curve: list[float]) -> NDArray[np.float64]:
        """Calculate simple returns from equity curve.

        Args:
            equity_curve: List of equity values over time

        Returns:
            Array of simple returns (current / previous - 1)
        """
        if len(equity_curve) < 2:
            return np.array([])

        equity_arr = np.asarray(equity_curve, dtype=np.float64)
        returns: NDArray[np.float64] = np.diff(equity_arr) / equity_arr[:-1]
        return returns

    def calculate_total_return(self, equity_curve: list[float]) -> float:
        """Calculate total return percentage.

        Args:
            equity_curve: List of equity values over time

        Returns:
            Total return as a decimal (e.g., 0.15 for 15%)
        """
        if len(equity_curve) < 2:
            return 0.0

        initial = equity_curve[0]
        final = equity_curve[-1]

        if initial == 0:
            return 0.0

        return (final - initial) / initial

    def calculate_annualized_return(
        self,
        equity_curve: list[float],
        timestamps: list[datetime],
    ) -> float:
        """Calculate annualized return (CAGR).

        Args:
            equity_curve: List of equity values over time
            timestamps: List of timestamps corresponding to equity values

        Returns:
            Annualized return as a decimal
        """
        if len(equity_curve) < 2 or len(timestamps) < 2:
            return 0.0

        total_return = self.calculate_total_return(equity_curve)

        # Calculate years between first and last timestamp
        days = (timestamps[-1] - timestamps[0]).days
        if days <= 0:
            return 0.0

        years = days / 365.25

        # CAGR = (1 + total_return)^(1/years) - 1
        annualized: float = float((1 + total_return) ** (1 / years) - 1)
        return annualized

    def calculate_max_drawdown(self, equity_curve: list[float]) -> dict[str, float]:
        """Calculate maximum drawdown and related statistics.

        Args:
            equity_curve: List of equity values over time

        Returns:
            Dictionary containing:
                - max_drawdown: Maximum drawdown as a positive decimal
                - max_drawdown_pct: Same as max_drawdown but as percentage
                - peak: Peak equity value before max drawdown
                - trough: Lowest equity value during max drawdown
                - recovery_index: Index where equity recovered to peak (or -1)
        """
        if len(equity_curve) < 2:
            return {
                "max_drawdown": 0.0,
                "max_drawdown_pct": 0.0,
                "peak": equity_curve[0] if equity_curve else 0.0,
                "trough": equity_curve[0] if equity_curve else 0.0,
                "recovery_index": -1,
            }

        equity = np.array(equity_curve)
        running_max = np.maximum.accumulate(equity)
        drawdowns = (running_max - equity) / running_max

        max_dd_idx = np.argmax(drawdowns)
        max_drawdown = drawdowns[max_dd_idx]

        # Handle edge case: no drawdown (always increasing)
        if max_drawdown == 0:
            peak_idx = len(equity) - 1
            peak = equity[peak_idx]
            trough = peak
        else:
            peak_idx = int(np.argmax(equity[: max_dd_idx + 1]))
            peak = equity[peak_idx]
            trough = equity[max_dd_idx]

        # Find recovery point (if any)
        recovery_idx = -1
        for i in range(max_dd_idx + 1, len(equity)):
            if equity[i] >= running_max[max_dd_idx]:
                recovery_idx = i
                break

        return {
            "max_drawdown": float(max_drawdown),
            "max_drawdown_pct": float(max_drawdown) * 100,
            "peak": float(peak),
            "trough": float(trough),
            "recovery_index": int(recovery_idx),
        }

    def calculate_sharpe_ratio(
        self,
        returns: NDArray[np.float64],
        risk_free_rate: float = 0.02,
        periods_per_year: int = 252,
    ) -> float:
        """Calculate annualized Sharpe ratio.

        The Sharpe ratio measures risk-adjusted return using total volatility.

        Args:
            returns: Array of returns (can be daily, hourly, etc.)
            risk_free_rate: Annual risk-free rate (default: 0.02 for 2%)
            periods_per_year: Number of periods in a year (default: 252 trading days)

        Returns:
            Annualized Sharpe ratio
        """
        if len(returns) < 2:
            return 0.0

        # Convert annual risk-free rate to per-period rate
        period_risk_free = risk_free_rate / periods_per_year

        # Calculate excess returns
        excess_returns = returns - period_risk_free

        # Calculate mean and std of excess returns
        mean_excess = np.mean(excess_returns)
        std_excess = np.std(excess_returns, ddof=1)

        if std_excess == 0 or np.isnan(std_excess):
            return 0.0

        # Annualize
        sharpe = mean_excess / std_excess * np.sqrt(periods_per_year)

        return float(sharpe)

    def calculate_sortino_ratio(
        self,
        returns: NDArray[np.float64],
        risk_free_rate: float = 0.02,
        periods_per_year: int = 252,
    ) -> float:
        """Calculate annualized Sortino ratio.

        The Sortino ratio measures risk-adjusted return using only
        downside volatility (returns below target).

        Args:
            returns: Array of returns
            risk_free_rate: Annual risk-free rate (default: 0.02 for 2%)
            periods_per_year: Number of periods in a year (default: 252)

        Returns:
            Annualized Sortino ratio
        """
        if len(returns) < 2:
            return 0.0

        # Convert annual risk-free rate to per-period rate
        period_risk_free = risk_free_rate / periods_per_year

        # Calculate excess returns
        excess_returns = returns - period_risk_free
        mean_excess = np.mean(excess_returns)

        # Calculate downside deviation (only negative returns)
        downside_returns = excess_returns[excess_returns < 0]

        if len(downside_returns) < 1:
            # No downside - infinite Sortino (return large number)
            return float("inf") if mean_excess > 0 else 0.0

        downside_std = np.std(downside_returns, ddof=1)

        if downside_std == 0 or np.isnan(downside_std):
            if mean_excess > 0:
                return float("inf")
            elif mean_excess < 0:
                return float("-inf")
            else:
                return 0.0

        # Annualize
        sortino = mean_excess / downside_std * np.sqrt(periods_per_year)

        return float(sortino)

    def calculate_calmar_ratio(
        self,
        returns: NDArray[np.float64],
        max_drawdown: float,
        periods_per_year: int = 252,
    ) -> float:
        """Calculate Calmar ratio.

        The Calmar ratio measures return relative to maximum drawdown.

        Args:
            returns: Array of returns
            max_drawdown: Maximum drawdown as a positive decimal
            periods_per_year: Number of periods in a year (default: 252)

        Returns:
            Calmar ratio
        """
        if len(returns) < 1 or max_drawdown <= 0:
            return 0.0

        # Calculate annualized return from mean return
        mean_return = np.mean(returns)
        annualized_return = mean_return * periods_per_year

        calmar = annualized_return / max_drawdown

        return float(calmar)

    def calculate_win_rate(self, trades: list[TradeRecord]) -> float:
        """Calculate win rate from trades.

        Args:
            trades: List of completed trades

        Returns:
            Win rate as a decimal (0.0 to 1.0)
        """
        if not trades:
            return 0.0

        winning_trades = sum(1 for t in trades if t.is_win)
        return winning_trades / len(trades)

    def calculate_loss_rate(self, trades: list[TradeRecord]) -> float:
        """Calculate loss rate from trades.

        Args:
            trades: List of completed trades

        Returns:
            Loss rate as a decimal (0.0 to 1.0)
        """
        if not trades:
            return 0.0

        return 1.0 - self.calculate_win_rate(trades)

    def calculate_profit_factor(self, trades: list[TradeRecord]) -> float:
        """Calculate profit factor.

        Profit factor = Gross Profit / Gross Loss
        A value > 1.0 indicates profitable strategy.

        Args:
            trades: List of completed trades

        Returns:
            Profit factor (inf if no losing trades)
        """
        if not trades:
            return 0.0

        gross_profit = sum(t.pnl for t in trades if t.pnl > 0)
        gross_loss = abs(sum(t.pnl for t in trades if t.pnl < 0))

        if gross_loss == 0:
            return float("inf") if gross_profit > 0 else 0.0

        return gross_profit / gross_loss

    def calculate_avg_trade(self, trades: list[TradeRecord]) -> dict[str, float]:
        """Calculate average trade statistics.

        Args:
            trades: List of completed trades

        Returns:
            Dictionary containing:
                - avg_pnl: Average P&L per trade
                - avg_win: Average winning trade P&L
                - avg_loss: Average losing trade P&L
                - win_loss_ratio: Ratio of avg_win to avg_loss
        """
        if not trades:
            return {
                "avg_pnl": 0.0,
                "avg_win": 0.0,
                "avg_loss": 0.0,
                "win_loss_ratio": 0.0,
            }

        pnls = [t.pnl for t in trades]
        wins = [t.pnl for t in trades if t.is_win]
        losses = [abs(t.pnl) for t in trades if not t.is_win]

        avg_pnl = np.mean(pnls)
        avg_win = np.mean(wins) if wins else 0.0
        avg_loss = np.mean(losses) if losses else 0.0

        win_loss_ratio = avg_win / avg_loss if avg_loss > 0 else float("inf")

        return {
            "avg_pnl": float(avg_pnl),
            "avg_win": float(avg_win),
            "avg_loss": float(avg_loss),
            "win_loss_ratio": float(win_loss_ratio),
        }

    def calculate_volatility(
        self,
        returns: NDArray[np.float64],
        annualize: bool = True,
        periods_per_year: int = 252,
    ) -> float:
        """Calculate return volatility (standard deviation).

        Args:
            returns: Array of returns
            annualize: Whether to annualize the result
            periods_per_year: Number of periods in a year (default: 252)

        Returns:
            Volatility as a decimal
        """
        if len(returns) < 2:
            return 0.0

        std = np.std(returns, ddof=1)

        if annualize:
            std *= np.sqrt(periods_per_year)

        return float(std)

    def calculate_var(
        self,
        returns: NDArray[np.float64],
        confidence: float = 0.05,
    ) -> float:
        """Calculate Value at Risk (VaR) using historical method.

        VaR represents the potential loss at a given confidence level.
        For example, a 5% VaR of -0.02 means there's a 5% chance of
        losing more than 2%.

        Args:
            returns: Array of historical returns
            confidence: Confidence level (default: 0.05 for 5%)

        Returns:
            VaR as a negative number (potential loss)
        """
        if len(returns) < 1:
            return 0.0

        return float(np.percentile(returns, confidence * 100))

    def calculate_cvar(
        self,
        returns: NDArray[np.float64],
        confidence: float = 0.05,
    ) -> float:
        """Calculate Conditional Value at Risk (CVaR) / Expected Shortfall.

        CVaR is the average loss when losses exceed VaR.

        Args:
            returns: Array of historical returns
            confidence: Confidence level (default: 0.05 for 5%)

        Returns:
            CVaR as a negative number (expected shortfall)
        """
        if len(returns) < 1:
            return 0.0

        var = self.calculate_var(returns, confidence)
        tail_returns = returns[returns <= var]

        if len(tail_returns) == 0:
            return var

        return float(np.mean(tail_returns))

    def calculate_consecutive_stats(self, trades: list[TradeRecord]) -> dict[str, Any]:
        """Calculate consecutive win/loss statistics.

        Args:
            trades: List of completed trades

        Returns:
            Dictionary containing:
                - max_consecutive_wins: Longest streak of winning trades
                - max_consecutive_losses: Longest streak of losing trades
                - current_streak: Current streak (positive for wins, negative for losses)
        """
        if not trades:
            return {
                "max_consecutive_wins": 0,
                "max_consecutive_losses": 0,
                "current_streak": 0,
            }

        max_wins = 0
        max_losses = 0
        current_streak = 0
        current_type = None

        for trade in trades:
            if trade.is_win:
                if current_type == "win":
                    current_streak += 1
                else:
                    current_type = "win"
                    current_streak = 1
                max_wins = max(max_wins, current_streak)
            else:
                if current_type == "loss":
                    current_streak -= 1
                else:
                    current_type = "loss"
                    current_streak = -1
                max_losses = max(max_losses, abs(current_streak))

        return {
            "max_consecutive_wins": max_wins,
            "max_consecutive_losses": max_losses,
            "current_streak": current_streak,
        }

    def generate_report(self, backtest_result: BacktestResult) -> dict[str, Any]:
        """Generate comprehensive backtest performance report.

        Args:
            backtest_result: Complete backtest result data

        Returns:
            Dictionary containing all performance metrics
        """
        equity_curve = backtest_result.equity_curve
        timestamps = backtest_result.timestamps
        trades = backtest_result.trades

        # Calculate returns
        returns = self.calculate_returns(equity_curve)

        # Basic metrics
        total_return = self.calculate_total_return(equity_curve)
        annualized_return = self.calculate_annualized_return(equity_curve, timestamps)

        # Drawdown analysis
        drawdown_stats = self.calculate_max_drawdown(equity_curve)

        # Risk metrics
        volatility = self.calculate_volatility(returns) if len(returns) > 0 else 0.0
        sharpe_ratio = self.calculate_sharpe_ratio(returns) if len(returns) > 0 else 0.0
        sortino_ratio = self.calculate_sortino_ratio(returns) if len(returns) > 0 else 0.0
        calmar_ratio = (
            self.calculate_calmar_ratio(returns, drawdown_stats["max_drawdown"])
            if len(returns) > 0
            else 0.0
        )

        # Trade statistics
        num_trades = len(trades)
        win_rate = self.calculate_win_rate(trades)
        loss_rate = self.calculate_loss_rate(trades)
        profit_factor = self.calculate_profit_factor(trades)
        avg_stats = self.calculate_avg_trade(trades)
        consecutive_stats = self.calculate_consecutive_stats(trades)

        # VaR and CVaR
        var_5 = self.calculate_var(returns, 0.05) if len(returns) > 0 else 0.0
        cvar_5 = self.calculate_cvar(returns, 0.05) if len(returns) > 0 else 0.0

        # Duration
        duration_days = (backtest_result.end_time - backtest_result.start_time).days

        return {
            # Summary
            "initial_capital": backtest_result.initial_capital,
            "final_capital": backtest_result.final_capital,
            "total_return": total_return,
            "total_return_pct": total_return * 100,
            "annualized_return": annualized_return,
            "annualized_return_pct": annualized_return * 100,
            # Trade counts
            "num_trades": num_trades,
            "num_winning_trades": sum(1 for t in trades if t.is_win),
            "num_losing_trades": sum(1 for t in trades if not t.is_win),
            # Win/Loss metrics
            "win_rate": win_rate,
            "win_rate_pct": win_rate * 100,
            "loss_rate": loss_rate,
            "loss_rate_pct": loss_rate * 100,
            "profit_factor": profit_factor,
            # Average trade stats
            "avg_pnl": avg_stats["avg_pnl"],
            "avg_win": avg_stats["avg_win"],
            "avg_loss": avg_stats["avg_loss"],
            "win_loss_ratio": avg_stats["win_loss_ratio"],
            # Drawdown
            "max_drawdown": drawdown_stats["max_drawdown"],
            "max_drawdown_pct": drawdown_stats["max_drawdown_pct"],
            # Risk-adjusted returns
            "volatility": volatility,
            "volatility_pct": volatility * 100,
            "sharpe_ratio": sharpe_ratio,
            "sortino_ratio": sortino_ratio,
            "calmar_ratio": calmar_ratio,
            # VaR/CVaR
            "var_5pct": var_5,
            "var_5pct_pct": var_5 * 100,
            "cvar_5pct": cvar_5,
            "cvar_5pct_pct": cvar_5 * 100,
            # Consecutive stats
            "max_consecutive_wins": consecutive_stats["max_consecutive_wins"],
            "max_consecutive_losses": consecutive_stats["max_consecutive_losses"],
            # Time
            "duration_days": duration_days,
            "start_time": backtest_result.start_time.isoformat(),
            "end_time": backtest_result.end_time.isoformat(),
        }

    def to_dataframe(self, backtest_result: BacktestResult) -> pd.DataFrame:
        """Convert equity curve to pandas DataFrame.

        Args:
            backtest_result: Complete backtest result data

        Returns:
            DataFrame with equity curve data and calculated metrics
        """
        data = {
            "timestamp": backtest_result.timestamps,
            "equity": backtest_result.equity_curve,
        }

        df = pd.DataFrame(data)

        # Calculate returns
        df["returns"] = df["equity"].pct_change()

        # Calculate drawdowns
        df["running_max"] = df["equity"].cummax()
        df["drawdown"] = (df["running_max"] - df["equity"]) / df["running_max"]

        return df
