"""Adalight serial LED client — sends pixel data over serial using the Adalight protocol."""

import asyncio
from datetime import datetime, timezone
from typing import Optional, Tuple

import numpy as np

from wled_controller.core.devices.led_client import DeviceHealth, LEDClient
from wled_controller.utils import get_logger

logger = get_logger(__name__)

DEFAULT_BAUD_RATE = 115200
ARDUINO_RESET_DELAY = 2.0  # seconds to wait after opening serial for Arduino bootloader


def parse_adalight_url(url: str) -> Tuple[str, int]:
    """Parse an Adalight URL into (port, baud_rate).

    Formats:
        "COM3"           -> ("COM3", 115200)
        "COM3:230400"    -> ("COM3", 230400)
        "/dev/ttyUSB0"   -> ("/dev/ttyUSB0", 115200)
    """
    url = url.strip()
    if ":" in url and not url.startswith("/"):
        # Windows COM port with baud: "COM3:230400"
        parts = url.rsplit(":", 1)
        try:
            baud = int(parts[1])
            return parts[0], baud
        except ValueError:
            pass
    elif ":" in url and url.startswith("/"):
        # Unix path with baud: "/dev/ttyUSB0:230400"
        parts = url.rsplit(":", 1)
        try:
            baud = int(parts[1])
            return parts[0], baud
        except ValueError:
            pass
    return url, DEFAULT_BAUD_RATE


def _build_adalight_header(led_count: int) -> bytes:
    """Build the 6-byte Adalight protocol header.

    Format: 'A' 'd' 'a' <count_hi> <count_lo> <checksum>
    where count = led_count - 1 (zero-indexed).
    """
    count = led_count - 1
    hi = (count >> 8) & 0xFF
    lo = count & 0xFF
    checksum = hi ^ lo ^ 0x55
    return bytes([ord("A"), ord("d"), ord("a"), hi, lo, checksum])


class AdalightClient(LEDClient):
    """LED client for Arduino Adalight serial devices."""

    def __init__(self, url: str, led_count: int = 0, baud_rate: Optional[int] = None, **kwargs):
        """Initialize Adalight client.

        Args:
            url: Serial port string, e.g. "COM3" or "COM3:230400"
            led_count: Number of LEDs on the strip (required for Adalight header)
            baud_rate: Override baud rate (if None, parsed from url or default 115200)
        """
        self._port, url_baud = parse_adalight_url(url)
        self._baud_rate = baud_rate or url_baud
        self._led_count = led_count
        self._serial = None
        self._connected = False

        # Pre-compute Adalight header if led_count is known
        self._header = _build_adalight_header(led_count) if led_count > 0 else b""

        # Pre-allocate numpy buffer for brightness scaling
        self._pixel_buf = None

    async def connect(self) -> bool:
        """Open serial port and wait for Arduino reset."""
        import serial

        try:
            self._serial = await asyncio.to_thread(
                serial.Serial, port=self._port, baudrate=self._baud_rate, timeout=1
            )
            # Wait for Arduino to finish bootloader reset (non-blocking)
            await asyncio.sleep(ARDUINO_RESET_DELAY)
            self._connected = True
            logger.info(
                f"Adalight connected: {self._port} @ {self._baud_rate} baud "
                f"({self._led_count} LEDs)"
            )
            return True
        except Exception as e:
            logger.error(f"Failed to open serial port {self._port}: {e}")
            raise RuntimeError(f"Failed to open serial port {self._port}: {e}")

    async def close(self) -> None:
        """Send black frame and close the serial port."""
        if self._connected and self._serial and self._serial.is_open and self._led_count > 0:
            try:
                black = np.zeros((self._led_count, 3), dtype=np.uint8)
                frame = self._build_frame(black, brightness=255)
                logger.info(
                    f"Adalight sending black frame: {self._port} "
                    f"({self._led_count} LEDs, {len(frame)} bytes)"
                )
                await asyncio.to_thread(self._serial.write, frame)
                await asyncio.to_thread(self._serial.flush)
                logger.info(f"Adalight black frame sent and flushed: {self._port}")
            except Exception as e:
                logger.warning(f"Failed to send black frame on close: {e}")
        else:
            logger.warning(
                f"Adalight close skipped black frame: port={self._port} "
                f"connected={self._connected} serial={self._serial is not None} "
                f"is_open={self._serial.is_open if self._serial else 'N/A'} "
                f"led_count={self._led_count}"
            )
        self._connected = False
        if self._serial and self._serial.is_open:
            try:
                self._serial.close()
            except Exception as e:
                logger.warning(f"Error closing serial port: {e}")
        self._serial = None
        logger.info(f"Adalight disconnected: {self._port}")

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

    async def send_pixels(
        self,
        pixels,
        brightness: int = 255,
    ) -> bool:
        """Send pixel data over serial using Adalight protocol (non-blocking).

        Args:
            pixels: numpy array (N, 3) uint8 or list of (R, G, B) tuples
            brightness: Global brightness (0-255)
        """
        if not self.is_connected:
            return False

        try:
            frame = self._build_frame(pixels, brightness)
            await asyncio.to_thread(self._serial.write, frame)
            return True
        except Exception as e:
            logger.error(f"Adalight send_pixels error: {e}")
            return False

    @property
    def supports_fast_send(self) -> bool:
        # Serial write is blocking — use async send_pixels path instead
        return False

    def _build_frame(self, pixels, brightness: int) -> bytes:
        """Build a complete Adalight frame: header + brightness-scaled RGB data."""
        if isinstance(pixels, np.ndarray):
            arr = pixels.astype(np.uint16)
        else:
            arr = np.array(pixels, dtype=np.uint16)

        # Note: brightness already applied by processor loop (_cached_brightness)
        np.clip(arr, 0, 255, out=arr)
        rgb_bytes = arr.astype(np.uint8).tobytes()
        return self._header + rgb_bytes

    @classmethod
    async def check_health(
        cls,
        url: str,
        http_client,
        prev_health: Optional[DeviceHealth] = None,
    ) -> DeviceHealth:
        """Check if the serial port exists without opening it.

        Enumerates COM ports to avoid exclusive-access conflicts on Windows.
        """
        port, _baud = parse_adalight_url(url)

        try:
            import serial.tools.list_ports

            available_ports = [p.device for p in serial.tools.list_ports.comports()]
            port_upper = port.upper()
            found = any(p.upper() == port_upper for p in available_ports)

            if found:
                return DeviceHealth(
                    online=True,
                    latency_ms=0.0,
                    last_checked=datetime.now(timezone.utc),
                    device_name=prev_health.device_name if prev_health else None,
                    device_version=None,
                    device_led_count=prev_health.device_led_count if prev_health else None,
                )
            else:
                return DeviceHealth(
                    online=False,
                    last_checked=datetime.now(timezone.utc),
                    error=f"Serial port {port} not found",
                )
        except Exception as e:
            return DeviceHealth(
                online=False,
                last_checked=datetime.now(timezone.utc),
                error=str(e),
            )
