"""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,
    GradientCSSResponse,
    KeyColorsCSSResponse,
    MappedCSSResponse,
    MathWaveCSSResponse,
    NotificationCSSResponse,
    PictureAdvancedCSSResponse,
    PictureCSSResponse,
    ProcessedCSSResponse,
    StaticCSSResponse,
    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 (
    AdvancedPictureColorStripSource,
    ApiInputColorStripSource,
    AudioColorStripSource,
    CandlelightColorStripSource,
    CompositeColorStripSource,
    DaylightColorStripSource,
    EffectColorStripSource,
    GradientColorStripSource,
    KeyColorsColorStripSource,
    MappedColorStripSource,
    MathWaveColorStripSource,
    NotificationColorStripSource,
    PictureColorStripSource,
    ProcessedColorStripSource,
    StaticColorStripSource,
    WeatherColorStripSource,
)
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 storage class → response builder lambda.
_RESPONSE_MAP: dict = {
    PictureColorStripSource: 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),
    ),
    AdvancedPictureColorStripSource: lambda s, kw: PictureAdvancedCSSResponse(
        **kw,
        smoothing=s.smoothing.to_dict(),
        interpolation_mode=s.interpolation_mode,
        calibration=_calibration_schema(s),
    ),
    StaticColorStripSource: lambda s, kw: StaticCSSResponse(
        **kw,
        color=s.color.to_dict(),
        animation=s.animation,
    ),
    GradientColorStripSource: lambda s, kw: GradientCSSResponse(
        **kw,
        stops=_stops_schema(s),
        animation=s.animation,
        easing=s.easing,
        gradient_id=s.gradient_id,
    ),
    EffectColorStripSource: 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,
    ),
    CompositeColorStripSource: lambda s, kw: CompositeCSSResponse(
        **kw,
        layers=[dict(layer) for layer in s.layers],
    ),
    MappedColorStripSource: lambda s, kw: MappedCSSResponse(
        **kw,
        zones=[dict(z) for z in s.zones],
    ),
    AudioColorStripSource: 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(),
    ),
    ApiInputColorStripSource: lambda s, kw: ApiInputCSSResponse(
        **kw,
        fallback_color=s.fallback_color.to_dict(),
        timeout=s.timeout.to_dict(),
        interpolation=s.interpolation,
    ),
    NotificationColorStripSource: 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),
    ),
    DaylightColorStripSource: lambda s, kw: DaylightCSSResponse(
        **kw,
        speed=s.speed.to_dict(),
        use_real_time=s.use_real_time,
        latitude=s.latitude,
        longitude=s.longitude,
    ),
    CandlelightColorStripSource: 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,
    ),
    ProcessedColorStripSource: lambda s, kw: ProcessedCSSResponse(
        **kw,
        input_source_id=s.input_source_id,
        processing_template_id=s.processing_template_id,
    ),
    WeatherColorStripSource: lambda s, kw: WeatherCSSResponse(
        **kw,
        weather_source_id=s.weather_source_id,
        speed=s.speed.to_dict(),
        temperature_influence=s.temperature_influence.to_dict(),
    ),
    KeyColorsColorStripSource: 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(),
    ),
    MathWaveColorStripSource: lambda s, kw: MathWaveCSSResponse(
        **kw,
        waves=s.waves,
        speed=s.speed.to_dict(),
        gradient_id=s.gradient_id,
    ),
}


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(type(source))
    if builder is None:
        # Fallback: use to_dict() and build a PictureCSSResponse
        logger.warning("No response builder for %s, falling back", type(source).__name__)
        return PictureCSSResponse(
            **kw,
            picture_source_id="",
            smoothing=0.3,
            interpolation_mode="average",
            calibration=None,
        )
    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
