"""Mapped color strip stream — places different sources at different LED ranges."""

import threading
import time
from typing import Dict, List, Optional

import numpy as np

from wled_controller.core.processing.color_strip_stream import ColorStripStream
from wled_controller.utils import get_logger

logger = get_logger(__name__)


class MappedColorStripStream(ColorStripStream):
    """Places multiple ColorStripStreams side-by-side at distinct LED ranges.

    Each zone references an existing (non-mapped) ColorStripSource and is
    assigned a start/end LED range.  Unlike composite (which blends layers
    covering ALL LEDs), mapped assigns each source to a distinct sub-range.
    Gaps between zones stay black (zeros).

    Processing runs in a background thread at 30 FPS, polling each
    sub-stream's latest colors and copying into the output array.
    """

    def __init__(self, source, css_manager):
        self._source_id: str = source.id
        self._zones: List[dict] = list(source.zones)
        self._led_count: int = source.led_count
        self._auto_size: bool = source.led_count == 0
        self._css_manager = css_manager
        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()

        # zone_index -> (source_id, consumer_id, stream)
        self._sub_streams: Dict[int, tuple] = {}
        # (src_len, dst_len) -> (src_x, dst_x, buffer) cache for zone resizing
        self._resize_cache: Dict[tuple, tuple] = {}
        self._sub_lock = threading.Lock()  # guards _sub_streams access across threads

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

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

    def set_capture_fps(self, fps: int) -> None:
        self._fps = max(1, min(90, 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:
        with self._sub_lock:
            self._acquire_sub_streams()
        self._running = True
        self._thread = threading.Thread(
            target=self._processing_loop, daemon=True,
            name=f"MappedCSS-{self._source_id[:12]}",
        )
        self._thread.start()
        logger.info(
            f"MappedColorStripStream started: {self._source_id} "
            f"({len(self._sub_streams)} zones, {self._led_count} LEDs)"
        )

    def stop(self) -> None:
        self._running = False
        if self._thread is not None:
            self._thread.join(timeout=5.0)
            self._thread = None
        with self._sub_lock:
            self._release_sub_streams()
        logger.info(f"MappedColorStripStream 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 self._auto_size and device_led_count > 0 and device_led_count != self._led_count:
            self._led_count = device_led_count
            with self._sub_lock:
                self._reconfigure_sub_streams()
            logger.debug(f"MappedColorStripStream auto-sized to {device_led_count} LEDs")

    def update_source(self, source) -> None:
        """Hot-update: rebuild sub-streams if zone config changed."""
        new_zones = list(source.zones)
        old_zone_ids = [(z.get("source_id"), z.get("start"), z.get("end"), z.get("reverse"))
                        for z in self._zones]
        new_zone_ids = [(z.get("source_id"), z.get("start"), z.get("end"), z.get("reverse"))
                        for z in new_zones]

        self._zones = new_zones

        if source.led_count != 0:
            self._led_count = source.led_count
            self._auto_size = False

        if old_zone_ids != new_zone_ids:
            with self._sub_lock:
                self._release_sub_streams()
                self._acquire_sub_streams()
            logger.info(f"MappedColorStripStream rebuilt sub-streams: {self._source_id}")

    # ── Sub-stream lifecycle ────────────────────────────────────

    def _acquire_sub_streams(self) -> None:
        for i, zone in enumerate(self._zones):
            src_id = zone.get("source_id", "")
            if not src_id:
                continue
            consumer_id = f"{self._source_id}__zone_{i}"
            try:
                stream = self._css_manager.acquire(src_id, consumer_id)
                zone_len = self._zone_length(zone)
                if hasattr(stream, "configure") and zone_len > 0:
                    stream.configure(zone_len)
                self._sub_streams[i] = (src_id, consumer_id, stream)
            except Exception as e:
                logger.warning(
                    f"Mapped zone {i} (source {src_id}) failed to acquire: {e}"
                )

    def _release_sub_streams(self) -> None:
        for _idx, (src_id, consumer_id, _stream) in list(self._sub_streams.items()):
            try:
                self._css_manager.release(src_id, consumer_id)
            except Exception as e:
                logger.warning(f"Mapped zone release error ({src_id}): {e}")
        self._sub_streams.clear()

    def _reconfigure_sub_streams(self) -> None:
        """Reconfigure zone sub-streams with updated LED ranges."""
        for i, zone in enumerate(self._zones):
            if i not in self._sub_streams:
                continue
            _src_id, _consumer_id, stream = self._sub_streams[i]
            zone_len = self._zone_length(zone)
            if hasattr(stream, "configure") and zone_len > 0:
                stream.configure(zone_len)

    def _zone_length(self, zone: dict) -> int:
        """Calculate LED count for a zone. end=0 means auto-fill to total."""
        start = zone.get("start", 0)
        end = zone.get("end", 0)
        if end <= 0:
            end = self._led_count
        return max(0, end - start)

    # ── Processing loop ─────────────────────────────────────────

    def _processing_loop(self) -> None:
        frame_time = self._frame_time
        _pool_n = 0
        _buf_a = _buf_b = None
        _use_a = True
        try:
            while self._running:
                loop_start = time.perf_counter()

                try:
                    target_n = self._led_count
                    if target_n <= 0:
                        time.sleep(frame_time)
                        continue

                    if target_n != _pool_n:
                        _pool_n = target_n
                        _buf_a = np.zeros((target_n, 3), dtype=np.uint8)
                        _buf_b = np.zeros((target_n, 3), dtype=np.uint8)

                    result = _buf_a if _use_a else _buf_b
                    _use_a = not _use_a
                    result[:] = 0

                    with self._sub_lock:
                        sub_snapshot = dict(self._sub_streams)

                    for i, zone in enumerate(self._zones):
                        if i not in sub_snapshot:
                            continue

                        _src_id, _consumer_id, stream = sub_snapshot[i]
                        colors = stream.get_latest_colors()
                        if colors is None:
                            continue

                        start = zone.get("start", 0)
                        end = zone.get("end", 0)
                        if end <= 0:
                            end = target_n
                        start = max(0, min(start, target_n))
                        end = max(start, min(end, target_n))
                        zone_len = end - start

                        if zone_len <= 0:
                            continue

                        # Resize sub-stream output to zone length if needed
                        if len(colors) != zone_len:
                            rkey = (len(colors), zone_len)
                            cached = self._resize_cache.get(rkey)
                            if cached is None:
                                cached = (
                                    np.linspace(0, 1, len(colors)),
                                    np.linspace(0, 1, zone_len),
                                    np.empty((zone_len, 3), dtype=np.uint8),
                                )
                                self._resize_cache[rkey] = cached
                            src_x, dst_x, resized = cached
                            for ch in range(3):
                                np.copyto(
                                    resized[:, ch],
                                    np.interp(dst_x, src_x, colors[:, ch]),
                                    casting="unsafe",
                                )
                            colors = resized

                        if zone.get("reverse", False):
                            colors = colors[::-1]

                        result[start:end] = colors

                    with self._colors_lock:
                        self._latest_colors = result

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

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