"""SPI Direct LED client — drives WS2812/SK6812 strips via Raspberry Pi GPIO/SPI."""

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

# Supported LED chipsets
LED_TYPES = {
    "WS2812": {"freq_hz": 800000, "invert": False, "strip_type": None},
    "WS2812B": {"freq_hz": 800000, "invert": False, "strip_type": None},
    "WS2811": {"freq_hz": 800000, "invert": False, "strip_type": None},
    "SK6812": {"freq_hz": 800000, "invert": False, "strip_type": None},
    "SK6812_RGBW": {"freq_hz": 800000, "invert": False, "strip_type": None},
}

# SPI speed for bitbang protocol
DEFAULT_SPI_SPEED = 800000

# GPIO pin for rpi_ws281x (GPIO 18 = PWM0, GPIO 10 = SPI0 MOSI)
DEFAULT_GPIO_PIN = 18


def _parse_spi_url(url: str) -> dict:
    """Parse SPI URL format.

    Formats:
        spi://gpio:18       — rpi_ws281x via GPIO pin 18
        spi://spidev:0.0    — SPI device /dev/spidev0.0
        spi://0             — shorthand for GPIO pin 0 (actually pin 18 default)
    """
    path = url.replace("spi://", "")

    if path.startswith("gpio:"):
        pin = int(path.split(":")[1])
        return {"method": "gpio", "gpio_pin": pin}
    elif path.startswith("spidev:"):
        dev = path.split(":")[1]
        bus, device = dev.split(".")
        return {"method": "spidev", "bus": int(bus), "device": int(device)}
    else:
        # Default: gpio pin
        try:
            pin = int(path) if path else DEFAULT_GPIO_PIN
        except ValueError:
            pin = DEFAULT_GPIO_PIN
        return {"method": "gpio", "gpio_pin": pin}


class SPIClient(LEDClient):
    """LED client that drives addressable LED strips directly via Raspberry Pi GPIO/SPI.

    Uses rpi_ws281x library for GPIO-based control, or spidev for raw SPI.
    Requires root privileges or SPI group membership.
    """

    def __init__(
        self,
        url: str = "",
        led_count: int = 0,
        spi_speed_hz: int = DEFAULT_SPI_SPEED,
        spi_led_type: str = "WS2812B",
        **kwargs,
    ):
        self._config = _parse_spi_url(url)
        self._led_count = led_count
        self._speed_hz = spi_speed_hz
        self._led_type = spi_led_type
        self._strip = None
        self._spi = None
        self._connected = False

    async def connect(self) -> bool:
        loop = asyncio.get_event_loop()

        if self._config["method"] == "gpio":
            await loop.run_in_executor(None, self._connect_rpi_ws281x)
        else:
            await loop.run_in_executor(None, self._connect_spidev)

        self._connected = True
        logger.info(
            "SPI client connected: method=%s leds=%d type=%s",
            self._config["method"], self._led_count, self._led_type,
        )
        return True

    def _connect_rpi_ws281x(self):
        """Connect via rpi_ws281x library (GPIO PWM/DMA)."""
        try:
            from rpi_ws281x import PixelStrip
        except ImportError:
            raise RuntimeError(
                "rpi_ws281x is required for GPIO LED control: "
                "pip install rpi_ws281x (Linux/RPi only)"
            )

        led_info = LED_TYPES.get(self._led_type, LED_TYPES["WS2812B"])
        gpio_pin = self._config["gpio_pin"]

        self._strip = PixelStrip(
            self._led_count,
            gpio_pin,
            led_info["freq_hz"],
            10,       # DMA channel
            led_info["invert"],
            255,      # max brightness
            0,        # channel (0 for GPIO 18, 1 for GPIO 13)
        )
        self._strip.begin()

    def _connect_spidev(self):
        """Connect via spidev (raw SPI bus)."""
        try:
            import spidev
        except ImportError:
            raise RuntimeError("spidev is required for SPI LED control: pip install spidev")

        bus = self._config["bus"]
        device = self._config["device"]
        self._spi = spidev.SpiDev()
        self._spi.open(bus, device)
        self._spi.max_speed_hz = self._speed_hz
        self._spi.mode = 0

    async def close(self) -> None:
        loop = asyncio.get_event_loop()
        if self._strip:
            # Turn off all LEDs
            def _clear():
                for i in range(self._led_count):
                    self._strip.setPixelColor(i, 0)
                self._strip.show()

            await loop.run_in_executor(None, _clear)
            self._strip = None
        if self._spi:
            await loop.run_in_executor(None, self._spi.close)
            self._spi = None
        self._connected = False
        logger.info("SPI client closed")

    @property
    def is_connected(self) -> bool:
        return self._connected

    @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._connected:
            return

        # Note: brightness already applied by processor loop (_cached_brightness)

        if isinstance(pixels, np.ndarray):
            pixel_arr = pixels
        else:
            pixel_arr = np.array(pixels, dtype=np.uint8)

        if self._strip:
            # rpi_ws281x path
            try:
                from rpi_ws281x import Color
            except ImportError:
                return

            self._strip.setBrightness(255)
            for i in range(min(len(pixel_arr), self._led_count)):
                r, g, b = pixel_arr[i]
                self._strip.setPixelColor(i, Color(int(r), int(g), int(b)))
            self._strip.show()

        elif self._spi:
            # SPI bitbang path: convert RGB to WS2812 wire format
            # Each bit is encoded as 3 SPI bits: 1=110, 0=100
            scaled = pixel_arr[:self._led_count]
            # GRB order for WS2812
            grb = scaled[:, [1, 0, 2]]
            raw_bytes = grb.tobytes()

            # Encode each byte as 3 SPI bytes (8 bits → 24 SPI bits)
            spi_data = bytearray()
            for byte in raw_bytes:
                for bit in range(7, -1, -1):
                    if byte & (1 << bit):
                        spi_data.append(0b110)
                    else:
                        spi_data.append(0b100)

            # Reset pulse (>50us low)
            spi_data.extend(b'\x00' * 20)

            self._spi.writebytes2(list(spi_data))

    async def send_pixels(
        self,
        pixels: Union[List[Tuple[int, int, int]], np.ndarray],
        brightness: int = 255,
    ) -> bool:
        if not self._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("SPI 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 SPI/GPIO device is accessible."""
        import platform

        if platform.system() != "Linux":
            return DeviceHealth(
                online=False,
                error="SPI direct is only available on Linux (Raspberry Pi)",
                last_checked=datetime.now(timezone.utc),
            )

        config = _parse_spi_url(url)
        if config["method"] == "spidev":
            import os

            dev_path = f"/dev/spidev{config['bus']}.{config['device']}"
            if os.path.exists(dev_path):
                return DeviceHealth(online=True, latency_ms=0.0, last_checked=datetime.now(timezone.utc))
            return DeviceHealth(
                online=False,
                error=f"SPI device {dev_path} not found",
                last_checked=datetime.now(timezone.utc),
            )
        else:
            # GPIO — check if we can import rpi_ws281x
            try:
                import importlib.util
                if importlib.util.find_spec("rpi_ws281x") is not None:
                    return DeviceHealth(online=True, latency_ms=0.0, last_checked=datetime.now(timezone.utc))
                raise ImportError("rpi_ws281x not found")
            except ImportError:
                return DeviceHealth(
                    online=False,
                    error="rpi_ws281x not installed",
                    last_checked=datetime.now(timezone.utc),
                )
