"""Abstract base class for LED device communication clients and provider registry."""

from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Dict, List, Optional, Tuple, Union

import numpy as np


@dataclass
class DeviceHealth:
    """Health check result for an LED device."""

    online: bool = False
    latency_ms: Optional[float] = None
    last_checked: Optional[datetime] = None
    # Device-reported metadata (populated by type-specific health check)
    device_name: Optional[str] = None
    device_version: Optional[str] = None
    device_led_count: Optional[int] = None
    device_rgbw: Optional[bool] = None
    device_led_type: Optional[str] = None
    device_fps: Optional[int] = None
    error: Optional[str] = None


@dataclass
class DiscoveredDevice:
    """A device found via network discovery."""

    name: str
    url: str
    device_type: str
    ip: str
    mac: str
    led_count: Optional[int]
    version: Optional[str]


class LEDClient(ABC):
    """Abstract base for LED device communication.

    Lifecycle:
        client = SomeLEDClient(url, ...)
        await client.connect()
        state = await client.snapshot_device_state()  # save before streaming
        client.send_pixels_fast(pixels, brightness)   # if supports_fast_send
        await client.send_pixels(pixels, brightness)
        await client.restore_device_state(state)       # restore after streaming
        await client.close()

    Or as async context manager:
        async with SomeLEDClient(url, ...) as client:
            ...
    """

    @abstractmethod
    async def connect(self) -> bool:
        """Establish connection. Returns True on success, raises on failure."""
        ...

    @abstractmethod
    async def close(self) -> None:
        """Close the connection and release resources."""
        ...

    @property
    @abstractmethod
    def is_connected(self) -> bool:
        """Check if connected."""
        ...

    @abstractmethod
    async def send_pixels(
        self,
        pixels: Union[List[Tuple[int, int, int]], np.ndarray],
        brightness: int = 255,
    ) -> bool:
        """Send pixel colors to the LED device (async).

        Args:
            pixels: List of (R, G, B) tuples
            brightness: Global brightness (0-255)
        """
        ...

    @property
    def supports_fast_send(self) -> bool:
        """Whether send_pixels_fast() is available (e.g. DDP UDP)."""
        return False

    def send_pixels_fast(
        self,
        pixels: List[Tuple[int, int, int]],
        brightness: int = 255,
    ) -> None:
        """Synchronous fire-and-forget send for the hot loop.

        Override in subclasses that support a fast protocol (e.g. DDP).
        """
        raise NotImplementedError("send_pixels_fast not supported for this device type")

    @property
    def device_led_count(self) -> Optional[int]:
        """Actual LED count discovered after connect(). None if not available."""
        return None

    async def snapshot_device_state(self) -> Optional[dict]:
        """Snapshot device state before streaming starts.

        Override in subclasses that need to save/restore state around streaming.
        Returns a state dict to pass to restore_device_state(), or None.
        """
        return None

    async def restore_device_state(self, state: Optional[dict]) -> None:
        """Restore device state after streaming stops.

        Args:
            state: State dict returned by snapshot_device_state(), or None.
        """
        pass

    @classmethod
    async def check_health(
        cls,
        url: str,
        http_client,
        prev_health: Optional[DeviceHealth] = None,
    ) -> DeviceHealth:
        """Check device health without a full client connection.

        Override in subclasses for type-specific health probes.
        Default: mark as online with no metadata.

        Args:
            url: Device URL
            http_client: Shared httpx.AsyncClient for HTTP requests
            prev_health: Previous health result (for preserving cached metadata)
        """
        return DeviceHealth(online=True, last_checked=datetime.now(timezone.utc))

    async def __aenter__(self):
        await self.connect()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.close()


# ===== LED DEVICE PROVIDER =====

