"""Device storage using SQLite."""

import uuid
from datetime import datetime, timezone
from typing import TYPE_CHECKING, List, Optional

from ledgrab.storage.base_sqlite_store import BaseSqliteStore
from ledgrab.storage.database import Database
from ledgrab.utils import get_logger

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

logger = get_logger(__name__)


class Device:
    """Represents a WLED device configuration.

    A device holds connection state and output settings.
    Calibration, processing settings, and picture source assignments
    now live on ColorStripSource and WledOutputTarget respectively.
    """

    def __init__(
        self,
        device_id: str,
        name: str,
        url: str,
        led_count: int,
        enabled: bool = True,
        device_type: str = "wled",
        baud_rate: Optional[int] = None,
        software_brightness: int = 255,
        auto_shutdown: bool = False,
        send_latency_ms: int = 0,
        rgbw: bool = False,
        zone_mode: str = "combined",
        tags: Optional[List[str]] = None,
        # DMX (Art-Net / sACN) fields
        dmx_protocol: str = "artnet",
        dmx_start_universe: int = 0,
        dmx_start_channel: int = 1,
        # ESP-NOW fields
        espnow_peer_mac: str = "",
        espnow_channel: int = 1,
        # Philips Hue fields
        hue_username: str = "",
        hue_client_key: str = "",
        hue_entertainment_group_id: str = "",
        # SPI Direct fields
        spi_speed_hz: int = 800000,
        spi_led_type: str = "WS2812B",
        # Razer Chroma fields
        chroma_device_type: str = "chromalink",
        # SteelSeries GameSense fields
        gamesense_device_type: str = "keyboard",
        # BLE controller fields (SP110E / Triones / Zengge / Govee)
        ble_family: str = "",
        ble_govee_key: str = "",
        # Default color strip processing template
        default_css_processing_template_id: str = "",
        # Group device fields
        group_device_ids: Optional[List[str]] = None,
        group_mode: str = "sequence",
        created_at: Optional[datetime] = None,
        updated_at: Optional[datetime] = None,
    ):
        self.id = device_id
        self.name = name
        self.url = url
        self.led_count = led_count
        self.enabled = enabled
        self.device_type = device_type
        self.baud_rate = baud_rate
        self.software_brightness = software_brightness
        self.auto_shutdown = auto_shutdown
        self.send_latency_ms = send_latency_ms
        self.rgbw = rgbw
        self.zone_mode = zone_mode
        self.tags = list(tags) if tags else []
        self.dmx_protocol = dmx_protocol
        self.dmx_start_universe = dmx_start_universe
        self.dmx_start_channel = dmx_start_channel
        self.espnow_peer_mac = espnow_peer_mac
        self.espnow_channel = espnow_channel
        self.hue_username = hue_username
        self.hue_client_key = hue_client_key
        self.hue_entertainment_group_id = hue_entertainment_group_id
        self.spi_speed_hz = spi_speed_hz
        self.spi_led_type = spi_led_type
        self.chroma_device_type = chroma_device_type
        self.gamesense_device_type = gamesense_device_type
        self.ble_family = ble_family
        self.ble_govee_key = ble_govee_key
        self.default_css_processing_template_id = default_css_processing_template_id
        self.group_device_ids = group_device_ids or []
        self.group_mode = group_mode
        self.created_at = created_at or datetime.now(timezone.utc)
        self.updated_at = updated_at or datetime.now(timezone.utc)

    def to_config(self) -> "DeviceConfig":
        """Return a typed, immutable config snapshot for this device.

        This is the single place that maps the flat Device storage model to the
        correct per-provider config subclass. test_mode_active is runtime state
        (not stored) and always defaults to False here.
        """
        from ledgrab.core.devices.device_config import (
            AdalightConfig,
            AmbiLEDConfig,
            BLEConfig,
            ChromaConfig,
            DemoConfig,
            DMXConfig,
            ESPNowConfig,
            GameSenseConfig,
            GroupConfig,
            HueConfig,
            MockConfig,
            MQTTConfig,
            OpenRGBConfig,
            SPIConfig,
            USBHIDConfig,
            WLEDConfig,
            WSConfig,
        )

        base = dict(
            device_id=self.id,
            device_url=self.url,
            led_count=self.led_count,
            software_brightness=self.software_brightness,
            auto_shutdown=self.auto_shutdown,
            rgbw=self.rgbw,
        )
        dt = self.device_type
        if dt == "wled":
            return WLEDConfig(**base)
        if dt == "adalight":
            return AdalightConfig(**base, baud_rate=self.baud_rate)
        if dt == "ambiled":
            return AmbiLEDConfig(**base, baud_rate=self.baud_rate)
        if dt == "dmx":
            return DMXConfig(
                **base,
                dmx_protocol=self.dmx_protocol,
                dmx_start_universe=self.dmx_start_universe,
                dmx_start_channel=self.dmx_start_channel,
            )
        if dt == "espnow":
            return ESPNowConfig(
                **base,
                baud_rate=self.baud_rate,
                espnow_peer_mac=self.espnow_peer_mac,
                espnow_channel=self.espnow_channel,
            )
        if dt == "hue":
            return HueConfig(
                **base,
                hue_username=self.hue_username,
                hue_client_key=self.hue_client_key,
                hue_entertainment_group_id=self.hue_entertainment_group_id,
            )
        if dt == "spi":
            return SPIConfig(**base, spi_speed_hz=self.spi_speed_hz, spi_led_type=self.spi_led_type)
        if dt == "chroma":
            return ChromaConfig(**base, chroma_device_type=self.chroma_device_type)
        if dt == "gamesense":
            return GameSenseConfig(**base, gamesense_device_type=self.gamesense_device_type)
        if dt == "ble":
            return BLEConfig(**base, ble_family=self.ble_family, ble_govee_key=self.ble_govee_key)
        if dt == "group":
            return GroupConfig(
                **base,
                group_mode=self.group_mode,
                group_device_ids=list(self.group_device_ids),
            )
        if dt == "openrgb":
            return OpenRGBConfig(**base, zone_mode=self.zone_mode)
        if dt == "mock":
            return MockConfig(**base, send_latency_ms=self.send_latency_ms)
        if dt == "demo":
            return DemoConfig(**base, send_latency_ms=self.send_latency_ms)
        if dt == "mqtt":
            return MQTTConfig(**base)
        if dt == "ws":
            return WSConfig(**base)
        if dt == "usbhid":
            return USBHIDConfig(**base)
        raise ValueError(f"Unknown device type: {dt!r}")

    def to_dict(self) -> dict:
        """Convert device to dictionary."""
        d = {
            "id": self.id,
            "name": self.name,
            "url": self.url,
            "led_count": self.led_count,
            "enabled": self.enabled,
            "device_type": self.device_type,
            "created_at": self.created_at.isoformat(),
            "updated_at": self.updated_at.isoformat(),
        }
        if self.baud_rate is not None:
            d["baud_rate"] = self.baud_rate
        if self.software_brightness != 255:
            d["software_brightness"] = self.software_brightness
        if self.auto_shutdown:
            d["auto_shutdown"] = True
        if self.send_latency_ms:
            d["send_latency_ms"] = self.send_latency_ms
        if self.rgbw:
            d["rgbw"] = True
        if self.zone_mode != "combined":
            d["zone_mode"] = self.zone_mode
        if self.tags:
            d["tags"] = self.tags
        if self.dmx_protocol != "artnet":
            d["dmx_protocol"] = self.dmx_protocol
        if self.dmx_start_universe != 0:
            d["dmx_start_universe"] = self.dmx_start_universe
        if self.dmx_start_channel != 1:
            d["dmx_start_channel"] = self.dmx_start_channel
        if self.espnow_peer_mac:
            d["espnow_peer_mac"] = self.espnow_peer_mac
        if self.espnow_channel != 1:
            d["espnow_channel"] = self.espnow_channel
        if self.hue_username:
            d["hue_username"] = self.hue_username
        if self.hue_client_key:
            d["hue_client_key"] = self.hue_client_key
        if self.hue_entertainment_group_id:
            d["hue_entertainment_group_id"] = self.hue_entertainment_group_id
        if self.spi_speed_hz != 800000:
            d["spi_speed_hz"] = self.spi_speed_hz
        if self.spi_led_type != "WS2812B":
            d["spi_led_type"] = self.spi_led_type
        if self.chroma_device_type != "chromalink":
            d["chroma_device_type"] = self.chroma_device_type
        if self.gamesense_device_type != "keyboard":
            d["gamesense_device_type"] = self.gamesense_device_type
        if self.ble_family:
            d["ble_family"] = self.ble_family
        if self.ble_govee_key:
            d["ble_govee_key"] = self.ble_govee_key
        if self.default_css_processing_template_id:
            d["default_css_processing_template_id"] = self.default_css_processing_template_id
        if self.group_device_ids:
            d["group_device_ids"] = self.group_device_ids
        if self.group_mode != "sequence":
            d["group_mode"] = self.group_mode
        return d

    @classmethod
    def from_dict(cls, data: dict) -> "Device":
        """Create device from dictionary."""
        return cls(
            device_id=data["id"],
            name=data["name"],
            url=data["url"],
            led_count=data["led_count"],
            enabled=data.get("enabled", True),
            device_type=data.get("device_type", "wled"),
            baud_rate=data.get("baud_rate"),
            software_brightness=data.get("software_brightness", 255),
            auto_shutdown=data.get("auto_shutdown", False),
            send_latency_ms=data.get("send_latency_ms", 0),
            rgbw=data.get("rgbw", False),
            zone_mode=data.get("zone_mode", "combined"),
            tags=data.get("tags", []),
            dmx_protocol=data.get("dmx_protocol", "artnet"),
            dmx_start_universe=data.get("dmx_start_universe", 0),
            dmx_start_channel=data.get("dmx_start_channel", 1),
            espnow_peer_mac=data.get("espnow_peer_mac", ""),
            espnow_channel=data.get("espnow_channel", 1),
            hue_username=data.get("hue_username", ""),
            hue_client_key=data.get("hue_client_key", ""),
            hue_entertainment_group_id=data.get("hue_entertainment_group_id", ""),
            spi_speed_hz=data.get("spi_speed_hz", 800000),
            spi_led_type=data.get("spi_led_type", "WS2812B"),
            chroma_device_type=data.get("chroma_device_type", "chromalink"),
            gamesense_device_type=data.get("gamesense_device_type", "keyboard"),
            ble_family=data.get("ble_family", ""),
            ble_govee_key=data.get("ble_govee_key", ""),
            default_css_processing_template_id=data.get("default_css_processing_template_id", ""),
            group_device_ids=data.get("group_device_ids", []),
            group_mode=data.get("group_mode", "sequence"),
            created_at=datetime.fromisoformat(
                data.get("created_at", datetime.now(timezone.utc).isoformat())
            ),
            updated_at=datetime.fromisoformat(
                data.get("updated_at", datetime.now(timezone.utc).isoformat())
            ),
        )


