"""Strategy comparator for multi-strategy backtest comparison.

This module provides the StrategyComparator class for running parallel
backtests and comparing multiple trading strategies.
"""

from __future__ import annotations

import concurrent.futures
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Callable

import numpy as np
from numpy.typing import NDArray

from openclaw.comparison.metrics import ComparisonMetrics, MetricFilter, MultiObjectiveOptimizer
from openclaw.comparison.statistical_tests import StatisticalTests

if TYPE_CHECKING:
    from openclaw.backtest.analyzer import BacktestResult
    from openclaw.backtest.engine import BacktestEngine


@dataclass
class ComparisonResult:
    """Result of comparing multiple strategies.

    Attributes:
        metrics: List of comparison metrics for each strategy
        best_strategy: Name of the best performing strategy
        rankings: Dictionary of metric names to sorted strategy names
        statistical_tests: Results of statistical significance tests
        recommendations: List of strategy recommendations
    """

    metrics: list[ComparisonMetrics] = field(default_factory=list)
    best_strategy: str = ""
    rankings: dict[str, list[str]] = field(default_factory=dict)
    statistical_tests: dict[str, dict] = field(default_factory=dict)
    recommendations: list[str] = field(default_factory=list)

    def get_metric(self, strategy_name: str) -> ComparisonMetrics | None:
        """Get metrics for a specific strategy.

        Args:
            strategy_name: Name of the strategy

        Returns:
            ComparisonMetrics if found, None otherwise
        """
        for metric in self.metrics:
            if metric.strategy_name == strategy_name:
                return metric
        return None

    def get_top_strategies(self, n: int = 3) -> list[ComparisonMetrics]:
        """Get top N strategies by total return.

        Args:
            n: Number of top strategies to return

        Returns:
            List of top N comparison metrics
        """
        sorted_metrics = sorted(
            self.metrics,
            key=lambda m: m.total_return,
            reverse=True,
        )
        return sorted_metrics[:n]

    def to_dict(self) -> dict:
        """Convert comparison result to dictionary."""
        return {
            "metrics": [m.to_dict() for m in self.metrics],
            "best_strategy": self.best_strategy,
            "rankings": self.rankings,
            "statistical_tests": self.statistical_tests,
            "recommendations": self.recommendations,
        }


