from __future__ import annotations

from typing import Dict, Tuple

import numpy as np


def contingency_counts(pred_event: np.ndarray, obs_event: np.ndarray) -> Dict[str, int]:
    """Return contingency-table counts for binary forecast and observation events.

    H  = hit; M = miss; F = false alarm; CN = correct negative.
    Inputs can contain NaN/missing; only finite pairs are used. Boolean inputs are expected,
    but numeric arrays are accepted.
    """
    p = np.asarray(pred_event)
    o = np.asarray(obs_event)
    valid = np.isfinite(p.astype(float)) & np.isfinite(o.astype(float))
    if valid.sum() == 0:
        return {"N": 0, "H": 0, "M": 0, "F": 0, "CN": 0}
    pb = p[valid].astype(bool)
    ob = o[valid].astype(bool)
    H = int(np.sum(pb & ob))
    M = int(np.sum((~pb) & ob))
    F = int(np.sum(pb & (~ob)))
    CN = int(np.sum((~pb) & (~ob)))
    return {"N": int(valid.sum()), "H": H, "M": M, "F": F, "CN": CN}


def safe_div(num: float, den: float) -> float:
    if den == 0:
        return float("nan")
    return float(num / den)


def categorical_scores_from_counts(c: Dict[str, int]) -> Dict[str, float]:
    H, M, F, CN, N = c["H"], c["M"], c["F"], c["CN"], c["N"]
    return {
        "N": N,
        "H": H,
        "M": M,
        "F": F,
        "CN": CN,
        "PC": safe_div(H + CN, N),
        "POD": safe_div(H, H + M),
        "POFD": safe_div(F, F + CN),
        "FAR": safe_div(F, H + F),
        "FBI": safe_div(H + F, H + M),
        "CSI": safe_div(H, H + M + F),
        "event_frequency_obs": safe_div(H + M, N),
        "event_frequency_pred": safe_div(H + F, N),
    }


def categorical_scores(pred_event: np.ndarray, obs_event: np.ndarray) -> Dict[str, float]:
    return categorical_scores_from_counts(contingency_counts(pred_event, obs_event))


def brier_score(prob: np.ndarray, obs_event: np.ndarray) -> Tuple[float, int]:
    """Brier score for probability in 0..1 and binary obs event."""
    p = np.asarray(prob, dtype=float)
    o = np.asarray(obs_event, dtype=float)
    ok = np.isfinite(p) & np.isfinite(o)
    if ok.sum() == 0:
        return float("nan"), 0
    oe = o[ok].astype(bool).astype(float)
    pp = np.clip(p[ok], 0.0, 1.0)
    return float(np.mean((pp - oe) ** 2)), int(ok.sum())


def roc_auc_score_binary(prob: np.ndarray, obs_event: np.ndarray) -> Tuple[float, int, int]:
    """Compute ROC AUC for binary events without requiring sklearn.

    Uses average ranks, equivalent to the Mann-Whitney U statistic.
    Returns (auc, n_positive, n_negative). If one class is absent, auc is NaN.
    """
    p = np.asarray(prob, dtype=float).ravel()
    o = np.asarray(obs_event, dtype=float).ravel()
    ok = np.isfinite(p) & np.isfinite(o)
    if ok.sum() == 0:
        return float("nan"), 0, 0
    scores = p[ok]
    labels = o[ok].astype(bool)
    n_pos = int(labels.sum())
    n_neg = int((~labels).sum())
    if n_pos == 0 or n_neg == 0:
        return float("nan"), n_pos, n_neg

    order = np.argsort(scores, kind="mergesort")
    sorted_scores = scores[order]
    ranks = np.empty(len(scores), dtype=float)
    # Average ranks for ties, 1-based ranks.
    i = 0
    while i < len(scores):
        j = i + 1
        while j < len(scores) and sorted_scores[j] == sorted_scores[i]:
            j += 1
        avg_rank = 0.5 * ((i + 1) + j)
        ranks[order[i:j]] = avg_rank
        i = j
    sum_ranks_pos = float(np.sum(ranks[labels]))
    auc = (sum_ranks_pos - n_pos * (n_pos + 1) / 2.0) / (n_pos * n_neg)
    return float(auc), n_pos, n_neg
