"""Nanoleaf OpenAPI LED client.

Nanoleaf controllers (Light Panels, Canvas, Shapes, Lines, Elements) expose
an HTTP REST API on port 16021. Pairing follows the documented two-step
handshake: the user holds the controller's power button for 5 seconds to
open a 30-second pairing window, then we POST to ``/api/v1/new`` to claim
an auth token. The token is long-lived and gets stored on the device.

Once paired, color control is a simple ``PUT /api/v1/{token}/state`` with
HSBT (hue / saturation / brightness; kelvin only matters when sat=0).
LedGrab averages the incoming strip to one HSB triple. Per-panel streaming
mode (``extControl`` UDP, ~60 Hz, addresses each panel individually) is
documented but not implemented here — the MVP keeps the device acting as
a single-pixel target like Yeelight / Hue.

URL scheme: ``nanoleaf://<host>``. Port is fixed at 16021 on the protocol
side. The auth token is stored separately on the device config, not in
the URL — putting it in the URL would leak the token into log files.

Reference: https://forum.nanoleaf.me/docs/openapi
"""

from __future__ import annotations

import asyncio
from datetime import datetime, timezone
from typing import List, Optional, Tuple, Union
from urllib.parse import urlparse

import httpx
import numpy as np

from ledgrab.core.devices.led_client import DeviceHealth, LEDClient, PairingNotReady
from ledgrab.core.devices.pixel_reduce import average_color as _average_color
from ledgrab.utils import get_logger

logger = get_logger(__name__)

NANOLEAF_PORT = 16021
DEFAULT_MIN_INTERVAL_S = 0.1  # 10 Hz; HTTP per frame, plenty for averaged ambilight


def parse_nanoleaf_url(url: str) -> str:
    """Pull the host out of ``nanoleaf://host`` or accept a bare host.

    The TCP port is fixed at 16021 on the protocol side; we ignore any
    port specifier rather than silently accept one the controller won't
    answer on.
    """
    if not url:
        raise ValueError("Nanoleaf URL is empty")
    raw = url.strip()
    if "://" in raw:
        parsed = urlparse(raw)
        host = parsed.hostname or ""
    else:
        parsed = urlparse(f"nanoleaf://{raw}")
        host = parsed.hostname or ""
    if not host:
        raise ValueError(f"Nanoleaf URL has no host: {url!r}")
    return host


def rgb_to_hsb(r: int, g: int, b: int) -> Tuple[int, int, int]:
    """Convert 8-bit RGB to Nanoleaf HSB (hue 0-360, sat 0-100, bri 0-100).

    All outputs are integers; the Nanoleaf API rejects fractional values.
    """
    r_n = max(0, min(255, r)) / 255.0
    g_n = max(0, min(255, g)) / 255.0
    b_n = max(0, min(255, b)) / 255.0
    c_max = max(r_n, g_n, b_n)
    c_min = min(r_n, g_n, b_n)
    delta = c_max - c_min

    if delta == 0:
        h = 0.0
    elif c_max == r_n:
        h = 60.0 * (((g_n - b_n) / delta) % 6)
    elif c_max == g_n:
        h = 60.0 * (((b_n - r_n) / delta) + 2)
    else:
        h = 60.0 * (((r_n - g_n) / delta) + 4)
    hue = int(round(h)) % 360
    sat = 0 if c_max == 0 else int(round((delta / c_max) * 100))
    bri = int(round(c_max * 100))
    return hue, sat, bri


async def pair_nanoleaf(host: str, timeout_s: float = 4.0) -> str:
    """POST to ``/api/v1/new``; user must have just held the power button 5s.

    Returns the auth token on success. Raises ``PairingNotReady`` if the
    controller responds 403 (not in pairing mode) — the caller surfaces
    that as a 409 to the UI with a retry prompt.
    """
    base = f"http://{host}:{NANOLEAF_PORT}/api/v1/new"
    try:
        async with httpx.AsyncClient(timeout=timeout_s) as http_client:
            response = await http_client.post(base)
    except (httpx.HTTPError, httpx.TimeoutException) as exc:
        raise RuntimeError(f"Pairing transport failure for {host}: {exc}") from exc

    if response.status_code == 403:
        raise PairingNotReady(
            "Hold the power button on your Nanoleaf controller for 5 seconds "
            "until the LEDs flash, then click Try again."
        )
    if response.status_code != 200:
        raise RuntimeError(
            f"Nanoleaf at {host} returned HTTP {response.status_code} during pairing: "
            f"{response.text[:200]}"
        )
    try:
        token = response.json().get("auth_token")
    except ValueError as exc:
        raise RuntimeError(f"Malformed pairing response from {host}: {exc}") from exc
    if not token or not isinstance(token, str):
        raise RuntimeError(f"Nanoleaf at {host} returned no auth_token in pairing response")
    return token


