"""CUSUM structural-break detector (Page 1954) — faithful port of Rust ml-regime/cusum.rs.

Two-sided cumulative-sum changepoint detection: standardize each observation, accumulate the signed
drift-allowed deviation, and flag a break when either sum crosses the threshold. O(1) online — this is
the regime/edge-decay signal the forward-validation layer needs: when a deployed specialist's per-trade
returns shift, CUSUM catches the structural break with minimal delay (no fixed window to lag behind).
"""
from __future__ import annotations

from dataclasses import dataclass


@dataclass(frozen=True, slots=True)
class StructuralBreak:
    direction: str          # "positive" | "negative"
    magnitude: float        # CUSUM sum at detection (signed: + for upward, − for downward)
    observations_since_reset: int


class CusumDetector:
    """S⁺ₜ = max(0, S⁺ₜ₋₁ + zₜ − k);  S⁻ₜ = max(0, S⁻ₜ₋₁ − zₜ − k);  break when S⁺>h or S⁻>h.
    zₜ = (xₜ − μ)/σ, so k (drift) and h (threshold) are in σ units (typical k=0.5, h=4–5)."""

    def __init__(self, target_mean: float, target_std: float, drift_allowance: float,
                 detection_threshold: float) -> None:
        self._mean = target_mean
        self._std = target_std if target_std > 1e-15 else 1.0
        self._k = drift_allowance
        self._h = detection_threshold
        self._pos = 0.0
        self._neg = 0.0
        self._obs = 0

    def update(self, value: float) -> StructuralBreak | None:
        self._obs += 1
        z = (value - self._mean) / self._std
        self._pos = max(0.0, self._pos + z - self._k)
        self._neg = max(0.0, self._neg - z - self._k)
        if self._pos > self._h:
            return StructuralBreak("positive", self._pos, self._obs)
        if self._neg > self._h:
            return StructuralBreak("negative", -self._neg, self._obs)
        return None

    def reset(self) -> None:
        self._pos = self._neg = 0.0
        self._obs = 0

    @property
    def positive_sum(self) -> float:
        return self._pos

    @property
    def negative_sum(self) -> float:
        return self._neg
