"""Value stream — runtime scalar signal generators.

A ValueStream wraps a ValueSource config and computes a float (0.0–1.0)
on demand via ``get_value()``.  Six concrete types:

  StaticValueStream      — returns a constant
  AnimatedValueStream    — evaluates a periodic waveform (sine/triangle/square/sawtooth)
  AudioValueStream       — polls audio analysis for RMS/peak/beat, applies
                           sensitivity and temporal smoothing
  TimeOfDayValueStream   — interpolates brightness along a 24h schedule (adaptive_time)
  SceneValueStream       — derives brightness from a picture source's frame luminance (adaptive_scene)
  DaylightValueStream    — brightness derived from daylight cycle LUT (real-time or simulated)

ValueStreams are cheap (trivial math or single poll), so they compute inline
in the caller's processing loop — no background threads required.

ValueStreamManager owns all running ValueStreams, keyed by
``{vs_id}:{consumer_id}``.  Processors call acquire/release.
"""

from __future__ import annotations

import math
import time
from abc import ABC, abstractmethod
from datetime import datetime
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple

import numpy as np

from wled_controller.utils import get_logger

if TYPE_CHECKING:
    from wled_controller.core.audio.audio_capture import AudioCaptureManager
    from wled_controller.core.processing.live_stream_manager import LiveStreamManager
    from wled_controller.storage.audio_source_store import AudioSourceStore
    from wled_controller.storage.value_source import ValueSource
    from wled_controller.storage.value_source_store import ValueSourceStore

logger = get_logger(__name__)


# ---------------------------------------------------------------------------
# Base class
# ---------------------------------------------------------------------------

class ValueStream(ABC):
    """Abstract base for runtime value streams."""

    @abstractmethod
    def get_value(self) -> float:
        """Return current scalar value (0.0–1.0)."""
        ...

    def start(self) -> None:
        """Acquire resources (if any)."""

    def stop(self) -> None:
        """Release resources (if any)."""

    def update_source(self, source: "ValueSource") -> None:
        """Hot-update parameters from a modified ValueSource config."""


# ---------------------------------------------------------------------------
# Static
# ---------------------------------------------------------------------------

class StaticValueStream(ValueStream):
    """Returns a constant float."""

    def __init__(self, value: float = 1.0):
        self._value = max(0.0, min(1.0, value))

    def get_value(self) -> float:
        return self._value

    def update_source(self, source: "ValueSource") -> None:
        from wled_controller.storage.value_source import StaticValueSource
        if isinstance(source, StaticValueSource):
            self._value = max(0.0, min(1.0, source.value))


# ---------------------------------------------------------------------------
# Animated
# ---------------------------------------------------------------------------

_TWO_PI = 2.0 * math.pi


class AnimatedValueStream(ValueStream):
    """Evaluates a periodic waveform from wall-clock time.

    Waveforms: sine, triangle, square, sawtooth.
    Speed is in cycles per minute (cpm).  Output is mapped to
    [min_value, max_value].
    """

    def __init__(
        self,
        waveform: str = "sine",
        speed: float = 10.0,
        min_value: float = 0.0,
        max_value: float = 1.0,
    ):
        self._waveform = waveform
        self._speed = speed
        self._min = min_value
        self._max = max_value
        self._start_time = time.perf_counter()

    def get_value(self) -> float:
        elapsed = time.perf_counter() - self._start_time
        # phase in [0, 1)
        cycles_per_sec = self._speed / 60.0
        phase = (elapsed * cycles_per_sec) % 1.0

        # Raw waveform value in [0, 1]
        wf = self._waveform
        if wf == "sine":
            raw = 0.5 + 0.5 * math.sin(_TWO_PI * phase)
        elif wf == "triangle":
            raw = 1.0 - abs(2.0 * phase - 1.0)
        elif wf == "square":
            raw = 1.0 if phase < 0.5 else 0.0
        elif wf == "sawtooth":
            raw = phase
        else:
            raw = 0.5 + 0.5 * math.sin(_TWO_PI * phase)

        # Map to [min, max]
        return self._min + raw * (self._max - self._min)

    def update_source(self, source: "ValueSource") -> None:
        from wled_controller.storage.value_source import AnimatedValueSource
        if isinstance(source, AnimatedValueSource):
            self._waveform = source.waveform
            self._speed = source.speed
            self._min = source.min_value
            self._max = source.max_value


