from __future__ import annotations

import numpy as np
import pandas as pd


def classification_report_per_class(y_true, y_pred) -> pd.DataFrame:
    """Return per-class precision/recall/f1/support."""
    y_true = np.asarray(y_true)
    y_pred = np.asarray(y_pred)
    classes = np.unique(y_true)
    rows = []
    for c in classes:
        tp = ((y_true == c) & (y_pred == c)).sum()
        fp = ((y_true != c) & (y_pred == c)).sum()
        fn = ((y_true == c) & (y_pred != c)).sum()
        p = tp / (tp + fp) if tp + fp else 0.0
        r = tp / (tp + fn) if tp + fn else 0.0
        f1 = 2 * p * r / (p + r) if p + r else 0.0
        rows.append(
            {
                "class": c,
                "precision": p,
                "recall": r,
                "f1": f1,
                "support": int((y_true == c).sum()),
            }
        )
    return pd.DataFrame(rows)


def threshold_sweep(y_true, y_score, thresholds=None) -> pd.DataFrame:
    """Compute precision/recall/f1 over thresholds for binary tasks."""
    y_true = np.asarray(y_true)
    y_score = np.asarray(y_score)
    thresholds = np.asarray(
        thresholds if thresholds is not None else np.linspace(0.0, 1.0, 21)
    )
    rows = []
    for t in thresholds:
        y_pred = (y_score >= t).astype(int)
        rep = classification_report_per_class(y_true, y_pred)
        pos = rep[rep["class"] == 1]
        if len(pos) == 0:
            p = r = f1 = 0.0
        else:
            p, r, f1 = (
                float(pos["precision"].iloc[0]),
                float(pos["recall"].iloc[0]),
                float(pos["f1"].iloc[0]),
            )
        rows.append({"threshold": float(t), "precision": p, "recall": r, "f1": f1})
    return pd.DataFrame(rows)


def metric_confidence_interval(
    values, alpha: float = 0.95
) -> tuple[float, float, float]:
    """Normal-approx CI for metric samples."""
    arr = np.asarray(values, dtype=float)
    mean = arr.mean()
    std = arr.std(ddof=1) if len(arr) > 1 else 0.0
    z = 1.96 if alpha == 0.95 else 1.64
    half = z * std / np.sqrt(max(len(arr), 1))
    return float(mean), float(mean - half), float(mean + half)


def calibration_table(y_true, y_prob, bins: int = 10) -> pd.DataFrame:
    """Bin probabilities and compare confidence vs actual frequency."""
    y_true = np.asarray(y_true)
    y_prob = np.asarray(y_prob)
    edges = np.linspace(0.0, 1.0, bins + 1)
    rows = []
    for i in range(bins):
        lo, hi = edges[i], edges[i + 1]
        mask = (y_prob >= lo) & (y_prob < hi if i < bins - 1 else y_prob <= hi)
        if not mask.any():
            rows.append(
                {"bin": i, "count": 0, "mean_conf": np.nan, "empirical": np.nan}
            )
            continue
        rows.append(
            {
                "bin": i,
                "count": int(mask.sum()),
                "mean_conf": float(y_prob[mask].mean()),
                "empirical": float(y_true[mask].mean()),
            }
        )
    return pd.DataFrame(rows)
