"""ESP-NOW LED client — sends pixel data via serial to an ESP32 gateway which forwards over ESP-NOW."""

import asyncio
import struct
from datetime import datetime, timezone
from typing import List, Optional, Tuple, Union

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__)

# Gateway serial protocol constants
FRAME_START = 0xEE
FRAME_END = 0xEF


def _mac_str_to_bytes(mac: str) -> bytes:
    """Convert 'AA:BB:CC:DD:EE:FF' to 6 raw bytes."""
    parts = mac.replace("-", ":").split(":")
    if len(parts) != 6:
        raise ValueError(f"Invalid MAC address: {mac}")
    return bytes(int(p, 16) for p in parts)


def _build_frame(peer_mac: bytes, pixels: bytes, brightness: int) -> bytes:
    """Build a serial frame for the ESP-NOW gateway.

    Wire format:
        [0xEE][MAC 6B][LED_COUNT 2B LE][BRIGHTNESS 1B][RGB...][CHECKSUM 1B][0xEF]

    Checksum is XOR of all bytes between START and END (exclusive).
    """
    led_count = len(pixels) // 3
    header = struct.pack("<B6sHB", FRAME_START, peer_mac, led_count, brightness)
    payload = header[1:] + pixels  # everything after START for checksum
    checksum = 0
    for b in payload:
        checksum ^= b
    return header + pixels + bytes([checksum, FRAME_END])


class ESPNowClient(LEDClient):
    """LED client that sends pixel data to an ESP32 ESP-NOW gateway over serial.

    The gateway ESP32 receives serial frames and forwards LED data via
    ESP-NOW to a peer ESP32 which drives the LED strip.
    """

    def __init__(
        self,
        url: str = "",
        led_count: int = 0,
        baud_rate: int = 921600,
        espnow_peer_mac: str = "FF:FF:FF:FF:FF:FF",
        espnow_channel: int = 1,
        **kwargs,
    ):
        self._port = url
        self._led_count = led_count
        self._baud_rate = baud_rate
        self._peer_mac = _mac_str_to_bytes(espnow_peer_mac)
        self._channel = espnow_channel
        self._serial = None
        self._connected = False
        self._lock = asyncio.Lock()

    async def connect(self) -> bool:
        try:
            import serial as pyserial
        except ImportError:
            raise RuntimeError("pyserial is required for ESP-NOW devices: pip install pyserial")

        loop = asyncio.get_event_loop()
        self._serial = await loop.run_in_executor(
            None,
            lambda: pyserial.Serial(self._port, self._baud_rate, timeout=1),
        )
        self._connected = True
        logger.info(
            "ESP-NOW client connected: port=%s baud=%d peer=%s channel=%d leds=%d",
            self._port, self._baud_rate,
            ":".join(f"{b:02X}" for b in self._peer_mac),
            self._channel, self._led_count,
        )
        return True

    async def close(self) -> None:
        if self._serial and self._serial.is_open:
            loop = asyncio.get_event_loop()
            await loop.run_in_executor(None, self._serial.close)
        self._serial = None
        self._connected = False
        logger.info("ESP-NOW client closed: port=%s", self._port)

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

    @property
    def supports_fast_send(self) -> bool:
        return True

    def send_pixels_fast(
        self,
        pixels: Union[List[Tuple[int, int, int]], np.ndarray],
        brightness: int = 255,
    ) -> None:
        if not self.is_connected:
            return
        if isinstance(pixels, np.ndarray):
            pixel_bytes = pixels.astype(np.uint8).tobytes()
        else:
            pixel_bytes = bytes(c for rgb in pixels for c in rgb)

        # Note: brightness already applied by processor loop; pass 255 to firmware
        frame = _build_frame(self._peer_mac, pixel_bytes, 255)
        try:
            self._serial.write(frame)
        except Exception as e:
            logger.warning("ESP-NOW send failed: %s", e)
            self._connected = False

    async def send_pixels(
        self,
        pixels: Union[List[Tuple[int, int, int]], np.ndarray],
        brightness: int = 255,
    ) -> bool:
        if not self.is_connected:
            return False
        loop = asyncio.get_event_loop()
        try:
            await loop.run_in_executor(
                None,
                lambda: self.send_pixels_fast(pixels, brightness),
            )
            return True
        except Exception as e:
            logger.error("ESP-NOW async send failed: %s", e)
            return False

    @classmethod
    async def check_health(
        cls,
        url: str,
        http_client,
        prev_health: Optional[DeviceHealth] = None,
    ) -> DeviceHealth:
        """Check if the serial port is available."""
        try:
            import serial as pyserial

            s = pyserial.Serial(url, timeout=0.1)
            s.close()
            return DeviceHealth(online=True, latency_ms=0.0, last_checked=datetime.now(timezone.utc))
        except Exception as e:
            return DeviceHealth(
                online=False,
                error=str(e),
                last_checked=datetime.now(timezone.utc),
            )
