"""Exposure-based rates and equal-tailed Garwood intervals.

The Poisson model is an assumption, not a claim that field failures are independent.
Install requirements-research.txt to reproduce the numerical environment.
"""
import math
from numbers import Integral, Real

from scipy.stats import chi2


def _count(failures):
    if isinstance(failures, bool) or not isinstance(failures, Integral) or failures < 0:
        raise ValueError("failures must be a non-negative integer")


def _positive(value, name):
    if isinstance(value, bool) or not isinstance(value, Real) or not math.isfinite(value) or value <= 0:
        raise ValueError(f"{name} must be finite and positive")


def garwood(failures, a=0.025):
    """Count bounds; a is the probability in EACH tail (0.025 gives 95%)."""
    _count(failures)
    if isinstance(a, bool) or not isinstance(a, Real) or not math.isfinite(a) or not 0 < a < 0.5:
        raise ValueError("tail probability must be between 0 and 0.5")
    lower = 0.0 if failures == 0 else float(chi2.ppf(a, 2 * failures) / 2)
    # isf avoids losing a small upper-tail probability in the subtraction 1 - a.
    upper = float(chi2.isf(a, 2 * (failures + 1)) / 2)
    if not (math.isfinite(lower) and math.isfinite(upper) and 0 <= lower <= failures < upper):
        raise ArithmeticError("invalid Poisson interval")
    return lower, upper


def afr_percent(failures, drive_days, days_per_year=365):
    """Failures per 100 drive-years; not a one-year cumulative probability."""
    _count(failures)
    _positive(drive_days, "drive_days")
    _positive(days_per_year, "days_per_year")
    return failures / drive_days * days_per_year * 100


def probability_under_constant_hazard(annual_rate):
    """One-year probability for an assumed constant hazard, rate in 1/year."""
    if isinstance(annual_rate, bool) or not isinstance(annual_rate, Real) or not math.isfinite(annual_rate) or annual_rate < 0:
        raise ValueError("annual_rate must be finite and non-negative")
    return -math.expm1(-annual_rate)
