"""Abstract base class for target processors.

A TargetProcessor encapsulates the processing loop and state for a single
output target. Concrete subclasses (WledTargetProcessor, HALightTargetProcessor)
implement the target-specific capture→process→output pipeline.

ProcessorManager creates and owns TargetProcessor instances, delegating
all target-specific operations through the uniform interface defined here.
"""

from __future__ import annotations

import asyncio
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime
from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple

from ledgrab.utils import get_logger

logger = get_logger(__name__)

if TYPE_CHECKING:
    from ledgrab.core.processing.color_strip_stream_manager import ColorStripStreamManager
    from ledgrab.core.processing.live_stream_manager import LiveStreamManager
    from ledgrab.core.processing.value_stream import ValueStreamManager
    from ledgrab.core.capture.screen_overlay import OverlayManager
    from ledgrab.storage import DeviceStore
    from ledgrab.storage.picture_source_store import PictureSourceStore
    from ledgrab.storage.template_store import TemplateStore
    from ledgrab.storage.postprocessing_template_store import PostprocessingTemplateStore
    from ledgrab.storage.color_strip_processing_template_store import (
        ColorStripProcessingTemplateStore,
    )


# ---------------------------------------------------------------------------
# Shared dataclasses
# ---------------------------------------------------------------------------


@dataclass
class ProcessingMetrics:
    """Metrics for processing performance."""

    frames_processed: int = 0
    frames_skipped: int = 0
    frames_keepalive: int = 0
    errors_count: int = 0
    last_error: str | None = None
    last_update: datetime | None = None
    last_update_mono: float = (
        0.0  # monotonic timestamp for hot-path; lazily converted to last_update on read
    )
    start_time: datetime | None = None
    fps_actual: float = 0.0
    fps_potential: float = 0.0
    fps_current: int = 0
    # Per-stage timing (ms), averaged over last 10 frames
    # LED targets
    timing_extract_ms: float = 0.0
    timing_map_leds_ms: float = 0.0
    timing_smooth_ms: float = 0.0
    timing_send_ms: float = 0.0
    timing_total_ms: float = 0.0
    # KC targets
    timing_calc_colors_ms: float = 0.0
    timing_broadcast_ms: float = 0.0
    # Streaming liveness (HTTP probe during DDP)
    device_streaming_reachable: bool | None = None
    fps_effective: int = 0
    # Cumulative LED-payload bytes sent to the device. Aggregated across
    # all running targets in MetricsHistory to derive a per-second
    # network throughput sparkline. Counts the color-array payload only;
    # protocol overhead (DDP/UDP/IP headers) is sub-5 % for any
    # non-trivial LED count and is intentionally ignored to keep the
    # counter cheap (`np.ndarray.nbytes`, no per-frame allocation).
    bytes_sent: int = 0


@dataclass
class TargetContext:
    """Shared infrastructure bag passed to every TargetProcessor.

    Avoids circular imports — processors never import ProcessorManager.
    """

    live_stream_manager: "LiveStreamManager"
    overlay_manager: "OverlayManager"
    picture_source_store: "PictureSourceStore" | None = None
    capture_template_store: "TemplateStore" | None = None
    pp_template_store: "PostprocessingTemplateStore" | None = None
    device_store: "DeviceStore" | None = None
    color_strip_stream_manager: "ColorStripStreamManager" | None = None
    value_stream_manager: "ValueStreamManager" | None = None
    cspt_store: "ColorStripProcessingTemplateStore" | None = None
    fire_event: Callable[[dict], None] = lambda e: None
    is_test_mode_active: Callable[[str], bool] = lambda _: False
    ha_manager: Any | None = None  # HomeAssistantManager (avoid circular import)
    mqtt_manager: Any | None = None  # MQTTManager (avoid circular import)


# ---------------------------------------------------------------------------
# Abstract base class
# ---------------------------------------------------------------------------


