"""Stream-rate health classifier — pure Python, no QGIS.

Fed one sample per second (how many messages arrived in that second), it tracks
a self-calibrated baseline rate and reports a health state for the stream-rate
graph, plus outage statistics for the hourly event-log summary.

  GREEN   rate looks normal
  YELLOW  rate sustained well below the recent baseline (degraded reception).
          Heuristic by nature — aggregate msgs/sec also drops simply because
          vessels leave range, which can't be told apart from rate alone.
  RED     stream stopped (no messages for >= stop_seconds). Robust: in any
          populated area you always receive something.

"normal for SOTDMA" is learned from the live stream (a rolling median of the
per-second rate) rather than hardcoded slot math, because the aggregate rate at
a receiver depends on how many transponders are in range.
"""
from collections import deque
import statistics

GREEN, YELLOW, RED = "green", "yellow", "red"


class StreamHealth:
    def __init__(self, stop_seconds=12, graph_window=180, baseline_window=300,
                 yellow_frac=0.4, sustain=4, min_baseline_samples=30):
        """Configure the rate classifier: how long of a gap counts as stopped,
        the graph/baseline windows, and the dip depth+duration that reads as
        degraded. All thresholds are relative to a self-learned baseline rate."""
        self.stop_seconds = stop_seconds
        self.graph_window = graph_window          # seconds shown on the sparkline
        self.baseline_window = baseline_window    # seconds the baseline averages over
        self.yellow_frac = yellow_frac            # < frac*baseline (sustained) -> yellow
        self.sustain = sustain                    # samples the dip must persist
        self.min_baseline_samples = min_baseline_samples

        self.samples = deque()        # (t, rate) within the graph window (for drawing)
        self._base = deque()          # (t, rate) within the baseline window
        self.state = GREEN
        self.baseline = None
        self._last_msg_t = None
        self._start_t = None
        self._red_start = None
        self._outages = []            # completed outage durations (s) in the current hour
        self._hour = None

    # ---- per-second sample -----------------------------------------------
    def sample(self, count, now):
        """count = messages received in the last second; now = epoch seconds."""
        if self._start_t is None:
            self._start_t = now
            self._last_msg_t = now    # assume freshly connected
            self._hour = int(now // 3600)

        self.samples.append((now, count))
        self._base.append((now, count))
        while self.samples and now - self.samples[0][0] > self.graph_window:
            self.samples.popleft()
        while self._base and now - self._base[0][0] > self.baseline_window:
            self._base.popleft()
        if count > 0:
            self._last_msg_t = now

        silence = now - self._last_msg_t
        new_state = self._classify(silence)
        self._track_outage(new_state, now)
        self.state = new_state
        return new_state

    def _classify(self, silence):
        """Map the current silence gap + recent rate vs baseline to GREEN /
        YELLOW / RED. Updates the learned baseline as a side effect."""
        if silence >= self.stop_seconds:
            return RED
        rates = [c for _t, c in self._base]
        if len(rates) >= self.min_baseline_samples:
            self.baseline = statistics.median(rates)   # robust to brief dips
        if self.baseline and self.baseline > 0:
            recent = [c for _t, c in list(self.samples)[-self.sustain:]]
            if len(recent) >= self.sustain:
                if (sum(recent) / len(recent)) < self.yellow_frac * self.baseline:
                    return YELLOW
        return GREEN

    def _track_outage(self, new_state, now):
        """Bookkeep outage durations on RED transitions, so the hourly report can
        count completed outages and their mean downtime."""
        if new_state == RED and self.state != RED:
            # the outage began at the last message received, not when we became
            # confident (stop_seconds later) — report the true downtime gap
            self._red_start = self._last_msg_t if self._last_msg_t is not None else now
        elif new_state != RED and self.state == RED and self._red_start is not None:
            self._outages.append(now - self._red_start)
            self._red_start = None

    # ---- hourly report ---------------------------------------------------
    def hourly_report(self, now):
        """Returns (num_outages, mean_downtime_s) once when the wall-clock hour
        rolls over, else None. Counts outages that completed within the hour."""
        h = int(now // 3600)
        if self._hour is None:
            self._hour = h
            return None
        if h != self._hour:
            n = len(self._outages)
            mean = (sum(self._outages) / n) if n else 0.0
            self._hour = h
            self._outages = []
            return (n, mean)
        return None

    # ---- for the graph widget -------------------------------------------
    def series(self):
        """[(t, rate), ...] within the graph window."""
        return list(self.samples)

    def max_rate(self):
        """Peak msgs/sec within the current graph window (for y-axis scaling)."""
        return max((c for _t, c in self.samples), default=0)
