"""Phi Accrual Failure Detector for per-symbol liveness detection (Argus).

Vendored and simplified from samueleresca/phi-accrual-failure-detector (MIT).
Original algorithm: Hayashibara et al., "The φ Accrual Failure Detector" (2004).
Reference impl: Akka PhiAccrualFailureDetector.scala

Instead of boolean alive/dead with a fixed timeout, outputs a continuous
suspicion level (phi). Higher phi = more likely dead. Adapts to observed
inter-arrival times per symbol, so quiet market periods produce low phi
while the same silence during peak hours produces high phi.

Usage in Argus:
    detector = PhiAccrualDetector()
    # On each bar emission:
    detector.heartbeat("BTCUSDT")
    # On each Argus check:
    phi = detector.phi("BTCUSDT")
    if phi > 8.0:  # dead
        alert(...)
    elif phi > 3.0:  # suspicious
        log(...)
"""

from __future__ import annotations

import math
import time
from collections import deque


class PhiAccrualDetector:
    """Per-key phi accrual failure detector with sliding window stats."""

    def __init__(
        self,
        threshold: float = 8.0,
        max_sample_size: int = 200,
        min_std_deviation_ms: float = 500.0,
        acceptable_heartbeat_pause_ms: float = 0.0,
        first_heartbeat_estimate_ms: float = 1000.0,
    ) -> None:
        self.threshold = threshold
        self.max_sample_size = max_sample_size
        self.min_std_deviation_ms = min_std_deviation_ms
        self.acceptable_heartbeat_pause_ms = acceptable_heartbeat_pause_ms
        self.first_heartbeat_estimate_ms = first_heartbeat_estimate_ms
        self._states: dict[str, _SymbolState] = {}

    def heartbeat(self, key: str, timestamp_ms: float | None = None) -> None:
        """Record a heartbeat (e.g., bar emission) for a key."""
        ts = timestamp_ms if timestamp_ms is not None else time.monotonic() * 1000
        state = self._states.get(key)
        if state is None:
            state = _SymbolState(self.max_sample_size, self.first_heartbeat_estimate_ms)
            self._states[key] = state
        state.record(ts)

    def phi(self, key: str, timestamp_ms: float | None = None) -> float:
        """Compute phi for a key. Returns 0.0 if no heartbeats recorded."""
        state = self._states.get(key)
        if state is None or state.last_ts is None:
            return 0.0
        ts = timestamp_ms if timestamp_ms is not None else time.monotonic() * 1000
        time_diff = ts - state.last_ts
        mean = state.mean() + self.acceptable_heartbeat_pause_ms
        std_dev = max(state.std_dev(), self.min_std_deviation_ms)
        return _calc_phi(time_diff, mean, std_dev)

    def is_available(self, key: str, timestamp_ms: float | None = None) -> bool:
        """True if phi < threshold (resource is alive)."""
        return self.phi(key, timestamp_ms) < self.threshold

    def reset(self, key: str) -> None:
        """Clear history for a key (e.g., after engine restart)."""
        self._states.pop(key, None)


class _SymbolState:
    """Sliding window of inter-heartbeat intervals for one symbol."""

    __slots__ = ("_intervals", "_max_size", "_sq_sum", "_sum", "last_ts")

    def __init__(self, max_size: int, first_estimate_ms: float) -> None:
        self._intervals: deque[float] = deque(maxlen=max_size)
        self._max_size = max_size
        self._sum = 0.0
        self._sq_sum = 0.0
        self.last_ts: float | None = None
        # Seed with 2 samples around the estimate (Akka pattern)
        std = first_estimate_ms / 4
        self._push(first_estimate_ms - std)
        self._push(first_estimate_ms + std)

    def record(self, timestamp_ms: float) -> None:
        if self.last_ts is not None:
            interval = timestamp_ms - self.last_ts
            if interval > 0:
                self._push(interval)
        self.last_ts = timestamp_ms

    def _push(self, interval: float) -> None:
        if len(self._intervals) >= self._max_size:
            old = self._intervals[0]
            self._sum -= old
            self._sq_sum -= old * old
        self._intervals.append(interval)
        self._sum += interval
        self._sq_sum += interval * interval

    def mean(self) -> float:
        n = len(self._intervals)
        return self._sum / n if n > 0 else 0.0

    _MIN_SAMPLES_FOR_STDDEV = 2

    def std_dev(self) -> float:
        n = len(self._intervals)
        if n < self._MIN_SAMPLES_FOR_STDDEV:
            return 0.0
        variance = (self._sq_sum / n) - (self._sum / n) ** 2
        return math.sqrt(max(0.0, variance))


def _calc_phi(time_diff: float, mean: float, std_dev: float) -> float:
    """Logistic approximation to CDF of normal distribution (Akka formula)."""
    if std_dev <= 0:
        return 0.0 if time_diff <= mean else 16.0  # Cap at 16
    y = (time_diff - mean) / std_dev
    try:
        e = math.exp(-y * (1.5976 + 0.070566 * y * y))
    except OverflowError:
        return 0.0 if y < 0 else 16.0
    if time_diff > mean:
        p = e / (1.0 + e)
        if p <= 0.0:
            return 16.0
        return min(-math.log10(p), 16.0)
    p = 1.0 - 1.0 / (1.0 + e)
    if p <= 0.0:
        return 0.0
    return min(-math.log10(p), 16.0)
