"""Statistical tests for strategy comparison.

This module provides statistical hypothesis tests for comparing
trading strategy performance.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np
from numpy.typing import NDArray
from scipy import stats

if TYPE_CHECKING:
    from openclaw.backtest.analyzer import BacktestResult


class StatisticalTests:
    """Statistical hypothesis tests for strategy comparison."""

    def __init__(self, risk_free_rate: float = 0.02) -> None:
        """Initialize statistical tests.

        Args:
            risk_free_rate: Annual risk-free rate for Sharpe calculations
        """
        self.risk_free_rate = risk_free_rate

    def t_test(
        self,
        returns1: NDArray,
        returns2: NDArray,
        equal_var: bool = False,
    ) -> tuple[float, float]:
        """Perform two-sample t-test on strategy returns.

        Tests whether two strategies have significantly different mean returns.

        Args:
            returns1: Return series for strategy 1
            returns2: Return series for strategy 2
            equal_var: Whether to assume equal variances (Welch's t-test if False)

        Returns:
            Tuple of (t-statistic, p-value)
        """
        if len(returns1) == 0 or len(returns2) == 0:
            return 0.0, 1.0

        t_stat, p_value = stats.ttest_ind(
            returns1,
            returns2,
            equal_var=equal_var,
        )

        return float(t_stat), float(p_value)

    def paired_t_test(
        self,
        returns1: NDArray,
        returns2: NDArray,
    ) -> tuple[float, float]:
        """Perform paired t-test on strategy returns.

        Use when returns are matched (e.g., same time periods).

        Args:
            returns1: Return series for strategy 1
            returns2: Return series for strategy 2

        Returns:
            Tuple of (t-statistic, p-value)
        """
        if len(returns1) != len(returns2) or len(returns1) == 0:
            return 0.0, 1.0

        t_stat, p_value = stats.ttest_rel(returns1, returns2)

        return float(t_stat), float(p_value)

    def sharpe_difference_test(
        self,
        returns1: NDArray,
        returns2: NDArray,
    ) -> tuple[float, float]:
        """Test for significant difference in Sharpe ratios.

        Uses the Jobson-Korkie test with Memmel correction.

        Args:
            returns1: Return series for strategy 1
            returns2: Return series for strategy 2

        Returns:
            Tuple of (z-statistic, p-value)
        """
        if len(returns1) == 0 or len(returns2) == 0:
            return 0.0, 1.0

        # Calculate Sharpe ratios
        mean1, std1 = np.mean(returns1), np.std(returns1, ddof=1)
        mean2, std2 = np.mean(returns2), np.std(returns2, ddof=1)

        if std1 == 0 or std2 == 0:
            return 0.0, 1.0

        sharpe1 = (mean1 - self.risk_free_rate / 252) / std1
        sharpe2 = (mean2 - self.risk_free_rate / 252) / std2

        # Jobson-Korkie test statistic
        n = min(len(returns1), len(returns2))
        correlation = np.corrcoef(returns1[:n], returns2[:n])[0, 1] if n > 1 else 0

        # Variance of Sharpe ratio difference
        var_sharpe1 = (1 + 0.5 * sharpe1**2) / len(returns1)
        var_sharpe2 = (1 + 0.5 * sharpe2**2) / len(returns2)
        covariance = correlation / n + (sharpe1 * sharpe2 * correlation**2) / (2 * n)

        var_diff = var_sharpe1 + var_sharpe2 - 2 * covariance

        if var_diff <= 0:
            return 0.0, 1.0

        # Z-statistic
        z_stat = (sharpe1 - sharpe2) / np.sqrt(var_diff)
        p_value = 2 * (1 - stats.norm.cdf(abs(z_stat)))

        return float(z_stat), float(p_value)

    def mann_whitney_u_test(
        self,
        returns1: NDArray,
        returns2: NDArray,
    ) -> tuple[float, float]:
        """Perform Mann-Whitney U test (non-parametric alternative to t-test).

        Use when returns may not be normally distributed.

        Args:
            returns1: Return series for strategy 1
            returns2: Return series for strategy 2

        Returns:
            Tuple of (u-statistic, p-value)
        """
        if len(returns1) == 0 or len(returns2) == 0:
            return 0.0, 1.0

        try:
            u_stat, p_value = stats.mannwhitneyu(
                returns1,
                returns2,
                alternative='two-sided',
            )
            return float(u_stat), float(p_value)
        except ValueError:
            return 0.0, 1.0

    def kolmogorov_smirnov_test(
        self,
        returns1: NDArray,
        returns2: NDArray,
    ) -> tuple[float, float]:
        """Perform Kolmogorov-Smirnov test on return distributions.

        Tests whether two return series come from the same distribution.

        Args:
            returns1: Return series for strategy 1
            returns2: Return series for strategy 2

        Returns:
            Tuple of (ks-statistic, p-value)
        """
        if len(returns1) == 0 or len(returns2) == 0:
            return 0.0, 1.0

        ks_stat, p_value = stats.ks_2samp(returns1, returns2)

        return float(ks_stat), float(p_value)

    def levene_test(
        self,
        returns1: NDArray,
        returns2: NDArray,
    ) -> tuple[float, float]:
        """Perform Levene test for equal variances.

        Tests whether two strategies have significantly different volatility.

        Args:
            returns1: Return series for strategy 1
            returns2: Return series for strategy 2

        Returns:
            Tuple of (w-statistic, p-value)
        """
        if len(returns1) == 0 or len(returns2) == 0:
            return 0.0, 1.0

        w_stat, p_value = stats.levene(returns1, returns2)

        return float(w_stat), float(p_value)

    def jarque_bera_test(self, returns: NDArray) -> tuple[float, float]:
        """Perform Jarque-Bera test for normality.

        Args:
            returns: Return series to test

        Returns:
            Tuple of (jb-statistic, p-value)
        """
        if len(returns) < 2:
            return 0.0, 1.0

        jb_stat, p_value = stats.jarque_bera(returns)

        return float(jb_stat), float(p_value)

    def is_normal_distribution(self, returns: NDArray, alpha: float = 0.05) -> bool:
        """Check if returns follow normal distribution.

        Args:
            returns: Return series
            alpha: Significance level

        Returns:
            True if normally distributed, False otherwise
        """
        _, p_value = self.jarque_bera_test(returns)
        return p_value > alpha

    def calculate_confidence_interval(
        self,
        returns: NDArray,
        confidence: float = 0.95,
    ) -> tuple[float, float]:
        """Calculate confidence interval for mean return.

        Args:
            returns: Return series
            confidence: Confidence level (e.g., 0.95 for 95%)

        Returns:
            Tuple of (lower_bound, upper_bound)
        """
        if len(returns) == 0:
            return 0.0, 0.0

        mean = np.mean(returns)
        sem = stats.sem(returns)  # Standard error of mean

        if sem == 0:
            return mean, mean

        df = len(returns) - 1
        t_value = stats.t.ppf((1 + confidence) / 2, df)
        margin = t_value * sem

        return float(mean - margin), float(mean + margin)

    def omega_ratio(
        self,
        returns: NDArray,
        threshold: float = 0.0,
    ) -> float:
        """Calculate Omega ratio.

        The Omega ratio is the ratio of gains above a threshold to losses below it.

        Args:
            returns: Return series
            threshold: Return threshold (default 0 for risk-free rate adjustment)

        Returns:
            Omega ratio
        """
        if len(returns) == 0:
            return 0.0

        excess_returns = returns - threshold
        gains = np.sum(excess_returns[excess_returns > 0])
        losses = abs(np.sum(excess_returns[excess_returns < 0]))

        return float(gains / losses) if losses > 0 else float('inf')

    def calculate_drawdown_statistics(
        self,
        equity_curve: NDArray,
    ) -> dict[str, float]:
        """Calculate drawdown statistics.

        Args:
            equity_curve: Equity curve array

        Returns:
            Dictionary of drawdown statistics
        """
        if len(equity_curve) == 0:
            return {
                "max_drawdown": 0.0,
                "avg_drawdown": 0.0,
                "drawdown_std": 0.0,
                "max_drawdown_duration": 0,
                "avg_drawdown_duration": 0.0,
            }

        # Calculate running maximum
        running_max = np.maximum.accumulate(equity_curve)

        # Calculate drawdowns
        drawdowns = (equity_curve - running_max) / running_max

        # Find drawdown periods
        in_drawdown = drawdowns < 0
        drawdown_periods = []
        current_start = None

        for i, is_dd in enumerate(in_drawdown):
            if is_dd and current_start is None:
                current_start = i
            elif not is_dd and current_start is not None:
                drawdown_periods.append((current_start, i))
                current_start = None

        if current_start is not None:
            drawdown_periods.append((current_start, len(drawdowns)))

        # Calculate statistics
        max_drawdown = float(np.min(drawdowns))
        avg_drawdown = float(np.mean(drawdowns[drawdowns < 0])) if np.any(drawdowns < 0) else 0.0
        drawdown_std = float(np.std(drawdowns[drawdowns < 0])) if np.any(drawdowns < 0) else 0.0

        # Drawdown durations
        durations = [end - start for start, end in drawdown_periods]
        max_duration = max(durations) if durations else 0
        avg_duration = float(np.mean(durations)) if durations else 0.0

        return {
            "max_drawdown": max_drawdown,
            "avg_drawdown": avg_drawdown,
            "drawdown_std": drawdown_std,
            "max_drawdown_duration": max_duration,
            "avg_drawdown_duration": avg_duration,
        }

    def compare_drawdowns(
        self,
        equity1: NDArray,
        equity2: NDArray,
    ) -> dict[str, float]:
        """Compare drawdown characteristics between two strategies.

        Args:
            equity1: Equity curve for strategy 1
            equity2: Equity curve for strategy 2

        Returns:
            Dictionary of comparison metrics
        """
        dd1 = self.calculate_drawdown_statistics(equity1)
        dd2 = self.calculate_drawdown_statistics(equity2)

        return {
            "max_dd_diff": dd1["max_drawdown"] - dd2["max_drawdown"],
            "avg_dd_diff": dd1["avg_drawdown"] - dd2["avg_drawdown"],
            "dd_duration_diff": dd1["avg_drawdown_duration"] - dd2["avg_drawdown_duration"],
            "max_dd_ratio": (
                dd1["max_drawdown"] / dd2["max_drawdown"]
                if dd2["max_drawdown"] != 0 else float('inf')
            ),
        }

    def calculate_information_ratio(
        self,
        returns: NDArray,
        benchmark_returns: NDArray,
    ) -> float:
        """Calculate Information Ratio.

        Measures active return per unit of active risk.

        Args:
            returns: Strategy returns
            benchmark_returns: Benchmark returns

        Returns:
            Information ratio
        """
        if len(returns) == 0 or len(benchmark_returns) == 0:
            return 0.0

        min_len = min(len(returns), len(benchmark_returns))
        active_returns = returns[:min_len] - benchmark_returns[:min_len]

        mean_active = np.mean(active_returns)
        tracking_error = np.std(active_returns, ddof=1)

        return float(mean_active / tracking_error) if tracking_error > 0 else 0.0

    def calculate_beta(
        self,
        returns: NDArray,
        market_returns: NDArray,
    ) -> float:
        """Calculate beta relative to market.

        Args:
            returns: Strategy returns
            market_returns: Market returns

        Returns:
            Beta value
        """
        if len(returns) == 0 or len(market_returns) == 0:
            return 1.0

        min_len = min(len(returns), len(market_returns))
        returns = returns[:min_len]
        market_returns = market_returns[:min_len]

        market_var = np.var(market_returns, ddof=1)

        if market_var == 0:
            return 1.0

        covariance = np.cov(returns, market_returns)[0, 1]

        return float(covariance / market_var)

    def calculate_alpha(
        self,
        returns: NDArray,
        market_returns: NDArray,
        risk_free_rate: float | None = None,
    ) -> float:
        """Calculate Jensen's Alpha.

        Args:
            returns: Strategy returns
            market_returns: Market returns
            risk_free_rate: Risk-free rate (uses instance default if None)

        Returns:
            Alpha value
        """
        if len(returns) == 0 or len(market_returns) == 0:
            return 0.0

        rf = risk_free_rate if risk_free_rate is not None else self.risk_free_rate / 252

        beta = self.calculate_beta(returns, market_returns)
        mean_return = np.mean(returns)
        mean_market = np.mean(market_returns)

        alpha = mean_return - (rf + beta * (mean_market - rf))

        return float(alpha)
