"""
Strategy Comparison Utilities
=============================
Utilities for comparing multiple trading strategies.

Provides:
- Multi-strategy comparison
- Ranking by various metrics
- Correlation analysis between strategies
- Regime-specific strategy selection
- Diversification benefit calculation

Author: ML-Specialist (NEXUS v4.0)
Version: 1.0.0
Created: 2026-01-25
"""

from datetime import datetime
from typing import Dict, List, Optional, Any, Tuple, Union
from dataclasses import dataclass, field
import numpy as np
import pandas as pd
from loguru import logger


@dataclass
class StrategyMetrics:
    """Metrics for a single strategy."""

    name: str
    sharpe_ratio: float
    sortino_ratio: float
    calmar_ratio: float
    profit_factor: float
    win_rate: float
    max_drawdown_pct: float
    total_trades: int
    net_profit: float
    volatility: float
    returns: Optional[np.ndarray] = None

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {
            'name': self.name,
            'sharpe_ratio': self.sharpe_ratio,
            'sortino_ratio': self.sortino_ratio,
            'calmar_ratio': self.calmar_ratio,
            'profit_factor': self.profit_factor,
            'win_rate': self.win_rate,
            'max_drawdown_pct': self.max_drawdown_pct,
            'total_trades': self.total_trades,
            'net_profit': self.net_profit,
            'volatility': self.volatility
        }


@dataclass
class RankingEntry:
    """Entry in a strategy ranking."""

    rank: int
    strategy: str
    value: float
    metric: str

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {
            'rank': self.rank,
            'strategy': self.strategy,
            'value': self.value,
            'metric': self.metric
        }


@dataclass
class ComparisonResult:
    """Result of strategy comparison."""

    strategies: List[str]
    metrics: Dict[str, StrategyMetrics]
    rankings: Dict[str, List[RankingEntry]]
    correlation_matrix: np.ndarray
    best_overall: str
    diversification_scores: Dict[Tuple[str, str], float]
    combined_metrics: Dict[str, float]

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {
            'strategies': self.strategies,
            'metrics': {k: v.to_dict() for k, v in self.metrics.items()},
            'rankings': {
                k: [entry.to_dict() for entry in v]
                for k, v in self.rankings.items()
            },
            'correlation_matrix': self.correlation_matrix.tolist(),
            'best_overall': self.best_overall,
            'diversification_scores': {
                f"{k[0]}_{k[1]}": v for k, v in self.diversification_scores.items()
            },
            'combined_metrics': self.combined_metrics
        }