# ---------------------------------------------------------------------------
# Audio
# ---------------------------------------------------------------------------

class AudioValueStream(ValueStream):
    """Polls audio analysis for a scalar value.

    Modes:
      rms   — root-mean-square level (overall volume)
      peak  — peak amplitude
      beat  — 1.0 on beat, decays toward 0.0 between beats
    """

    def __init__(
        self,
        audio_source_id: str,
        mode: str = "rms",
        sensitivity: float = 1.0,
        smoothing: float = 0.3,
        min_value: float = 0.0,
        max_value: float = 1.0,
        auto_gain: bool = False,
        audio_capture_manager: Optional["AudioCaptureManager"] = None,
        audio_source_store: Optional["AudioSourceStore"] = None,
        audio_template_store=None,
    ):
        self._audio_source_id = audio_source_id
        self._mode = mode
        self._sensitivity = sensitivity
        self._smoothing = smoothing
        self._min = min_value
        self._max = max_value
        self._auto_gain = auto_gain
        self._rolling_peak = 0.0       # tracks observed max raw audio value
        self._rolling_decay = 0.995    # slow decay (~5-10s adaptation)
        self._audio_capture_manager = audio_capture_manager
        self._audio_source_store = audio_source_store
        self._audio_template_store = audio_template_store

        # Resolved audio device params
        self._audio_device_index = -1
        self._audio_loopback = True
        self._audio_channel = "mono"
        self._audio_engine_type = None
        self._audio_engine_config = None

        self._audio_stream = None
        self._prev_value = 0.0
        self._beat_brightness = 0.0

        self._resolve_audio_source()

    def _resolve_audio_source(self) -> None:
        """Resolve audio source to device index / channel / engine info."""
        if self._audio_source_id and self._audio_source_store:
            try:
                device_index, is_loopback, channel, template_id = (
                    self._audio_source_store.resolve_audio_source(self._audio_source_id)
                )
                self._audio_device_index = device_index
                self._audio_loopback = is_loopback
                self._audio_channel = channel
                if template_id and self._audio_template_store:
                    try:
                        tpl = self._audio_template_store.get_template(template_id)
                        self._audio_engine_type = tpl.engine_type
                        self._audio_engine_config = tpl.engine_config
                    except ValueError as e:
                        logger.warning("Audio template %s not found for value stream, using default engine: %s", template_id, e)
                        pass
            except ValueError as e:
                logger.warning(f"Failed to resolve audio source {self._audio_source_id}: {e}")

    def start(self) -> None:
        if self._audio_capture_manager is None:
            return
        self._audio_stream = self._audio_capture_manager.acquire(
            self._audio_device_index, self._audio_loopback,
            engine_type=self._audio_engine_type,
            engine_config=self._audio_engine_config,
        )
        logger.info(
            f"AudioValueStream started (mode={self._mode}, engine={self._audio_engine_type}, "
            f"device={self._audio_device_index}, loopback={self._audio_loopback})"
        )

    def stop(self) -> None:
        if self._audio_stream is not None and self._audio_capture_manager is not None:
            self._audio_capture_manager.release(
                self._audio_device_index, self._audio_loopback,
                engine_type=self._audio_engine_type,
            )
            self._audio_stream = None
        self._prev_value = 0.0
        self._beat_brightness = 0.0

    def get_value(self) -> float:
        if self._audio_stream is None:
            return self._min

        analysis = self._audio_stream.get_latest_analysis()
        if analysis is None:
            return self._prev_value

        raw = self._extract_raw(analysis)

        # Auto-gain: normalize raw against rolling observed peak
        if self._auto_gain:
            self._rolling_peak = max(raw, self._rolling_peak * self._rolling_decay)
            if self._rolling_peak > 0.001:
                raw = raw / self._rolling_peak

        raw = min(1.0, raw * self._sensitivity)

        # Temporal smoothing
        smoothed = self._smoothing * self._prev_value + (1.0 - self._smoothing) * raw
        self._prev_value = smoothed

        # Map to [min, max]
        mapped = self._min + smoothed * (self._max - self._min)
        return max(0.0, min(1.0, mapped))

    def _extract_raw(self, analysis) -> float:
        """Extract raw scalar from audio analysis based on mode."""
        if self._mode == "peak":
            return self._pick_peak(analysis)
        if self._mode == "beat":
            return self._compute_beat(analysis)
        # Default: rms
        return self._pick_rms(analysis)

    def _pick_rms(self, analysis) -> float:
        if self._audio_channel == "left":
            return getattr(analysis, "left_rms", 0.0)
        if self._audio_channel == "right":
            return getattr(analysis, "right_rms", 0.0)
        return getattr(analysis, "rms", 0.0)

    def _pick_peak(self, analysis) -> float:
        if self._audio_channel == "left":
            return getattr(analysis, "left_peak", 0.0)
        if self._audio_channel == "right":
            return getattr(analysis, "right_peak", 0.0)
        return getattr(analysis, "peak", 0.0)

    def _compute_beat(self, analysis) -> float:
        if getattr(analysis, "beat", False):
            self._beat_brightness = 1.0
        else:
            decay = 0.05 + 0.15 * (1.0 / max(self._sensitivity, 0.1))
            self._beat_brightness = max(0.0, self._beat_brightness - decay)
        return self._beat_brightness

    def update_source(self, source: "ValueSource") -> None:
        from wled_controller.storage.value_source import AudioValueSource
        if not isinstance(source, AudioValueSource):
            return

        old_source_id = self._audio_source_id
        old_auto_gain = self._auto_gain
        self._audio_source_id = source.audio_source_id
        self._mode = source.mode
        self._sensitivity = source.sensitivity
        self._smoothing = source.smoothing
        self._min = source.min_value
        self._max = source.max_value
        self._auto_gain = source.auto_gain

        # Reset rolling peak when auto-gain is toggled on
        if self._auto_gain and not old_auto_gain:
            self._rolling_peak = 0.0

        # If audio source changed, re-resolve and swap capture stream
        if source.audio_source_id != old_source_id:
            old_device = self._audio_device_index
            old_loopback = self._audio_loopback
            old_engine_type = self._audio_engine_type
            self._resolve_audio_source()
            if self._audio_stream is not None and self._audio_capture_manager is not None:
                self._audio_capture_manager.release(
                    old_device, old_loopback, engine_type=old_engine_type,
                )
                self._audio_stream = self._audio_capture_manager.acquire(
                    self._audio_device_index, self._audio_loopback,
                    engine_type=self._audio_engine_type,
                    engine_config=self._audio_engine_config,
                )
                logger.info(
                    f"AudioValueStream swapped audio device: "
                    f"{old_engine_type}:{old_device}:{old_loopback} → "
                    f"{self._audio_engine_type}:{self._audio_device_index}:{self._audio_loopback}"
                )