# Fields that can be updated (all Device.__init__ params except identity/timestamps)
_UPDATABLE_FIELDS: frozenset[str] = frozenset(
    {
        "name",
        "url",
        "led_count",
        "enabled",
        "device_type",
        "baud_rate",
        "software_brightness",
        "auto_shutdown",
        "send_latency_ms",
        "rgbw",
        "zone_mode",
        "tags",
        "dmx_protocol",
        "dmx_start_universe",
        "dmx_start_channel",
        "espnow_peer_mac",
        "espnow_channel",
        "hue_username",
        "hue_client_key",
        "hue_entertainment_group_id",
        "spi_speed_hz",
        "spi_led_type",
        "chroma_device_type",
        "gamesense_device_type",
        "ble_family",
        "ble_govee_key",
        "default_css_processing_template_id",
        "group_device_ids",
        "group_mode",
    }
)


class DeviceStore(BaseSqliteStore[Device]):
    """Persistent storage for WLED devices."""

    _table_name = "devices"
    _entity_name = "Device"

    def __init__(self, db: Database):
        super().__init__(db, Device.from_dict)
        logger.info(f"Device store initialized with {len(self._items)} devices")

    # ── Backward-compat aliases (thin re-export of base methods) ─
    get_device = BaseSqliteStore.get
    get_all_devices = BaseSqliteStore.get_all
    delete_device = BaseSqliteStore.delete

    # ── Create / Update ──────────────────────────────────────────

    def create_device(
        self,
        name: str,
        url: str,
        led_count: int,
        device_type: str = "wled",
        baud_rate: Optional[int] = None,
        auto_shutdown: bool = False,
        send_latency_ms: int = 0,
        rgbw: bool = False,
        zone_mode: str = "combined",
        tags: Optional[List[str]] = None,
        dmx_protocol: str = "artnet",
        dmx_start_universe: int = 0,
        dmx_start_channel: int = 1,
        espnow_peer_mac: str = "",
        espnow_channel: int = 1,
        hue_username: str = "",
        hue_client_key: str = "",
        hue_entertainment_group_id: str = "",
        spi_speed_hz: int = 800000,
        spi_led_type: str = "WS2812B",
        chroma_device_type: str = "chromalink",
        gamesense_device_type: str = "keyboard",
        ble_family: str = "",
        ble_govee_key: str = "",
        group_device_ids: Optional[List[str]] = None,
        group_mode: str = "sequence",
    ) -> Device:
        """Create a new device."""
        with self._lock:
            self._check_name_unique(name)

            device_id = f"device_{uuid.uuid4().hex[:8]}"

            # Mock devices use their device ID as the URL authority
            if device_type == "mock":
                url = f"mock://{device_id}"

            device = Device(
                device_id=device_id,
                name=name,
                url=url,
                led_count=led_count,
                device_type=device_type,
                baud_rate=baud_rate,
                auto_shutdown=auto_shutdown,
                send_latency_ms=send_latency_ms,
                rgbw=rgbw,
                zone_mode=zone_mode,
                tags=tags or [],
                dmx_protocol=dmx_protocol,
                dmx_start_universe=dmx_start_universe,
                dmx_start_channel=dmx_start_channel,
                espnow_peer_mac=espnow_peer_mac,
                espnow_channel=espnow_channel,
                hue_username=hue_username,
                hue_client_key=hue_client_key,
                hue_entertainment_group_id=hue_entertainment_group_id,
                spi_speed_hz=spi_speed_hz,
                spi_led_type=spi_led_type,
                chroma_device_type=chroma_device_type,
                gamesense_device_type=gamesense_device_type,
                ble_family=ble_family,
                ble_govee_key=ble_govee_key,
                group_device_ids=group_device_ids or [],
                group_mode=group_mode,
            )

            self._items[device_id] = device
            self._save_item(device_id, device)

            logger.info(f"Created device {device_id}: {name}")
            return device

    def update_device(self, device_id: str, **kwargs) -> Device:
        """Update device fields.

        Pass any updatable Device field as a keyword argument.
        ``None`` values are ignored (no change).
        """
        with self._lock:
            device = self.get(device_id)  # raises ValueError if not found

            # Collect updates (ignore None values and unknown fields)
            updates = {
                key: value
                for key, value in kwargs.items()
                if value is not None and key in _UPDATABLE_FIELDS
            }

            # Check name uniqueness if name is being changed
            new_name = updates.get("name")
            if new_name is not None and new_name != device.name:
                self._check_name_unique(new_name, exclude_id=device_id)

            # Build new Device from existing fields + updates (immutable pattern)
            device_fields = device.to_dict()
            # Map 'id' back to 'device_id' for the constructor
            device_fields["device_id"] = device_fields.pop("id")
            # Restore datetime objects (to_dict serializes them as ISO strings)
            device_fields["created_at"] = device.created_at
            device_fields["updated_at"] = datetime.now(timezone.utc)
            # Apply updates
            device_fields.update(updates)

            new_device = Device(**device_fields)
            self._items[device_id] = new_device
            self._save_item(device_id, new_device)

            logger.info(f"Updated device {device_id}")
            return new_device

    # ── Unique helpers ───────────────────────────────────────────

    def device_exists(self, device_id: str) -> bool:
        """Check if device exists."""
        return device_id in self._items

    # ── Group helpers ───────────────────────────────────────────

    def validate_group_no_cycles(
        self,
        device_id: Optional[str],
        group_device_ids: List[str],
    ) -> None:
        """Raise ValueError if adding these children would create a cycle.

        Uses DFS with backtracking — diamond DAGs (A→B, A→C, B→D, C→D) are
        allowed; only true cycles are rejected.
        """
        ancestors = set()
        if device_id:
            ancestors.add(device_id)

        def _dfs(child_ids: List[str]) -> None:
            for cid in child_ids:
                if cid in ancestors:
                    raise ValueError(f"Circular group reference detected: {cid}")
                device = self._items.get(cid)
                if device is None:
                    raise ValueError(f"Referenced device not found: {cid}")
                if device.group_device_ids:
                    ancestors.add(cid)
                    _dfs(device.group_device_ids)
                    ancestors.discard(cid)

        _dfs(group_device_ids)

    def resolve_group_led_count(
        self,
        device_ids: List[str],
        _seen: Optional[set] = None,
    ) -> int:
        """Sum led_counts of devices, recursively resolving nested sequence groups."""
        if _seen is None:
            _seen = set()
        total = 0
        for did in device_ids:
            if did in _seen:
                continue
            _seen.add(did)
            device = self._items.get(did)
            if device is None:
                continue
            if (
                device.device_type == "group"
                and device.group_mode == "sequence"
                and device.group_device_ids
            ):
                total += self.resolve_group_led_count(device.group_device_ids, _seen)
            else:
                total += device.led_count
        return total

    def resolve_group_max_led_count(
        self,
        device_ids: List[str],
    ) -> int:
        """Return max led_count among children (for independent mode default)."""
        max_count = 0
        for did in device_ids:
            device = self._items.get(did)
            if device is None:
                continue
            max_count = max(max_count, device.led_count)
        return max_count or 1

    def get_groups_referencing(self, device_id: str) -> List["Device"]:
        """Return all group devices whose group_device_ids contains device_id."""
        return [
            d
            for d in self._items.values()
            if d.device_type == "group" and device_id in d.group_device_ids
        ]

    def clear(self):
        """Clear all devices (for testing)."""
        self._items.clear()
        self._db.delete_all(self._table_name)
        logger.warning("Cleared all devices from storage")