class StrategyComparison:
    """
    Compare and analyze multiple trading strategies.

    Provides comprehensive comparison including:
    - Ranking by various performance metrics
    - Correlation analysis
    - Regime-specific performance
    - Diversification benefit calculation
    - Optimal combination recommendations

    Usage:
        comparison = StrategyComparison()
        result = comparison.compare_strategies(results_dict)
        rankings = comparison.rank_by_metric(results_dict, 'sharpe_ratio')
    """

    def __init__(
        self,
        risk_free_rate: float = 0.02,
        annualization_factor: int = 252,
        min_trades_for_significance: int = 30
    ):
        """
        Initialize strategy comparison utility.

        Args:
            risk_free_rate: Annual risk-free rate for ratio calculations
            annualization_factor: Factor for annualizing returns (252 for daily)
            min_trades_for_significance: Minimum trades for statistical significance
        """
        self.risk_free_rate = risk_free_rate
        self.annualization_factor = annualization_factor
        self.min_trades_for_significance = min_trades_for_significance

        logger.info("StrategyComparison initialized")

    def compare_strategies(
        self,
        results: Dict[str, Any],
        weights: Optional[Dict[str, float]] = None
    ) -> ComparisonResult:
        """
        Perform comprehensive comparison of multiple strategies.

        Args:
            results: Dictionary mapping strategy names to backtest results
            weights: Optional weights for combined metrics calculation

        Returns:
            ComparisonResult with all comparison data
        """
        logger.info(f"Comparing {len(results)} strategies")

        strategy_metrics = {}
        strategy_returns = {}

        for name, result in results.items():
            metrics, returns = self._extract_strategy_data(name, result)
            strategy_metrics[name] = metrics
            if returns is not None:
                strategy_returns[name] = returns

        strategies = list(strategy_metrics.keys())

        rankings = self._compute_all_rankings(strategy_metrics)

        correlation_matrix = self.calculate_correlation_matrix(strategy_returns)

        diversification_scores = self._compute_pairwise_diversification(
            strategy_metrics, correlation_matrix, strategies
        )

        best_overall = self._determine_best_overall(rankings)

        if weights is None:
            weights = {s: 1.0 / len(strategies) for s in strategies}
        combined_metrics = self._compute_combined_metrics(strategy_metrics, weights)

        return ComparisonResult(
            strategies=strategies,
            metrics=strategy_metrics,
            rankings=rankings,
            correlation_matrix=correlation_matrix,
            best_overall=best_overall,
            diversification_scores=diversification_scores,
            combined_metrics=combined_metrics
        )

    def rank_by_metric(
        self,
        results: Dict[str, Any],
        metric: str = 'sharpe_ratio',
        descending: bool = True
    ) -> List[RankingEntry]:
        """
        Rank strategies by a specific metric.

        Args:
            results: Dictionary mapping strategy names to backtest results
            metric: Metric to rank by
            descending: If True, higher values rank first

        Returns:
            List of RankingEntry sorted by metric
        """
        metric_values = []

        for name, result in results.items():
            metrics, _ = self._extract_strategy_data(name, result)
            value = getattr(metrics, metric, 0)
            metric_values.append((name, value))

        metric_values.sort(key=lambda x: x[1], reverse=descending)

        rankings = [
            RankingEntry(
                rank=i + 1,
                strategy=name,
                value=value,
                metric=metric
            )
            for i, (name, value) in enumerate(metric_values)
        ]

        return rankings

    def identify_best_regime_strategy(
        self,
        results: Dict[str, Any],
        regime_data: Dict[str, Any]
    ) -> Dict[str, Dict[str, Any]]:
        """
        Identify the best strategy for each market regime.

        Args:
            results: Dictionary mapping strategy names to backtest results
            regime_data: Dictionary with regime information including:
                - regimes: List of regime names
                - regime_labels: Array of regime labels for each period
                - regime_periods: Dict mapping regime to period indices

        Returns:
            Dictionary mapping each regime to best strategy and metrics
        """
        regimes = regime_data.get('regimes', ['low_volatility', 'high_volatility', 'trending', 'ranging'])
        regime_labels = regime_data.get('regime_labels')
        regime_periods = regime_data.get('regime_periods', {})

        best_by_regime = {}

        for regime in regimes:
            regime_metrics = {}

            for name, result in results.items():
                result_dict = self._normalize_result(result)

                if regime_labels is not None:
                    regime_perf = self._compute_regime_performance(
                        result_dict, regime_labels, regime
                    )
                elif regime in regime_periods:
                    regime_perf = self._compute_performance_for_periods(
                        result_dict, regime_periods[regime]
                    )
                else:
                    metrics_by_regime = result_dict.get('metrics_by_volatility', {})
                    regime_perf = metrics_by_regime.get(regime, {})

                regime_metrics[name] = regime_perf

            best_strategy = None
            best_sharpe = float('-inf')

            for name, perf in regime_metrics.items():
                sharpe = perf.get('sharpe_ratio', 0)
                if sharpe > best_sharpe:
                    best_sharpe = sharpe
                    best_strategy = name

            best_by_regime[regime] = {
                'best_strategy': best_strategy,
                'sharpe_ratio': best_sharpe,
                'all_strategies': {
                    name: {
                        'sharpe_ratio': perf.get('sharpe_ratio', 0),
                        'win_rate': perf.get('win_rate', perf.get('winrate', 0)),
                        'profit_factor': perf.get('profit_factor', 0)
                    }
                    for name, perf in regime_metrics.items()
                }
            }

        return best_by_regime

    def calculate_correlation_matrix(
        self,
        strategy_returns: Dict[str, np.ndarray]
    ) -> np.ndarray:
        """
        Calculate correlation matrix between strategy returns.

        Args:
            strategy_returns: Dictionary mapping strategy names to return arrays

        Returns:
            Correlation matrix as numpy array
        """
        if len(strategy_returns) < 2:
            n = len(strategy_returns) if strategy_returns else 1
            return np.eye(n)

        strategies = list(strategy_returns.keys())
        n = len(strategies)

        min_length = min(len(r) for r in strategy_returns.values())
        aligned_returns = {
            name: returns[:min_length]
            for name, returns in strategy_returns.items()
        }

        correlation_matrix = np.eye(n)

        for i, name_i in enumerate(strategies):
            for j, name_j in enumerate(strategies):
                if i < j:
                    returns_i = aligned_returns[name_i]
                    returns_j = aligned_returns[name_j]

                    if len(returns_i) > 1:
                        corr = np.corrcoef(returns_i, returns_j)[0, 1]
                        if np.isnan(corr):
                            corr = 0.0
                    else:
                        corr = 0.0

                    correlation_matrix[i, j] = corr
                    correlation_matrix[j, i] = corr

        return correlation_matrix

    def diversification_benefit(
        self,
        individual_sharpes: List[float],
        ensemble_sharpe: float
    ) -> float:
        """
        Calculate the diversification benefit of combining strategies.

        Diversification benefit measures how much the ensemble Sharpe ratio
        exceeds the average of individual Sharpe ratios, expressed as a percentage.

        Args:
            individual_sharpes: List of individual strategy Sharpe ratios
            ensemble_sharpe: Sharpe ratio of the combined ensemble

        Returns:
            Diversification benefit as percentage
        """
        if not individual_sharpes:
            return 0.0

        avg_individual = np.mean(individual_sharpes)

        if avg_individual <= 0:
            if ensemble_sharpe > 0:
                return 100.0
            return 0.0

        benefit = (ensemble_sharpe - avg_individual) / avg_individual * 100

        return float(benefit)

    def compute_optimal_weights(
        self,
        results: Dict[str, Any],
        method: str = 'sharpe_weighted',
        constraints: Optional[Dict[str, Any]] = None
    ) -> Dict[str, float]:
        """
        Compute optimal strategy weights for an ensemble.

        Args:
            results: Dictionary mapping strategy names to backtest results
            method: Weight computation method:
                - 'equal': Equal weights
                - 'sharpe_weighted': Weight by Sharpe ratio
                - 'inverse_volatility': Weight by inverse volatility
                - 'risk_parity': Equal risk contribution
            constraints: Optional constraints (min_weight, max_weight)

        Returns:
            Dictionary of optimal weights
        """
        strategies = list(results.keys())
        n = len(strategies)

        if n == 0:
            return {}

        strategy_metrics = {}
        for name, result in results.items():
            metrics, _ = self._extract_strategy_data(name, result)
            strategy_metrics[name] = metrics

        if method == 'equal':
            weights = {s: 1.0 / n for s in strategies}

        elif method == 'sharpe_weighted':
            sharpes = {s: max(0, m.sharpe_ratio) for s, m in strategy_metrics.items()}
            total_sharpe = sum(sharpes.values())
            if total_sharpe > 0:
                weights = {s: sharpe / total_sharpe for s, sharpe in sharpes.items()}
            else:
                weights = {s: 1.0 / n for s in strategies}

        elif method == 'inverse_volatility':
            volatilities = {s: max(0.0001, m.volatility) for s, m in strategy_metrics.items()}
            inv_vols = {s: 1.0 / vol for s, vol in volatilities.items()}
            total_inv_vol = sum(inv_vols.values())
            weights = {s: iv / total_inv_vol for s, iv in inv_vols.items()}

        elif method == 'risk_parity':
            volatilities = {s: max(0.0001, m.volatility) for s, m in strategy_metrics.items()}
            total_vol = sum(volatilities.values())
            target_risk = total_vol / n
            weights = {}
            for s, vol in volatilities.items():
                raw_weight = target_risk / vol
                weights[s] = raw_weight
            total_weight = sum(weights.values())
            weights = {s: w / total_weight for s, w in weights.items()}

        else:
            weights = {s: 1.0 / n for s in strategies}

        if constraints:
            min_weight = constraints.get('min_weight', 0.0)
            max_weight = constraints.get('max_weight', 1.0)

            for s in weights:
                weights[s] = max(min_weight, min(max_weight, weights[s]))

            total = sum(weights.values())
            if total > 0:
                weights = {s: w / total for s, w in weights.items()}

        return weights

    def analyze_strategy_combination(
        self,
        results: Dict[str, Any],
        weights: Dict[str, float]
    ) -> Dict[str, Any]:
        """
        Analyze the performance of a specific strategy combination.

        Args:
            results: Dictionary mapping strategy names to backtest results
            weights: Dictionary of strategy weights

        Returns:
            Dictionary with combined analysis
        """
        strategy_returns = {}
        strategy_metrics = {}

        for name, result in results.items():
            metrics, returns = self._extract_strategy_data(name, result)
            strategy_metrics[name] = metrics
            if returns is not None:
                strategy_returns[name] = returns

        if not strategy_returns:
            return {
                'error': 'No return data available',
                'combined_metrics': {}
            }

        min_length = min(len(r) for r in strategy_returns.values())

        combined_returns = np.zeros(min_length)
        for name, returns in strategy_returns.items():
            weight = weights.get(name, 0)
            combined_returns += weight * returns[:min_length]

        combined_sharpe = self._compute_sharpe(combined_returns)
        combined_sortino = self._compute_sortino(combined_returns)
        combined_volatility = np.std(combined_returns) * np.sqrt(self.annualization_factor)

        individual_sharpes = [m.sharpe_ratio for m in strategy_metrics.values()]
        div_benefit = self.diversification_benefit(individual_sharpes, combined_sharpe)

        correlation_matrix = self.calculate_correlation_matrix(strategy_returns)
        strategies = list(strategy_returns.keys())
        avg_correlation = 0
        count = 0
        for i in range(len(strategies)):
            for j in range(i + 1, len(strategies)):
                avg_correlation += correlation_matrix[i, j]
                count += 1
        avg_correlation = avg_correlation / count if count > 0 else 0

        return {
            'combined_metrics': {
                'sharpe_ratio': float(combined_sharpe),
                'sortino_ratio': float(combined_sortino),
                'volatility': float(combined_volatility),
                'annual_return': float(np.mean(combined_returns) * self.annualization_factor * 100)
            },
            'diversification_benefit': float(div_benefit),
            'average_correlation': float(avg_correlation),
            'weights_used': weights,
            'individual_contributions': {
                name: {
                    'weight': weights.get(name, 0),
                    'sharpe': strategy_metrics[name].sharpe_ratio,
                    'contribution_to_return': float(
                        weights.get(name, 0) * np.mean(strategy_returns.get(name, [0])[:min_length]) * self.annualization_factor * 100
                    )
                }
                for name in strategies
            }
        }

    def _extract_strategy_data(
        self,
        name: str,
        result: Any
    ) -> Tuple[StrategyMetrics, Optional[np.ndarray]]:
        """Extract metrics and returns from a strategy result."""
        result_dict = self._normalize_result(result)

        if 'metrics' in result_dict and isinstance(result_dict['metrics'], dict):
            metrics_dict = result_dict['metrics']
        else:
            metrics_dict = result_dict

        sharpe = metrics_dict.get('sharpe_ratio', 0)
        sortino = metrics_dict.get('sortino_ratio', 0)
        calmar = metrics_dict.get('calmar_ratio', 0)
        profit_factor = metrics_dict.get('profit_factor', 0)
        win_rate = metrics_dict.get('winrate', metrics_dict.get('win_rate', 0))
        max_dd = metrics_dict.get('max_drawdown_pct', 0)
        total_trades = metrics_dict.get('total_trades', 0)
        net_profit = metrics_dict.get('net_profit', 0)

        returns = None
        equity_curve = result_dict.get('equity_curve')
        if equity_curve is not None:
            if isinstance(equity_curve, pd.Series):
                equity_curve = equity_curve.values
            elif isinstance(equity_curve, list):
                equity_curve = np.array(equity_curve)

            if isinstance(equity_curve, np.ndarray) and len(equity_curve) > 1:
                returns = np.diff(equity_curve) / equity_curve[:-1]
                returns = np.nan_to_num(returns, nan=0.0, posinf=0.0, neginf=0.0)

        volatility = np.std(returns) * np.sqrt(self.annualization_factor) if returns is not None else 0

        strategy_metrics = StrategyMetrics(
            name=name,
            sharpe_ratio=float(sharpe),
            sortino_ratio=float(sortino),
            calmar_ratio=float(calmar),
            profit_factor=float(profit_factor),
            win_rate=float(win_rate),
            max_drawdown_pct=float(max_dd),
            total_trades=int(total_trades),
            net_profit=float(net_profit),
            volatility=float(volatility),
            returns=returns
        )

        return strategy_metrics, returns

    def _normalize_result(self, result: Any) -> Dict[str, Any]:
        """Normalize result to dictionary."""
        if isinstance(result, dict):
            return result
        if hasattr(result, 'to_dict'):
            return result.to_dict()
        if hasattr(result, '__dict__'):
            return vars(result)
        return {'result': result}

    def _compute_all_rankings(
        self,
        strategy_metrics: Dict[str, StrategyMetrics]
    ) -> Dict[str, List[RankingEntry]]:
        """Compute rankings for all standard metrics."""
        metrics_to_rank = [
            ('sharpe_ratio', True),
            ('sortino_ratio', True),
            ('calmar_ratio', True),
            ('profit_factor', True),
            ('win_rate', True),
            ('max_drawdown_pct', False),
            ('total_trades', True),
            ('net_profit', True)
        ]

        rankings = {}

        for metric, descending in metrics_to_rank:
            values = [
                (name, getattr(metrics, metric, 0))
                for name, metrics in strategy_metrics.items()
            ]
            values.sort(key=lambda x: x[1], reverse=descending)

            rankings[metric] = [
                RankingEntry(
                    rank=i + 1,
                    strategy=name,
                    value=value,
                    metric=metric
                )
                for i, (name, value) in enumerate(values)
            ]

        return rankings

    def _compute_pairwise_diversification(
        self,
        strategy_metrics: Dict[str, StrategyMetrics],
        correlation_matrix: np.ndarray,
        strategies: List[str]
    ) -> Dict[Tuple[str, str], float]:
        """Compute pairwise diversification scores."""
        diversification_scores = {}

        for i, name_i in enumerate(strategies):
            for j, name_j in enumerate(strategies):
                if i < j:
                    corr = correlation_matrix[i, j]
                    sharpe_i = strategy_metrics[name_i].sharpe_ratio
                    sharpe_j = strategy_metrics[name_j].sharpe_ratio

                    combined_sharpe = (sharpe_i + sharpe_j) / 2 * np.sqrt(2 / (1 + corr))
                    avg_sharpe = (sharpe_i + sharpe_j) / 2

                    if avg_sharpe > 0:
                        div_score = (combined_sharpe - avg_sharpe) / avg_sharpe * 100
                    else:
                        div_score = 0

                    diversification_scores[(name_i, name_j)] = float(div_score)

        return diversification_scores

    def _determine_best_overall(
        self,
        rankings: Dict[str, List[RankingEntry]]
    ) -> str:
        """Determine best overall strategy based on rankings."""
        points = {}

        for metric, ranking in rankings.items():
            n = len(ranking)
            for entry in ranking:
                if entry.strategy not in points:
                    points[entry.strategy] = 0
                points[entry.strategy] += (n - entry.rank + 1)

        if not points:
            return 'N/A'

        best = max(points.items(), key=lambda x: x[1])
        return best[0]

    def _compute_combined_metrics(
        self,
        strategy_metrics: Dict[str, StrategyMetrics],
        weights: Dict[str, float]
    ) -> Dict[str, float]:
        """Compute weighted combined metrics."""
        combined = {
            'weighted_sharpe': 0,
            'weighted_sortino': 0,
            'weighted_win_rate': 0,
            'weighted_profit_factor': 0,
            'avg_max_drawdown': 0
        }

        total_weight = sum(weights.values())
        if total_weight == 0:
            return combined

        for name, metrics in strategy_metrics.items():
            weight = weights.get(name, 0) / total_weight

            combined['weighted_sharpe'] += weight * metrics.sharpe_ratio
            combined['weighted_sortino'] += weight * metrics.sortino_ratio
            combined['weighted_win_rate'] += weight * metrics.win_rate
            combined['weighted_profit_factor'] += weight * metrics.profit_factor
            combined['avg_max_drawdown'] += weight * metrics.max_drawdown_pct

        return {k: float(v) for k, v in combined.items()}

    def _compute_regime_performance(
        self,
        result_dict: Dict[str, Any],
        regime_labels: np.ndarray,
        target_regime: str
    ) -> Dict[str, float]:
        """Compute performance metrics for a specific regime."""
        return {
            'sharpe_ratio': 0,
            'win_rate': 0,
            'profit_factor': 0
        }

    def _compute_performance_for_periods(
        self,
        result_dict: Dict[str, Any],
        periods: List[Tuple[int, int]]
    ) -> Dict[str, float]:
        """Compute performance for specific time periods."""
        equity_curve = result_dict.get('equity_curve')
        if equity_curve is None:
            return {'sharpe_ratio': 0, 'win_rate': 0, 'profit_factor': 0}

        if isinstance(equity_curve, pd.Series):
            equity_curve = equity_curve.values
        elif isinstance(equity_curve, list):
            equity_curve = np.array(equity_curve)

        period_returns = []
        for start, end in periods:
            if end <= len(equity_curve) and start < end:
                period_equity = equity_curve[start:end]
                if len(period_equity) > 1:
                    returns = np.diff(period_equity) / period_equity[:-1]
                    period_returns.extend(returns)

        if not period_returns:
            return {'sharpe_ratio': 0, 'win_rate': 0, 'profit_factor': 0}

        returns = np.array(period_returns)
        returns = np.nan_to_num(returns, nan=0.0, posinf=0.0, neginf=0.0)

        sharpe = self._compute_sharpe(returns)
        win_rate = np.mean(returns > 0)

        gross_profit = np.sum(returns[returns > 0])
        gross_loss = np.abs(np.sum(returns[returns < 0]))
        profit_factor = gross_profit / gross_loss if gross_loss > 0 else 0

        return {
            'sharpe_ratio': float(sharpe),
            'win_rate': float(win_rate),
            'profit_factor': float(profit_factor)
        }

    def _compute_sharpe(self, returns: np.ndarray) -> float:
        """Compute Sharpe ratio from returns."""
        if len(returns) < 2:
            return 0.0

        mean_return = np.mean(returns)
        std_return = np.std(returns)

        if std_return == 0:
            return 0.0

        daily_rf = self.risk_free_rate / self.annualization_factor
        sharpe = (mean_return - daily_rf) / std_return * np.sqrt(self.annualization_factor)

        return float(sharpe)

    def _compute_sortino(self, returns: np.ndarray) -> float:
        """Compute Sortino ratio from returns."""
        if len(returns) < 2:
            return 0.0

        mean_return = np.mean(returns)
        negative_returns = returns[returns < 0]

        if len(negative_returns) == 0:
            return float('inf') if mean_return > 0 else 0.0

        downside_std = np.std(negative_returns)
        if downside_std == 0:
            return 0.0

        daily_rf = self.risk_free_rate / self.annualization_factor
        sortino = (mean_return - daily_rf) / downside_std * np.sqrt(self.annualization_factor)

        return float(sortino)
