"""Daylight cycle LED stream — simulates natural daylight color temperature.

Implements DaylightColorStripStream which produces a uniform LED color array
that transitions through dawn, daylight, sunset, and night over a continuous
24-hour cycle.  Can use real wall-clock time or a configurable simulation speed.

When latitude and longitude are provided, sunrise/sunset times are computed
via simplified NOAA solar equations so the daylight curve automatically adapts
to the user's location and the current season.
"""

import datetime
import math
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.utils import get_logger
from wled_controller.utils.timer import high_resolution_timer

logger = get_logger(__name__)

# ── Daylight color table ────────────────────────────────────────────────
#
# Canonical hour control points (0–24) → RGB.  Designed for a default
# sunrise of 6 h and sunset of 19 h.  At render time the curve is remapped
# to the actual solar times for the location.
#
# Format: (hour, R, G, B)
_DAYLIGHT_CURVE = [
    (0.0, 10, 10, 30),  # midnight — deep blue
    (4.0, 10, 10, 40),  # pre-dawn — dark blue
    (5.5, 40, 20, 60),  # first light — purple hint
    (6.0, 255, 100, 30),  # sunrise — warm orange
    (7.0, 255, 170, 80),  # early morning — golden
    (8.0, 255, 220, 160),  # morning — warm white
    (10.0, 255, 245, 230),  # mid-morning — neutral warm
    (12.0, 240, 248, 255),  # noon — cool white / slight blue
    (14.0, 255, 250, 240),  # afternoon — neutral
    (16.0, 255, 230, 180),  # late afternoon — warm
    (17.5, 255, 180, 100),  # pre-sunset — golden
    (18.5, 255, 100, 40),  # sunset — deep orange
    (19.0, 200, 60, 40),  # late sunset — red
    (19.5, 100, 30, 60),  # dusk — purple
    (20.0, 40, 20, 60),  # twilight — dark purple
    (21.0, 15, 15, 45),  # night — dark blue
    (24.0, 10, 10, 30),  # midnight (wrap)
]

# Reference solar times the canonical curve was designed around
_DEFAULT_SUNRISE = 6.0
_DEFAULT_SUNSET = 19.0

# Global cache of the default static LUT (lazy-built once)
_daylight_lut: Optional[np.ndarray] = None


# ── Solar position helpers ──────────────────────────────────────────────


def _compute_solar_times(latitude: float, longitude: float, day_of_year: int) -> tuple:
    """Return (sunrise_hour, sunset_hour) in local solar time.

    Uses simplified NOAA solar equations:
      - declination: decl = 23.45 * sin(2π * (284 + doy) / 365)
      - hour angle:  cos(ha) = -tan(lat) * tan(decl)
      - sunrise/sunset: 12 ∓ ha/15, shifted by longitude

    Polar day and polar night are clamped to visible ranges.
    """
    deg2rad = math.pi / 180.0

    decl_deg = 23.45 * math.sin(2.0 * math.pi * (284 + day_of_year) / 365.0)
    decl_rad = decl_deg * deg2rad
    lat_rad = latitude * deg2rad

    cos_ha = -math.tan(lat_rad) * math.tan(decl_rad)

    if cos_ha <= -1.0:
        # Polar day — sun never sets
        sunrise = 3.0
        sunset = 21.0
    elif cos_ha >= 1.0:
        # Polar night — sun never rises
        sunrise = 12.0
        sunset = 12.0
    else:
        ha_hours = math.acos(cos_ha) / (deg2rad * 15.0)
        lon_offset = longitude / 15.0
        solar_noon = 12.0 - lon_offset
        sunrise = solar_noon - ha_hours
        sunset = solar_noon + ha_hours

    # Clamp to sane ranges
    sunrise = max(3.0, min(10.0, sunrise))
    sunset = max(14.0, min(21.0, sunset))
    return sunrise, sunset


