"""Effect size calculations for model comparisons."""
from __future__ import annotations

import logging
from dataclasses import dataclass, field
from typing import Any, Dict, Optional, Tuple

import numpy as np
from scipy import stats

logger = logging.getLogger(__name__)


@dataclass
class EffectSizeResult:
    """Result of effect size calculation.

    Args:
        name: Name of the effect size measure.
        value: Effect size value.
        ci: Confidence interval (lower, upper).
        interpretation: Qualitative interpretation.
        details: Additional information.
    """

    name: str
    value: float
    ci: Optional[Tuple[float, float]]
    interpretation: str
    details: Dict[str, Any] = field(default_factory=dict)

    def __post_init__(self) -> None:
        if self.details is None:
            self.details = {}

    def __str__(self) -> str:
        if self.ci:
            return (
                f"{self.name}={self.value:.3f} "
                f"[{self.ci[0]:.3f}, {self.ci[1]:.3f}] ({self.interpretation})"
            )
        return f"{self.name}={self.value:.3f} ({self.interpretation})"


def cohens_d(
    values_a: np.ndarray,
    values_b: np.ndarray,
    paired: bool = True,
    confidence_level: float = 0.95,
) -> EffectSizeResult:
    """Compute Cohen's d effect size.

    Args:
        values_a: Scores from model A.
        values_b: Scores from model B.
        paired: Whether samples are paired.
        confidence_level: For CI calculation.

    Returns:
        EffectSizeResult with Cohen's d value.
    """
    values_a = np.asarray(values_a)
    values_b = np.asarray(values_b)

    mean_a = float(np.mean(values_a))
    mean_b = float(np.mean(values_b))
    mean_diff = mean_a - mean_b

    if paired:
        diff = values_a - values_b
        sd = float(np.std(diff, ddof=1))
        n = len(diff)
    else:
        n_a = len(values_a)
        n_b = len(values_b)
        var_a = float(np.var(values_a, ddof=1))
        var_b = float(np.var(values_b, ddof=1))
        sd = float(np.sqrt(((n_a - 1) * var_a + (n_b - 1) * var_b) / (n_a + n_b - 2)))
        n = n_a + n_b

    if sd == 0:
        d = 0.0 if mean_diff == 0 else float(np.sign(mean_diff)) * float("inf")
    else:
        d = mean_diff / sd

    ci = _cohens_d_ci(d, n, confidence_level, paired)
    interpretation = _interpret_cohens_d(abs(d))

    return EffectSizeResult(
        name="cohens_d",
        value=float(d),
        ci=ci,
        interpretation=interpretation,
        details={
            "mean_a": mean_a,
            "mean_b": mean_b,
            "mean_difference": mean_diff,
            "pooled_sd": sd,
            "paired": paired,
        },
    )


def _cohens_d_ci(
    d: float,
    n: int,
    confidence_level: float,
    paired: bool,
) -> Tuple[float, float]:
    """Compute CI for Cohen's d."""
    if not np.isfinite(d):
        return (float("-inf"), float("inf"))

    if paired:
        se = float(np.sqrt(1 / n + d**2 / (2 * n)))
    else:
        se = float(np.sqrt(2 / n + d**2 / (2 * n)))

    alpha = 1 - confidence_level
    z = float(stats.norm.ppf(1 - alpha / 2))

    return (d - z * se, d + z * se)


def _interpret_cohens_d(d: float) -> str:
    """Interpret Cohen's d magnitude."""
    if d < 0.2:
        return "negligible"
    elif d < 0.5:
        return "small"
    elif d < 0.8:
        return "medium"
    else:
        return "large"


