"""Server-side ring buffer for system and per-target metrics."""

import asyncio
import os
from collections import deque
from datetime import datetime, timezone
from typing import Dict, Optional

from ledgrab.utils import get_logger
from ledgrab.utils.gpu import (
    nvml_available as _nvml_available,
    nvml as _nvml,
    nvml_handle as _nvml_handle,
)
from ledgrab.utils.metrics import get_metrics_provider

logger = get_logger(__name__)

MAX_SAMPLES = 120  # ~2 minutes at 1-second interval
SAMPLE_INTERVAL = 1.0  # seconds


def _collect_system_snapshot() -> dict:
    """Collect CPU/RAM/GPU metrics (blocking — run in thread pool).

    Returns a dict suitable for direct JSON serialization.
    """
    metrics = get_metrics_provider()
    mem = metrics.virtual_memory()
    proc = metrics.process_snapshot()
    thermals = metrics.thermals()
    snapshot = {
        "t": datetime.now(timezone.utc).isoformat(),
        "cpu": metrics.cpu_percent(),
        "ram_pct": mem.percent,
        "ram_used": round(mem.used_bytes / 1024 / 1024, 1),
        "ram_total": round(mem.total_bytes / 1024 / 1024, 1),
        "app_cpu": proc.cpu_percent,
        "app_ram": round(proc.rss_bytes / 1024 / 1024, 1),
        "gpu_util": None,
        "gpu_temp": None,
        "app_gpu_mem": None,
        "cpu_temp": thermals.cpu_temp_c,
        "battery_pct": thermals.battery_percent,
        "battery_temp": thermals.battery_temp_c,
    }

    try:
        if _nvml_available:
            util = _nvml.nvmlDeviceGetUtilizationRates(_nvml_handle)
            _ = os.getpid  # keep import lint-clean for the os.getpid call below
            temp = _nvml.nvmlDeviceGetTemperature(_nvml_handle, _nvml.NVML_TEMPERATURE_GPU)
            snapshot["gpu_util"] = float(util.gpu)
            snapshot["gpu_temp"] = float(temp)
            try:
                pid = os.getpid()
                for proc_info in _nvml.nvmlDeviceGetComputeRunningProcesses(_nvml_handle):
                    if proc_info.pid == pid and proc_info.usedGpuMemory:
                        snapshot["app_gpu_mem"] = round(proc_info.usedGpuMemory / 1024 / 1024, 1)
                        break
            except Exception:
                pass
    except Exception as e:
        logger.debug("GPU metrics collection failed: %s", e)
        pass

    return snapshot


