"""Optimization analysis for OpenClaw Trading.

This module provides tools for analyzing optimization results including
parameter sensitivity analysis, optimization curves, and overfitting detection.
"""

from dataclasses import dataclass
from typing import Any

import numpy as np

from openclaw.optimizer.base import OptimizationResult


@dataclass
class SensitivityAnalysis:
    """Result of parameter sensitivity analysis.

    Attributes:
        parameter_name: Name of the parameter analyzed
        values: Parameter values tested
        scores: Scores achieved for each value
        sensitivity_score: Overall sensitivity score (0-1)
        optimal_range: Recommended optimal range
    """

    parameter_name: str
    values: list[Any]
    scores: list[float]
    sensitivity_score: float
    optimal_range: tuple[float, float]


@dataclass
class OverfittingResult:
    """Result of overfitting analysis.

    Attributes:
        is_overfitted: Whether overfitting is detected
        train_score: Score on training data
        validation_score: Score on validation data
        overfitting_ratio: Ratio of train to validation performance
        severity: Overfitting severity ("none", "low", "medium", "high")
    """

    is_overfitted: bool
    train_score: float
    validation_score: float
    overfitting_ratio: float
    severity: str


class OptimizationAnalyzer:
    """Analyzer for optimization results.

    This class provides various analysis tools for understanding
    optimization results and detecting potential issues.
    """

    def __init__(self):
        """Initialize the analyzer."""
        from openclaw.utils.logging import get_logger

        self.logger = get_logger("optimizer.analyzer")

    def analyze_parameter_sensitivity(
        self,
        opt_result: OptimizationResult,
        parameter_name: str,
        n_bins: int = 10,
    ) -> SensitivityAnalysis:
        """Analyze sensitivity of results to a specific parameter.

        Args:
            opt_result: Optimization result
            parameter_name: Name of parameter to analyze
            n_bins: Number of bins for continuous parameters

        Returns:
            SensitivityAnalysis result
        """
        if not opt_result.all_results:
            raise ValueError("No results to analyze")

        # Extract parameter values and scores
        param_values = []
        scores = []

        for params, score, _ in opt_result.all_results:
            if parameter_name in params:
                param_values.append(params[parameter_name])
                scores.append(score)

        if not param_values:
            raise ValueError(f"Parameter '{parameter_name}' not found in results")

        # Determine if parameter is numeric
        is_numeric = all(isinstance(v, (int, float)) for v in param_values)

        if is_numeric:
            # Bin numeric values
            numeric_values = [float(v) for v in param_values]
            min_val, max_val = min(numeric_values), max(numeric_values)

            if min_val == max_val:
                # All values are the same
                sensitivity_score = 0.0
                optimal_range = (min_val, max_val)
            else:
                # Create bins and calculate mean score per bin
                bins = np.linspace(min_val, max_val, n_bins + 1)
                bin_centers = (bins[:-1] + bins[1:]) / 2
                bin_scores = [[] for _ in range(n_bins)]

                for val, score in zip(numeric_values, scores, strict=False):
                    bin_idx = min(
                        int((val - min_val) / (max_val - min_val) * n_bins),
                        n_bins - 1,
                    )
                    bin_scores[bin_idx].append(score)

                bin_mean_scores = [
                    np.mean(scores) if scores else 0.0 for scores in bin_scores
                ]

                # Calculate sensitivity as variance of scores across bins
                sensitivity_score = np.std(bin_mean_scores) / (
                    abs(np.mean(bin_mean_scores)) + 1e-10
                )

                # Find optimal range (bins with scores above 80th percentile)
                threshold = np.percentile(bin_mean_scores, 80)
                optimal_indices = [
                    i for i, s in enumerate(bin_mean_scores) if s >= threshold
                ]

                if optimal_indices:
                    optimal_range = (
                        float(bins[min(optimal_indices)]),
                        float(bins[max(optimal_indices) + 1]),
                    )
                else:
                    optimal_range = (min_val, max_val)

                values = bin_centers.tolist()
                scores = bin_mean_scores
        else:
            # Categorical parameter - group by value
            value_scores: dict[Any, list[float]] = {}
            for val, score in zip(param_values, scores, strict=False):
                if val not in value_scores:
                    value_scores[val] = []
                value_scores[val].append(score)

            values = list(value_scores.keys())
            scores = [np.mean(value_scores[v]) for v in values]

            # Sensitivity as variance across categories
            sensitivity_score = np.std(scores) / (abs(np.mean(scores)) + 1e-10)
            optimal_range = (0.0, 0.0)  # Not applicable for categorical

        return SensitivityAnalysis(
            parameter_name=parameter_name,
            values=values,
            scores=scores,
            sensitivity_score=min(1.0, sensitivity_score),
            optimal_range=optimal_range,
        )

    def detect_overfitting(
        self,
        train_result: OptimizationResult,
        validation_result: OptimizationResult,
        threshold: float = 0.2,
    ) -> OverfittingResult:
        """Detect overfitting by comparing training and validation performance.

        Args:
            train_result: Optimization result on training data
            validation_result: Optimization result on validation data
            threshold: Threshold for overfitting detection (0-1)

        Returns:
            OverfittingResult
        """
        train_score = train_result.best_score
        validation_score = validation_result.best_score

        if train_score == 0:
            overfitting_ratio = 0.0
        else:
            overfitting_ratio = (train_score - validation_score) / abs(train_score)

        # Determine severity
        if overfitting_ratio <= 0:
            severity = "none"
        elif overfitting_ratio < threshold / 2:
            severity = "low"
        elif overfitting_ratio < threshold:
            severity = "medium"
        else:
            severity = "high"

        is_overfitted = overfitting_ratio > threshold

        return OverfittingResult(
            is_overfitted=is_overfitted,
            train_score=train_score,
            validation_score=validation_score,
            overfitting_ratio=overfitting_ratio,
            severity=severity,
        )

    def get_optimization_curve(
        self, opt_result: OptimizationResult
    ) -> tuple[list[int], list[float]]:
        """Get the optimization curve (best score vs iteration).

        Args:
            opt_result: Optimization result

        Returns:
            Tuple of (iterations, best_scores)
        """
        if not opt_result.all_results:
            return [], []

        iterations = []
        best_scores = []
        current_best = float("-inf")

        for i, (_, score, _) in enumerate(opt_result.all_results):
            current_best = max(current_best, score)
            iterations.append(i)
            best_scores.append(current_best)

        return iterations, best_scores

    def get_convergence_rate(
        self, opt_result: OptimizationResult, window_size: int = 10
    ) -> float:
        """Calculate the convergence rate of optimization.

        Args:
            opt_result: Optimization result
            window_size: Window size for calculating rate

        Returns:
            Convergence rate (improvement per iteration)
        """
        if len(opt_result.all_results) < window_size * 2:
            return 0.0

        # Get best scores
        _, best_scores = self.get_optimization_curve(opt_result)

        if len(best_scores) < window_size * 2:
            return 0.0

        # Calculate improvement in first and last windows
        first_window = best_scores[:window_size]
        last_window = best_scores[-window_size:]

        first_improvement = max(first_window) - min(first_window)
        last_improvement = max(last_window) - min(last_window)

        # Convergence rate is the ratio of improvements
        if first_improvement == 0:
            return 0.0

        convergence_rate = last_improvement / first_improvement
        return convergence_rate

    def analyze_parameter_correlations(
        self, opt_result: OptimizationResult
    ) -> dict[tuple[str, str], float]:
        """Analyze correlations between parameters.

        Args:
            opt_result: Optimization result

        Returns:
            Dictionary mapping (param1, param2) to correlation coefficient
        """
        if not opt_result.all_results:
            return {}

        # Get all parameter names
        param_names = list(opt_result.all_results[0][0].keys())
        correlations = {}

        for i, name1 in enumerate(param_names):
            for name2 in param_names[i + 1 :]:
                values1 = []
                values2 = []

                for params, _, _ in opt_result.all_results:
                    val1 = params.get(name1)
                    val2 = params.get(name2)

                    # Only include numeric values
                    if isinstance(val1, (int, float)) and isinstance(
                        val2, (int, float)
                    ):
                        values1.append(float(val1))
                        values2.append(float(val2))

                if len(values1) > 1 and len(set(values1)) > 1 and len(set(values2)) > 1:
                    correlation = np.corrcoef(values1, values2)[0, 1]
                    correlations[(name1, name2)] = correlation

        return correlations

    def get_top_configurations(
        self,
        opt_result: OptimizationResult,
        n_top: int = 5,
    ) -> list[tuple[dict[str, Any], float]]:
        """Get top N parameter configurations.

        Args:
            opt_result: Optimization result
            n_top: Number of top configurations to return

        Returns:
            List of (params, score) tuples sorted by score
        """
        if not opt_result.all_results:
            return []

        # Sort by score
        sorted_results = sorted(
            opt_result.all_results, key=lambda x: x[1], reverse=True
        )

        # Return top N unique configurations
        top_configs = []
        seen = set()

        for params, score, _ in sorted_results:
            # Create a hashable representation
            param_tuple = tuple(sorted(params.items()))
            if param_tuple not in seen:
                seen.add(param_tuple)
                top_configs.append((params, score))

            if len(top_configs) >= n_top:
                break

        return top_configs

    def calculate_robustness_score(
        self,
        opt_result: OptimizationResult,
        n_bootstrap: int = 100,
    ) -> float:
        """Calculate robustness score using bootstrap sampling.

        Args:
            opt_result: Optimization result
            n_bootstrap: Number of bootstrap samples

        Returns:
            Robustness score (0-1, higher is more robust)
        """
        if len(opt_result.all_results) < 10:
            return 0.0

        scores = [r[1] for r in opt_result.all_results]

        # Bootstrap sampling
        bootstrap_means = []
        for _ in range(n_bootstrap):
            sample = np.random.choice(scores, size=len(scores), replace=True)
            bootstrap_means.append(np.mean(sample))

        # Robustness is inverse of coefficient of variation
        mean_score = np.mean(bootstrap_means)
        std_score = np.std(bootstrap_means)

        if mean_score == 0:
            return 0.0

        cv = abs(std_score / mean_score)
        robustness = 1.0 / (1.0 + cv)

        return min(1.0, robustness)

    def generate_report(self, opt_result: OptimizationResult) -> dict[str, Any]:
        """Generate a comprehensive analysis report.

        Args:
            opt_result: Optimization result

        Returns:
            Dictionary containing analysis report
        """
        if not opt_result.all_results:
            return {"error": "No results to analyze"}

        # Get parameter names
        param_names = list(opt_result.all_results[0][0].keys())

        # Sensitivity analysis for all parameters
        sensitivity_results = {}
        for param_name in param_names:
            try:
                sensitivity = self.analyze_parameter_sensitivity(
                    opt_result, param_name
                )
                sensitivity_results[param_name] = {
                    "sensitivity_score": sensitivity.sensitivity_score,
                    "optimal_range": sensitivity.optimal_range,
                }
            except Exception as e:
                self.logger.warning(f"Failed to analyze {param_name}: {e}")

        # Convergence rate
        convergence_rate = self.get_convergence_rate(opt_result)

        # Top configurations
        top_configs = self.get_top_configurations(opt_result, n_top=5)

        # Robustness score
        robustness = self.calculate_robustness_score(opt_result)

        # Parameter correlations
        correlations = self.analyze_parameter_correlations(opt_result)

        # Score statistics
        scores = [r[1] for r in opt_result.all_results]
        score_stats = {
            "mean": float(np.mean(scores)),
            "std": float(np.std(scores)),
            "min": float(np.min(scores)),
            "max": float(np.max(scores)),
            "median": float(np.median(scores)),
        }

        return {
            "best_params": opt_result.best_params,
            "best_score": opt_result.best_score,
            "n_iterations": opt_result.n_iterations,
            "optimization_time": opt_result.optimization_time,
            "converged": opt_result.converged,
            "convergence_rate": convergence_rate,
            "robustness_score": robustness,
            "score_statistics": score_stats,
            "parameter_sensitivity": sensitivity_results,
            "top_configurations": top_configs,
            "parameter_correlations": {
                f"{k[0]}_{k[1]}": v for k, v in correlations.items()
            },
        }
