"""
CalibrationConvergenceMonitor — data-driven phase transition signal.

Patent 6 (MonitorAgent convergence scoring) ported into llm-guard-kit.

Replaces the fixed n≥50/n_wrong≥10 threshold in GuardPhaseController
with a stability signal: convergence is declared when the rolling std
of precision@FPR10 drops below stability_threshold.

IMPORTANT: this monitor is a standalone signal generator. It has no
knowledge of chain counts. The n≥20 hard floor is enforced by
GuardPhaseController, not by this class. If you use this monitor
outside GuardPhaseController, you own floor enforcement.

Usage
-----
    monitor = CalibrationConvergenceMonitor(window=5, stability_threshold=0.05)
    # After each batch of labels, compute precision@FPR10 on the CUMULATIVE
    # labeled pool (not just the new batch) and call record_round().
    monitor.record_round(precision=0.87)
    if monitor.is_converged():
        print(f"Converged at round {monitor.convergence_round}")
Purpose:    Earlier standalone version of CalibrationConvergenceMonitor ported from Patent 6. Superseded by the version in calibration_convergence_monitor.py; kept for backward compatibility.
Exports:    CalibrationConvergenceMonitor
Added:      v0.44.0
Status:     Deprecated
"""
from __future__ import annotations

from typing import List, Optional

import numpy as np


class CalibrationConvergenceMonitor:
    """
    Tracks rolling precision@FPR10 across labeling rounds.

    Declares convergence when std(last `window` rounds) < stability_threshold.
    The check is inclusive — convergence can first be declared exactly at
    round `window`.

    Parameters
    ----------
    window : int
        Number of recent rounds to include in the stability check. Default 10.
    stability_threshold : float
        Maximum std of precision@FPR10 in the window to declare convergence.
        Default 0.05 (5pp std). Lower = stricter convergence requirement.
        Note: precision@FPR10 fluctuates ±0.03–0.08 on small label pools;
        threshold=0.02 is too tight for early-phase calibration.
    """

    def __init__(self, window: int = 5, stability_threshold: float = 0.05) -> None:
        if window < 2:
            raise ValueError(f"window must be >= 2, got {window}")
        if stability_threshold <= 0:
            raise ValueError(f"stability_threshold must be > 0, got {stability_threshold}")
        self._window    = window
        self._threshold = stability_threshold
        self._history: List[float] = []
        self._convergence_round: Optional[int] = None

    def record_round(self, precision: float) -> None:
        """
        Record precision@FPR10 for the current labeling round.

        precision must be computed on the CUMULATIVE labeled set so far,
        not just the current batch. Per-batch precision is too noisy at
        small batch sizes.
        """
        self._history.append(float(precision))
        # Check convergence at every new round (once per record_round call)
        if self._convergence_round is None:
            n = len(self._history)
            if n >= self._window:
                window_std = float(np.std(self._history[-self._window:]))
                if window_std < self._threshold:
                    self._convergence_round = n

    def is_converged(self) -> bool:
        """True when rolling std(last window) < stability_threshold."""
        return self._convergence_round is not None

    def should_collect_more_labels(self) -> bool:
        """True when more labels are needed (inverse of is_converged)."""
        return not self.is_converged()

    @property
    def convergence_round(self) -> Optional[int]:
        """Round number at which convergence was declared, or None."""
        return self._convergence_round

    @property
    def precision_history(self) -> List[float]:
        """Full list of recorded precision values."""
        return list(self._history)
