"""Weather-reactive color strip stream — maps weather conditions to ambient LED colors."""

import random
import threading
import time
from typing import Optional

import numpy as np

from wled_controller.core.processing.color_strip_stream import ColorStripStream
from wled_controller.core.weather.weather_manager import WeatherManager
from wled_controller.storage.bindable import bfloat
from wled_controller.core.weather.weather_provider import DEFAULT_WEATHER, WeatherData
from wled_controller.utils import get_logger

logger = get_logger(__name__)

# ── WMO code → palette mapping ──────────────────────────────────────────
# Each entry: (start_rgb, end_rgb) as (R, G, B) tuples.
# Codes are matched by range.

_PALETTES = {
    # Clear sky / mainly clear
    (0, 1): ((255, 220, 100), (255, 180, 80)),
    # Partly cloudy / overcast
    (2, 3): ((150, 180, 220), (240, 235, 220)),
    # Fog
    (45, 48): ((180, 190, 200), (210, 215, 220)),
    # Drizzle (light, moderate, dense, freezing)
    (51, 53, 55, 56, 57): ((100, 160, 220), (80, 180, 190)),
    # Rain (slight, moderate, heavy, freezing)
    (61, 63, 65, 66, 67): ((40, 80, 180), (60, 140, 170)),
    # Snow (slight, moderate, heavy, grains)
    (71, 73, 75, 77): ((200, 210, 240), (180, 200, 255)),
    # Rain showers
    (80, 81, 82): ((30, 60, 160), (80, 70, 170)),
    # Snow showers
    (85, 86): ((220, 225, 240), (190, 185, 220)),
    # Thunderstorm
    (95, 96, 99): ((60, 20, 120), (40, 60, 200)),
}

# Default palette (partly cloudy)
_DEFAULT_PALETTE = ((150, 180, 220), (240, 235, 220))


def _resolve_palette(code: int) -> tuple:
    """Map a WMO weather code to a (start_rgb, end_rgb) palette."""
    for codes, palette in _PALETTES.items():
        if code in codes:
            return palette
    return _DEFAULT_PALETTE


def _apply_temperature_shift(color: np.ndarray, temperature: float, influence: float) -> np.ndarray:
    """Shift color array warm/cool based on temperature.

    > 25°C: shift toward warm (add red, reduce blue)
    < 5°C: shift toward cool (add blue, reduce red)
    Between 5-25°C: linear interpolation (no shift at 15°C midpoint)
    """
    if influence <= 0.0:
        return color

    # Normalize temperature to -1..+1 range (cold..hot)
    t = (temperature - 15.0) / 10.0  # -1 at 5°C, 0 at 15°C, +1 at 25°C
    t = max(-1.0, min(1.0, t))
    shift = t * influence * 30.0  # max ±30 RGB units

    result = color.astype(np.int16)
    result[:, 0] += int(shift)  # red
    result[:, 2] -= int(shift)  # blue
    np.clip(result, 0, 255, out=result)
    return result.astype(np.uint8)


def _smoothstep(x: float) -> float:
    """Hermite smoothstep: smooth cubic interpolation."""
    x = max(0.0, min(1.0, x))
    return x * x * (3.0 - 2.0 * x)