class NanoleafClient(LEDClient):
    """LEDClient for a single Nanoleaf controller (Panels / Canvas / Shapes)."""

    def __init__(
        self,
        url: str,
        led_count: int = 1,
        *,
        auth_token: str = "",
        min_interval_s: float = DEFAULT_MIN_INTERVAL_S,
        request_timeout_s: float = 3.0,
    ):
        self._host = parse_nanoleaf_url(url)
        self._token = auth_token
        self._led_count = led_count
        self._min_interval_s = max(0.0, min_interval_s)
        self._request_timeout_s = request_timeout_s
        self._http: Optional[httpx.AsyncClient] = None
        self._connected = False
        self._next_tx_at: float = 0.0

    @property
    def host(self) -> str:
        return self._host

    @property
    def port(self) -> int:
        """Fixed at the protocol-mandated 16021. Exposed for cross-driver
        symmetry with the UDP/TCP clients that also surface ``.port``."""
        return NANOLEAF_PORT

    @property
    def is_connected(self) -> bool:
        return self._connected and self._http is not None

    @property
    def device_led_count(self) -> Optional[int]:
        return self._led_count or None

    def _state_url(self) -> str:
        return f"http://{self._host}:{NANOLEAF_PORT}/api/v1/{self._token}/state"

    async def connect(self) -> bool:
        if self._connected and self._http is not None:
            return True
        if not self._token:
            raise RuntimeError("NanoleafClient requires an auth_token; pair the device first")
        self._http = httpx.AsyncClient(timeout=self._request_timeout_s)
        self._connected = True
        logger.info("NanoleafClient connected to %s:%d", self._host, NANOLEAF_PORT)
        return True

    async def close(self) -> None:
        if self._http is not None:
            try:
                await self._http.aclose()
            except (httpx.HTTPError, RuntimeError):
                pass
        self._http = None
        self._connected = False

    async def _put_state(self, body: dict) -> None:
        if self._http is None:
            raise RuntimeError("NanoleafClient not connected")
        response = await self._http.put(self._state_url(), json=body)
        # 204 No Content is the documented success; 200 also acceptable in practice.
        if response.status_code not in (200, 204):
            raise RuntimeError(
                f"Nanoleaf rejected state update ({response.status_code}): "
                f"{response.text[:200]}"
            )

    async def send_pixels(
        self,
        pixels: Union[List[Tuple[int, int, int]], np.ndarray],
        brightness: int = 255,
    ) -> bool:
        """Average the strip and PUT a single HSB state update."""
        if not self.is_connected:
            raise RuntimeError("NanoleafClient not connected")
        loop_now = asyncio.get_event_loop().time()
        if loop_now < self._next_tx_at:
            return True
        r, g, b = _average_color(pixels)
        if brightness < 255:
            scale = max(0, min(255, brightness)) / 255.0
            r = int(r * scale)
            g = int(g * scale)
            b = int(b * scale)
        hue, sat, bri = rgb_to_hsb(r, g, b)
        # Nanoleaf rejects brightness=0; clamp to 1 so the user can still see
        # "almost off" without falling off the API.
        bri = max(1, bri)
        await self._put_state(
            {
                "hue": {"value": hue},
                "sat": {"value": sat},
                "brightness": {"value": bri, "duration": 0},
            }
        )
        self._next_tx_at = loop_now + self._min_interval_s
        return True

    async def set_color(self, r: int, g: int, b: int) -> None:
        if not self.is_connected:
            raise RuntimeError("NanoleafClient not connected")
        hue, sat, bri = rgb_to_hsb(r, g, b)
        bri = max(1, bri)
        await self._put_state(
            {
                "hue": {"value": hue},
                "sat": {"value": sat},
                "brightness": {"value": bri, "duration": 0},
            }
        )

    async def set_power(self, on: bool) -> None:
        if not self.is_connected:
            raise RuntimeError("NanoleafClient not connected")
        await self._put_state({"on": {"value": on}})

    async def set_brightness(self, brightness_0_100: int) -> None:
        if not self.is_connected:
            raise RuntimeError("NanoleafClient not connected")
        clamped = max(0, min(100, brightness_0_100))
        await self._put_state({"brightness": {"value": clamped, "duration": 0}})

    @classmethod
    async def check_health(
        cls,
        url: str,
        http_client,
        prev_health: Optional[DeviceHealth] = None,
    ) -> DeviceHealth:
        """GET ``/api/v1/<token>/info``. Without a token we can't authenticate,
        so we fall back to GET ``/api/v1`` which returns 401 when the host is
        a real Nanoleaf controller and connection-error otherwise."""
        now = datetime.now(timezone.utc)
        try:
            host = parse_nanoleaf_url(url)
        except ValueError as exc:
            return DeviceHealth(online=False, last_checked=now, error=str(exc))
        probe_url = f"http://{host}:{NANOLEAF_PORT}/api/v1"
        loop = asyncio.get_running_loop()
        start = loop.time()
        try:
            response = await http_client.get(probe_url, timeout=2.0)
        except (httpx.HTTPError, httpx.TimeoutException) as exc:
            return DeviceHealth(
                online=False,
                last_checked=now,
                error=f"Nanoleaf unreachable at {host}: {exc}",
            )
        latency_ms = (loop.time() - start) * 1000.0
        # Real Nanoleaf responds 401/403 on /api/v1 without an auth token;
        # anything else may still be alive. Both signal "host is up".
        return DeviceHealth(
            online=True,
            latency_ms=latency_ms,
            last_checked=now,
            device_version=str(response.status_code),
        )
