"""WLED device provider — consolidates all WLED-specific dispatch logic."""

import asyncio
from typing import List, Optional, Tuple

import httpx
from zeroconf import ServiceStateChange
from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf

from wled_controller.core.devices.led_client import (
    DeviceHealth,
    DiscoveredDevice,
    LEDClient,
    LEDDeviceProvider,
)
from wled_controller.utils import get_logger

logger = get_logger(__name__)

WLED_MDNS_TYPE = "_wled._tcp.local."
DEFAULT_SCAN_TIMEOUT = 3.0


class WLEDDeviceProvider(LEDDeviceProvider):
    """Provider for WLED LED controllers."""

    def __init__(self):
        self._http_client: Optional[httpx.AsyncClient] = None
        self._client_lock = asyncio.Lock()

    async def _get_client(self) -> httpx.AsyncClient:
        """Return a shared HTTP client (connection-pooled, thread-safe init)."""
        if self._http_client is not None and not self._http_client.is_closed:
            return self._http_client
        async with self._client_lock:
            if self._http_client is None or self._http_client.is_closed:
                self._http_client = httpx.AsyncClient(timeout=5.0)
        return self._http_client

    async def close(self):
        """Close the shared HTTP client."""
        if self._http_client is not None and not self._http_client.is_closed:
            await self._http_client.aclose()
            self._http_client = None

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

    @property
    def capabilities(self) -> set:
        return {"brightness_control", "power_control", "standby_required", "static_color", "health_check", "auto_restore"}

    def create_client(self, url: str, **kwargs) -> LEDClient:
        from wled_controller.core.devices.wled_client import WLEDClient
        return WLEDClient(
            url,
            use_ddp=kwargs.get("use_ddp", False),
        )

    async def check_health(self, url: str, http_client, prev_health=None) -> DeviceHealth:
        from wled_controller.core.devices.wled_client import WLEDClient
        return await WLEDClient.check_health(url, http_client, prev_health)

    async def validate_device(self, url: str) -> dict:
        """Validate a WLED device URL by probing /json/info.

        Returns:
            dict with 'led_count' key.

        Raises:
            httpx.ConnectError: Device unreachable.
            httpx.TimeoutException: Connection timed out.
            ValueError: Invalid LED count.
        """
        url = url.rstrip("/")
        async with httpx.AsyncClient(timeout=5) as client:
            response = await client.get(f"{url}/json/info")
            response.raise_for_status()
            wled_info = response.json()
            led_count = wled_info.get("leds", {}).get("count")
            if not led_count or led_count < 1:
                raise ValueError(
                    f"WLED device at {url} reported invalid LED count: {led_count}"
                )
            logger.info(
                f"WLED device reachable: {wled_info.get('name', 'Unknown')} "
                f"v{wled_info.get('ver', '?')} ({led_count} LEDs)"
            )
            return {"led_count": led_count}

    # ===== DISCOVERY =====

    async def discover(self, timeout: float = DEFAULT_SCAN_TIMEOUT) -> List[DiscoveredDevice]:
        """Scan the local network for WLED devices via mDNS."""
        discovered: dict[str, AsyncServiceInfo] = {}

        def on_state_change(**kwargs):
            service_type = kwargs.get("service_type", "")
            name = kwargs.get("name", "")
            state_change = kwargs.get("state_change")
            if state_change in (ServiceStateChange.Added, ServiceStateChange.Updated):
                discovered[name] = AsyncServiceInfo(service_type, name)

        aiozc = AsyncZeroconf()
        browser = AsyncServiceBrowser(
            aiozc.zeroconf, WLED_MDNS_TYPE, handlers=[on_state_change]
        )

        await asyncio.sleep(timeout)

        # Resolve all discovered services
        for info in discovered.values():
            await info.async_request(aiozc.zeroconf, timeout=2000)

        await browser.async_cancel()

        # Build raw list with IPs, then enrich in parallel
        raw: list[tuple[str, str, str]] = []  # (service_name, ip, url)
        for name, info in discovered.items():
            addrs = info.parsed_addresses()
            if not addrs:
                continue
            ip = addrs[0]
            port = info.port or 80
            url = f"http://{ip}" if port == 80 else f"http://{ip}:{port}"
            service_name = name.replace(f".{WLED_MDNS_TYPE}", "")
            raw.append((service_name, ip, url))

        # Enrich all devices in parallel
        enrichment = await asyncio.gather(
            *[self._enrich_device(url, sname) for sname, _, url in raw]
        )

        results: List[DiscoveredDevice] = []
        for (service_name, ip, url), (wled_name, version, led_count, mac) in zip(
            raw, enrichment
        ):
            results.append(
                DiscoveredDevice(
                    name=wled_name,
                    url=url,
                    device_type="wled",
                    ip=ip,
                    mac=mac,
                    led_count=led_count,
                    version=version,
                )
            )

        await aiozc.async_close()
        logger.info(f"mDNS scan found {len(results)} WLED device(s)")
        return results

    @staticmethod
    async def _enrich_device(
        url: str, fallback_name: str
    ) -> tuple[str, Optional[str], Optional[int], str]:
        """Probe a WLED device's /json/info to get name, version, LED count, MAC."""
        try:
            async with httpx.AsyncClient(timeout=2) as client:
                resp = await client.get(f"{url}/json/info")
                resp.raise_for_status()
                data = resp.json()
                return (
                    data.get("name", fallback_name),
                    data.get("ver"),
                    data.get("leds", {}).get("count"),
                    data.get("mac", ""),
                )
        except Exception as e:
            logger.debug(f"Could not fetch WLED info from {url}: {e}")
            return fallback_name, None, None, ""

    # ===== BRIGHTNESS =====

    async def get_brightness(self, url: str) -> int:
        url = url.rstrip("/")
        client = await self._get_client()
        resp = await client.get(f"{url}/json/state")
        resp.raise_for_status()
        state = resp.json()
        return state.get("bri", 255)

    async def set_brightness(self, url: str, brightness: int) -> None:
        url = url.rstrip("/")
        client = await self._get_client()
        resp = await client.post(
            f"{url}/json/state",
            json={"bri": brightness},
        )
        resp.raise_for_status()

    async def get_power(self, url: str, **kwargs) -> bool:
        url = url.rstrip("/")
        client = await self._get_client()
        resp = await client.get(f"{url}/json/state")
        resp.raise_for_status()
        return resp.json().get("on", False)

    async def set_power(self, url: str, on: bool, **kwargs) -> None:
        url = url.rstrip("/")
        client = await self._get_client()
        resp = await client.post(
            f"{url}/json/state",
            json={"on": on},
        )
        resp.raise_for_status()

    async def set_color(self, url: str, color: Tuple[int, int, int], **kwargs) -> None:
        """Set WLED to a solid color using the native segment color API."""
        url = url.rstrip("/")
        client = await self._get_client()
        resp = await client.post(
            f"{url}/json/state",
            json={
                "on": True,
                "seg": [{"col": [[color[0], color[1], color[2]]], "fx": 0}],
            },
        )
        resp.raise_for_status()