class StrategyComparator:
    """Comparator for multiple trading strategies.

    Runs parallel backtests and provides comprehensive comparison
    of strategy performance.
    """

    def __init__(
        self,
        engine_factory: Callable[[], BacktestEngine],
        max_workers: int = 4,
    ) -> None:
        """Initialize strategy comparator.

        Args:
            engine_factory: Factory function that creates BacktestEngine instances
            max_workers: Maximum number of parallel workers for backtesting
        """
        self.engine_factory = engine_factory
        self.max_workers = max_workers
        self.statistical_tests = StatisticalTests()
        self.optimizer = MultiObjectiveOptimizer()

    def compare(
        self,
        strategies: dict[str, Callable],
        data: NDArray,
        initial_capital: float = 100000.0,
        commission: float = 0.001,
        slippage: float = 0.0005,
    ) -> ComparisonResult:
        """Compare multiple strategies with parallel backtesting.

        Args:
            strategies: Dictionary mapping strategy names to strategy functions
            data: Market data for backtesting
            initial_capital: Initial capital for backtest
            commission: Commission rate per trade
            slippage: Slippage rate per trade

        Returns:
            ComparisonResult with all comparison data
        """
        # Run parallel backtests
        backtest_results = self._run_parallel_backtests(
            strategies=strategies,
            data=data,
            initial_capital=initial_capital,
            commission=commission,
            slippage=slippage,
        )

        # Convert to comparison metrics
        metrics = []
        for name, result in backtest_results.items():
            metric = ComparisonMetrics.from_backtest_result(name, result)
            metrics.append(metric)

        # Calculate rankings across different metrics
        rankings = self._calculate_rankings(metrics)

        # Run statistical tests
        statistical_tests = self._run_statistical_tests(backtest_results, metrics)

        # Determine best strategy using multi-objective optimization
        best_strategy = self._select_best_strategy(metrics)

        # Generate recommendations
        recommendations = self._generate_recommendations(metrics, rankings)

        return ComparisonResult(
            metrics=metrics,
            best_strategy=best_strategy,
            rankings=rankings,
            statistical_tests=statistical_tests,
            recommendations=recommendations,
        )

    def _run_parallel_backtests(
        self,
        strategies: dict[str, Callable],
        data: NDArray,
        initial_capital: float,
        commission: float,
        slippage: float,
    ) -> dict[str, BacktestResult]:
        """Run backtests in parallel for all strategies.

        Args:
            strategies: Dictionary of strategy names to functions
            data: Market data
            initial_capital: Initial capital
            commission: Commission rate
            slippage: Slippage rate

        Returns:
            Dictionary mapping strategy names to backtest results
        """
        results: dict[str, BacktestResult] = {}

        with concurrent.futures.ThreadPoolExecutor(
            max_workers=self.max_workers
        ) as executor:
            # Submit all backtest tasks
            future_to_name = {}
            for name, strategy_fn in strategies.items():
                future = executor.submit(
                    self._run_single_backtest,
                    name,
                    strategy_fn,
                    data,
                    initial_capital,
                    commission,
                    slippage,
                )
                future_to_name[future] = name

            # Collect results as they complete
            for future in concurrent.futures.as_completed(future_to_name):
                name = future_to_name[future]
                try:
                    result = future.result()
                    results[name] = result
                except Exception as e:
                    print(f"Backtest failed for strategy {name}: {e}")
                    # Create empty result for failed backtest
                    results[name] = self._create_empty_result(initial_capital)

        return results

    def _run_single_backtest(
        self,
        name: str,
        strategy_fn: Callable,
        data: NDArray,
        initial_capital: float,
        commission: float,
        slippage: float,
    ) -> BacktestResult:
        """Run a single backtest.

        Args:
            name: Strategy name
            strategy_fn: Strategy function
            data: Market data
            initial_capital: Initial capital
            commission: Commission rate
            slippage: Slippage rate

        Returns:
            BacktestResult
        """
        engine = self.engine_factory()
        # Note: This assumes the engine has methods to configure and run
        # Actual implementation depends on BacktestEngine interface
        result = engine.run(
            strategy=strategy_fn,
            data=data,
            initial_capital=initial_capital,
            commission=commission,
            slippage=slippage,
        )
        return result

    def _create_empty_result(self, initial_capital: float) -> BacktestResult:
        """Create an empty result for failed backtests.

        Args:
            initial_capital: Initial capital value

        Returns:
            Empty BacktestResult
        """
        from datetime import datetime

        from openclaw.backtest.analyzer import BacktestResult

        now = datetime.now()
        return BacktestResult(
            initial_capital=initial_capital,
            final_capital=initial_capital,
            equity_curve=[initial_capital],
            timestamps=[now],
            trades=[],
            start_time=now,
            end_time=now,
        )

    def _calculate_rankings(
        self,
        metrics: list[ComparisonMetrics],
    ) -> dict[str, list[str]]:
        """Calculate rankings across different metrics.

        Args:
            metrics: List of comparison metrics

        Returns:
            Dictionary mapping metric names to sorted strategy names
        """
        rankings: dict[str, list[str]] = {}

        # Rank by total return (higher is better)
        rankings["total_return"] = [
            m.strategy_name
            for m in sorted(metrics, key=lambda x: x.total_return, reverse=True)
        ]

        # Rank by Sharpe ratio (higher is better)
        rankings["sharpe_ratio"] = [
            m.strategy_name
            for m in sorted(metrics, key=lambda x: x.sharpe_ratio, reverse=True)
        ]

        # Rank by max drawdown (lower is better)
        rankings["max_drawdown"] = [
            m.strategy_name
            for m in sorted(metrics, key=lambda x: x.max_drawdown, reverse=False)
        ]

        # Rank by win rate (higher is better)
        rankings["win_rate"] = [
            m.strategy_name
            for m in sorted(metrics, key=lambda x: x.win_rate, reverse=True)
        ]

        # Rank by profit factor (higher is better)
        rankings["profit_factor"] = [
            m.strategy_name
            for m in sorted(metrics, key=lambda x: x.profit_factor, reverse=True)
        ]

        # Rank by risk-adjusted return
        rankings["risk_adjusted"] = [
            m.strategy_name
            for m in sorted(metrics, key=lambda x: x.return_risk_ratio, reverse=True)
        ]

        return rankings

    def _run_statistical_tests(
        self,
        backtest_results: dict[str, BacktestResult],
        metrics: list[ComparisonMetrics],
    ) -> dict[str, dict]:
        """Run statistical tests on strategy performance.

        Args:
            backtest_results: Dictionary of backtest results
            metrics: List of comparison metrics

        Returns:
            Dictionary of test results
        """
        tests: dict[str, dict] = {}

        if len(metrics) < 2:
            return tests

        # Get returns series for each strategy
        returns_series = {}
        for name, result in backtest_results.items():
            if len(result.equity_curve) > 1:
                returns = np.diff(result.equity_curve) / np.array(result.equity_curve[:-1])
                returns_series[name] = returns

        # Pairwise t-tests between strategies
        strategy_names = list(returns_series.keys())
        for i in range(len(strategy_names)):
            for j in range(i + 1, len(strategy_names)):
                name1, name2 = strategy_names[i], strategy_names[j]
                returns1, returns2 = returns_series[name1], returns_series[name2]

                t_stat, p_value = self.statistical_tests.t_test(returns1, returns2)
                test_key = f"t_test_{name1}_vs_{name2}"
                tests[test_key] = {
                    "strategy1": name1,
                    "strategy2": name2,
                    "t_statistic": float(t_stat),
                    "p_value": float(p_value),
                    "significant": p_value < 0.05,
                }

        # Sharpe ratio comparison for top 2 strategies by Sharpe
        sorted_by_sharpe = sorted(metrics, key=lambda x: x.sharpe_ratio, reverse=True)
        if len(sorted_by_sharpe) >= 2:
            top1, top2 = sorted_by_sharpe[0], sorted_by_sharpe[1]
            if top1.strategy_name in returns_series and top2.strategy_name in returns_series:
                sharpe_diff, sharpe_p = self.statistical_tests.sharpe_difference_test(
                    returns_series[top1.strategy_name],
                    returns_series[top2.strategy_name],
                )
                tests["sharpe_comparison"] = {
                    "strategy1": top1.strategy_name,
                    "strategy2": top2.strategy_name,
                    "sharpe1": top1.sharpe_ratio,
                    "sharpe2": top2.sharpe_ratio,
                    "difference": float(sharpe_diff),
                    "p_value": float(sharpe_p),
                    "significant": sharpe_p < 0.05,
                }

        return tests

    def _select_best_strategy(self, metrics: list[ComparisonMetrics]) -> str:
        """Select the best strategy using multi-objective optimization.

        Args:
            metrics: List of comparison metrics

        Returns:
            Name of the best strategy
        """
        if not metrics:
            return ""

        ranked = self.optimizer.rank(metrics)
        return ranked[0][0].strategy_name if ranked else ""

    def _generate_recommendations(
        self,
        metrics: list[ComparisonMetrics],
        rankings: dict[str, list[str]],
    ) -> list[str]:
        """Generate strategy recommendations based on comparison.

        Args:
            metrics: List of comparison metrics
            rankings: Dictionary of rankings

        Returns:
            List of recommendation strings
        """
        recommendations = []

        if not metrics:
            return recommendations

        # Find best strategies by different criteria
        best_return = max(metrics, key=lambda x: x.total_return)
        best_sharpe = max(metrics, key=lambda x: x.sharpe_ratio)
        lowest_drawdown = min(metrics, key=lambda x: x.max_drawdown)
        best_risk_adjusted = max(metrics, key=lambda x: x.return_risk_ratio)

        # Overall best
        recommendations.append(
            f"综合表现最佳: {best_risk_adjusted.strategy_name} "
            f"(收益风险比: {best_risk_adjusted.return_risk_ratio:.2f})"
        )

        # Highest return
        recommendations.append(
            f"最高收益: {best_return.strategy_name} "
            f"(总收益: {best_return.total_return * 100:.2f}%)"
        )

        # Best risk-adjusted
        if best_sharpe.strategy_name != best_risk_adjusted.strategy_name:
            recommendations.append(
                f"最佳风险调整收益: {best_sharpe.strategy_name} "
                f"(夏普比率: {best_sharpe.sharpe_ratio:.2f})"
            )

        # Lowest risk
        recommendations.append(
            f"最低风险: {lowest_drawdown.strategy_name} "
            f"(最大回撤: {lowest_drawdown.max_drawdown * 100:.2f}%)"
        )

        # Risk level recommendations
        conservative = [m for m in metrics if m.risk_level.value == "conservative"]
        if conservative:
            best_conservative = max(conservative, key=lambda x: x.total_return)
            recommendations.append(
                f"保守型推荐: {best_conservative.strategy_name} "
                f"(收益: {best_conservative.total_return * 100:.2f}%)"
            )

        return recommendations

    def filter_strategies(
        self,
        metrics: list[ComparisonMetrics],
        filter_criteria: MetricFilter,
    ) -> list[ComparisonMetrics]:
        """Filter strategies based on criteria.

        Args:
            metrics: List of comparison metrics
            filter_criteria: Filter criteria

        Returns:
            Filtered list of metrics
        """
        return [m for m in metrics if filter_criteria.matches(m)]

    def compare_with_baseline(
        self,
        strategies: dict[str, Callable],
        baseline_strategy: str,
        data: NDArray,
        initial_capital: float = 100000.0,
    ) -> ComparisonResult:
        """Compare strategies against a baseline strategy.

        Args:
            strategies: Dictionary of strategy names to functions
            baseline_strategy: Name of the baseline strategy to compare against
            data: Market data
            initial_capital: Initial capital

        Returns:
            ComparisonResult with baseline comparison
        """
        result = self.compare(strategies, data, initial_capital)

        # Add baseline comparison metrics
        baseline_metric = result.get_metric(baseline_strategy)
        if baseline_metric:
            for metric in result.metrics:
                if metric.strategy_name != baseline_strategy:
                    # Calculate relative performance
                    relative_return = (
                        metric.total_return - baseline_metric.total_return
                    ) / abs(baseline_metric.total_return) if baseline_metric.total_return != 0 else 0
                    metric.relative_return = relative_return  # type: ignore

        return result