def _build_lut_for_solar_times(sunrise: float, sunset: float) -> np.ndarray:
    """Build a 1440-entry uint8 RGB LUT scaled to the given sunrise/sunset hours.

    The canonical _DAYLIGHT_CURVE is remapped so that:
      - Night before dawn: 0 h → sunrise  maps to  0 h → _DEFAULT_SUNRISE
      - Daylight window:  sunrise → sunset  maps to  _DEFAULT_SUNRISE → _DEFAULT_SUNSET
      - Night after dusk: sunset → 24 h  maps to  _DEFAULT_SUNSET → 24 h
    """
    default_night_before = _DEFAULT_SUNRISE
    default_day_len = _DEFAULT_SUNSET - _DEFAULT_SUNRISE
    default_night_after = 24.0 - _DEFAULT_SUNSET

    actual_night_before = max(sunrise, 0.01)
    actual_day_len = max(sunset - sunrise, 0.25)
    actual_night_after = max(24.0 - sunset, 0.01)

    lut = np.zeros((1440, 3), dtype=np.uint8)
    for minute in range(1440):
        hour = minute / 60.0

        if hour < sunrise:
            frac = hour / actual_night_before
            canon_hour = frac * default_night_before
        elif hour < sunset:
            frac = (hour - sunrise) / actual_day_len
            canon_hour = _DEFAULT_SUNRISE + frac * default_day_len
        else:
            frac = (hour - sunset) / actual_night_after
            canon_hour = _DEFAULT_SUNSET + frac * default_night_after

        canon_hour = max(0.0, min(23.99, canon_hour))

        # Locate surrounding curve control points
        prev = _DAYLIGHT_CURVE[0]
        nxt = _DAYLIGHT_CURVE[-1]
        for i in range(len(_DAYLIGHT_CURVE) - 1):
            if _DAYLIGHT_CURVE[i][0] <= canon_hour <= _DAYLIGHT_CURVE[i + 1][0]:
                prev = _DAYLIGHT_CURVE[i]
                nxt = _DAYLIGHT_CURVE[i + 1]
                break

        span = nxt[0] - prev[0]
        t = (canon_hour - prev[0]) / span if span > 0 else 0.0
        t = t * t * (3.0 - 2.0 * t)  # smoothstep

        for ch in range(3):
            lut[minute, ch] = int(prev[ch + 1] + (nxt[ch + 1] - prev[ch + 1]) * t + 0.5)

    return lut


def _get_daylight_lut() -> np.ndarray:
    """Return the static default LUT (built once, cached globally)."""
    global _daylight_lut
    if _daylight_lut is None:
        _daylight_lut = _build_lut_for_solar_times(_DEFAULT_SUNRISE, _DEFAULT_SUNSET)
    return _daylight_lut


# ── Stream class ────────────────────────────────────────────────────────


