"""Single source of truth for value-stream construction.

``ValueStreamManager._create_stream`` used to be a 168-line ``isinstance``
ladder over 14 ``ValueSource`` subclasses with a silent fallback to
``StaticValueStream(value=1.0)``. The ladder forced any new value kind to
edit the factory plus the storage subclass plus the schemas plus the store's
``create_source`` — and a missing branch corrupted the stream at runtime.

This module replaces the ladder with a single ``STREAM_BUILDERS`` registry
keyed by the ``source_type`` string (matching the storage layer's
``_VALUE_SOURCE_MAP``). An import-time coverage assertion guarantees the two
registries stay aligned.

Builders take a ``(source, deps: ValueStreamDeps) -> ValueStream`` shape so
both the production manager and any preview / test harness can populate the
deps from their own context.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Callable

if TYPE_CHECKING:
    # Typed forward references so mypy/pyright catch typos like
    # ``d.gradient_stroe`` at static-analysis time. At runtime these are
    # ``Any`` — the live objects come from the FastAPI / manager wiring.
    from ledgrab.core.audio.audio_capture_manager import AudioCaptureManager
    from ledgrab.core.game_integration.event_bus import GameEventBus
    from ledgrab.core.home_assistant.ha_manager import HomeAssistantManager
    from ledgrab.core.processing.color_strip_stream_manager import (
        ColorStripStreamManager,
    )
    from ledgrab.core.processing.live_stream_manager import LiveStreamManager
    from ledgrab.core.processing.sync_clock_manager import SyncClockRuntime
    from ledgrab.storage.audio_processing_template_store import (
        AudioProcessingTemplateStore,
    )
    from ledgrab.storage.audio_source_store import AudioSourceStore
    from ledgrab.storage.audio_template_store import AudioTemplateStore
    from ledgrab.storage.gradient_store import GradientStore
    from ledgrab.storage.http_endpoint_store import HTTPEndpointStore


@dataclass(frozen=True)
class ValueStreamDeps:
    """Dependency bag for value-stream construction.

    Builders only read the subset they need. ``value_stream_manager`` is the
    one truly load-bearing field — it is referenced by
    ``GradientMapValueStream`` so it can recursively acquire its source
    value stream.

    ``clock_runtime`` is pre-acquired by the manager exclusively for the
    ``animated_color`` kind (see :data:`NEEDS_CLOCK_RUNTIME`). The manager
    owns the bookkeeping for tracking ``vs_id → clock_id`` so the builder
    stays a pure ``(source, deps) -> stream`` mapping. If a new kind ever
    grows a clock dependency, add it to ``NEEDS_CLOCK_RUNTIME`` AND surface
    a separate field — sharing ``clock_runtime`` across kinds invites the
    wrong runtime being passed to the wrong builder.

    Field types are quoted so they stay informational under
    ``TYPE_CHECKING`` and the dataclass still accepts plain mocks at
    runtime. Builders therefore get IDE/lint help against typos like
    ``d.gradient_stroe`` while production code remains duck-typed.
    """

    value_stream_manager: "Any"
    audio_capture_manager: "AudioCaptureManager" | None = None
    audio_source_store: "AudioSourceStore" | None = None
    audio_template_store: "AudioTemplateStore" | None = None
    audio_processing_template_store: "AudioProcessingTemplateStore" | None = None
    live_stream_manager: "LiveStreamManager" | None = None
    ha_manager: "HomeAssistantManager" | None = None
    gradient_store: "GradientStore" | None = None
    css_stream_manager: "ColorStripStreamManager" | None = None
    event_bus: "GameEventBus" | None = None
    http_endpoint_store: "HTTPEndpointStore" | None = None
    clock_runtime: "SyncClockRuntime" | None = None


# ---------------------------------------------------------------------------
# Per-kind builders
# ---------------------------------------------------------------------------


def _build_static(source, _d: ValueStreamDeps):
    from ledgrab.core.processing.value_stream import StaticValueStream

    return StaticValueStream(value=source.value)


def _build_animated(source, _d: ValueStreamDeps):
    from ledgrab.core.processing.value_stream import AnimatedValueStream

    return AnimatedValueStream(
        waveform=source.waveform,
        speed=source.speed,
        min_value=source.min_value,
        max_value=source.max_value,
    )


def _build_audio(source, d: ValueStreamDeps):
    from ledgrab.core.processing.value_stream import AudioValueStream

    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=d.audio_capture_manager,
        audio_source_store=d.audio_source_store,
        audio_template_store=d.audio_template_store,
        audio_processing_template_store=d.audio_processing_template_store,
    )


def _build_daylight(source, _d: ValueStreamDeps):
    from ledgrab.core.processing.value_stream import DaylightValueStream

    return DaylightValueStream(
        speed=source.speed,
        use_real_time=source.use_real_time,
        latitude=source.latitude,
        longitude=source.longitude,
        min_value=source.min_value,
        max_value=source.max_value,
    )


def _build_adaptive_time(source, _d: ValueStreamDeps):
    from ledgrab.core.processing.value_stream import TimeOfDayValueStream

    return TimeOfDayValueStream(
        schedule=source.schedule,
        min_value=source.min_value,
        max_value=source.max_value,
    )


def _build_adaptive_scene(source, d: ValueStreamDeps):
    from ledgrab.core.processing.value_stream import SceneValueStream

    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=d.live_stream_manager,
    )


def _build_static_color(source, _d: ValueStreamDeps):
    from ledgrab.core.processing.value_stream import StaticColorValueStream

    return StaticColorValueStream(color=source.color)


def _build_animated_color(source, d: ValueStreamDeps):
    # See NEEDS_CLOCK_RUNTIME below: ``d.clock_runtime`` is pre-acquired by
    # ``ValueStreamManager._create_stream`` exclusively for this kind. Any
    # other builder that ever needs a clock should add its own deps field
    # rather than read this one.
    from ledgrab.core.processing.value_stream import AnimatedColorValueStream

    return AnimatedColorValueStream(
        colors=source.colors,
        speed=source.speed,
        easing=source.easing,
        clock=d.clock_runtime,
    )


def _build_adaptive_time_color(source, _d: ValueStreamDeps):
    from ledgrab.core.processing.value_stream import AdaptiveTimeColorValueStream

    return AdaptiveTimeColorValueStream(schedule=source.schedule)


def _build_ha_entity(source, d: ValueStreamDeps):
    from ledgrab.core.processing.value_stream import HAEntityValueStream

    return HAEntityValueStream(
        ha_source_id=source.ha_source_id,
        entity_id=source.entity_id,
        attribute=source.attribute,
        min_ha_value=source.min_ha_value,
        max_ha_value=source.max_ha_value,
        smoothing=source.smoothing,
        ha_manager=d.ha_manager,
    )


def _build_gradient_map(source, d: ValueStreamDeps):
    from ledgrab.core.processing.value_stream import GradientMapValueStream

    return GradientMapValueStream(
        value_source_id=source.value_source_id,
        gradient_id=source.gradient_id,
        easing=source.easing,
        value_stream_manager=d.value_stream_manager,
        gradient_store=d.gradient_store,
    )


def _build_css_extract(source, d: ValueStreamDeps):
    from ledgrab.core.processing.value_stream import CSSExtractValueStream

    return CSSExtractValueStream(
        color_strip_source_id=source.color_strip_source_id,
        led_start=source.led_start,
        led_end=source.led_end,
        css_stream_manager=d.css_stream_manager,
    )


def _build_system_metrics(source, _d: ValueStreamDeps):
    from ledgrab.core.processing.value_stream import SystemMetricsValueStream

    return SystemMetricsValueStream(
        metric=source.metric,
        min_value=source.min_value,
        max_value=source.max_value,
        max_rate=source.max_rate,
        disk_path=source.disk_path,
        sensor_label=source.sensor_label,
        poll_interval=source.poll_interval,
        smoothing=source.smoothing,
    )


def _build_game_event(source, d: ValueStreamDeps):
    # Late import: ``GameEventValueStream`` lives in a separate sub-package
    # to keep the game-event subsystem (which transitively pulls in the
    # game-integration adapters) off the cold-start path for installs that
    # never use game events.
    from ledgrab.core.value_sources.game_event_value_source import GameEventValueStream

    return GameEventValueStream(
        event_type=source.event_type,
        min_game_value=source.min_game_value,
        max_game_value=source.max_game_value,
        smoothing=source.smoothing,
        default_value=source.default_value,
        timeout=source.timeout,
        event_bus=d.event_bus,
    )


def _build_http(source, d: ValueStreamDeps):
    from ledgrab.core.processing.value_stream import HTTPValueStream

    return HTTPValueStream(
        endpoint_id=source.http_endpoint_id,
        json_path=source.json_path,
        interval_s=source.interval_s,
        min_value=source.min_value,
        max_value=source.max_value,
        smoothing=source.smoothing,
        http_endpoint_store=d.http_endpoint_store,
    )


# ---------------------------------------------------------------------------
# Registry
# ---------------------------------------------------------------------------


StreamBuilder = Callable[[Any, ValueStreamDeps], Any]

STREAM_BUILDERS: dict[str, StreamBuilder] = {
    "static": _build_static,
    "animated": _build_animated,
    "audio": _build_audio,
    "daylight": _build_daylight,
    "adaptive_time": _build_adaptive_time,
    "adaptive_scene": _build_adaptive_scene,
    "static_color": _build_static_color,
    "animated_color": _build_animated_color,
    "adaptive_time_color": _build_adaptive_time_color,
    "ha_entity": _build_ha_entity,
    "gradient_map": _build_gradient_map,
    "css_extract": _build_css_extract,
    "system_metrics": _build_system_metrics,
    "game_event": _build_game_event,
    "http": _build_http,
}


# ``animated_color`` is the only kind that needs a pre-acquired SyncClockRuntime
# from the manager; the rest derive everything they need from ``source`` and
# ``deps``. Exposing this set lets the manager perform the side-effecting
# acquisition step exactly once, before delegating to the registry.
NEEDS_CLOCK_RUNTIME: frozenset[str] = frozenset({"animated_color"})


def build_stream(source, deps: ValueStreamDeps):
    """Build a ValueStream for *source*.

    Raises ``ValueError`` when no builder is registered for
    ``source.source_type``. Coverage is asserted at module import, so this
    only fires for an in-flight instance whose ``source_type`` somehow
    drifted from the registered set.
    """
    builder = STREAM_BUILDERS.get(source.source_type)
    if builder is None:
        raise ValueError(
            f"No value-stream builder for source_type {source.source_type!r} "
            f"(id={getattr(source, 'id', '?')!r})"
        )
    return builder(source, deps)


def _assert_value_kind_coverage() -> None:
    """Verify the registry and storage's ``_VALUE_SOURCE_MAP`` agree.

    Runs at module import. Symmetric: every kind in storage must have a
    builder, and every builder must correspond to a real storage kind.
    Also asserts ``NEEDS_CLOCK_RUNTIME`` only names kinds that exist in the
    registry, so a typo there fails the boot loudly instead of silently
    leaking a ``SyncClockRuntime`` acquisition.
    """
    from ledgrab.storage.value_source import _VALUE_SOURCE_MAP

    storage_kinds = set(_VALUE_SOURCE_MAP.keys())
    builder_kinds = set(STREAM_BUILDERS.keys())
    missing = storage_kinds - builder_kinds
    extra = builder_kinds - storage_kinds
    if missing or extra:
        problems = []
        if missing:
            problems.append(f"missing builders: {sorted(missing)}")
        if extra:
            problems.append(f"unregistered kinds: {sorted(extra)}")
        raise RuntimeError(
            "value_kinds.STREAM_BUILDERS is out of sync with storage._VALUE_SOURCE_MAP: "
            + "; ".join(problems)
        )

    rogue_clock_kinds = NEEDS_CLOCK_RUNTIME - builder_kinds
    if rogue_clock_kinds:
        raise RuntimeError(
            "value_kinds.NEEDS_CLOCK_RUNTIME names kinds with no registered "
            f"builder: {sorted(rogue_clock_kinds)}"
        )


_assert_value_kind_coverage()
