"""Single source of truth for non-sharable color-strip stream construction.

Both the preview WebSocket (``api/routes/color_strip_sources/ws_stream.py``)
and the production ``ColorStripStreamManager.acquire`` used to maintain
parallel ``if source.source_type == "..." elif ... else ..._SIMPLE_STREAM_MAP``
chains. Adding a new kind required keeping those two chains in lockstep, and
silently fell through to a generic stream class when an entry was missed.

This module replaces both chains with a single ``STREAM_BUILDERS`` registry
plus a small ``StreamDeps`` dependency bag. Each caller populates the bag
from its own context (DI container, processor manager, etc.) and looks the
builder up by ``source.source_type``. A coverage assertion at import time
guarantees every kind in ``storage._SOURCE_TYPE_MAP`` is either sharable or
has a builder here — silent fall-throughs are no longer possible.

Sharable kinds (``picture``, ``picture_advanced``, ``key_colors``) are NOT in
this registry: they require an injected ``LiveStream`` whose acquisition is
intertwined with the source's calibration, which does not fit a uniform
factory signature. Those continue to use bespoke paths inside
``ColorStripStreamManager``.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Callable


@dataclass(frozen=True)
class StreamDeps:
    """Dependency bag for non-sharable stream construction.

    Each call site (preview WebSocket, production stream manager) builds one
    of these from its own context before invoking :func:`build_stream`.
    Fields are ``Any`` (with ``None`` defaults) because individual builders
    only consume a subset; tests can supply a minimal bag.

    ``frozen=True`` guards against a builder accidentally reassigning a
    field; it does NOT make the referenced objects immutable — the
    ``css_manager``, stores, etc. are live mutable services.

    ``css_manager`` is needed by composite/mapped/processed builders so they
    can recursively acquire dependent streams. Single-kind builders ignore
    it. The field has no default so callers are forced to think about which
    manager they are wiring through.
    """

    css_manager: Any
    value_stream_manager: Any = None
    cspt_store: Any = None  # ColorStripProcessingTemplateStore
    weather_manager: Any = None
    audio_capture_manager: Any = None
    audio_source_store: Any = None
    audio_template_store: Any = None
    audio_processing_template_store: Any = None
    game_event_bus: Any = None
    depth: int = 0  # composite nesting depth — passed through verbatim


# ---------------------------------------------------------------------------
# Per-kind builders
#
# Each builder is a small free function ``(source, deps) -> ColorStripStream``.
# Imports are deferred to keep this module cheap to import (the production
# processing graph is large).
# ---------------------------------------------------------------------------


def _build_audio(source, d: StreamDeps):
    from ledgrab.core.processing.audio_stream import AudioColorStripStream

    return AudioColorStripStream(
        source,
        d.audio_capture_manager,
        d.audio_source_store,
        d.audio_template_store,
        d.audio_processing_template_store,
    )


def _build_composite(source, d: StreamDeps):
    from ledgrab.core.processing.composite_stream import CompositeColorStripStream

    return CompositeColorStripStream(
        source,
        d.css_manager,
        d.value_stream_manager,
        d.cspt_store,
        depth=d.depth,
    )


def _build_mapped(source, d: StreamDeps):
    from ledgrab.core.processing.mapped_stream import MappedColorStripStream

    return MappedColorStripStream(source, d.css_manager)


def _build_processed(source, d: StreamDeps):
    from ledgrab.core.processing.processed_stream import ProcessedColorStripStream

    return ProcessedColorStripStream(source, d.css_manager, d.cspt_store)


def _build_weather(source, d: StreamDeps):
    from ledgrab.core.processing.weather_stream import WeatherColorStripStream

    return WeatherColorStripStream(source, d.weather_manager)


def _build_game_event(source, d: StreamDeps):
    from ledgrab.core.processing.game_event_stream import GameEventColorStripStream

    stream = GameEventColorStripStream(source)
    if d.game_event_bus is not None:
        stream.set_event_bus(d.game_event_bus)
    return stream


def _make_source_only_builder(loader: Callable[[], type]) -> Callable[[Any, StreamDeps], Any]:
    """Wrap a class-loader so it produces a uniform ``(source, deps) -> stream`` builder.

    The loader is called on each invocation but module caching makes the
    import a single dict lookup after the first call.
    """

    def _build(source, _deps: StreamDeps):
        return loader()(source)

    return _build


def _single_color_cls() -> type:
    from ledgrab.core.processing.color_strip_stream import SingleColorStripStream

    return SingleColorStripStream


def _gradient_cls() -> type:
    from ledgrab.core.processing.color_strip_stream import GradientColorStripStream

    return GradientColorStripStream


def _effect_cls() -> type:
    from ledgrab.core.processing.effect_stream import EffectColorStripStream

    return EffectColorStripStream


def _api_input_cls() -> type:
    from ledgrab.core.processing.api_input_stream import ApiInputColorStripStream

    return ApiInputColorStripStream


def _notification_cls() -> type:
    from ledgrab.core.processing.notification_stream import NotificationColorStripStream

    return NotificationColorStripStream


def _daylight_cls() -> type:
    from ledgrab.core.processing.daylight_stream import DaylightColorStripStream

    return DaylightColorStripStream


def _candlelight_cls() -> type:
    from ledgrab.core.processing.candlelight_stream import CandlelightColorStripStream

    return CandlelightColorStripStream


def _math_wave_cls() -> type:
    from ledgrab.core.processing.math_wave_stream import MathWaveColorStripStream

    return MathWaveColorStripStream


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


StreamBuilder = Callable[[Any, StreamDeps], Any]

STREAM_BUILDERS: dict[str, StreamBuilder] = {
    "audio": _build_audio,
    "composite": _build_composite,
    "mapped": _build_mapped,
    "processed": _build_processed,
    "weather": _build_weather,
    "game_event": _build_game_event,
    "single_color": _make_source_only_builder(_single_color_cls),
    # Legacy alias: pre-rename rows used "static". The data migration rewrites
    # the on-disk source_type on startup, but this alias keeps an in-flight
    # legacy entry resolving to the right stream class.
    "static": _make_source_only_builder(_single_color_cls),
    "gradient": _make_source_only_builder(_gradient_cls),
    "effect": _make_source_only_builder(_effect_cls),
    "api_input": _make_source_only_builder(_api_input_cls),
    "notification": _make_source_only_builder(_notification_cls),
    "daylight": _make_source_only_builder(_daylight_cls),
    "candlelight": _make_source_only_builder(_candlelight_cls),
    "math_wave": _make_source_only_builder(_math_wave_cls),
}


# Sharable kinds are handled by dedicated LiveStream-acquisition paths in
# ColorStripStreamManager (their construction depends on calibration → picture
# source resolution, which does not fit a uniform factory signature).
SHARABLE_KINDS: frozenset[str] = frozenset({"picture", "picture_advanced", "key_colors"})


def build_stream(source, deps: StreamDeps):
    """Build a non-sharable color-strip stream for *source*.

    Raises ``ValueError`` if the kind has no registered builder (which
    would indicate a sharable source slipped through the caller's
    ``sharable`` gate, or a new kind missing from this registry).
    """
    builder = STREAM_BUILDERS.get(source.source_type)
    if builder is None:
        raise ValueError(
            f"No stream builder for non-sharable color-strip-source kind "
            f"{source.source_type!r} (id={getattr(source, 'id', '?')!r})"
        )
    return builder(source, deps)


def _assert_stream_kind_coverage() -> None:
    """Verify the registry is a strict partition: every kind from storage is
    either listed in SHARABLE_KINDS or has a STREAM_BUILDERS entry.

    Runs at module import so a kind added to ``_SOURCE_TYPE_MAP`` without a
    corresponding builder fails the server boot loudly instead of silently
    falling through at request time.

    Contract note
    -------------
    This check is **asymmetric** (``STREAM_BUILDERS ∪ SHARABLE_KINDS ==
    storage_kinds``) because sharable kinds are constructed by a separate
    path inside ``ColorStripStreamManager``. The sister assertion in
    ``api/routes/color_strip_sources/_helpers.py::_assert_response_map_coverage``
    is **symmetric** (``_RESPONSE_MAP keys == storage_kinds``) because every
    kind, sharable or not, still needs a response shape. Both assertions key
    by the ``source_type`` string; adding a new kind requires updates to
    storage, ``_RESPONSE_MAP``, and either ``STREAM_BUILDERS`` or
    ``SHARABLE_KINDS``. Both assertions catch missing entries; only this one
    expects a subset relationship.
    """
    from ledgrab.storage.color_strip_source import _SOURCE_TYPE_MAP

    storage_kinds = set(_SOURCE_TYPE_MAP.keys())
    builder_kinds = set(STREAM_BUILDERS.keys())
    expected_non_sharable = storage_kinds - SHARABLE_KINDS

    missing = expected_non_sharable - 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(
            "color_strip_kinds.STREAM_BUILDERS is out of sync with storage._SOURCE_TYPE_MAP: "
            + "; ".join(problems)
        )


_assert_stream_kind_coverage()
