"""Telemetry Collector — measures video pipeline performance metrics."""

from __future__ import annotations

import time
from collections import deque
from dataclasses import dataclass

import psutil
from PySide6.QtCore import QObject, QTimer, Signal
from PySide6.QtMultimedia import QVideoFrame

from app.config import TELEMETRY_UPDATE_INTERVAL_MS


@dataclass
class TelemetrySnapshot:
    """Immutable snapshot of current performance metrics."""

    fps: float                    # actual frames received in the last second
    target_fps: float | None      # FPS requested from the camera (None = unknown)
    frame_time_ms: float          # average inter-frame time in ms
    dropped_frames: int           # cumulative dropped frames detected
    cpu_percent_sys: float        # process CPU as % of total system capacity
                                  # (divided by cpu_count) — matches Task Manager
    cpu_percent_core: float       # process CPU per single core — can exceed 100%
    memory_mb: float | None       # process private working set in MB
    timestamp: float              # time.perf_counter() when snapshot was taken
    # Inference fields — None when inference is disabled / model not loaded
    inference_device: str | None = None   # e.g. "cpu", "mps"
    inference_time_ms: float | None = None  # rolling average of model() call time


class TelemetryCollector(QObject):
    """
    Frame subscriber that measures pipeline performance.

    Connect to FrameDispatcher:
        dispatcher.subscribe(collector.on_frame, drop_if_busy=False)

    Receive target FPS updates from CameraService:
        camera_service.format_changed.connect(collector.set_target_fps)

    Listen to metrics updates:
        collector.metrics_updated.connect(my_slot)
    """

    metrics_updated = Signal(object)  # emits TelemetrySnapshot

    def __init__(
        self,
        update_interval_ms: int = TELEMETRY_UPDATE_INTERVAL_MS,
        parent: QObject | None = None,
    ) -> None:
        super().__init__(parent)

        self._update_interval_ms = update_interval_ms
        self._target_fps: float | None = None

        # frame timing ring-buffer (last 120 samples)
        self._frame_times: deque[float] = deque(maxlen=120)
        self._last_frame_time: float = 0.0
        self._total_frames: int = 0
        self._dropped_frames: int = 0

        # FPS window — count frames in the last second
        self._fps_window: deque[float] = deque()
        self._fps_window_size_s: float = 1.0

        # psutil — initialise baseline so first real reading is non-zero
        self._process = psutil.Process()
        self._process.cpu_percent()  # first call always returns 0.0; discard
        self._cpu_count: int = max(psutil.cpu_count(logical=True) or 1, 1)

        # Inference stats (updated externally via set_inference_stats)
        self._inference_device: str | None = None
        self._inference_time_ms: float | None = None

        # periodic snapshot timer
        self._timer = QTimer(self)
        self._timer.setInterval(update_interval_ms)
        self._timer.timeout.connect(self._emit_snapshot)
        self._timer.start()

        self._latest: TelemetrySnapshot = self._make_empty_snapshot()

    # ------------------------------------------------------------------
    # Configuration
    # ------------------------------------------------------------------

    def set_target_fps(self, fps: float | None) -> None:
        """Record the FPS that was requested from the camera."""
        self._target_fps = fps

    def set_inference_stats(self, device: str, avg_ms: float) -> None:
        """Update inference device and average inference time (called from MainWindow)."""
        self._inference_device: str | None = device
        self._inference_time_ms: float | None = avg_ms

    def clear_inference_stats(self) -> None:
        """Clear inference stats when inference is disabled."""
        self._inference_device = None
        self._inference_time_ms = None

    # ------------------------------------------------------------------
    # Frame subscriber callback
    # ------------------------------------------------------------------

    def on_frame(self, frame: QVideoFrame) -> None:
        """Called by FrameDispatcher for every frame. Must be fast."""
        now = time.perf_counter()

        if self._last_frame_time > 0:
            delta = now - self._last_frame_time
            self._frame_times.append(delta)

            # drop detection: gap > 2.5× rolling average
            if len(self._frame_times) >= 5:
                avg = sum(self._frame_times) / len(self._frame_times)
                if delta > avg * 2.5:
                    self._dropped_frames += 1

        self._last_frame_time = now
        self._total_frames += 1

        self._fps_window.append(now)
        cutoff = now - self._fps_window_size_s
        while self._fps_window and self._fps_window[0] < cutoff:
            self._fps_window.popleft()

    # ------------------------------------------------------------------
    # Snapshot
    # ------------------------------------------------------------------

    def latest_snapshot(self) -> TelemetrySnapshot:
        return self._latest

    def reset_counters(self) -> None:
        """Reset cumulative counters (e.g. after camera switch)."""
        self._frame_times.clear()
        self._fps_window.clear()
        self._last_frame_time = 0.0
        self._total_frames = 0
        self._dropped_frames = 0

    # ------------------------------------------------------------------
    # Internal
    # ------------------------------------------------------------------

    def _emit_snapshot(self) -> None:
        snapshot = self._compute_snapshot()
        self._latest = snapshot
        self.metrics_updated.emit(snapshot)

    def _compute_snapshot(self) -> TelemetrySnapshot:
        now = time.perf_counter()

        # FPS — prune stale entries before counting
        cutoff = now - self._fps_window_size_s
        while self._fps_window and self._fps_window[0] < cutoff:
            self._fps_window.popleft()
        fps = float(len(self._fps_window))

        # average frame time
        avg_frame_time_ms = (
            (sum(self._frame_times) / len(self._frame_times)) * 1000.0
            if self._frame_times
            else 0.0
        )

        # CPU — per-core reading, then derive system-normalised value
        try:
            cpu_core = self._process.cpu_percent()
        except Exception:
            cpu_core = 0.0
        cpu_sys = cpu_core / self._cpu_count

        # Memory — private working set (Windows) or RSS (macOS/Linux)
        try:
            mem_info = self._process.memory_info()
            mem_bytes = getattr(mem_info, "wset", None) or mem_info.rss
            mem_mb: float | None = mem_bytes / (1024 * 1024)
        except Exception:
            mem_mb = None

        return TelemetrySnapshot(
            fps=round(fps, 1),
            target_fps=self._target_fps,
            frame_time_ms=round(avg_frame_time_ms, 2),
            dropped_frames=self._dropped_frames,
            cpu_percent_sys=round(cpu_sys, 1),
            cpu_percent_core=round(cpu_core, 1),
            memory_mb=round(mem_mb, 1) if mem_mb is not None else None,
            timestamp=now,
            inference_device=self._inference_device,
            inference_time_ms=(
                round(self._inference_time_ms, 1)
                if self._inference_time_ms is not None
                else None
            ),
        )

    def _make_empty_snapshot(self) -> TelemetrySnapshot:
        return TelemetrySnapshot(
            fps=0.0,
            target_fps=self._target_fps,
            frame_time_ms=0.0,
            dropped_frames=0,
            cpu_percent_sys=0.0,
            cpu_percent_core=0.0,
            memory_mb=None,
            timestamp=time.perf_counter(),
            inference_device=None,
            inference_time_ms=None,
        )
