"""发布负样本抽检的保守召回率估计。"""

from __future__ import annotations

from dataclasses import dataclass
from math import isfinite, sqrt


@dataclass(frozen=True, slots=True)
class ReleaseRecallEstimate:
    """候选规则召回率及其保守下界。

    ``disagreement_false_negative_count`` 是基线已命中、候选未命中的全量复核结果；
    ``random_*`` 是候选和基线均未命中人群中的独立随机抽检。两层证据不能直接
    混成普通样本比例，因此先估算漏检总量，再计算召回率。
    """

    recall: float | None
    recall_lower_bound: float | None
    estimated_false_negative_count: float | None
    false_negative_upper_bound: float | None
    random_false_negative_rate: float | None
    random_false_negative_rate_upper_bound: float | None
    confidence_level: float
    method: str


def estimate_release_recall(
    *,
    true_positive_count: int,
    disagreement_false_negative_count: int,
    random_negative_population_count: int,
    random_reviewed_count: int,
    random_false_negative_count: int,
    confidence_level: float = 0.95,
) -> ReleaseRecallEstimate:
    """使用分层抽检和 Wilson 上界生成保守召回率。

    随机层的漏检率用 Wilson 单侧保守上界近似；该上界投影到完整候选负例人群
    后，再反推召回率下界。调用方必须只传已经达到独立复核法定票数的样本。
    """

    true_positives = _count(true_positive_count, "true_positive_count")
    disagreement_false_negatives = _count(
        disagreement_false_negative_count,
        "disagreement_false_negative_count",
    )
    random_population = _count(
        random_negative_population_count,
        "random_negative_population_count",
    )
    random_reviewed = _count(random_reviewed_count, "random_reviewed_count")
    random_false_negatives = _count(
        random_false_negative_count,
        "random_false_negative_count",
    )
    if random_reviewed > random_population:
        raise ValueError("random_reviewed_count cannot exceed its population.")
    if random_false_negatives > random_reviewed:
        raise ValueError("random_false_negative_count cannot exceed reviewed samples.")

    confidence = _confidence(confidence_level)
    if random_population and not random_reviewed:
        return _unavailable(confidence)

    random_rate = (
        random_false_negatives / random_reviewed if random_reviewed else 0.0
    )
    random_upper = (
        _wilson_upper_bound(
            successes=random_false_negatives,
            total=random_reviewed,
            z=_z_score(confidence),
        )
        if random_reviewed
        else 0.0
    )
    estimated_false_negatives = (
        float(disagreement_false_negatives) + random_rate * random_population
    )
    false_negative_upper = (
        float(disagreement_false_negatives) + random_upper * random_population
    )
    recall = _recall(true_positives, estimated_false_negatives)
    recall_lower_bound = _recall(true_positives, false_negative_upper)
    return ReleaseRecallEstimate(
        recall=_rounded(recall),
        recall_lower_bound=_rounded(recall_lower_bound),
        estimated_false_negative_count=_rounded(estimated_false_negatives),
        false_negative_upper_bound=_rounded(false_negative_upper),
        random_false_negative_rate=_rounded(random_rate),
        random_false_negative_rate_upper_bound=_rounded(random_upper),
        confidence_level=confidence,
        method="stratified_random_audit_wilson_upper_bound",
    )


def _unavailable(confidence: float) -> ReleaseRecallEstimate:
    return ReleaseRecallEstimate(
        recall=None,
        recall_lower_bound=None,
        estimated_false_negative_count=None,
        false_negative_upper_bound=None,
        random_false_negative_rate=None,
        random_false_negative_rate_upper_bound=None,
        confidence_level=confidence,
        method="stratified_random_audit_wilson_upper_bound",
    )


def _wilson_upper_bound(*, successes: int, total: int, z: float) -> float:
    if total <= 0:
        raise ValueError("Wilson interval requires at least one reviewed sample.")
    probability = successes / total
    z_squared = z * z
    denominator = 1.0 + z_squared / total
    centre = probability + z_squared / (2.0 * total)
    margin = z * sqrt(
        probability * (1.0 - probability) / total
        + z_squared / (4.0 * total * total)
    )
    return min(1.0, max(0.0, (centre + margin) / denominator))


def _recall(true_positives: int, false_negatives: float) -> float | None:
    denominator = float(true_positives) + false_negatives
    if denominator <= 0:
        return None
    return true_positives / denominator


def _count(value: int, field: str) -> int:
    if isinstance(value, bool):
        raise ValueError(f"{field} must be a non-negative integer.")
    try:
        parsed = int(value)
    except (TypeError, ValueError, OverflowError) as error:
        raise ValueError(f"{field} must be a non-negative integer.") from error
    if parsed != value or parsed < 0:
        raise ValueError(f"{field} must be a non-negative integer.")
    return parsed


def _confidence(value: float) -> float:
    try:
        parsed = float(value)
    except (TypeError, ValueError, OverflowError) as error:
        raise ValueError("confidence_level must be 0.90, 0.95 or 0.99.") from error
    if not isfinite(parsed) or parsed not in {0.9, 0.95, 0.99}:
        raise ValueError("confidence_level must be 0.90, 0.95 or 0.99.")
    return parsed


def _z_score(confidence: float) -> float:
    return {0.9: 1.6448536269514722, 0.95: 1.959963984540054, 0.99: 2.5758293035489004}[
        confidence
    ]


def _rounded(value: float | None) -> float | None:
    return None if value is None else round(value, 6)
