"""Timestamp distance and sample-phase diagnostics across QTFM sensors."""

from __future__ import annotations

import statistics
from collections import deque
from dataclasses import dataclass

from serial_connection import QTFMMeasurement


@dataclass(frozen=True)
class TimestampSyncStatus:
    ready: bool
    distance_ms: float | None
    sample_interval_ms: int | None
    phase_offset_ms: int | None
    needs_sync: bool
    reason: str


class TimestampSyncMonitor:
    """Compare device clocks independently of USB arrival batching."""

    TIMESTAMP_MODULUS = 2**32
    DISTANCE_LIMIT_MS = 1000.0
    MAX_NORMAL_STEP_MS = 10_000

    def __init__(
        self,
        sensors: tuple[str, ...] = ("A", "B"),
        reference: str = "A",
    ) -> None:
        self._sensors: tuple[str, ...] = ()
        self._reference = reference
        self._latest: dict[str, QTFMMeasurement | None] = {}
        self._last_timestamp: dict[str, int | None] = {}
        self._steps: dict[str, deque[int]] = {}
        self._clock_distances: dict[str, deque[float]] = {}
        self.set_sensors(sensors, reference)

    # ------------------------------------------------------------- lifecycle
    def set_sensors(
        self,
        sensors: tuple[str, ...],
        reference: str | None = None,
    ) -> None:
        """Track a new set of sensors, discarding any previous history."""
        self._sensors = tuple(sensors)
        if reference is not None:
            self._reference = reference
        if self._reference not in self._sensors and self._sensors:
            self._reference = self._sensors[0]
        self.clear()

    @property
    def sensors(self) -> tuple[str, ...]:
        return self._sensors

    @property
    def reference(self) -> str:
        return self._reference

    def clear(self) -> None:
        self._latest = {key: None for key in self._sensors}
        self._last_timestamp = {key: None for key in self._sensors}
        self._steps = {key: deque(maxlen=64) for key in self._sensors}
        self._clock_distances = {
            key: deque(maxlen=64)
            for key in self._sensors
            if key != self._reference
        }

    def clear_sensor(self, sensor: str) -> None:
        if sensor not in self._latest:
            return
        self._latest[sensor] = None
        self._last_timestamp[sensor] = None
        self._steps[sensor].clear()
        for distances in self._clock_distances.values():
            distances.clear()

    # ------------------------------------------------------------------ data
    def add(
        self,
        sensor: str,
        measurement: QTFMMeasurement,
    ) -> TimestampSyncStatus:
        if sensor not in self._latest or measurement.timestamp_ms is None:
            return self.status()

        timestamp_ms = measurement.timestamp_ms
        previous = self._last_timestamp[sensor]
        if previous is not None:
            step = (timestamp_ms - previous) % self.TIMESTAMP_MODULUS
            if 0 < step <= self.MAX_NORMAL_STEP_MS:
                self._steps[sensor].append(step)
            elif step > self.MAX_NORMAL_STEP_MS:
                # Timestamp reset/reboot: discard the previous epoch.
                self._steps[sensor].clear()
                for distances in self._clock_distances.values():
                    distances.clear()

        self._last_timestamp[sensor] = timestamp_ms
        self._latest[sensor] = measurement
        self._record_clock_distances()
        return self.status()

    # ---------------------------------------------------------------- status
    def status(self) -> TimestampSyncStatus:
        if len(self._sensors) < 2:
            return TimestampSyncStatus(
                ready=False,
                distance_ms=None,
                sample_interval_ms=None,
                phase_offset_ms=None,
                needs_sync=False,
                reason="At least two sensors are required.",
            )

        missing = [
            key
            for key in self._sensors
            if self._latest[key] is None
            or self._latest[key].timestamp_ms is None
        ]
        if missing:
            return TimestampSyncStatus(
                ready=False,
                distance_ms=None,
                sample_interval_ms=None,
                phase_offset_ms=None,
                needs_sync=False,
                reason=(
                    "Waiting for MS timestamps from "
                    + ", ".join(missing)
                    + "."
                ),
            )

        distance_ms = self._worst_distance()
        intervals = {key: self._sample_interval(key) for key in self._sensors}
        distinct_intervals = {
            value for value in intervals.values() if value is not None
        }
        shared_interval = (
            next(iter(distinct_intervals))
            if len(distinct_intervals) == 1
            else None
        )

        if distance_ms is not None and distance_ms > self.DISTANCE_LIMIT_MS:
            return TimestampSyncStatus(
                ready=True,
                distance_ms=distance_ms,
                sample_interval_ms=shared_interval,
                phase_offset_ms=None,
                needs_sync=True,
                reason="Timestamp distance is over 1000 ms.",
            )

        if any(value is None for value in intervals.values()):
            return TimestampSyncStatus(
                ready=False,
                distance_ms=distance_ms,
                sample_interval_ms=None,
                phase_offset_ms=None,
                needs_sync=False,
                reason="Measuring timestamp cadence…",
            )

        if shared_interval is None:
            detail = ", ".join(
                f"{key} {intervals[key]} ms" for key in self._sensors
            )
            return TimestampSyncStatus(
                ready=True,
                distance_ms=distance_ms,
                sample_interval_ms=None,
                phase_offset_ms=None,
                needs_sync=True,
                reason=f"Sensor intervals differ: {detail}.",
            )

        phase_offset_ms, phase_sensor = self._worst_phase(shared_interval)
        if phase_offset_ms:
            return TimestampSyncStatus(
                ready=True,
                distance_ms=distance_ms,
                sample_interval_ms=shared_interval,
                phase_offset_ms=phase_offset_ms,
                needs_sync=True,
                reason=(
                    f"Sample phases differ by {phase_offset_ms} ms "
                    f"({self._reference} vs {phase_sensor}) at a "
                    f"{shared_interval} ms interval."
                ),
            )

        return TimestampSyncStatus(
            ready=True,
            distance_ms=distance_ms,
            sample_interval_ms=shared_interval,
            phase_offset_ms=0,
            needs_sync=False,
            reason="Timestamp cadence and sample phase are aligned.",
        )

    # ---------------------------------------------------------------- detail
    def _sample_interval(self, sensor: str) -> int | None:
        steps = self._steps.get(sensor)
        if steps is None or len(steps) < 3:
            return None
        return int(round(statistics.median(steps)))

    def _worst_distance(self) -> float | None:
        distances = [
            abs(statistics.median(samples))
            for samples in self._clock_distances.values()
            if samples
        ]
        return max(distances) if distances else None

    def _worst_phase(self, interval_ms: int) -> tuple[int, str | None]:
        reference = self._latest[self._reference]
        if reference is None or reference.timestamp_ms is None:
            return 0, None

        reference_phase = reference.timestamp_ms % interval_ms
        worst = 0
        worst_sensor: str | None = None
        for key in self._sensors:
            if key == self._reference:
                continue
            measurement = self._latest[key]
            if measurement is None or measurement.timestamp_ms is None:
                continue
            raw = abs(reference_phase - measurement.timestamp_ms % interval_ms)
            offset = min(raw, interval_ms - raw)
            if offset > worst:
                worst = offset
                worst_sensor = key
        return worst, worst_sensor

    def _record_clock_distances(self) -> None:
        reference = self._latest.get(self._reference)
        if reference is None or reference.timestamp_ms is None:
            return
        reference_offset = self._host_offset(reference)

        for key, distances in self._clock_distances.items():
            measurement = self._latest.get(key)
            if measurement is None or measurement.timestamp_ms is None:
                continue
            distances.append(
                float(
                    self._signed_circular_difference(
                        reference_offset,
                        self._host_offset(measurement),
                    )
                )
            )

    @classmethod
    def _host_offset(cls, measurement: QTFMMeasurement) -> int:
        return (
            measurement.timestamp_ms - round(measurement.host_time * 1000)
        ) % cls.TIMESTAMP_MODULUS

    @classmethod
    def _signed_circular_difference(cls, first: int, second: int) -> int:
        half = cls.TIMESTAMP_MODULUS // 2
        return (
            (first - second + half) % cls.TIMESTAMP_MODULUS
        ) - half
