"""Base provider for serial LED controllers (Adalight, AmbiLED, etc.).

Subclasses only need to override ``device_type`` and ``create_client()``.
All common serial-device logic (COM port validation, discovery, health
checks, power control via black frames) lives here.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, List

import numpy as np

from ledgrab.core.devices.led_client import (
    DeviceHealth,
    DiscoveredDevice,
    LEDClient,
    LEDDeviceProvider,
    ProviderDeps,
)
from ledgrab.core.devices.serial_transport import (
    list_serial_ports,
    port_exists,
)
from ledgrab.utils import get_logger

if TYPE_CHECKING:
    from ledgrab.core.devices.device_config import BaseDeviceConfig

logger = get_logger(__name__)


class SerialDeviceProvider(LEDDeviceProvider):
    """Base provider for serial LED controllers.

    Subclasses declare ``client_cls`` and ``_device_type`` as class attributes
    instead of overriding ``create_client`` / ``device_type`` directly.
    Subclasses that need custom client construction may still override
    ``create_client``.
    """

    # Class-level config — subclasses override these.
    client_cls = None  # e.g. AdalightClient
    _device_type: str = ""

    @property
    def device_type(self) -> str:
        return self._device_type

    def create_client(self, config: "BaseDeviceConfig", *, deps: ProviderDeps) -> LEDClient:
        if self.client_cls is None:
            raise NotImplementedError(
                f"{type(self).__name__} must set client_cls or override create_client"
            )
        return self.client_cls(
            config.device_url,
            led_count=config.led_count,
            baud_rate=getattr(config, "baud_rate", None),
        )

    @property
    def capabilities(self) -> set:
        # manual_led_count: user must specify LED count (can't auto-detect)
        # power_control: can blank LEDs by sending all-black pixels
        # brightness_control: software brightness (multiplies pixel values before sending)
        # health_check: serial port availability probe
        return {"manual_led_count", "power_control", "brightness_control", "health_check"}

    async def check_health(self, url: str, http_client, prev_health=None) -> DeviceHealth:
        # Generic serial port health check — enumerate COM ports
        from ledgrab.core.devices.adalight_client import AdalightClient

        return await AdalightClient.check_health(url, http_client, prev_health)

    async def validate_device(self, url: str) -> dict:
        """Validate that the serial port exists.

        Returns empty dict — serial devices don't report LED count,
        so it must be provided by the user.
        """
        from ledgrab.core.devices.adalight_client import parse_adalight_url

        port, _baud = parse_adalight_url(url)

        try:
            if not port_exists(port):
                available = ", ".join(p.device for p in list_serial_ports()) or "none"
                raise ValueError(f"Serial port {port} not found. Available ports: {available}")
        except ValueError:
            raise
        except Exception as e:
            raise ValueError(f"Failed to enumerate serial ports: {e}")

        logger.info(f"{self.device_type} device validated: port {port}")
        return {}

    async def discover(self, timeout: float = 3.0) -> List[DiscoveredDevice]:
        """Discover serial ports that could be LED devices."""
        try:
            ports = list_serial_ports()
            results = [
                DiscoveredDevice(
                    name=port_info.description,
                    url=port_info.device,
                    device_type=self.device_type,
                    ip=port_info.device,
                    mac="",
                    led_count=None,
                    version=None,
                )
                for port_info in ports
            ]
            logger.info(f"{self.device_type} serial port scan found {len(results)} port(s)")
            return results
        except Exception as e:
            logger.error(f"{self.device_type} serial port discovery failed: {e}")
            return []

    async def get_power(self, url: str, **kwargs) -> bool:
        # Serial devices have no hardware power query; assume on
        return True

    async def set_power(self, url: str, on: bool, **kwargs) -> None:
        """Turn device on/off by sending an all-black frame (off) or no-op (on).

        Requires kwargs: led_count (int), baud_rate (int | None).
        """
        if on:
            return  # "on" is a no-op — next processing frame lights LEDs up

        led_count = kwargs.get("led_count", 0)
        baud_rate = kwargs.get("baud_rate")
        if led_count <= 0:
            raise ValueError(
                f"led_count is required to send black frame to {self.device_type} device"
            )
        if self.client_cls is None:
            raise NotImplementedError(
                f"{type(self).__name__} must set client_cls or override set_power"
            )

        client = self.client_cls(url, led_count=led_count, baud_rate=baud_rate)
        try:
            await client.connect()
            black = np.zeros((led_count, 3), dtype=np.uint8)
            await client.send_pixels(black, brightness=255)
            logger.info(f"{self.device_type} power off: sent black frame to {url}")
        finally:
            await client.close()
