"""
Configurable thresholds for insurance model monitoring metrics.

Defaults are based on industry practice from credit scoring (PSI) and
actuarial convention (A/E ratios). Override them per deployment to match
your portfolio size and monitoring frequency.

Notes on calibration:
- PSI thresholds (0.1 / 0.25) originate from FICO credit score monitoring and
  have been adopted wholesale by insurance. They were derived empirically for
  monthly monitoring of large credit portfolios. For quarterly insurance
  monitoring at smaller volumes they may be too sensitive or too lenient
  depending on your portfolio size.
- A/E thresholds assume fully developed claims. Apply development factors before
  comparing against these bands.
- Gini drift thresholds: arXiv 2510.04556 explicitly recommends against using
  alpha=0.05 for routine monitoring. The paper argues that alpha=0.32 (the
  one-sigma rule) gives better detection power during ongoing monitoring — more
  frequent early signals in exchange for more false positives. Reserve alpha=0.05
  for the formal governance escalation decision. The new defaults reflect this
  recommendation: amber_p_value=0.32, red_p_value=0.10.
"""

from dataclasses import dataclass, field


@dataclass
class PSIThresholds:
    """Traffic light thresholds for Population Stability Index.

    Attributes
    ----------
    green_max:
        PSI below this value: no significant population shift.
    amber_max:
        PSI between green_max and amber_max: investigate, likely some drift.
    red_min:
        PSI at or above amber_max (= red_min): significant shift, action required.
    """
    green_max: float = 0.10
    amber_max: float = 0.25

    def classify(self, psi: float) -> str:
        """Return 'green', 'amber', or 'red' for a PSI value."""
        if psi < self.green_max:
            return "green"
        elif psi < self.amber_max:
            return "amber"
        else:
            return "red"


@dataclass
class AERatioThresholds:
    """Traffic light thresholds for Actual/Expected ratio.

    Industry convention for mature accident periods (12+ months developed).
    Tighter bands trigger investigation before escalation.

    Attributes
    ----------
    green_lower, green_upper:
        A/E within this band: model calibrated, no action.
    amber_lower, amber_upper:
        A/E outside green but within amber: investigate.
    red_lower, red_upper:
        A/E outside amber: escalate (board-level model risk event).
    """
    green_lower: float = 0.95
    green_upper: float = 1.05
    amber_lower: float = 0.90
    amber_upper: float = 1.10
    red_lower: float = 0.80
    red_upper: float = 1.20

    def classify(self, ae: float) -> str:
        """Return 'green', 'amber', or 'red' for an A/E ratio value."""
        if self.green_lower <= ae <= self.green_upper:
            return "green"
        elif self.amber_lower <= ae <= self.amber_upper:
            return "amber"
        else:
            return "red"


@dataclass
class GiniDriftThresholds:
    """Traffic light thresholds for Gini drift z-test.

    Based on the asymptotic normality result from arXiv 2510.04556 (Theorem 1).
    The z-test compares the current Gini coefficient against the reference
    period distribution.

    The paper explicitly recommends alpha=0.32 (one-sigma rule) for routine
    monitoring rather than the conventional alpha=0.05. The reasoning: in a
    monitoring context, missing a real deterioration (Type II error) is more
    costly than a false alarm (Type I error). Frequent signals at alpha=0.32
    prompt human review; the formal governance escalation uses alpha=0.05.

    Attributes
    ----------
    amber_p_value:
        Two-sided p-value below which we flag amber (monitor closely).
        Default 0.32 per arXiv 2510.04556 monitoring recommendation.
    red_p_value:
        Two-sided p-value below which we flag red (Gini has significantly
        degraded, escalate). Default 0.10 for monitoring context.

    Notes
    -----
    To use traditional statistical thresholds (e.g. for regulatory reporting
    or backtesting), set amber_p_value=0.10 and red_p_value=0.05::

        GiniDriftThresholds(amber_p_value=0.10, red_p_value=0.05)
    """
    amber_p_value: float = 0.32
    red_p_value: float = 0.10

    def classify(self, p_value: float) -> str:
        """Return 'green', 'amber', or 'red' for a Gini drift p-value."""
        if p_value >= self.amber_p_value:
            return "green"
        elif p_value >= self.red_p_value:
            return "amber"
        else:
            return "red"


@dataclass
class MonitoringThresholds:
    """Aggregate threshold configuration passed to MonitoringReport.

    Example
    -------
    Tighten PSI thresholds for a large motor book with monthly monitoring::

        from insurance_monitoring.thresholds import MonitoringThresholds, PSIThresholds
        thresholds = MonitoringThresholds(
            psi=PSIThresholds(green_max=0.05, amber_max=0.15),
        )

    Use traditional statistical thresholds for regulatory/backtesting use::

        from insurance_monitoring.thresholds import GiniDriftThresholds
        thresholds = MonitoringThresholds(
            gini_drift=GiniDriftThresholds(amber_p_value=0.10, red_p_value=0.05),
        )
    """
    psi: PSIThresholds = field(default_factory=PSIThresholds)
    ae_ratio: AERatioThresholds = field(default_factory=AERatioThresholds)
    gini_drift: GiniDriftThresholds = field(default_factory=GiniDriftThresholds)