# ---------------------------------------------------------------------------
# Time of Day
# ---------------------------------------------------------------------------

_MINUTES_PER_DAY = 1440  # 24 * 60


class TimeOfDayValueStream(ValueStream):
    """Interpolates brightness along a 24-hour schedule.

    Schedule is a list of {"time": "HH:MM", "value": 0.0–1.0} dicts.
    At runtime, finds the two surrounding points and linearly interpolates.
    The schedule wraps around midnight.
    """

    def __init__(
        self,
        schedule: List[dict],
        min_value: float = 0.0,
        max_value: float = 1.0,
    ):
        self._points: List[Tuple[float, float]] = []  # (minutes, value)
        self._min = min_value
        self._max = max_value
        self._parse_schedule(schedule)

    def _parse_schedule(self, schedule: List[dict]) -> None:
        """Parse schedule into sorted (minutes, value) tuples."""
        points = []
        for entry in schedule:
            t_str = entry.get("time", "00:00")
            parts = t_str.split(":")
            h = int(parts[0])
            m = int(parts[1]) if len(parts) > 1 else 0
            minutes = h * 60 + m
            val = max(0.0, min(1.0, float(entry.get("value", 1.0))))
            points.append((minutes, val))
        points.sort(key=lambda p: p[0])
        self._points = points

    def get_value(self) -> float:
        if len(self._points) < 2:
            return self._max  # fallback: full brightness

        now = datetime.now()
        current = now.hour * 60 + now.minute + now.second / 60.0

        points = self._points
        n = len(points)

        # Find the first point whose time is > current
        right_idx = 0
        for i, (t, _) in enumerate(points):
            if t > current:
                right_idx = i
                break
        else:
            right_idx = 0  # all points <= current: wrap to first

        left_idx = (right_idx - 1) % n
        t_left, v_left = points[left_idx]
        t_right, v_right = points[right_idx]

        # Compute interval length (handling midnight wrap)
        if t_right > t_left:
            interval = t_right - t_left
            elapsed = current - t_left
        else:
            interval = (_MINUTES_PER_DAY - t_left) + t_right
            elapsed = (current - t_left) if current >= t_left else (_MINUTES_PER_DAY - t_left) + current

        frac = elapsed / interval if interval > 0 else 0.0
        raw = v_left + frac * (v_right - v_left)

        # Map to output range
        return self._min + max(0.0, min(1.0, raw)) * (self._max - self._min)

    def update_source(self, source: "ValueSource") -> None:
        from wled_controller.storage.value_source import AdaptiveValueSource
        if isinstance(source, AdaptiveValueSource) and source.source_type == "adaptive_time":
            self._parse_schedule(source.schedule)
            self._min = source.min_value
            self._max = source.max_value