class TargetProcessor(ABC):
    """Abstract base class for target processors.

    Lifecycle: register → start → (running loop) → stop → unregister
    """

    def __init__(self, target_id: str, ctx: TargetContext, picture_source_id: str = ""):
        self._target_id = target_id
        self._picture_source_id = picture_source_id
        self._ctx = ctx
        self._is_running = False
        self._task: asyncio.Task | None = None
        self._metrics = ProcessingMetrics()

    # ----- Properties -----

    @property
    def target_id(self) -> str:
        return self._target_id

    @property
    def picture_source_id(self) -> str:
        return self._picture_source_id

    @property
    def is_running(self) -> bool:
        return self._is_running

    @property
    def metrics(self) -> ProcessingMetrics:
        return self._metrics

    # ----- Lifecycle (concrete helpers + abstract hook) -----

    @abstractmethod
    async def start(self) -> None:
        """Start the processing loop.

        Implementations should acquire resources, reset metrics,
        create the asyncio task, and set _is_running = True.
        """
        ...

    @abstractmethod
    async def stop(self) -> None:
        """Stop the processing loop and release resources.

        Implementations should set _is_running = False, cancel the task,
        release the live stream, and close any connections.
        """
        ...

    async def cancel_task(self) -> None:
        """Cancel the processing task without restoring device state.

        Used by ``ProcessorManager.stop_all(restore_devices=False)`` at
        server shutdown when the user has chosen "Nothing" — LEDs should
        keep displaying their last frame, so we skip the per-device
        ``stop()`` path that sends restore frames. We still flip
        ``_is_running`` and await the cancellation so the loop's current
        iteration completes (no half-written frame on the wire).

        Subclasses with extra non-device cleanup (e.g. live-stream
        release) may override this; the default just stops the task.
        """
        self._is_running = False
        task = self._task
        if task is not None and not task.done():
            task.cancel()
            try:
                await task
            except asyncio.CancelledError:
                pass
            except Exception:
                # Log but don't propagate — caller is shutting down.
                logger.debug("Task raised during cancel_task", exc_info=True)
        self._task = None

    # ----- Settings -----

    @abstractmethod
    def update_settings(self, settings) -> None:
        """Update processing settings (type depends on subclass)."""
        ...

    def update_source(self, picture_source_id: str) -> None:
        """Update the picture source ID."""
        self._picture_source_id = picture_source_id

    def update_device(self, device_id: str) -> None:
        """Update device association. Raises for targets without devices."""
        raise ValueError(f"Target {self._target_id} does not support device assignment")

    def update_css_source(self, color_strip_source_id: str) -> None:
        """Update the color strip source. No-op for targets that don't use CSS."""
        pass

    # ----- Device / display info (overridden by device-aware subclasses) -----

    @property
    def device_id(self) -> str | None:
        """Device ID this processor streams to, or None."""
        return None

    @property
    def led_client(self):
        """Active LED client, or None."""
        return None

    def get_display_index(self) -> int | None:
        """Display index being captured, or None."""
        return None

    # ----- State / Metrics reporting -----

    @abstractmethod
    def get_state(self) -> dict:
        """Return current processing state as a JSON-serializable dict."""
        ...

    @abstractmethod
    def get_metrics(self) -> dict:
        """Return processing metrics as a JSON-serializable dict."""
        ...

    # ----- Optional capabilities (default no-ops) -----

    def supports_overlay(self) -> bool:
        """Whether this target supports screen overlay visualization."""
        return False

    async def start_overlay(self, target_name: str | None = None) -> None:
        """Start overlay visualization (if supported)."""
        raise NotImplementedError(f"{type(self).__name__} does not support overlays")

    async def stop_overlay(self) -> None:
        """Stop overlay visualization (if supported)."""
        raise NotImplementedError(f"{type(self).__name__} does not support overlays")

    def is_overlay_active(self) -> bool:
        """Check if overlay is currently active."""
        return False

    def supports_websocket(self) -> bool:
        """Whether this target supports WebSocket color streaming."""
        return False

    def add_ws_client(self, ws) -> None:
        """Add a WebSocket client for live color updates."""
        raise NotImplementedError(f"{type(self).__name__} does not support WebSockets")

    def remove_ws_client(self, ws) -> None:
        """Remove a WebSocket client."""
        raise NotImplementedError(f"{type(self).__name__} does not support WebSockets")

    def add_led_preview_client(self, ws) -> None:
        """Add a WebSocket client for live LED strip preview."""
        raise NotImplementedError(f"{type(self).__name__} does not support LED preview")

    def remove_led_preview_client(self, ws) -> None:
        """Remove a LED preview WebSocket client."""
        raise NotImplementedError(f"{type(self).__name__} does not support LED preview")

    def get_latest_colors(self) -> Dict[str, Tuple[int, int, int]]:
        """Get latest extracted colors (KC targets only)."""
        return {}
