"""Shared helpers for color strip source routes."""

from ledgrab.api.schemas.color_strip_sources import (
    ApiInputCSSResponse,
    AudioCSSResponse,
    CandlelightCSSResponse,
    ColorStop as ColorStopSchema,
    ColorStripSourceResponse,
    CompositeCSSResponse,
    DaylightCSSResponse,
    EffectCSSResponse,
    GameEventCSSResponse,
    GradientCSSResponse,
    KeyColorsCSSResponse,
    MappedCSSResponse,
    MathWaveCSSResponse,
    NotificationCSSResponse,
    PictureAdvancedCSSResponse,
    PictureCSSResponse,
    ProcessedCSSResponse,
    SingleColorCSSResponse,
    WeatherCSSResponse,
)
from ledgrab.api.schemas.devices import Calibration as CalibrationSchema
from ledgrab.core.capture.calibration import (
    calibration_from_dict,
    calibration_to_dict,
)
from ledgrab.storage.color_strip_source import (
    _SOURCE_TYPE_MAP as _STORAGE_TYPE_MAP,
)
from ledgrab.storage.picture_source import (
    ProcessedPictureSource,
    ScreenCapturePictureSource,
)
from ledgrab.storage.picture_source_store import PictureSourceStore
from ledgrab.utils import get_logger

logger = get_logger(__name__)


def _common_response_kwargs(source, overlay_active: bool = False) -> dict:
    """Shared response fields from any ColorStripSource."""
    return dict(
        id=source.id,
        name=source.name,
        description=source.description,
        led_count=getattr(source, "led_count", 0),
        overlay_active=overlay_active,
        clock_id=source.clock_id,
        tags=source.tags,
        created_at=source.created_at,
        updated_at=source.updated_at,
        icon=getattr(source, "icon", "") or "",
        icon_color=getattr(source, "icon_color", "") or "",
    )


def _calibration_schema(source) -> CalibrationSchema | None:
    """Convert a source's calibration to a schema object, or None."""
    cal = getattr(source, "calibration", None)
    if cal is None:
        return None
    try:
        return CalibrationSchema(**calibration_to_dict(cal))
    except Exception as e:
        logger.debug("calibration schema build failed: %s", e)
        return None


def _stops_schema(source) -> list[ColorStopSchema] | None:
    """Convert a source's stops list to schema objects, or None."""
    raw = getattr(source, "stops", None)
    if raw is None:
        return None
    try:
        return [ColorStopSchema(**dict(s)) for s in raw]
    except Exception as e:
        logger.debug("stops schema build failed: %s", e)
        return None