# ---------------------------------------------------------------------------
# Scene
# ---------------------------------------------------------------------------

class SceneValueStream(ValueStream):
    """Derives brightness from a picture source's average frame luminance.

    Acquires a LiveStream from LiveStreamManager on start().  On each
    get_value() call, reads the latest frame, subsamples, computes mean
    luminance (BT.601), applies sensitivity/smoothing/mapping.

    Behaviors:
      complement — dark scene → high brightness (ambient backlight visibility)
      match      — bright scene → high brightness
    """

    def __init__(
        self,
        picture_source_id: str,
        scene_behavior: str = "complement",
        sensitivity: float = 1.0,
        smoothing: float = 0.3,
        min_value: float = 0.0,
        max_value: float = 1.0,
        live_stream_manager: Optional["LiveStreamManager"] = None,
    ):
        self._picture_source_id = picture_source_id
        self._behavior = scene_behavior
        self._sensitivity = sensitivity
        self._smoothing = smoothing
        self._min = min_value
        self._max = max_value
        self._live_stream_manager = live_stream_manager
        self._live_stream = None
        self._prev_value = None  # None = no frame seen yet; skip smoothing on first frame

    def start(self) -> None:
        if self._live_stream_manager and self._picture_source_id:
            try:
                self._live_stream = self._live_stream_manager.acquire(
                    self._picture_source_id
                )
                logger.info(
                    f"SceneValueStream acquired live stream for {self._picture_source_id}"
                )
            except Exception as e:
                logger.warning(f"SceneValueStream failed to acquire live stream: {e}")
                self._live_stream = None

    def stop(self) -> None:
        if self._live_stream is not None and self._live_stream_manager:
            try:
                self._live_stream_manager.release(self._picture_source_id)
            except Exception as e:
                logger.warning(f"SceneValueStream failed to release live stream: {e}")
            self._live_stream = None
        self._prev_value = None

    def get_value(self) -> float:
        if self._live_stream is None:
            return self._prev_value if self._prev_value is not None else 0.0

        frame = self._live_stream.get_latest_frame()
        if frame is None:
            return self._prev_value if self._prev_value is not None else 0.0

        # Fast luminance: subsample to ~64x64 via numpy stride (zero-copy view)
        img = frame.image
        h, w = img.shape[:2]
        step_h = max(1, h // 64)
        step_w = max(1, w // 64)
        sampled = img[::step_h, ::step_w].astype(np.float32)

        # BT.601 weighted luminance, normalized to [0, 1]
        luminance = float(
            (0.299 * sampled[:, :, 0] + 0.587 * sampled[:, :, 1] + 0.114 * sampled[:, :, 2]).mean()
        ) / 255.0

        # Apply sensitivity
        raw = min(1.0, luminance * self._sensitivity)

        # Apply behavior
        if self._behavior == "complement":
            raw = 1.0 - raw

        # Temporal smoothing (EMA) — skip on first frame (no history to blend with)
        if self._prev_value is None:
            smoothed = raw
        else:
            smoothed = self._smoothing * self._prev_value + (1.0 - self._smoothing) * raw
        self._prev_value = smoothed

        # Map to output range
        clamped = max(0.0, min(1.0, smoothed))
        return self._min + clamped * (self._max - self._min)

    def update_source(self, source: "ValueSource") -> None:
        from wled_controller.storage.value_source import AdaptiveValueSource
        if not isinstance(source, AdaptiveValueSource) or source.source_type != "adaptive_scene":
            return

        self._behavior = source.scene_behavior
        self._sensitivity = source.sensitivity
        self._smoothing = source.smoothing
        self._min = source.min_value
        self._max = source.max_value

        # If picture source changed, swap live streams
        if source.picture_source_id != self._picture_source_id:
            old_id = self._picture_source_id
            self._picture_source_id = source.picture_source_id
            if self._live_stream is not None and self._live_stream_manager:
                self._live_stream_manager.release(old_id)
                try:
                    self._live_stream = self._live_stream_manager.acquire(
                        self._picture_source_id
                    )
                    logger.info(
                        f"SceneValueStream swapped live stream: {old_id} → "
                        f"{self._picture_source_id}"
                    )
                except Exception as e:
                    logger.warning(f"SceneValueStream failed to swap live stream: {e}")
                    self._live_stream = None


# ---------------------------------------------------------------------------
# Daylight
# ---------------------------------------------------------------------------


class DaylightValueStream(ValueStream):
    """Brightness derived from the daylight color cycle LUT.

    Computes BT.601 luminance from the RGB sky color at the current
    simulated (or real-time) minute of day, then maps to [min, max].
    Cheap inline computation — no background thread needed.
    """

    def __init__(
        self,
        speed: float = 1.0,
        use_real_time: bool = False,
        latitude: float = 50.0,
        min_value: float = 0.0,
        max_value: float = 1.0,
    ):
        from wled_controller.core.processing.daylight_stream import _get_daylight_lut
        self._lut = _get_daylight_lut()
        self._speed = speed
        self._use_real_time = use_real_time
        self._latitude = latitude
        self._min = min_value
        self._max = max_value
        self._start_time = time.perf_counter()

    def get_value(self) -> float:
        if self._use_real_time:
            now = datetime.now()
            minute_of_day = now.hour * 60 + now.minute + now.second / 60.0
        else:
            t_elapsed = time.perf_counter() - self._start_time
            cycle_seconds = 240.0 / max(self._speed, 0.01)
            phase = (t_elapsed % cycle_seconds) / cycle_seconds
            minute_of_day = phase * 1440.0

        idx = int(minute_of_day) % 1440
        r, g, b = self._lut[idx]

        # BT.601 luminance → 0..1
        luminance = (0.299 * float(r) + 0.587 * float(g) + 0.114 * float(b)) / 255.0

        return self._min + luminance * (self._max - self._min)

    def update_source(self, source: "ValueSource") -> None:
        from wled_controller.storage.value_source import DaylightValueSource
        if isinstance(source, DaylightValueSource):
            self._speed = source.speed
            self._use_real_time = source.use_real_time
            self._latitude = source.latitude
            self._min = source.min_value
            self._max = source.max_value


# ---------------------------------------------------------------------------
# Manager
# ---------------------------------------------------------------------------

class ValueStreamManager:
    """Owns running ValueStream instances, shared and ref-counted by vs_id.

    Value streams produce scalars (not LED-count-dependent), so a single
    stream instance is shared across all consumers that use the same
    ValueSource.  Ref-counting ensures the stream is stopped only when
    the last consumer releases it.
    """

    def __init__(
        self,
        value_source_store: "ValueSourceStore",
        audio_capture_manager: Optional["AudioCaptureManager"] = None,
        audio_source_store: Optional["AudioSourceStore"] = None,
        live_stream_manager: Optional["LiveStreamManager"] = None,
        audio_template_store=None,
    ):
        self._value_source_store = value_source_store
        self._audio_capture_manager = audio_capture_manager
        self._audio_source_store = audio_source_store
        self._live_stream_manager = live_stream_manager
        self._audio_template_store = audio_template_store
        self._streams: Dict[str, ValueStream] = {}      # vs_id → stream
        self._ref_counts: Dict[str, int] = {}            # vs_id → ref count

    def acquire(self, vs_id: str) -> ValueStream:
        """Get or create a shared ValueStream for the given ValueSource.

        Increments the ref count.  The stream is stopped only when all
        consumers have called :meth:`release`.
        """
        if vs_id in self._streams:
            self._ref_counts[vs_id] += 1
            logger.info(f"Shared value stream {vs_id} (refs={self._ref_counts[vs_id]})")
            return self._streams[vs_id]

        source = self._value_source_store.get_source(vs_id)
        stream = self._create_stream(source)
        stream.start()
        self._streams[vs_id] = stream
        self._ref_counts[vs_id] = 1
        logger.info(f"Acquired value stream {vs_id} (type={source.source_type})")
        return stream

    def release(self, vs_id: str) -> None:
        """Decrement ref count; stop the stream when it reaches zero."""
        if vs_id not in self._ref_counts:
            return

        self._ref_counts[vs_id] -= 1
        refs = self._ref_counts[vs_id]

        if refs <= 0:
            stream = self._streams.pop(vs_id, None)
            if stream:
                stream.stop()
            del self._ref_counts[vs_id]
            logger.info(f"Released value stream {vs_id} (last ref)")
        else:
            logger.info(f"Released ref for value stream {vs_id} (refs={refs})")

    def update_source(self, vs_id: str) -> None:
        """Hot-update the shared stream for the given ValueSource."""
        try:
            source = self._value_source_store.get_source(vs_id)
        except ValueError as e:
            logger.debug("Value source %s not found for hot-update: %s", vs_id, e)
            return

        stream = self._streams.get(vs_id)
        if stream:
            stream.update_source(source)
            logger.debug(f"Updated value stream {vs_id}")

    def release_all(self) -> None:
        """Stop and remove all managed streams. Called on shutdown."""
        for vs_id, stream in self._streams.items():
            try:
                stream.stop()
            except Exception as e:
                logger.error(f"Error stopping value stream {vs_id}: {e}")
        self._streams.clear()
        self._ref_counts.clear()
        logger.info("Released all value streams")

    def _create_stream(self, source: "ValueSource") -> ValueStream:
        """Factory: create the appropriate ValueStream for a ValueSource."""
        from wled_controller.storage.value_source import (
            AdaptiveValueSource,
            AnimatedValueSource,
            AudioValueSource,
            DaylightValueSource,
            StaticValueSource,
        )

        if isinstance(source, StaticValueSource):
            return StaticValueStream(value=source.value)

        if isinstance(source, AnimatedValueSource):
            return AnimatedValueStream(
                waveform=source.waveform,
                speed=source.speed,
                min_value=source.min_value,
                max_value=source.max_value,
            )

        if isinstance(source, AudioValueSource):
            return AudioValueStream(
                audio_source_id=source.audio_source_id,
                mode=source.mode,
                sensitivity=source.sensitivity,
                smoothing=source.smoothing,
                min_value=source.min_value,
                max_value=source.max_value,
                auto_gain=source.auto_gain,
                audio_capture_manager=self._audio_capture_manager,
                audio_source_store=self._audio_source_store,
                audio_template_store=self._audio_template_store,
            )

        if isinstance(source, DaylightValueSource):
            return DaylightValueStream(
                speed=source.speed,
                use_real_time=source.use_real_time,
                latitude=source.latitude,
                min_value=source.min_value,
                max_value=source.max_value,
            )

        if isinstance(source, AdaptiveValueSource):
            if source.source_type == "adaptive_scene":
                return SceneValueStream(
                    picture_source_id=source.picture_source_id,
                    scene_behavior=source.scene_behavior,
                    sensitivity=source.sensitivity,
                    smoothing=source.smoothing,
                    min_value=source.min_value,
                    max_value=source.max_value,
                    live_stream_manager=self._live_stream_manager,
                )
            return TimeOfDayValueStream(
                schedule=source.schedule,
                min_value=source.min_value,
                max_value=source.max_value,
            )

        # Fallback
        return StaticValueStream(value=1.0)
