"""Comparison metrics for strategy evaluation.

This module provides metrics and criteria for comparing multiple
 trading strategies' performance.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING, Callable

import numpy as np
from numpy.typing import NDArray

if TYPE_CHECKING:
    from openclaw.backtest.analyzer import BacktestResult


class RiskLevel(str, Enum):
    """Risk level classification for strategies."""

    CONSERVATIVE = "conservative"
    MODERATE = "moderate"
    AGGRESSIVE = "aggressive"
    SPECULATIVE = "speculative"


@dataclass
class ComparisonMetrics:
    """Container for strategy comparison metrics.

    Attributes:
        strategy_name: Name of the strategy
        total_return: Total return as decimal
        annualized_return: Annualized return as decimal
        sharpe_ratio: Sharpe ratio
        sortino_ratio: Sortino ratio
        max_drawdown: Maximum drawdown as decimal
        max_drawdown_duration: Max drawdown duration in days
        win_rate: Win rate as decimal
        profit_factor: Profit factor
        win_loss_ratio: Win/loss ratio
        volatility: Annualized volatility as decimal
        calmar_ratio: Calmar ratio
        num_trades: Total number of trades
        avg_trade: Average P&L per trade
        var_95: Value at Risk (95% confidence)
        cvar_95: Conditional VaR (95% confidence)
    """

    strategy_name: str
    total_return: float = 0.0
    annualized_return: float = 0.0
    sharpe_ratio: float = 0.0
    sortino_ratio: float = 0.0
    max_drawdown: float = 0.0
    max_drawdown_duration: int = 0
    win_rate: float = 0.0
    profit_factor: float = 0.0
    win_loss_ratio: float = 0.0
    volatility: float = 0.0
    calmar_ratio: float = 0.0
    num_trades: int = 0
    avg_trade: float = 0.0
    var_95: float = 0.0
    cvar_95: float = 0.0

    @property
    def risk_level(self) -> RiskLevel:
        """Classify strategy risk level based on drawdown and volatility."""
        if self.max_drawdown > 0.25 or self.volatility > 0.4:
            return RiskLevel.SPECULATIVE
        if self.max_drawdown > 0.15 or self.volatility > 0.25:
            return RiskLevel.AGGRESSIVE
        if self.max_drawdown > 0.08 or self.volatility > 0.15:
            return RiskLevel.MODERATE
        return RiskLevel.CONSERVATIVE

    @property
    def risk_score(self) -> float:
        """Calculate composite risk score (0-100, higher = riskier)."""
        # Normalize each component to 0-100 scale
        dd_score = min(self.max_drawdown * 200, 100)  # 50% dd = 100 score
        vol_score = min(self.volatility * 200, 100)  # 50% vol = 100 score
        var_score = min(abs(self.var_95) * 1000, 100)  # 10% var = 100 score

        # Weighted combination
        return float(0.4 * dd_score + 0.4 * vol_score + 0.2 * var_score)

    @property
    def return_risk_ratio(self) -> float:
        """Calculate return to risk ratio."""
        risk = self.risk_score
        return self.total_return / risk if risk > 0 else float("inf")

    def to_dict(self) -> dict[str, float | int | str]:
        """Convert metrics to dictionary."""
        return {
            "strategy_name": self.strategy_name,
            "total_return": self.total_return,
            "total_return_pct": self.total_return * 100,
            "annualized_return": self.annualized_return,
            "annualized_return_pct": self.annualized_return * 100,
            "sharpe_ratio": self.sharpe_ratio,
            "sortino_ratio": self.sortino_ratio,
            "max_drawdown": self.max_drawdown,
            "max_drawdown_pct": self.max_drawdown * 100,
            "max_drawdown_duration": self.max_drawdown_duration,
            "win_rate": self.win_rate,
            "win_rate_pct": self.win_rate * 100,
            "profit_factor": self.profit_factor,
            "win_loss_ratio": self.win_loss_ratio,
            "volatility": self.volatility,
            "volatility_pct": self.volatility * 100,
            "calmar_ratio": self.calmar_ratio,
            "num_trades": self.num_trades,
            "avg_trade": self.avg_trade,
            "var_95": self.var_95,
            "var_95_pct": self.var_95 * 100,
            "cvar_95": self.cvar_95,
            "cvar_95_pct": self.cvar_95 * 100,
            "risk_level": self.risk_level.value,
            "risk_score": self.risk_score,
            "return_risk_ratio": self.return_risk_ratio,
        }

    @classmethod
    def from_backtest_result(
        cls,
        strategy_name: str,
        result: BacktestResult,
    ) -> ComparisonMetrics:
        """Create ComparisonMetrics from BacktestResult.

        Args:
            strategy_name: Name of the strategy
            result: Backtest result from analyzer

        Returns:
            ComparisonMetrics instance
        """
        from openclaw.backtest.analyzer import PerformanceAnalyzer

        analyzer = PerformanceAnalyzer()

        # Get basic data
        equity_curve = result.equity_curve
        trades = result.trades

        # Calculate returns series
        if len(equity_curve) > 1:
            returns = np.diff(np.array(equity_curve)) / np.array(equity_curve[:-1])
            volatility = float(np.std(returns, ddof=1) * np.sqrt(252))
        else:
            returns = np.array([])
            volatility = 0.0

        # Calculate VaR and CVaR
        if len(returns) > 0:
            var_95 = float(np.percentile(returns, 5))
            tail_returns = returns[returns <= var_95]
            cvar_95 = float(np.mean(tail_returns)) if len(tail_returns) > 0 else var_95
        else:
            var_95 = 0.0
            cvar_95 = 0.0

        # Calculate total return
        if len(equity_curve) >= 2:
            total_return = (equity_curve[-1] - equity_curve[0]) / equity_curve[0]
        else:
            total_return = 0.0

        # Calculate Sharpe ratio
        sharpe_ratio = analyzer.calculate_sharpe_ratio(returns) if len(returns) > 0 else 0.0

        # Calculate Sortino ratio
        sortino_ratio = analyzer.calculate_sortino_ratio(returns) if len(returns) > 0 else 0.0

        # Calculate drawdown stats
        drawdown_stats = analyzer.calculate_max_drawdown(equity_curve)
        max_drawdown = drawdown_stats["max_drawdown"]  # Already as positive decimal

        # Calculate Calmar ratio
        calmar_ratio = (
            analyzer.calculate_calmar_ratio(returns, max_drawdown)
            if len(returns) > 0 and max_drawdown > 0
            else 0.0
        )

        # Trade statistics
        num_trades = len(trades)
        win_rate = analyzer.calculate_win_rate(trades)
        profit_factor = analyzer.calculate_profit_factor(trades)
        avg_stats = analyzer.calculate_avg_trade(trades)

        # Calculate average trade P&L
        if trades:
            avg_trade = float(np.mean([t.pnl for t in trades]))
        else:
            avg_trade = 0.0

        return cls(
            strategy_name=strategy_name,
            total_return=total_return,
            annualized_return=total_return,  # Simplified - should use actual time period
            sharpe_ratio=sharpe_ratio,
            sortino_ratio=sortino_ratio,
            max_drawdown=max_drawdown,
            max_drawdown_duration=drawdown_stats.get("recovery_index", 0),
            win_rate=win_rate,
            profit_factor=profit_factor,
            win_loss_ratio=avg_stats["win_loss_ratio"],
            volatility=volatility,
            calmar_ratio=calmar_ratio,
            num_trades=num_trades,
            avg_trade=avg_trade,
            var_95=var_95,
            cvar_95=cvar_95,
        )


@dataclass
class MetricFilter:
    """Filter criteria for strategy selection.

    Attributes:
        min_sharpe: Minimum Sharpe ratio
        min_return: Minimum total return
        max_drawdown: Maximum allowed drawdown
        min_win_rate: Minimum win rate
        min_profit_factor: Minimum profit factor
        risk_levels: Allowed risk levels
        min_trades: Minimum number of trades
    """

    min_sharpe: float | None = None
    min_return: float | None = None
    max_drawdown: float | None = None
    min_win_rate: float | None = None
    min_profit_factor: float | None = None
    risk_levels: list[RiskLevel] | None = None
    min_trades: int | None = None

    def matches(self, metrics: ComparisonMetrics) -> bool:
        """Check if metrics match the filter criteria.

        Args:
            metrics: Strategy metrics to check

        Returns:
            True if all criteria are satisfied
        """
        if self.min_sharpe is not None and metrics.sharpe_ratio < self.min_sharpe:
            return False
        if self.min_return is not None and metrics.total_return < self.min_return:
            return False
        if self.max_drawdown is not None and metrics.max_drawdown > self.max_drawdown:
            return False
        if self.min_win_rate is not None and metrics.win_rate < self.min_win_rate:
            return False
        if self.min_profit_factor is not None and metrics.profit_factor < self.min_profit_factor:
            return False
        if self.risk_levels is not None and metrics.risk_level not in self.risk_levels:
            return False
        if self.min_trades is not None and metrics.num_trades < self.min_trades:
            return False
        return True


class MultiObjectiveOptimizer:
    """Multi-objective optimizer for strategy selection.

    Optimizes across multiple criteria using weighted scoring.
    """

    def __init__(
        self,
        weights: dict[str, float] | None = None,
    ) -> None:
        """Initialize optimizer with weights.

        Args:
            weights: Dictionary of metric names to weights.
                    Default balances return, risk, and consistency.
        """
        self.weights = weights or {
            "return": 0.25,
            "sharpe": 0.25,
            "drawdown": 0.20,
            "win_rate": 0.15,
            "profit_factor": 0.15,
        }

    def score(self, metrics: ComparisonMetrics) -> float:
        """Calculate weighted score for a strategy.

        Args:
            metrics: Strategy metrics

        Returns:
            Weighted score (higher is better)
        """
        score = 0.0

        # Normalize and weight each component
        if "return" in self.weights:
            # Normalize: 50% return = 100 score
            return_score = min(metrics.total_return / 0.5, 1.0) * 100
            score += self.weights["return"] * return_score

        if "sharpe" in self.weights:
            # Sharpe ratio: 2.0 = 100 score
            sharpe_score = min(max(metrics.sharpe_ratio, 0) / 2.0, 1.0) * 100
            score += self.weights["sharpe"] * sharpe_score

        if "drawdown" in self.weights:
            # Lower drawdown is better: 0% = 100 score
            dd_score = max(0, 1.0 - metrics.max_drawdown / 0.5) * 100
            score += self.weights["drawdown"] * dd_score

        if "win_rate" in self.weights:
            # Win rate: 70% = 100 score
            win_score = min(metrics.win_rate / 0.7, 1.0) * 100
            score += self.weights["win_rate"] * win_score

        if "profit_factor" in self.weights:
            # Profit factor: 2.0 = 100 score
            pf_score = min(max(metrics.profit_factor, 0) / 2.0, 1.0) * 100
            score += self.weights["profit_factor"] * pf_score

        if "calmar" in self.weights:
            # Calmar ratio: 3.0 = 100 score
            calmar_score = min(max(metrics.calmar_ratio, 0) / 3.0, 1.0) * 100
            score += self.weights["calmar"] * calmar_score

        return score

    def rank(
        self,
        metrics_list: list[ComparisonMetrics],
    ) -> list[tuple[ComparisonMetrics, float]]:
        """Rank strategies by weighted score.

        Args:
            metrics_list: List of strategy metrics

        Returns:
            List of (metrics, score) tuples sorted by score descending
        """
        scored = [(m, self.score(m)) for m in metrics_list]
        return sorted(scored, key=lambda x: x[1], reverse=True)

    def select_best(
        self,
        metrics_list: list[ComparisonMetrics],
        top_n: int = 3,
    ) -> list[tuple[ComparisonMetrics, float]]:
        """Select top N strategies by score.

        Args:
            metrics_list: List of strategy metrics
            top_n: Number of top strategies to return

        Returns:
            Top N (metrics, score) tuples
        """
        ranked = self.rank(metrics_list)
        return ranked[:top_n]
