"""BLE device provider — dispatch for BLE LED controllers."""

from __future__ import annotations

from typing import TYPE_CHECKING, List, Tuple

from ledgrab.core.devices.ble_client import BLEClient, _strip_ble_scheme
from ledgrab.core.devices.ble_protocols import (
    all_protocols,
    identify_family,
    identify_family_by_service_uuids,
)
from ledgrab.core.devices.ble_transport import scan as ble_scan
from ledgrab.core.devices.led_client import (
    DeviceHealth,
    DiscoveredDevice,
    LEDClient,
    LEDDeviceProvider,
    ProviderDeps,
)
from ledgrab.utils import get_logger

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

logger = get_logger(__name__)


class BLEDeviceProvider(LEDDeviceProvider):
    """Provider for BLE LED controllers (SP110E / Triones / Zengge / Govee).

    URL format: ``ble://<address>``. The controller family is stored on
    the device record as ``ble_family`` — not in the URL — because the
    same MAC can advertise under different protocol variants depending
    on firmware.
    """

    @property
    def device_type(self) -> str:
        return "ble"

    @property
    def capabilities(self) -> set:
        return {
            "manual_led_count",
            "power_control",
            "static_color",
            # BLE cannot do per-pixel streaming — no fast_send / brightness_control.
        }

    def create_client(self, config: "BLEConfig", *, deps: ProviderDeps) -> LEDClient:
        if not config.ble_family:
            raise ValueError(
                "BLE device requires 'ble_family' (one of: "
                + ", ".join(sorted(all_protocols()))
                + ")"
            )
        return BLEClient(
            url=config.device_url,
            ble_family=config.ble_family,
            led_count=config.led_count,
            ble_govee_key=config.ble_govee_key,
        )

    async def check_health(self, url: str, http_client, prev_health=None) -> DeviceHealth:
        return await BLEClient.check_health(url, http_client, prev_health)

    async def validate_device(self, url: str) -> dict:
        """BLE has no cheap, non-intrusive probe — a GATT connect is the only real signal
        and would contend with an active streaming session, so we only sanity-check shape.

        The live connection will surface a clear error on first ``send_pixels``
        if the address is wrong or the protocol family was mis-picked.
        """
        address = _strip_ble_scheme(url)
        if not address:
            raise ValueError("BLE device URL must be 'ble://<address>'")
        return {}

    async def discover(self, timeout: float = 4.0) -> List[DiscoveredDevice]:
        """Scan for BLE peripherals and classify by advertised name prefix."""
        try:
            found = await ble_scan(timeout=timeout)
        except RuntimeError as exc:
            logger.warning("BLE discovery unavailable: %s", exc)
            return []

        results: List[DiscoveredDevice] = []
        for device in found:
            family = identify_family(device.name)
            if family is None:
                # Windows often omits the advertisement name for non-paired
                # devices — fall back to service UUID matching.
                family = identify_family_by_service_uuids(device.service_uuids)
                if family is not None:
                    logger.debug(
                        "BLE device %s (%s) identified by service UUID as %s",
                        device.address,
                        device.name,
                        family,
                    )
            if family is not None:
                display_name = f"{device.name} [{family}]"
            else:
                # Unknown device — include it so the user can add it manually.
                logger.debug(
                    "BLE device %s (%s) does not match a known LED family",
                    device.address,
                    device.name,
                )
                display_name = device.name
            results.append(
                DiscoveredDevice(
                    name=display_name,
                    url=f"ble://{device.address}",
                    device_type="ble",
                    ip=device.address,
                    mac=device.address,
                    led_count=None,
                    version=None,
                    ble_family=family,
                )
            )
        return results

    async def set_power(self, url: str, on: bool, **kwargs) -> None:
        """Open a short-lived BLE session to toggle power, then close."""
        family = kwargs.get("ble_family")
        if not family:
            raise ValueError("BLE power control requires 'ble_family'")
        client = BLEClient(
            url=url, ble_family=family, ble_govee_key=kwargs.get("ble_govee_key", "")
        )
        try:
            await client.connect()
            await client.set_power(on)
        finally:
            await client.close()

    async def set_color(self, url: str, color: Tuple[int, int, int], **kwargs) -> None:
        """Open a short-lived BLE session to set a solid color, then close."""
        family = kwargs.get("ble_family")
        if not family:
            raise ValueError("BLE color control requires 'ble_family'")
        brightness: int = kwargs.get("brightness", 255)
        client = BLEClient(
            url=url, ble_family=family, ble_govee_key=kwargs.get("ble_govee_key", "")
        )
        try:
            await client.connect()
            await client.send_pixels([color], brightness=brightness)
        finally:
            await client.close()

    async def get_power(self, url: str, **kwargs) -> bool:
        # None of the supported BLE protocols expose a "get power state" read.
        # Treat as always-on so the UI doesn't show a misleading OFF indicator.
        return True

    def list_families(self) -> List[dict]:
        """Enumerate registered BLE protocol families (for the frontend device form)."""
        return [
            {"family": proto.family, "display_name": proto.display_name}
            for proto in all_protocols().values()
        ]


def get_ble_provider() -> "BLEDeviceProvider" | None:
    """Return the registered BLE provider, or ``None`` if not registered."""
    from ledgrab.core.devices.led_client import get_provider

    try:
        provider = get_provider("ble")
    except ValueError:
        return None
    return provider if isinstance(provider, BLEDeviceProvider) else None