class WeatherColorStripStream(ColorStripStream):
    """Generates ambient LED colors based on real-time weather data.

    Fetches weather data from a WeatherManager (which polls the API),
    maps the WMO condition code to a color palette, applies temperature
    influence, and animates a slow gradient drift across the strip.
    """

    def __init__(self, source, weather_manager: WeatherManager):
        self._source_id = source.id
        self._weather_source_id: str = source.weather_source_id
        self._speed: float = bfloat(source.speed, 1.0)
        self._temperature_influence: float = bfloat(source.temperature_influence, 0.5)
        self._clock_id: Optional[str] = source.clock_id
        self._weather_manager = weather_manager

        self._led_count: int = 0  # auto-size from device
        self._fps: int = 30
        self._frame_time: float = 1.0 / 30

        self._running = False
        self._thread: Optional[threading.Thread] = None
        self._latest_colors: Optional[np.ndarray] = None
        self._colors_lock = threading.Lock()

        # Pre-allocated buffers
        self._buf_a: Optional[np.ndarray] = None
        self._buf_b: Optional[np.ndarray] = None
        self._use_a = True
        self._pool_n = 0

        # Thunderstorm flash state
        self._flash_remaining = 0

    # ── ColorStripStream interface ──────────────────────────────────

    @property
    def target_fps(self) -> int:
        return self._fps

    def set_capture_fps(self, fps: int) -> None:
        self._fps = max(1, min(60, fps))
        self._frame_time = 1.0 / self._fps

    @property
    def led_count(self) -> int:
        return self._led_count

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

    def start(self) -> None:
        # Acquire weather runtime (increments ref count)
        if self._weather_source_id:
            try:
                self._weather_manager.acquire(self._weather_source_id)
            except Exception as e:
                logger.warning(
                    f"Weather stream {self._source_id}: failed to acquire weather source: {e}"
                )

        self._running = True
        self._thread = threading.Thread(
            target=self._animate_loop,
            daemon=True,
            name=f"WeatherCSS-{self._source_id[:12]}",
        )
        self._thread.start()
        logger.info(f"WeatherColorStripStream started: {self._source_id}")

    def stop(self) -> None:
        self._running = False
        if self._thread is not None:
            self._thread.join(timeout=5.0)
            self._thread = None

        # Release weather runtime
        if self._weather_source_id:
            try:
                self._weather_manager.release(self._weather_source_id)
            except Exception as e:
                logger.warning(
                    f"Weather stream {self._source_id}: failed to release weather source: {e}"
                )

        logger.info(f"WeatherColorStripStream stopped: {self._source_id}")

    def get_latest_colors(self) -> Optional[np.ndarray]:
        with self._colors_lock:
            return self._latest_colors

    def configure(self, device_led_count: int) -> None:
        if device_led_count > 0 and device_led_count != self._led_count:
            self._led_count = device_led_count

    def update_source(self, source) -> None:
        self._speed = bfloat(source.speed, 1.0)
        self._temperature_influence = bfloat(source.temperature_influence, 0.5)
        self._clock_id = source.clock_id

        # If weather source changed, release old + acquire new
        new_ws_id = source.weather_source_id
        if new_ws_id != self._weather_source_id:
            if self._weather_source_id:
                try:
                    self._weather_manager.release(self._weather_source_id)
                except Exception as e:
                    logger.debug("Weather source release during update: %s", e)
                    pass
            self._weather_source_id = new_ws_id
            if new_ws_id:
                try:
                    self._weather_manager.acquire(new_ws_id)
                except Exception as e:
                    logger.warning(f"Weather stream: failed to acquire new source {new_ws_id}: {e}")

    # ── Animation loop ──────────────────────────────────────────────

    def _ensure_pool(self, n: int) -> None:
        if n == self._pool_n or n <= 0:
            return
        self._pool_n = n
        self._buf_a = np.zeros((n, 3), dtype=np.uint8)
        self._buf_b = np.zeros((n, 3), dtype=np.uint8)

    def _get_clock_time(self) -> Optional[float]:
        """Get time from sync clock if configured."""
        if not self._clock_id:
            return None
        try:
            # Access via weather manager's store isn't ideal, but clocks
            # are looked up at the ProcessorManager level when the stream
            # is created.  For now, return None and use wall time.
            return None
        except Exception as e:
            logger.debug("Sync clock lookup failed for weather stream: %s", e)
            return None

    def _animate_loop(self) -> None:
        start_time = time.perf_counter()
        try:
            while self._running:
                loop_start = time.perf_counter()

                try:
                    n = self._led_count
                    if n <= 0:
                        time.sleep(self._frame_time)
                        continue

                    self._ensure_pool(n)
                    buf = self._buf_a if self._use_a else self._buf_b
                    self._use_a = not self._use_a

                    # Get weather data
                    weather = self._get_weather()
                    palette_start, palette_end = _resolve_palette(weather.code)

                    # Convert to arrays
                    c0 = np.array(palette_start, dtype=np.float32)
                    c1 = np.array(palette_end, dtype=np.float32)

                    # Compute animation phase
                    t = time.perf_counter() - start_time
                    phase = (t * self.resolve("speed", self._speed) * 0.1) % 1.0

                    # Generate gradient with drift
                    for i in range(n):
                        frac = ((i / max(n - 1, 1)) + phase) % 1.0
                        s = _smoothstep(frac)
                        buf[i] = (c0 * (1.0 - s) + c1 * s).astype(np.uint8)

                    # Apply temperature shift
                    temp_inf = self.resolve("temperature_influence", self._temperature_influence)
                    if temp_inf > 0.0:
                        buf[:] = _apply_temperature_shift(buf, weather.temperature, temp_inf)

                    # Thunderstorm flash effect
                    is_thunderstorm = weather.code in (95, 96, 99)
                    if is_thunderstorm:
                        if self._flash_remaining > 0:
                            buf[:] = 255
                            self._flash_remaining -= 1
                        elif random.random() < 0.015:  # ~1.5% chance per frame
                            self._flash_remaining = random.randint(2, 5)

                    with self._colors_lock:
                        self._latest_colors = buf

                except Exception as e:
                    logger.error(f"WeatherColorStripStream error: {e}", exc_info=True)

                elapsed = time.perf_counter() - loop_start
                time.sleep(max(self._frame_time - elapsed, 0.001))

        except Exception as e:
            logger.error(f"Fatal WeatherColorStripStream error: {e}", exc_info=True)
        finally:
            self._running = False

    def _get_weather(self) -> WeatherData:
        """Get current weather data from the manager."""
        if not self._weather_source_id:
            return DEFAULT_WEATHER
        try:
            return self._weather_manager.get_data(self._weather_source_id)
        except Exception as e:
            logger.debug("Weather data fetch failed, using default: %s", e)
            return DEFAULT_WEATHER
