"""Inference routines, including exact Monte Carlo p-values."""

from __future__ import annotations

from dataclasses import asdict, dataclass

import numpy as np


@dataclass
class MonteCarloInference:
    statistic_observed: float
    null_statistics: list[float]
    p_value: float
    method: str
    n_replicates: int
    mc_resolution: float
    warning_flags: list[str]

    def to_dict(self) -> dict[str, object]:
        return asdict(self)


def exact_upper_tail_pvalue(observed: float, null_stats: np.ndarray) -> float:
    null = np.asarray(null_stats, dtype=float)
    n = null.size
    exceed = int(np.sum(null >= observed))
    return float((exceed + 1) / (n + 1))


def exact_two_sided_symmetric_pvalue(observed: float, null_stats: np.ndarray) -> float:
    null = np.asarray(null_stats, dtype=float)
    pooled = np.concatenate([[float(observed)], null])
    center = float(np.median(pooled))
    obs_dev = abs(float(observed) - center)
    null_dev = np.abs(null - center)
    exceed = int(np.sum(null_dev >= obs_dev))
    return float((exceed + 1) / (len(null) + 1))


def infer_pvalue(
    observed: float,
    null_stats: np.ndarray,
    two_sided: bool = False,
    warning_threshold: int = 199,
) -> MonteCarloInference:
    null = np.asarray(null_stats, dtype=float)
    if null.ndim != 1 or null.size == 0:
        raise ValueError("Null statistics must be a non-empty 1D array")

    if two_sided:
        p = exact_two_sided_symmetric_pvalue(observed, null)
        method = "exact_rank_two_sided_symmetric"
    else:
        p = exact_upper_tail_pvalue(observed, null)
        method = "exact_rank_upper_tail"

    resolution = 1.0 / (len(null) + 1)
    flags: list[str] = []
    if len(null) < warning_threshold:
        flags.append("low_monte_carlo_resolution")

    return MonteCarloInference(
        statistic_observed=float(observed),
        null_statistics=[float(x) for x in null],
        p_value=float(p),
        method=method,
        n_replicates=len(null),
        mc_resolution=float(resolution),
        warning_flags=flags,
    )


def effect_size_summary(observed: float, null_stats: np.ndarray) -> dict[str, float]:
    null = np.asarray(null_stats, dtype=float)
    mean = float(np.mean(null))
    sd = float(np.std(null, ddof=1)) if len(null) > 1 else 0.0
    z = (observed - mean) / sd if sd > 0 else float("nan")
    delta = observed - mean
    return {
        "null_mean": mean,
        "null_sd": sd,
        "delta_vs_null": float(delta),
        "z_vs_null": float(z),
    }