class DaylightColorStripStream(ColorStripStream):
    """Color strip stream simulating a 24-hour daylight cycle.

    All LEDs display the same color at any moment.  The color smoothly
    transitions through a pre-defined daylight curve whose sunrise/sunset
    times are computed from latitude, longitude, and day of year.
    """

    def __init__(self, source):
        self._colors_lock = threading.Lock()
        self._running = False
        self._thread: Optional[threading.Thread] = None
        self._fps = 10
        self._frame_time = 1.0 / 10
        self._clock = None
        self._led_count = 1
        self._auto_size = True
        # Per-instance LUT cache: {(sr_min, ss_min): np.ndarray}
        self._lut_cache: dict = {}
        self._update_from_source(source)

    def _update_from_source(self, source) -> None:
        from wled_controller.storage.bindable import bfloat

        self._speed = bfloat(getattr(source, "speed", 1.0), 1.0)
        self._use_real_time = bool(getattr(source, "use_real_time", False))
        self._latitude = float(getattr(source, "latitude", 50.0))
        self._longitude = float(getattr(source, "longitude", 0.0))
        _lc = getattr(source, "led_count", 0)
        self._auto_size = not _lc
        self._led_count = _lc if _lc and _lc > 0 else 1
        self._lut_cache = {}
        with self._colors_lock:
            self._colors: Optional[np.ndarray] = None

    def _get_lut_for_day(self, day_of_year: int) -> np.ndarray:
        """Return a solar-time-aware LUT for the given day (cached)."""
        sunrise, sunset = _compute_solar_times(self._latitude, self._longitude, day_of_year)
        sr_key = int(round(sunrise * 60))
        ss_key = int(round(sunset * 60))
        cache_key = (sr_key, ss_key)
        lut = self._lut_cache.get(cache_key)
        if lut is None:
            lut = _build_lut_for_solar_times(sunrise, sunset)
            if len(self._lut_cache) > 8:
                self._lut_cache.clear()
            self._lut_cache[cache_key] = lut
        return lut

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

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

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

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

    def start(self) -> None:
        if self._running:
            return
        self._running = True
        self._thread = threading.Thread(
            target=self._animate_loop,
            name="css-daylight",
            daemon=True,
        )
        self._thread.start()
        logger.info(f"DaylightColorStripStream started (leds={self._led_count})")

    def stop(self) -> None:
        self._running = False
        if self._thread:
            self._thread.join(timeout=5.0)
            if self._thread.is_alive():
                logger.warning("DaylightColorStripStream thread did not terminate within 5s")
            self._thread = None
        logger.info("DaylightColorStripStream stopped")

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

    def update_source(self, source) -> None:
        from wled_controller.storage.color_strip_source import DaylightColorStripSource

        if isinstance(source, DaylightColorStripSource):
            prev_led_count = self._led_count if self._auto_size else None
            self._update_from_source(source)
            if prev_led_count and self._auto_size:
                self._led_count = prev_led_count
            logger.info("DaylightColorStripStream params updated in-place")

    def set_clock(self, clock) -> None:
        self._clock = clock

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

    def _animate_loop(self) -> None:
        _pool_n = 0
        _buf_a = _buf_b = None
        _use_a = True

        try:
            with high_resolution_timer():
                while self._running:
                    wall_start = time.perf_counter()
                    frame_time = self._frame_time
                    try:
                        clock = self._clock
                        if clock:
                            if not clock.is_running:
                                time.sleep(0.1)
                                continue
                            t = clock.get_time()
                            speed = clock.speed
                        else:
                            t = wall_start
                            speed = self.resolve("speed", self._speed)

                        n = self._led_count
                        if n != _pool_n:
                            _pool_n = n
                            _buf_a = np.empty((n, 3), dtype=np.uint8)
                            _buf_b = np.empty((n, 3), dtype=np.uint8)

                        buf = _buf_a if _use_a else _buf_b
                        _use_a = not _use_a

                        if self._use_real_time:
                            now = datetime.datetime.now()
                            day_of_year = now.timetuple().tm_yday
                            minute_of_day = now.hour * 60 + now.minute + now.second / 60.0
                        else:
                            # Simulated: speed=1.0 → full 24h in 240s.
                            # Use summer solstice (day 172) for maximum day length.
                            day_of_year = 172
                            cycle_seconds = 240.0 / max(speed, 0.01)
                            phase = (t % cycle_seconds) / cycle_seconds
                            minute_of_day = phase * 1440.0

                        lut = self._get_lut_for_day(day_of_year)
                        idx = int(minute_of_day) % 1440
                        color = lut[idx]
                        buf[:] = color

                        with self._colors_lock:
                            self._colors = buf
                    except Exception as e:
                        logger.error(f"DaylightColorStripStream animation error: {e}")

                    elapsed = time.perf_counter() - wall_start
                    time.sleep(max(frame_time - elapsed, 0.001))
        except Exception as e:
            logger.error(f"Fatal DaylightColorStripStream loop error: {e}", exc_info=True)
        finally:
            self._running = False