# Maps ``source_type`` string → response builder.
#
# Keying by source_type (rather than type(source)) lets the import-time
# coverage check use the storage registry's keys directly, with no
# inversion or duplicate-class handling for legacy aliases.
_RESPONSE_MAP: dict = {
    "picture": lambda s, kw: PictureCSSResponse(
        **kw,
        picture_source_id=s.picture_source_id,
        smoothing=s.smoothing.to_dict(),
        interpolation_mode=s.interpolation_mode,
        calibration=_calibration_schema(s),
    ),
    "picture_advanced": lambda s, kw: PictureAdvancedCSSResponse(
        **kw,
        smoothing=s.smoothing.to_dict(),
        interpolation_mode=s.interpolation_mode,
        calibration=_calibration_schema(s),
    ),
    "single_color": lambda s, kw: SingleColorCSSResponse(
        **kw,
        color=s.color.to_dict(),
        animation=s.animation,
    ),
    # Legacy alias: pre-rename rows used "static"; the data migration rewrites
    # them on first store load but a stale in-flight instance would still
    # carry source_type='static' until the next reload.
    "static": lambda s, kw: SingleColorCSSResponse(
        **kw,
        color=s.color.to_dict(),
        animation=s.animation,
    ),
    "gradient": lambda s, kw: GradientCSSResponse(
        **kw,
        stops=_stops_schema(s),
        animation=s.animation,
        easing=s.easing,
        gradient_id=s.gradient_id,
    ),
    "effect": lambda s, kw: EffectCSSResponse(
        **kw,
        effect_type=s.effect_type,
        palette=s.palette,
        gradient_id=s.gradient_id,
        color=s.color.to_dict(),
        intensity=s.intensity.to_dict(),
        scale=s.scale.to_dict(),
        mirror=s.mirror,
        custom_palette=s.custom_palette,
    ),
    "composite": lambda s, kw: CompositeCSSResponse(
        **kw,
        layers=[dict(layer) for layer in s.layers],
    ),
    "mapped": lambda s, kw: MappedCSSResponse(
        **kw,
        zones=[dict(z) for z in s.zones],
    ),
    "audio": lambda s, kw: AudioCSSResponse(
        **kw,
        visualization_mode=s.visualization_mode,
        audio_source_id=s.audio_source_id,
        sensitivity=s.sensitivity.to_dict(),
        smoothing=s.smoothing.to_dict(),
        palette=s.palette,
        gradient_id=s.gradient_id,
        color=s.color.to_dict(),
        color_peak=s.color_peak.to_dict(),
        mirror=s.mirror,
        beat_decay=s.beat_decay.to_dict(),
    ),
    "api_input": lambda s, kw: ApiInputCSSResponse(
        **kw,
        fallback_color=s.fallback_color.to_dict(),
        timeout=s.timeout.to_dict(),
        interpolation=s.interpolation,
    ),
    "notification": lambda s, kw: NotificationCSSResponse(
        **kw,
        notification_effect=s.notification_effect,
        duration_ms=s.duration_ms.to_dict(),
        default_color=s.default_color.to_dict(),
        app_colors=dict(s.app_colors),
        app_filter_mode=s.app_filter_mode,
        app_filter_list=list(s.app_filter_list),
        os_listener=s.os_listener,
        sound_asset_id=s.sound_asset_id,
        sound_volume=s.sound_volume.to_dict(),
        app_sounds=dict(s.app_sounds),
    ),
    "daylight": lambda s, kw: DaylightCSSResponse(
        **kw,
        speed=s.speed.to_dict(),
        use_real_time=s.use_real_time,
        latitude=s.latitude,
        longitude=s.longitude,
    ),
    "candlelight": lambda s, kw: CandlelightCSSResponse(
        **kw,
        color=s.color.to_dict(),
        intensity=s.intensity.to_dict(),
        num_candles=s.num_candles,
        speed=s.speed.to_dict(),
        wind_strength=s.wind_strength.to_dict(),
        candle_type=s.candle_type,
    ),
    "processed": lambda s, kw: ProcessedCSSResponse(
        **kw,
        input_source_id=s.input_source_id,
        processing_template_id=s.processing_template_id,
    ),
    "weather": lambda s, kw: WeatherCSSResponse(
        **kw,
        weather_source_id=s.weather_source_id,
        speed=s.speed.to_dict(),
        temperature_influence=s.temperature_influence.to_dict(),
    ),
    "key_colors": lambda s, kw: KeyColorsCSSResponse(
        **kw,
        picture_source_id=s.picture_source_id,
        rectangles=[r.to_dict() for r in s.rectangles],
        interpolation_mode=s.interpolation_mode,
        smoothing=s.smoothing.to_dict(),
        brightness=s.brightness.to_dict(),
    ),
    "math_wave": lambda s, kw: MathWaveCSSResponse(
        **kw,
        waves=s.waves,
        speed=s.speed.to_dict(),
        gradient_id=s.gradient_id,
    ),
    "game_event": lambda s, kw: GameEventCSSResponse(
        **kw,
        game_integration_id=s.game_integration_id,
        idle_color=s.idle_color.to_dict(),
        event_mappings=[dict(m) for m in s.event_mappings],
    ),
}


