"""BLE LED controller protocols.

Each submodule implements one controller family's wire protocol as a set
of pure byte-encoding functions. The :class:`BLEProtocol` contract defines
the minimal surface every family must expose; :func:`get_protocol` looks
one up by family identifier.

Protocols live here as pure functions (no BLE dependency) so they can be
unit-tested without hardware and without the ``bleak`` package installed.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Callable, Dict, Tuple


@dataclass(frozen=True)
class BLEProtocol:
    """Wire protocol for one BLE LED controller family.

    Attributes:
        family: Short identifier (``sp110e``, ``triones``, ``zengge``, ``govee``).
        display_name: Human-readable name for UIs.
        service_uuid: GATT service UUID containing the write characteristic.
        write_char_uuid: Write characteristic UUID.
        write_with_response: If True, use Write Request; else Write Without Response.
        encode_color: ``(r, g, b, brightness) -> bytes`` — frame setting a solid color.
        encode_power: ``on -> bytes`` — frame toggling power.
        name_prefixes: Advertisement-name prefixes that identify this family.
        init_writes: Optional vendor-specific handshake performed right after
            GATT connect. Each tuple is ``(char_uuid, payload)``. Some
            controllers (notably SP110E) drop the link within ~1 s if this
            handshake is missing, which is why our first real write hangs
            for 30 s and then fails — the peripheral has already torn down
            the connection, we just haven't noticed.
    """

    family: str
    display_name: str
    service_uuid: str
    write_char_uuid: str
    write_with_response: bool
    encode_color: Callable[[int, int, int, int], bytes]
    encode_power: Callable[[bool], bytes]
    name_prefixes: Tuple[str, ...]
    init_writes: Tuple[Tuple[str, bytes], ...] = ()


_registry: Dict[str, BLEProtocol] = {}


def register_protocol(protocol: BLEProtocol) -> None:
    """Register a protocol so :func:`get_protocol` can find it."""
    _registry[protocol.family] = protocol


def get_protocol(family: str) -> BLEProtocol:
    """Look up a registered protocol by family identifier."""
    try:
        return _registry[family]
    except KeyError as exc:
        raise ValueError(f"Unknown BLE family: {family!r}") from exc


def all_protocols() -> Dict[str, BLEProtocol]:
    """Return a copy of the registry (family → protocol)."""
    return dict(_registry)


def identify_family(advertised_name: str) -> str | None:
    """Best-effort family detection from a BLE advertisement name.

    Returns the family identifier if the name matches a known prefix,
    otherwise ``None``.
    """
    if not advertised_name:
        return None
    for proto in _registry.values():
        for prefix in proto.name_prefixes:
            if advertised_name.startswith(prefix):
                return proto.family
    return None


def identify_family_by_service_uuids(service_uuids: Tuple[str, ...]) -> str | None:
    """Best-effort family detection from advertised GATT service UUIDs.

    Returns the first matching family or ``None``. Families that share the
    same service UUID (e.g. SP110E and Zengge both use FFE0) are matched in
    registration order — SP110E is registered first so it wins the tie.
    """
    if not service_uuids:
        return None
    uuids_lower = {u.lower() for u in service_uuids}
    for proto in _registry.values():
        if proto.service_uuid.lower() in uuids_lower:
            return proto.family
    return None


def _register_builtins() -> None:
    # Imported lazily to avoid circular imports during module init.
    from ledgrab.core.devices.ble_protocols import govee, sp110e, triones, zengge

    register_protocol(sp110e.PROTOCOL)
    register_protocol(triones.PROTOCOL)
    register_protocol(zengge.PROTOCOL)
    register_protocol(govee.PROTOCOL)


_register_builtins()
