"""Statistical summaries (mean, stddev, p50/95/99, 95% CI) over per-run records."""

import math
import statistics
from collections import defaultdict
from collections.abc import Iterable
from typing import Any

from abench_speckz.runspec import AggregateRow

_T_CRIT_95: dict[int, float] = {
    1: 12.706,
    2: 4.303,
    3: 3.182,
    4: 2.776,
    5: 2.571,
    6: 2.447,
    7: 2.365,
    8: 2.306,
    9: 2.262,
    10: 2.228,
    11: 2.201,
    12: 2.179,
    13: 2.160,
    14: 2.145,
    15: 2.131,
    16: 2.120,
    17: 2.110,
    18: 2.101,
    19: 2.093,
    20: 2.086,
    21: 2.080,
    22: 2.074,
    23: 2.069,
    24: 2.064,
    25: 2.060,
    26: 2.056,
    27: 2.052,
    28: 2.048,
    29: 2.045,
}


def _t_critical_95(df: int) -> float:
    if df < 1:
        return float("nan")
    return _T_CRIT_95.get(df, 1.96)


def _percentiles(values: list[float], pcts: list[int]) -> list[float]:
    if len(values) < 2:
        return [values[0]] * len(pcts) if values else [float("nan")] * len(pcts)
    qs = statistics.quantiles(values, n=100, method="inclusive")
    return [qs[p - 1] for p in pcts]


def stats_for(values: list[float]) -> dict[str, Any]:
    n = len(values)
    if n == 0:
        return {
            "n": 0,
            "mean": None,
            "stddev": None,
            "p50": None,
            "p95": None,
            "p99": None,
            "ci95_low": None,
            "ci95_high": None,
        }
    mean = statistics.fmean(values)
    stddev = statistics.stdev(values) if n >= 2 else 0.0
    p50, p95, p99 = _percentiles(values, [50, 95, 99])
    ci_low: float | None
    ci_high: float | None
    if n >= 2:
        t = _t_critical_95(n - 1)
        half = t * stddev / math.sqrt(n)
        ci_low, ci_high = mean - half, mean + half
    else:
        ci_low = ci_high = None
    return {
        "n": n,
        "mean": mean,
        "stddev": stddev,
        "p50": p50,
        "p95": p95,
        "p99": p99,
        "ci95_low": ci_low,
        "ci95_high": ci_high,
    }


def compute_aggregate_rows(runs: list[dict[str, Any]]) -> list[AggregateRow]:
    if not runs:
        return []

    successful = [r for r in runs if r.get("exit_code") == 0 and r.get("results")]
    n_failed = sum(1 for r in runs if r is not None and (r.get("exit_code") != 0 or r.get("failure_reason")))

    config_hash = runs[0]["config_hash"]
    vars_ = runs[0].get("vars") or {}
    tags = sorted({t for r in runs for t in (r.get("tags") or [])})

    by_metric: dict[str, list[float]] = defaultdict(list)
    for r in successful:
        if "warmup" in (r.get("tags") or []):
            continue
        for k, v in (r.get("results") or {}).items():
            if isinstance(v, (int, float)) and not isinstance(v, bool):
                by_metric[k].append(float(v))

    rows: list[AggregateRow] = []
    for metric, values in sorted(by_metric.items()):
        s = stats_for(values)
        rows.append(
            AggregateRow(
                config_hash=config_hash,
                metric=metric,
                n=s["n"],
                n_failed=n_failed,
                mean=s["mean"],
                stddev=s["stddev"],
                p50=s["p50"],
                p95=s["p95"],
                p99=s["p99"],
                ci95_low=s["ci95_low"],
                ci95_high=s["ci95_high"],
                vars=dict(vars_),
                tags=tags,
            )
        )
    return rows


def rebuild_table(runs: Iterable[dict[str, Any]]) -> dict[str, list[AggregateRow]]:
    by_hash: dict[str, list[dict[str, Any]]] = defaultdict(list)
    for r in runs:
        by_hash[r["config_hash"]].append(r)
    return {h: compute_aggregate_rows(rs) for h, rs in by_hash.items()}