class MetricsHistory:
    """In-memory ring buffer collecting system and per-target metrics."""

    def __init__(self, processor_manager):
        self._manager = processor_manager
        self._system: deque = deque(maxlen=MAX_SAMPLES)
        self._targets: Dict[str, deque] = {}
        self._task: Optional[asyncio.Task] = None
        # Baselines for converting cumulative `errors_count` /
        # `frames_skipped` into per-second rates inside the system ring
        # buffer. None until the first sample arrives so we don't
        # synthesize a fake initial spike from "0 → live count".
        self._prev_total_errors: Optional[int] = None
        self._prev_total_skipped: Optional[int] = None
        # Same shape, but for the network throughput counter. Reset to
        # None when the cumulative sum drops (target stopped, counter
        # reset) so we never emit a negative rate.
        self._prev_total_bytes_sent: Optional[int] = None

    async def start(self):
        """Start the background sampling loop."""
        if self._task and not self._task.done():
            return
        self._task = asyncio.create_task(self._sample_loop())
        logger.info("Metrics history sampling started")

    async def stop(self):
        """Stop the background sampling loop."""
        if self._task:
            self._task.cancel()
            try:
                await self._task
            except asyncio.CancelledError:
                logger.debug("Metrics history collection task cancelled")
                pass
            self._task = None
        logger.info("Metrics history sampling stopped")

    async def _sample_loop(self):
        """Sample system + target metrics every SAMPLE_INTERVAL seconds."""
        while True:
            try:
                await self._sample()
            except asyncio.CancelledError:
                raise
            except Exception as e:
                logger.warning(f"Metrics sampling error: {e}")
            await asyncio.sleep(SAMPLE_INTERVAL)

    async def _sample(self):
        """Collect one snapshot of system and target metrics."""
        # System metrics (blocking psutil/nvml calls in thread pool)
        sys_snap = await asyncio.to_thread(_collect_system_snapshot)

        # Per-target metrics from processor states
        try:
            all_states = self._manager.get_all_target_states()
        except Exception as e:
            logger.error("Failed to get target states: %s", e)
            all_states = {}

        now = datetime.now(timezone.utc).isoformat()
        active_ids = set()

        # Aggregates across running targets — mirrors the dashboard's
        # frontend computation so the FPS / Capture FPS / Errors cells
        # can seed their sparklines from this ring buffer and survive
        # a page reload, the same way CPU / RAM already do.
        total_fps = 0.0
        total_capture_fps = 0.0
        total_capture_fps_actual = 0.0
        capture_actual_count = 0
        total_fps_target = 0.0
        total_errors_count = 0
        total_frames_skipped = 0
        running_count = 0
        # Network / send-timing aggregates across running targets.
        # `send_timing_*` reads "is the LED transport keeping up?" — a
        # leading indicator of network congestion that fires before
        # frames actually start dropping.
        total_bytes_sent = 0
        send_timing_max_ms = 0.0
        send_timing_sum_ms = 0.0
        send_timing_count = 0

        for target_id, state in all_states.items():
            active_ids.add(target_id)
            if target_id not in self._targets:
                self._targets[target_id] = deque(maxlen=MAX_SAMPLES)
            if state.get("processing"):
                running_count += 1
                fps_actual = state.get("fps_actual")
                if isinstance(fps_actual, (int, float)) and fps_actual > 0:
                    total_fps += float(fps_actual)
                fps_capture = state.get("fps_capture")
                if isinstance(fps_capture, (int, float)) and fps_capture > 0:
                    total_capture_fps += float(fps_capture)
                fps_capture_actual = state.get("fps_capture_actual")
                # `None` means the stream type doesn't measure capture
                # (synthetic streams). Counted separately so the cell
                # can read "0 of 0" vs "0 of N stalled".
                if isinstance(fps_capture_actual, (int, float)):
                    total_capture_fps_actual += float(fps_capture_actual)
                    capture_actual_count += 1
                fps_target = state.get("fps_target")
                if isinstance(fps_target, (int, float)) and fps_target > 0:
                    total_fps_target += float(fps_target)
                errors_count = state.get("errors_count")
                if isinstance(errors_count, (int, float)) and errors_count > 0:
                    total_errors_count += int(errors_count)
                frames_skipped = state.get("frames_skipped")
                if isinstance(frames_skipped, (int, float)) and frames_skipped > 0:
                    total_frames_skipped += int(frames_skipped)
                bytes_sent = state.get("bytes_sent")
                if isinstance(bytes_sent, (int, float)) and bytes_sent > 0:
                    total_bytes_sent += int(bytes_sent)
                send_timing = state.get("timing_send_ms")
                if isinstance(send_timing, (int, float)) and send_timing >= 0:
                    send_timing_sum_ms += float(send_timing)
                    send_timing_count += 1
                    if send_timing > send_timing_max_ms:
                        send_timing_max_ms = float(send_timing)

                self._targets[target_id].append(
                    {
                        "t": now,
                        "fps": fps_actual,
                        "fps_current": state.get("fps_current"),
                        "fps_target": fps_target,
                        "timing": state.get("timing_total_ms"),
                        "errors": state.get("errors_count", 0),
                    }
                )

        # Convert the cumulative error/skipped totals into per-second
        # rates. Guard against the first sample (no previous baseline)
        # and against counter resets when a target stops or restarts
        # (delta < 0 → treat as 0).
        errors_per_sec = 0.0
        skipped_per_sec = 0.0
        bytes_per_sec = 0.0
        if self._prev_total_errors is not None:
            delta = max(0, total_errors_count - self._prev_total_errors)
            errors_per_sec = delta / SAMPLE_INTERVAL
        if self._prev_total_skipped is not None:
            delta = max(0, total_frames_skipped - self._prev_total_skipped)
            skipped_per_sec = delta / SAMPLE_INTERVAL
        if self._prev_total_bytes_sent is not None:
            delta_b = max(0, total_bytes_sent - self._prev_total_bytes_sent)
            bytes_per_sec = delta_b / SAMPLE_INTERVAL
        self._prev_total_errors = total_errors_count
        self._prev_total_skipped = total_frames_skipped
        self._prev_total_bytes_sent = total_bytes_sent

        # Device latency aggregates — pulled from the manager's
        # device-health view rather than re-deriving from per-target
        # state, so devices that are shared by multiple targets only
        # count once.
        device_latency_avg_ms: Optional[float] = None
        device_latency_max_ms: Optional[float] = None
        device_online_count = 0
        device_total_count = 0
        try:
            health_dicts = self._manager.get_all_device_health_dicts()
        except Exception as e:
            logger.error("Failed to get device health: %s", e)
            health_dicts = {}
        latency_sum = 0.0
        latency_n = 0
        latency_max = 0.0
        for _did, h in health_dicts.items():
            device_total_count += 1
            if h.get("device_online"):
                device_online_count += 1
            lat = h.get("device_latency_ms")
            if isinstance(lat, (int, float)) and lat >= 0:
                latency_sum += float(lat)
                latency_n += 1
                if lat > latency_max:
                    latency_max = float(lat)
        if latency_n > 0:
            device_latency_avg_ms = round(latency_sum / latency_n, 1)
            device_latency_max_ms = round(latency_max, 1)

        sys_snap["total_fps"] = round(total_fps, 1)
        sys_snap["total_capture_fps"] = round(total_capture_fps, 1)
        sys_snap["total_capture_fps_actual"] = round(total_capture_fps_actual, 1)
        sys_snap["capture_actual_count"] = capture_actual_count
        sys_snap["total_fps_target"] = round(total_fps_target, 1)
        sys_snap["total_errors_count"] = total_errors_count
        sys_snap["total_frames_skipped"] = total_frames_skipped
        sys_snap["errors_per_sec"] = round(errors_per_sec, 3)
        sys_snap["skipped_per_sec"] = round(skipped_per_sec, 3)
        sys_snap["running_count"] = running_count
        sys_snap["total_bytes_sent"] = total_bytes_sent
        sys_snap["bytes_per_sec"] = round(bytes_per_sec, 1)
        sys_snap["send_timing_avg_ms"] = (
            round(send_timing_sum_ms / send_timing_count, 2) if send_timing_count > 0 else 0.0
        )
        sys_snap["send_timing_max_ms"] = round(send_timing_max_ms, 2)
        sys_snap["send_timing_count"] = send_timing_count
        sys_snap["device_latency_avg_ms"] = device_latency_avg_ms
        sys_snap["device_latency_max_ms"] = device_latency_max_ms
        sys_snap["device_online_count"] = device_online_count
        sys_snap["device_total_count"] = device_total_count

        self._system.append(sys_snap)

        # Prune deques for targets no longer registered
        for tid in list(self._targets.keys()):
            if tid not in active_ids:
                del self._targets[tid]

    def get_history(self) -> dict:
        """Return all history for the API response."""
        return {
            "system": list(self._system),
            "targets": {tid: list(dq) for tid, dq in self._targets.items()},
        }
