# platform/deskclaw/experiment/stats.py
# ═══════════════════════════════════════════════════════════════
# Statistical Analysis — T-tests, effect sizes, confidence intervals
# ═══════════════════════════════════════════════════════════════

from __future__ import annotations

import math
from dataclasses import dataclass
from typing import Any


@dataclass(frozen=True)
class TTestResult:
    t_statistic: float
    p_value: float
    degrees_of_freedom: float
    significant: bool
    effect_size: float = 0.0

    def to_dict(self) -> dict[str, Any]:
        return {
            "t_statistic": round(self.t_statistic, 4),
            "p_value": round(self.p_value, 6),
            "degrees_of_freedom": round(self.degrees_of_freedom, 2),
            "significant": self.significant,
            "effect_size": round(self.effect_size, 4),
        }


def welch_t_test(a: list[float], b: list[float], alpha: float = 0.05) -> TTestResult:
    try:
        from scipy import stats as sp_stats

        result = sp_stats.ttest_ind(a, b, equal_var=False)
        t_stat = float(result.statistic)
        p_val = float(result.pvalue)
        n1, n2 = len(a), len(b)
        s1_sq = _variance(a)
        s2_sq = _variance(b)
        denom = s1_sq / n1 + s2_sq / n2
        if denom > 0:
            df = denom**2 / (
                (s1_sq / n1) ** 2 / (n1 - 1) + (s2_sq / n2) ** 2 / (n2 - 1)
            )
        else:
            df = float(n1 + n2 - 2)
    except ImportError:
        t_stat, p_val, df = _manual_welch_t(a, b)

    d = cohens_d(a, b)
    return TTestResult(
        t_statistic=t_stat,
        p_value=p_val,
        degrees_of_freedom=df,
        significant=p_val < alpha,
        effect_size=d,
    )


def cohens_d(a: list[float], b: list[float]) -> float:
    if len(a) < 2 or len(b) < 2:
        return 0.0
    mean_a = sum(a) / len(a)
    mean_b = sum(b) / len(b)
    s1 = _variance(a)
    s2 = _variance(b)
    pooled_std = math.sqrt(
        (s1 * (len(a) - 1) + s2 * (len(b) - 1)) / (len(a) + len(b) - 2)
    )
    if pooled_std == 0:
        return 0.0
    return (mean_a - mean_b) / pooled_std


def confidence_interval(
    data: list[float], confidence: float = 0.95
) -> tuple[float, float]:
    n = len(data)
    if n < 2:
        mean = data[0] if data else 0.0
        return (mean, mean)

    mean = sum(data) / n
    se = math.sqrt(_variance(data) / n)

    try:
        from scipy import stats as sp_stats

        t_crit = sp_stats.t.ppf((1 + confidence) / 2, df=n - 1)
    except ImportError:
        z_map = {0.90: 1.645, 0.95: 1.96, 0.99: 2.576}
        t_crit = z_map.get(confidence, 1.96)

    margin = t_crit * se
    return (mean - margin, mean + margin)


def pairwise_comparison(
    groups: dict[str, list[float]],
) -> dict[tuple[str, str], TTestResult]:
    names = sorted(groups.keys())
    results: dict[tuple[str, str], TTestResult] = {}
    for i, name_a in enumerate(names):
        for name_b in names[i + 1 :]:
            results[(name_a, name_b)] = welch_t_test(groups[name_a], groups[name_b])
    return results


def _variance(data: list[float]) -> float:
    n = len(data)
    if n < 2:
        return 0.0
    mean = sum(data) / n
    return sum((x - mean) ** 2 for x in data) / (n - 1)


def _manual_welch_t(a: list[float], b: list[float]) -> tuple[float, float, float]:
    n1, n2 = len(a), len(b)
    if n1 < 2 or n2 < 2:
        return (0.0, 1.0, 0.0)

    mean1 = sum(a) / n1
    mean2 = sum(b) / n2
    s1_sq = _variance(a)
    s2_sq = _variance(b)

    denom = math.sqrt(s1_sq / n1 + s2_sq / n2)
    if denom == 0:
        return (0.0, 1.0, float(n1 + n2 - 2))

    t_stat = (mean1 - mean2) / denom
    nu_num = (s1_sq / n1 + s2_sq / n2) ** 2
    nu_den = (s1_sq / n1) ** 2 / (n1 - 1) + (s2_sq / n2) ** 2 / (n2 - 1)
    df = nu_num / nu_den if nu_den > 0 else float(n1 + n2 - 2)

    try:
        from scipy import stats as sp_stats

        p_val = float(2 * sp_stats.t.sf(abs(t_stat), df))
    except ImportError:
        p_val = _approx_p_value(abs(t_stat), df)

    return (t_stat, p_val, df)


def _approx_p_value(t_abs: float, df: float) -> float:
    if df <= 0:
        return 1.0
    z = t_abs * (1 - 1 / (4 * max(df, 1)))
    return 2 * (1 - _norm_cdf(z))


def _norm_cdf(x: float) -> float:
    return 0.5 * (1 + math.erf(x / math.sqrt(2)))