def hedges_g(
    values_a: np.ndarray,
    values_b: np.ndarray,
    confidence_level: float = 0.95,
) -> EffectSizeResult:
    """Compute Hedges' g (small-sample corrected Cohen's d).

    Args:
        values_a: Scores from model A.
        values_b: Scores from model B.
        confidence_level: For CI calculation.

    Returns:
        EffectSizeResult with Hedges' g value.
    """
    values_a = np.asarray(values_a)
    values_b = np.asarray(values_b)

    d_result = cohens_d(values_a, values_b, paired=False, confidence_level=confidence_level)
    d = d_result.value

    n_a = len(values_a)
    n_b = len(values_b)
    df = n_a + n_b - 2

    correction = 1 - 3 / (4 * df - 1) if df > 0 else 1.0
    g = d * correction

    ci = None
    if d_result.ci:
        ci = (d_result.ci[0] * correction, d_result.ci[1] * correction)

    return EffectSizeResult(
        name="hedges_g",
        value=float(g),
        ci=ci,
        interpretation=_interpret_cohens_d(abs(g)),
        details={
            "cohens_d": float(d),
            "correction_factor": float(correction),
            "df": df,
        },
    )


def odds_ratio(
    correct_a: np.ndarray,
    correct_b: np.ndarray,
    confidence_level: float = 0.95,
) -> EffectSizeResult:
    """Compute odds ratio for paired binary outcomes.

    Args:
        correct_a: Boolean array - True where model A correct.
        correct_b: Boolean array - True where model B correct.
        confidence_level: For CI calculation.

    Returns:
        EffectSizeResult with odds ratio.
    """
    correct_a = np.asarray(correct_a, dtype=bool)
    correct_b = np.asarray(correct_b, dtype=bool)

    b01 = int(np.sum(correct_a & ~correct_b))
    b10 = int(np.sum(~correct_a & correct_b))

    if b10 == 0 and b01 == 0:
        return EffectSizeResult(
            name="odds_ratio", value=1.0, ci=(1.0, 1.0),
            interpretation="no difference",
            details={"a_only_correct": b01, "b_only_correct": b10},
        )
    elif b10 == 0:
        return EffectSizeResult(
            name="odds_ratio", value=float("inf"), ci=None,
            interpretation="A always wins on discordant",
            details={"a_only_correct": b01, "b_only_correct": b10},
        )
    elif b01 == 0:
        return EffectSizeResult(
            name="odds_ratio", value=0.0, ci=None,
            interpretation="B always wins on discordant",
            details={"a_only_correct": b01, "b_only_correct": b10},
        )

    or_value = b01 / b10
    log_or = float(np.log(or_value))
    se_log = float(np.sqrt(1 / b01 + 1 / b10))

    alpha = 1 - confidence_level
    z = float(stats.norm.ppf(1 - alpha / 2))

    ci = (float(np.exp(log_or - z * se_log)), float(np.exp(log_or + z * se_log)))

    if or_value > 1.5:
        interpretation = "A better"
    elif or_value < 0.67:
        interpretation = "B better"
    else:
        interpretation = "similar"

    return EffectSizeResult(
        name="odds_ratio",
        value=float(or_value),
        ci=ci,
        interpretation=interpretation,
        details={"a_only_correct": b01, "b_only_correct": b10},
    )


def relative_improvement(
    value_a: float,
    value_b: float,
    baseline_is_b: bool = True,
) -> EffectSizeResult:
    """Compute relative improvement percentage.

    Args:
        value_a: Metric value for model A.
        value_b: Metric value for model B.
        baseline_is_b: If True, computes improvement of A over B.

    Returns:
        EffectSizeResult with relative improvement.
    """
    if baseline_is_b:
        baseline = value_b
        comparison = value_a
    else:
        baseline = value_a
        comparison = value_b

    if baseline == 0:
        if comparison == 0:
            rel_imp = 0.0
            interpretation = "no change"
        else:
            rel_imp = float("inf")
            interpretation = "infinite improvement"
    else:
        rel_imp = (comparison - baseline) / abs(baseline) * 100

        if rel_imp > 10:
            interpretation = "large improvement"
        elif rel_imp > 2:
            interpretation = "moderate improvement"
        elif rel_imp > -2:
            interpretation = "negligible change"
        elif rel_imp > -10:
            interpretation = "moderate decline"
        else:
            interpretation = "large decline"

    return EffectSizeResult(
        name="relative_improvement_pct",
        value=float(rel_imp),
        ci=None,
        interpretation=interpretation,
        details={
            "baseline_value": float(baseline),
            "comparison_value": float(comparison),
            "absolute_difference": float(comparison - baseline),
        },
    )