class LEDDeviceProvider(ABC):
    """Encapsulates everything about a specific LED device type.

    Implement one subclass per device type (WLED, etc.) and register it
    via register_provider(). All type-specific dispatch (client creation,
    health checks, discovery, validation, brightness) goes through the provider.
    """

    @property
    @abstractmethod
    def device_type(self) -> str:
        """Type identifier string, e.g. 'wled'."""
        ...

    @property
    def capabilities(self) -> set:
        """Capability set for this device type. Override to add capabilities."""
        return set()

    @abstractmethod
    def create_client(self, url: str, **kwargs) -> LEDClient:
        """Create a connected-ready LEDClient for this device type."""
        ...

    @abstractmethod
    async def check_health(self, url: str, http_client, prev_health=None) -> DeviceHealth:
        """Check device health without a full client connection."""
        ...

    @abstractmethod
    async def validate_device(self, url: str) -> dict:
        """Validate a device URL before adding it.

        Returns:
            dict with at least {'led_count': int}

        Raises:
            Exception on validation failure (caller converts to HTTP error).
        """
        ...

    async def discover(self, timeout: float = 3.0) -> List[DiscoveredDevice]:
        """Discover devices on the network.

        Override in providers that support discovery. Default: empty list.
        """
        return []

    async def get_brightness(self, url: str) -> int:
        """Get device brightness (0-255). Override if capabilities include brightness_control."""
        raise NotImplementedError

    async def set_brightness(self, url: str, brightness: int) -> None:
        """Set device brightness (0-255). Override if capabilities include brightness_control."""
        raise NotImplementedError

    async def get_power(self, url: str, **kwargs) -> bool:
        """Get device power state. Override if capabilities include power_control."""
        raise NotImplementedError

    async def set_power(self, url: str, on: bool, **kwargs) -> None:
        """Set device power state. Override if capabilities include power_control."""
        raise NotImplementedError

    async def set_color(self, url: str, color: Tuple[int, int, int], **kwargs) -> None:
        """Set all LEDs to a solid color. Override if capabilities include static_color."""
        raise NotImplementedError


# ===== PROVIDER REGISTRY =====

_provider_registry: Dict[str, LEDDeviceProvider] = {}


def register_provider(provider: LEDDeviceProvider) -> None:
    """Register a device provider."""
    _provider_registry[provider.device_type] = provider


def get_provider(device_type: str) -> LEDDeviceProvider:
    """Get the provider for a device type.

    Raises:
        ValueError: If device_type is unknown.
    """
    provider = _provider_registry.get(device_type)
    if not provider:
        raise ValueError(f"Unknown LED device type: {device_type}")
    return provider


def get_all_providers() -> Dict[str, LEDDeviceProvider]:
    """Return all registered providers."""
    return dict(_provider_registry)


# ===== FACTORY FUNCTIONS (delegate to providers) =====

def create_led_client(device_type: str, url: str, **kwargs) -> LEDClient:
    """Factory: create the right LEDClient subclass for a device type."""
    return get_provider(device_type).create_client(url, **kwargs)


async def check_device_health(
    device_type: str,
    url: str,
    http_client,
    prev_health: Optional[DeviceHealth] = None,
) -> DeviceHealth:
    """Factory: dispatch health check to the right provider."""
    return await get_provider(device_type).check_health(url, http_client, prev_health)


def get_device_capabilities(device_type: str) -> set:
    """Return the capability set for a device type."""
    try:
        return get_provider(device_type).capabilities
    except ValueError:
        return set()


# ===== AUTO-REGISTER BUILT-IN PROVIDERS =====

def _register_builtin_providers():
    from wled_controller.core.devices.wled_provider import WLEDDeviceProvider
    register_provider(WLEDDeviceProvider())

    from wled_controller.core.devices.adalight_provider import AdalightDeviceProvider
    register_provider(AdalightDeviceProvider())

    from wled_controller.core.devices.ambiled_provider import AmbiLEDDeviceProvider
    register_provider(AmbiLEDDeviceProvider())

    from wled_controller.core.devices.mock_provider import MockDeviceProvider
    register_provider(MockDeviceProvider())

    from wled_controller.core.devices.mqtt_provider import MQTTDeviceProvider
    register_provider(MQTTDeviceProvider())

    from wled_controller.core.devices.ws_provider import WSDeviceProvider
    register_provider(WSDeviceProvider())

    from wled_controller.core.devices.openrgb_provider import OpenRGBDeviceProvider
    register_provider(OpenRGBDeviceProvider())

    from wled_controller.core.devices.dmx_provider import DMXDeviceProvider
    register_provider(DMXDeviceProvider())

    from wled_controller.core.devices.espnow_provider import ESPNowDeviceProvider
    register_provider(ESPNowDeviceProvider())

    from wled_controller.core.devices.hue_provider import HueDeviceProvider
    register_provider(HueDeviceProvider())

    from wled_controller.core.devices.usbhid_provider import USBHIDDeviceProvider
    register_provider(USBHIDDeviceProvider())

    from wled_controller.core.devices.spi_provider import SPIDeviceProvider
    register_provider(SPIDeviceProvider())

    from wled_controller.core.devices.chroma_provider import ChromaDeviceProvider
    register_provider(ChromaDeviceProvider())

    from wled_controller.core.devices.gamesense_provider import GameSenseDeviceProvider
    register_provider(GameSenseDeviceProvider())

    from wled_controller.core.devices.demo_provider import DemoDeviceProvider
    register_provider(DemoDeviceProvider())


_register_builtin_providers()