def _assert_response_map_coverage() -> None:
    """Verify _RESPONSE_MAP has a builder for every kind in storage's registry.

    Runs at module import. Surfaces missing builders eagerly instead of
    letting a request fall through to a silent / wrong response shape.

    Contract note
    -------------
    This check is **symmetric** (``_RESPONSE_MAP keys == storage_kinds``)
    because every kind — sharable or not — needs a response shape. The
    sister assertion in
    ``core/processing/color_strip_kinds.py::_assert_stream_kind_coverage``
    is asymmetric because sharable kinds construct their streams via a
    different path. Adding a new kind requires keeping all three registries
    aligned: storage's ``_SOURCE_TYPE_MAP``, this ``_RESPONSE_MAP``, and
    either ``STREAM_BUILDERS`` or ``SHARABLE_KINDS``.
    """
    storage_kinds = set(_STORAGE_TYPE_MAP.keys())
    builder_kinds = set(_RESPONSE_MAP.keys())
    missing = storage_kinds - builder_kinds
    extra = builder_kinds - storage_kinds
    if missing or extra:
        problems = []
        if missing:
            problems.append(f"missing builders for: {sorted(missing)}")
        if extra:
            problems.append(f"unregistered kinds in _RESPONSE_MAP: {sorted(extra)}")
        raise RuntimeError(
            "_RESPONSE_MAP is out of sync with storage._SOURCE_TYPE_MAP: " + "; ".join(problems)
        )


_assert_response_map_coverage()


def _css_to_response(source, overlay_active: bool = False) -> ColorStripSourceResponse:
    """Convert a ColorStripSource to the matching per-type response schema."""
    kw = _common_response_kwargs(source, overlay_active)
    builder = _RESPONSE_MAP.get(source.source_type)
    if builder is None:
        # Coverage is asserted at import time, so reaching this branch means a
        # source was loaded with a source_type that is not registered.
        # Surface the bug instead of silently returning a wrong-shaped response.
        raise RuntimeError(
            f"No CSS response builder registered for source_type "
            f"{source.source_type!r} (class={type(source).__name__})"
        )
    return builder(source, kw)


def _resolve_display_index(
    picture_source_id: str, picture_source_store: PictureSourceStore, depth: int = 0
) -> int:
    """Resolve display index from a picture source, following processed source chains."""
    if not picture_source_id or depth > 5:
        return 0
    try:
        ps = picture_source_store.get_stream(picture_source_id)
    except Exception as e:
        logger.debug(
            "Failed to resolve display index for picture source %s: %s", picture_source_id, e
        )
        return 0
    if isinstance(ps, ScreenCapturePictureSource):
        return ps.display_index
    if isinstance(ps, ProcessedPictureSource):
        return _resolve_display_index(ps.source_stream_id, picture_source_store, depth + 1)
    return 0


def _extract_css_kwargs(data) -> dict:
    """Extract store-compatible kwargs from a Pydantic CSS create/update schema.

    Converts nested Pydantic models (calibration, stops, layers, zones,
    animation) to plain dicts/lists that the store expects.
    """
    # Exclude nested models that need special conversion
    exclude_fields = {"source_type"}
    for nested in ("calibration", "stops", "layers", "zones", "animation"):
        if hasattr(data, nested):
            exclude_fields.add(nested)
    kwargs = data.model_dump(exclude_unset=False, exclude=exclude_fields)

    # Convert nested Pydantic models → plain dicts for the store
    if hasattr(data, "calibration"):
        cal = getattr(data, "calibration", None)
        if cal is not None:
            kwargs["calibration"] = calibration_from_dict(cal.model_dump())
        else:
            kwargs["calibration"] = None

    if hasattr(data, "stops"):
        stops = getattr(data, "stops", None)
        kwargs["stops"] = [s.model_dump() for s in stops] if stops is not None else None

    if hasattr(data, "layers"):
        layers = getattr(data, "layers", None)
        kwargs["layers"] = [layer.model_dump() for layer in layers] if layers is not None else None

    if hasattr(data, "zones"):
        zones = getattr(data, "zones", None)
        kwargs["zones"] = [z.model_dump() for z in zones] if zones is not None else None

    if hasattr(data, "animation"):
        anim = getattr(data, "animation", None)
        kwargs["animation"] = anim.model_dump() if anim else None

    return kwargs
