"""Color strip source schemas — discriminated unions per source type."""

from datetime import datetime
from typing import Annotated, Any, Dict, List, Literal, Optional, Union

from pydantic import BaseModel, Discriminator, Field, Tag, model_validator

from wled_controller.api.schemas.devices import Calibration


# =====================================================================
# Helper models (unchanged)
# =====================================================================


class AppSoundOverride(BaseModel):
    """Per-application sound override for notification sources."""

    sound_asset_id: Optional[str] = Field(
        None, description="Asset ID for the sound (None = mute this app)"
    )
    volume: Optional[float] = Field(
        None, ge=0.0, le=1.0, description="Volume override (None = use global)"
    )


class AnimationConfig(BaseModel):
    """Procedural animation configuration for static/gradient color strip sources."""

    enabled: bool = True
    type: str = "breathing"  # breathing | color_cycle | gradient_shift | wave
    speed: float = Field(1.0, ge=0.1, le=10.0, description="Speed multiplier (0.1-10.0)")


class ColorStop(BaseModel):
    """A single color stop in a gradient."""

    position: float = Field(
        description="Relative position along the strip (0.0-1.0)", ge=0.0, le=1.0
    )
    color: List[int] = Field(description="Primary RGB color [R, G, B] (0-255 each)")
    color_right: Optional[List[int]] = Field(
        None,
        description="Optional right-side RGB color for a hard edge (bidirectional stop)",
    )


class CompositeLayer(BaseModel):
    """A single layer in a composite color strip source."""

    source_id: str = Field(description="ID of the layer's color strip source")
    blend_mode: str = Field(
        default="normal", description="Blend mode: normal|add|multiply|screen|override"
    )
    opacity: float = Field(default=1.0, ge=0.0, le=1.0, description="Layer opacity 0.0-1.0")
    enabled: bool = Field(default=True, description="Whether this layer is active")
    brightness_source_id: Optional[str] = Field(
        None, description="Optional value source ID for dynamic brightness"
    )
    processing_template_id: Optional[str] = Field(
        None, description="Optional color strip processing template ID"
    )
    start: int = Field(default=0, ge=0, description="First LED index for range (0 = full strip)")
    end: int = Field(
        default=0, ge=0, description="Last LED index exclusive for range (0 = full strip)"
    )
    reverse: bool = Field(default=False, description="Reverse layer output within its range")


class MappedZone(BaseModel):
    """A single zone in a mapped color strip source."""

    source_id: str = Field(description="ID of the zone's color strip source")
    start: int = Field(default=0, ge=0, description="First LED index (0-based)")
    end: int = Field(default=0, ge=0, description="Last LED index (exclusive); 0 = auto-fill")
    reverse: bool = Field(default=False, description="Reverse zone output")


# =====================================================================
# Response schemas (per-type, discriminated union)
# =====================================================================


class _CSSResponseBase(BaseModel):
    """Shared fields for all color strip source responses."""

    id: str = Field(description="Source ID")
    name: str = Field(description="Source name")
    description: Optional[str] = Field(None, description="Description")
    led_count: int = Field(0, description="Total LED count (0 = auto)")
    overlay_active: bool = Field(
        False, description="Whether the screen overlay is currently active"
    )
    clock_id: Optional[str] = Field(None, description="Optional sync clock ID")
    tags: List[str] = Field(default_factory=list, description="User-defined tags")
    created_at: datetime = Field(description="Creation timestamp")
    updated_at: datetime = Field(description="Last update timestamp")


class PictureCSSResponse(_CSSResponseBase):
    source_type: Literal["picture"] = "picture"
    picture_source_id: str = Field(description="Picture source ID")
    smoothing: Any = Field(description="Temporal smoothing")
    interpolation_mode: str = Field(description="Interpolation mode")
    calibration: Optional[Calibration] = Field(None, description="LED calibration")


class PictureAdvancedCSSResponse(_CSSResponseBase):
    source_type: Literal["picture_advanced"] = "picture_advanced"
    smoothing: Any = Field(description="Temporal smoothing")
    interpolation_mode: str = Field(description="Interpolation mode")
    calibration: Optional[Calibration] = Field(None, description="LED calibration")


class StaticCSSResponse(_CSSResponseBase):
    source_type: Literal["static"] = "static"
    color: Any = Field(description="Static RGB color")
    animation: Optional[AnimationConfig] = Field(None, description="Procedural animation config")


class GradientCSSResponse(_CSSResponseBase):
    source_type: Literal["gradient"] = "gradient"
    stops: Optional[List[ColorStop]] = Field(None, description="Color stops")
    animation: Optional[AnimationConfig] = Field(None, description="Procedural animation config")
    easing: str = Field(description="Gradient interpolation easing")
    gradient_id: Optional[str] = Field(None, description="Gradient entity ID")


class ColorCycleCSSResponse(_CSSResponseBase):
    source_type: Literal["color_cycle"] = "color_cycle"
    colors: List[List[int]] = Field(description="List of [R,G,B] colors to cycle")


class EffectCSSResponse(_CSSResponseBase):
    source_type: Literal["effect"] = "effect"
    effect_type: str = Field(description="Effect algorithm")
    palette: str = Field(description="Named palette")
    gradient_id: Optional[str] = Field(None, description="Gradient entity ID")
    color: Any = Field(description="Primary color")
    intensity: Any = Field(description="Effect intensity")
    scale: Any = Field(description="Spatial scale")
    mirror: bool = Field(description="Mirror/bounce mode")
    custom_palette: Optional[List[List[float]]] = Field(None, description="Custom palette stops")


class CompositeCSSResponse(_CSSResponseBase):
    source_type: Literal["composite"] = "composite"
    layers: List[dict] = Field(default_factory=list, description="Composite layers")


class MappedCSSResponse(_CSSResponseBase):
    source_type: Literal["mapped"] = "mapped"
    zones: List[dict] = Field(default_factory=list, description="Mapped zones")


class AudioCSSResponse(_CSSResponseBase):
    source_type: Literal["audio"] = "audio"
    visualization_mode: str = Field(description="Audio visualization mode")
    audio_source_id: str = Field(description="Mono audio source ID")
    sensitivity: Any = Field(description="Audio sensitivity")
    smoothing: Any = Field(description="Temporal smoothing")
    palette: str = Field(description="Named palette")
    gradient_id: Optional[str] = Field(None, description="Gradient entity ID")
    color: Any = Field(description="Primary color")
    color_peak: Any = Field(description="Peak color")
    mirror: bool = Field(description="Mirror mode")


class ApiInputCSSResponse(_CSSResponseBase):
    source_type: Literal["api_input"] = "api_input"
    fallback_color: Any = Field(description="Fallback RGB color")
    timeout: Any = Field(description="Timeout before fallback")
    interpolation: str = Field(description="LED count interpolation mode")


class NotificationCSSResponse(_CSSResponseBase):
    source_type: Literal["notification"] = "notification"
    notification_effect: str = Field(description="Notification effect")
    duration_ms: Any = Field(description="Effect duration in milliseconds")
    default_color: Any = Field(None, description="Default color")
    app_colors: Dict[str, str] = Field(default_factory=dict, description="Per-app hex colors")
    app_filter_mode: str = Field(description="App filter mode")
    app_filter_list: List[str] = Field(default_factory=list, description="App names for filter")
    os_listener: bool = Field(description="Whether to listen for OS notifications")
    sound_asset_id: Optional[str] = Field(None, description="Global notification sound asset ID")
    sound_volume: Any = Field(description="Global notification sound volume")
    app_sounds: Dict[str, dict] = Field(default_factory=dict, description="Per-app sound overrides")


class DaylightCSSResponse(_CSSResponseBase):
    source_type: Literal["daylight"] = "daylight"
    speed: Any = Field(description="Cycle speed multiplier")
    use_real_time: bool = Field(description="Use wall-clock time")
    latitude: float = Field(description="Latitude for daylight timing")
    longitude: float = Field(description="Longitude for daylight timing")


class CandlelightCSSResponse(_CSSResponseBase):
    source_type: Literal["candlelight"] = "candlelight"
    color: Any = Field(description="Candle color")
    intensity: Any = Field(description="Candle intensity")
    num_candles: int = Field(description="Number of independent candle sources")
    speed: Any = Field(description="Flicker speed multiplier")
    wind_strength: Any = Field(description="Wind simulation strength")
    candle_type: str = Field(description="Candle type preset")


class ProcessedCSSResponse(_CSSResponseBase):
    source_type: Literal["processed"] = "processed"
    input_source_id: str = Field(description="Input color strip source ID")
    processing_template_id: str = Field(description="Color strip processing template ID")


class WeatherCSSResponse(_CSSResponseBase):
    source_type: Literal["weather"] = "weather"
    weather_source_id: str = Field(description="Weather source entity ID")
    speed: Any = Field(description="Speed multiplier")
    temperature_influence: Any = Field(description="Temperature color shift strength")


class KeyColorsCSSResponse(_CSSResponseBase):
    source_type: Literal["key_colors"] = "key_colors"
    picture_source_id: str = Field(description="Picture source ID")
    rectangles: List[dict] = Field(default_factory=list, description="Named screen regions")
    interpolation_mode: str = Field(description="Interpolation mode")
    smoothing: Any = Field(description="Temporal smoothing")
    brightness: Any = Field(description="Brightness")


ColorStripSourceResponse = Annotated[
    Union[
        Annotated[PictureCSSResponse, Tag("picture")],
        Annotated[PictureAdvancedCSSResponse, Tag("picture_advanced")],
        Annotated[StaticCSSResponse, Tag("static")],
        Annotated[GradientCSSResponse, Tag("gradient")],
        Annotated[ColorCycleCSSResponse, Tag("color_cycle")],
        Annotated[EffectCSSResponse, Tag("effect")],
        Annotated[CompositeCSSResponse, Tag("composite")],
        Annotated[MappedCSSResponse, Tag("mapped")],
        Annotated[AudioCSSResponse, Tag("audio")],
        Annotated[ApiInputCSSResponse, Tag("api_input")],
        Annotated[NotificationCSSResponse, Tag("notification")],
        Annotated[DaylightCSSResponse, Tag("daylight")],
        Annotated[CandlelightCSSResponse, Tag("candlelight")],
        Annotated[ProcessedCSSResponse, Tag("processed")],
        Annotated[WeatherCSSResponse, Tag("weather")],
        Annotated[KeyColorsCSSResponse, Tag("key_colors")],
    ],
    Discriminator("source_type"),
]


# =====================================================================
# Create schemas (per-type, discriminated union)
# =====================================================================


class _CSSCreateBase(BaseModel):
    """Shared fields for all color strip source create requests."""

    name: str = Field(description="Source name", min_length=1, max_length=100)
    led_count: int = Field(default=0, description="Total LED count (0 = auto)", ge=0)
    description: Optional[str] = Field(None, description="Optional description", max_length=500)
    clock_id: Optional[str] = Field(None, description="Optional sync clock ID")
    tags: List[str] = Field(default_factory=list, description="User-defined tags")


class PictureCSSCreate(_CSSCreateBase):
    source_type: Literal["picture"] = "picture"
    picture_source_id: str = Field(default="", description="Picture source ID")
    smoothing: Any = Field(default=0.3, description="Temporal smoothing (0.0-1.0)")
    interpolation_mode: str = Field(default="average", description="Interpolation mode")
    calibration: Optional[Calibration] = Field(None, description="LED calibration")


class PictureAdvancedCSSCreate(_CSSCreateBase):
    source_type: Literal["picture_advanced"] = "picture_advanced"
    smoothing: Any = Field(default=0.3, description="Temporal smoothing (0.0-1.0)")
    interpolation_mode: str = Field(default="average", description="Interpolation mode")
    calibration: Optional[Calibration] = Field(None, description="LED calibration")


class StaticCSSCreate(_CSSCreateBase):
    source_type: Literal["static"] = "static"
    color: Any = Field(default=None, description="Static RGB color [R,G,B]")
    animation: Optional[AnimationConfig] = Field(None, description="Procedural animation config")


class GradientCSSCreate(_CSSCreateBase):
    source_type: Literal["gradient"] = "gradient"
    stops: Optional[List[ColorStop]] = Field(None, description="Color stops")
    animation: Optional[AnimationConfig] = Field(None, description="Procedural animation config")
    easing: Optional[str] = Field(None, description="Gradient easing")
    gradient_id: Optional[str] = Field(None, description="Gradient entity ID")


class ColorCycleCSSCreate(_CSSCreateBase):
    source_type: Literal["color_cycle"] = "color_cycle"
    colors: Optional[List[List[int]]] = Field(None, description="List of [R,G,B] colors to cycle")


class EffectCSSCreate(_CSSCreateBase):
    source_type: Literal["effect"] = "effect"
    effect_type: Optional[str] = Field(None, description="Effect algorithm")
    palette: Optional[str] = Field(None, description="Named palette")
    gradient_id: Optional[str] = Field(None, description="Gradient entity ID")
    color: Any = Field(default=None, description="Primary color")
    intensity: Any = Field(default=None, description="Effect intensity (0.1-2.0)")
    scale: Any = Field(default=None, description="Spatial scale (0.5-5.0)")
    mirror: Optional[bool] = Field(None, description="Mirror/bounce mode")
    custom_palette: Optional[List[List[float]]] = Field(None, description="Custom palette stops")


class CompositeCSSCreate(_CSSCreateBase):
    source_type: Literal["composite"] = "composite"
    layers: Optional[List[CompositeLayer]] = Field(None, description="Layers for composite type")


class MappedCSSCreate(_CSSCreateBase):
    source_type: Literal["mapped"] = "mapped"
    zones: Optional[List[MappedZone]] = Field(None, description="Zones for mapped type")


class AudioCSSCreate(_CSSCreateBase):
    source_type: Literal["audio"] = "audio"
    visualization_mode: Optional[str] = Field(None, description="Audio visualization mode")
    audio_source_id: Optional[str] = Field(None, description="Mono audio source ID")
    sensitivity: Any = Field(default=None, description="Audio sensitivity (0.1-5.0)")
    smoothing: Any = Field(default=0.3, description="Temporal smoothing (0.0-1.0)")
    palette: Optional[str] = Field(None, description="Named palette")
    gradient_id: Optional[str] = Field(None, description="Gradient entity ID")
    color: Any = Field(default=None, description="Primary color")
    color_peak: Any = Field(default=None, description="Peak color [R,G,B]")
    mirror: Optional[bool] = Field(None, description="Mirror mode")


class ApiInputCSSCreate(_CSSCreateBase):
    source_type: Literal["api_input"] = "api_input"
    fallback_color: Any = Field(default=None, description="Fallback RGB color [R,G,B]")
    timeout: Any = Field(default=None, description="Timeout before fallback (0.0-300.0)")
    interpolation: Optional[str] = Field(None, description="LED count interpolation mode")


class NotificationCSSCreate(_CSSCreateBase):
    source_type: Literal["notification"] = "notification"
    notification_effect: Optional[str] = Field(None, description="Notification effect")
    duration_ms: Any = Field(default=None, description="Effect duration in milliseconds")
    default_color: Optional[Union[List[int], Dict[str, Any], str]] = Field(
        None, description="Default color"
    )
    app_colors: Optional[Dict[str, str]] = Field(None, description="Per-app hex colors")
    app_filter_mode: Optional[str] = Field(None, description="App filter mode")
    app_filter_list: Optional[List[str]] = Field(None, description="App names for filter")
    os_listener: Optional[bool] = Field(None, description="Listen for OS notifications")
    sound_asset_id: Optional[str] = Field(None, description="Global notification sound asset ID")
    sound_volume: Any = Field(default=None, description="Global notification sound volume")
    app_sounds: Optional[Dict[str, AppSoundOverride]] = Field(
        None, description="Per-app sound overrides"
    )


class DaylightCSSCreate(_CSSCreateBase):
    source_type: Literal["daylight"] = "daylight"
    speed: Any = Field(default=None, description="Cycle speed multiplier (0.1-10.0)")
    use_real_time: Optional[bool] = Field(None, description="Use wall-clock time")
    latitude: Optional[float] = Field(None, description="Latitude (-90 to 90)", ge=-90.0, le=90.0)
    longitude: Optional[float] = Field(
        None, description="Longitude (-180 to 180)", ge=-180.0, le=180.0
    )


class CandlelightCSSCreate(_CSSCreateBase):
    source_type: Literal["candlelight"] = "candlelight"
    color: Any = Field(default=None, description="Candle color [R,G,B]")
    intensity: Any = Field(default=None, description="Candle intensity (0.1-2.0)")
    num_candles: Optional[int] = Field(
        None, description="Number of candle sources (1-20)", ge=1, le=20
    )
    speed: Any = Field(default=None, description="Flicker speed (0.1-10.0)")
    wind_strength: Any = Field(default=None, description="Wind strength (0.0-2.0)")
    candle_type: Optional[str] = Field(None, description="Candle type preset")


class ProcessedCSSCreate(_CSSCreateBase):
    source_type: Literal["processed"] = "processed"
    input_source_id: Optional[str] = Field(None, description="Input color strip source ID")
    processing_template_id: Optional[str] = Field(None, description="Processing template ID")


class WeatherCSSCreate(_CSSCreateBase):
    source_type: Literal["weather"] = "weather"
    weather_source_id: Optional[str] = Field(None, description="Weather source entity ID")
    speed: Any = Field(default=None, description="Speed multiplier (0.1-10.0)")
    temperature_influence: Any = Field(default=None, description="Temperature influence (0.0-1.0)")


class KeyColorsCSSCreate(_CSSCreateBase):
    source_type: Literal["key_colors"] = "key_colors"
    picture_source_id: str = Field(default="", description="Picture source ID")
    rectangles: Optional[List[dict]] = Field(None, description="Named screen regions")
    interpolation_mode: str = Field(default="average", description="Interpolation mode")
    smoothing: Any = Field(default=0.3, description="Temporal smoothing (0.0-1.0)")
    brightness: Any = Field(default=None, description="Brightness (0.0-1.0)")
    brightness_value_source_id: Optional[str] = Field(
        None, description="Dynamic brightness value source ID"
    )


ColorStripSourceCreate = Annotated[
    Union[
        Annotated[PictureCSSCreate, Tag("picture")],
        Annotated[PictureAdvancedCSSCreate, Tag("picture_advanced")],
        Annotated[StaticCSSCreate, Tag("static")],
        Annotated[GradientCSSCreate, Tag("gradient")],
        Annotated[ColorCycleCSSCreate, Tag("color_cycle")],
        Annotated[EffectCSSCreate, Tag("effect")],
        Annotated[CompositeCSSCreate, Tag("composite")],
        Annotated[MappedCSSCreate, Tag("mapped")],
        Annotated[AudioCSSCreate, Tag("audio")],
        Annotated[ApiInputCSSCreate, Tag("api_input")],
        Annotated[NotificationCSSCreate, Tag("notification")],
        Annotated[DaylightCSSCreate, Tag("daylight")],
        Annotated[CandlelightCSSCreate, Tag("candlelight")],
        Annotated[ProcessedCSSCreate, Tag("processed")],
        Annotated[WeatherCSSCreate, Tag("weather")],
        Annotated[KeyColorsCSSCreate, Tag("key_colors")],
    ],
    Discriminator("source_type"),
]


# =====================================================================
# Update schemas (per-type, discriminated union)
# =====================================================================


class _CSSUpdateBase(BaseModel):
    """Shared fields for all color strip source update requests."""

    name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
    led_count: Optional[int] = Field(None, description="Total LED count (0 = auto)", ge=0)
    description: Optional[str] = Field(None, description="Optional description", max_length=500)
    clock_id: Optional[str] = Field(None, description="Optional sync clock ID")
    tags: Optional[List[str]] = None


class PictureCSSUpdate(_CSSUpdateBase):
    source_type: Literal["picture"] = "picture"
    picture_source_id: Optional[str] = Field(None, description="Picture source ID")
    smoothing: Any = Field(default=None, description="Temporal smoothing (0.0-1.0)")
    interpolation_mode: Optional[str] = Field(None, description="Interpolation mode")
    calibration: Optional[Calibration] = Field(None, description="LED calibration")


class PictureAdvancedCSSUpdate(_CSSUpdateBase):
    source_type: Literal["picture_advanced"] = "picture_advanced"
    smoothing: Any = Field(default=None, description="Temporal smoothing (0.0-1.0)")
    interpolation_mode: Optional[str] = Field(None, description="Interpolation mode")
    calibration: Optional[Calibration] = Field(None, description="LED calibration")


class StaticCSSUpdate(_CSSUpdateBase):
    source_type: Literal["static"] = "static"
    color: Any = Field(default=None, description="Static RGB color [R,G,B]")
    animation: Optional[AnimationConfig] = Field(None, description="Procedural animation config")


class GradientCSSUpdate(_CSSUpdateBase):
    source_type: Literal["gradient"] = "gradient"
    stops: Optional[List[ColorStop]] = Field(None, description="Color stops")
    animation: Optional[AnimationConfig] = Field(None, description="Procedural animation config")
    easing: Optional[str] = Field(None, description="Gradient easing")
    gradient_id: Optional[str] = Field(None, description="Gradient entity ID")


class ColorCycleCSSUpdate(_CSSUpdateBase):
    source_type: Literal["color_cycle"] = "color_cycle"
    colors: Optional[List[List[int]]] = Field(None, description="List of [R,G,B] colors to cycle")


class EffectCSSUpdate(_CSSUpdateBase):
    source_type: Literal["effect"] = "effect"
    effect_type: Optional[str] = Field(None, description="Effect algorithm")
    palette: Optional[str] = Field(None, description="Named palette")
    gradient_id: Optional[str] = Field(None, description="Gradient entity ID")
    color: Any = Field(default=None, description="Primary color")
    intensity: Any = Field(default=None, description="Effect intensity (0.1-2.0)")
    scale: Any = Field(default=None, description="Spatial scale (0.5-5.0)")
    mirror: Optional[bool] = Field(None, description="Mirror/bounce mode")
    custom_palette: Optional[List[List[float]]] = Field(None, description="Custom palette stops")


class CompositeCSSUpdate(_CSSUpdateBase):
    source_type: Literal["composite"] = "composite"
    layers: Optional[List[CompositeLayer]] = Field(None, description="Layers for composite type")


class MappedCSSUpdate(_CSSUpdateBase):
    source_type: Literal["mapped"] = "mapped"
    zones: Optional[List[MappedZone]] = Field(None, description="Zones for mapped type")


class AudioCSSUpdate(_CSSUpdateBase):
    source_type: Literal["audio"] = "audio"
    visualization_mode: Optional[str] = Field(None, description="Audio visualization mode")
    audio_source_id: Optional[str] = Field(None, description="Mono audio source ID")
    sensitivity: Any = Field(default=None, description="Audio sensitivity (0.1-5.0)")
    smoothing: Any = Field(default=None, description="Temporal smoothing (0.0-1.0)")
    palette: Optional[str] = Field(None, description="Named palette")
    gradient_id: Optional[str] = Field(None, description="Gradient entity ID")
    color: Any = Field(default=None, description="Primary color")
    color_peak: Any = Field(default=None, description="Peak color [R,G,B]")
    mirror: Optional[bool] = Field(None, description="Mirror mode")


class ApiInputCSSUpdate(_CSSUpdateBase):
    source_type: Literal["api_input"] = "api_input"
    fallback_color: Any = Field(default=None, description="Fallback RGB color [R,G,B]")
    timeout: Any = Field(default=None, description="Timeout before fallback (0.0-300.0)")
    interpolation: Optional[str] = Field(None, description="LED count interpolation mode")


class NotificationCSSUpdate(_CSSUpdateBase):
    source_type: Literal["notification"] = "notification"
    notification_effect: Optional[str] = Field(None, description="Notification effect")
    duration_ms: Any = Field(default=None, description="Effect duration in milliseconds")
    default_color: Optional[Union[List[int], Dict[str, Any], str]] = Field(
        None, description="Default color"
    )
    app_colors: Optional[Dict[str, str]] = Field(None, description="Per-app hex colors")
    app_filter_mode: Optional[str] = Field(None, description="App filter mode")
    app_filter_list: Optional[List[str]] = Field(None, description="App names for filter")
    os_listener: Optional[bool] = Field(None, description="Listen for OS notifications")
    sound_asset_id: Optional[str] = Field(None, description="Global notification sound asset ID")
    sound_volume: Any = Field(default=None, description="Global notification sound volume")
    app_sounds: Optional[Dict[str, AppSoundOverride]] = Field(
        None, description="Per-app sound overrides"
    )


class DaylightCSSUpdate(_CSSUpdateBase):
    source_type: Literal["daylight"] = "daylight"
    speed: Any = Field(default=None, description="Cycle speed multiplier (0.1-10.0)")
    use_real_time: Optional[bool] = Field(None, description="Use wall-clock time")
    latitude: Optional[float] = Field(None, description="Latitude (-90 to 90)", ge=-90.0, le=90.0)
    longitude: Optional[float] = Field(
        None, description="Longitude (-180 to 180)", ge=-180.0, le=180.0
    )


class CandlelightCSSUpdate(_CSSUpdateBase):
    source_type: Literal["candlelight"] = "candlelight"
    color: Any = Field(default=None, description="Candle color [R,G,B]")
    intensity: Any = Field(default=None, description="Candle intensity (0.1-2.0)")
    num_candles: Optional[int] = Field(
        None, description="Number of candle sources (1-20)", ge=1, le=20
    )
    speed: Any = Field(default=None, description="Flicker speed (0.1-10.0)")
    wind_strength: Any = Field(default=None, description="Wind strength (0.0-2.0)")
    candle_type: Optional[str] = Field(None, description="Candle type preset")


class ProcessedCSSUpdate(_CSSUpdateBase):
    source_type: Literal["processed"] = "processed"
    input_source_id: Optional[str] = Field(None, description="Input color strip source ID")
    processing_template_id: Optional[str] = Field(None, description="Processing template ID")


class WeatherCSSUpdate(_CSSUpdateBase):
    source_type: Literal["weather"] = "weather"
    weather_source_id: Optional[str] = Field(None, description="Weather source entity ID")
    speed: Any = Field(default=None, description="Speed multiplier (0.1-10.0)")
    temperature_influence: Any = Field(default=None, description="Temperature influence (0.0-1.0)")


class KeyColorsCSSUpdate(_CSSUpdateBase):
    source_type: Literal["key_colors"] = "key_colors"
    picture_source_id: Optional[str] = Field(None, description="Picture source ID")
    rectangles: Optional[List[dict]] = Field(None, description="Named screen regions")
    interpolation_mode: Optional[str] = Field(None, description="Interpolation mode")
    smoothing: Any = Field(default=None, description="Temporal smoothing (0.0-1.0)")
    brightness: Any = Field(default=None, description="Brightness (0.0-1.0)")
    brightness_value_source_id: Optional[str] = Field(
        None, description="Dynamic brightness value source ID"
    )


ColorStripSourceUpdate = Annotated[
    Union[
        Annotated[PictureCSSUpdate, Tag("picture")],
        Annotated[PictureAdvancedCSSUpdate, Tag("picture_advanced")],
        Annotated[StaticCSSUpdate, Tag("static")],
        Annotated[GradientCSSUpdate, Tag("gradient")],
        Annotated[ColorCycleCSSUpdate, Tag("color_cycle")],
        Annotated[EffectCSSUpdate, Tag("effect")],
        Annotated[CompositeCSSUpdate, Tag("composite")],
        Annotated[MappedCSSUpdate, Tag("mapped")],
        Annotated[AudioCSSUpdate, Tag("audio")],
        Annotated[ApiInputCSSUpdate, Tag("api_input")],
        Annotated[NotificationCSSUpdate, Tag("notification")],
        Annotated[DaylightCSSUpdate, Tag("daylight")],
        Annotated[CandlelightCSSUpdate, Tag("candlelight")],
        Annotated[ProcessedCSSUpdate, Tag("processed")],
        Annotated[WeatherCSSUpdate, Tag("weather")],
        Annotated[KeyColorsCSSUpdate, Tag("key_colors")],
    ],
    Discriminator("source_type"),
]


# =====================================================================
# List response
# =====================================================================


class ColorStripSourceListResponse(BaseModel):
    """List of color strip sources."""

    sources: List[ColorStripSourceResponse] = Field(description="List of color strip sources")
    count: int = Field(description="Number of sources")


# =====================================================================
# Request-only schemas (unchanged)
# =====================================================================


class SegmentPayload(BaseModel):
    """A single segment for segment-based LED color updates."""

    start: int = Field(ge=0, description="Starting LED index")
    length: int = Field(ge=1, description="Number of LEDs in segment")
    mode: Literal["solid", "per_pixel", "gradient"] = Field(description="Fill mode")
    color: Optional[List[int]] = Field(None, description="RGB for solid mode [R,G,B]")
    colors: Optional[List[List[int]]] = Field(
        None, description="Colors for per_pixel/gradient [[R,G,B],...]"
    )

    @model_validator(mode="after")
    def _validate_mode_fields(self) -> "SegmentPayload":
        if self.mode == "solid":
            if self.color is None or len(self.color) != 3:
                raise ValueError("solid mode requires 'color' as a list of 3 ints [R,G,B]")
            if not all(0 <= c <= 255 for c in self.color):
                raise ValueError("solid color values must be 0-255")
        elif self.mode == "per_pixel":
            if not self.colors:
                raise ValueError("per_pixel mode requires non-empty 'colors' list")
            for c in self.colors:
                if len(c) != 3:
                    raise ValueError("each color in per_pixel must be [R,G,B]")
        elif self.mode == "gradient":
            if not self.colors or len(self.colors) < 2:
                raise ValueError("gradient mode requires 'colors' with at least 2 stops")
            for c in self.colors:
                if len(c) != 3:
                    raise ValueError("each color stop in gradient must be [R,G,B]")
        return self


class ColorPushRequest(BaseModel):
    """Request to push raw LED colors to an api_input source.

    Accepts either 'colors' (legacy flat array) or 'segments' (new segment-based).
    At least one must be provided.
    """

    colors: Optional[List[List[int]]] = Field(
        None, description="LED color array [[R,G,B], ...] (0-255 each)"
    )
    segments: Optional[List[SegmentPayload]] = Field(
        None, description="Segment-based color updates"
    )

    @model_validator(mode="after")
    def _require_colors_or_segments(self) -> "ColorPushRequest":
        if self.colors is None and self.segments is None:
            raise ValueError("Either 'colors' or 'segments' must be provided")
        return self


class NotifyRequest(BaseModel):
    """Request to trigger a notification on a notification color strip source."""

    app: Optional[str] = Field(None, description="App name for color lookup")
    color: Optional[str] = Field(None, description="Hex color override (#RRGGBB)")


class CSSCalibrationTestRequest(BaseModel):
    """Request to run a calibration test for a color strip source on a specific device."""

    device_id: str = Field(description="Device ID to send test pixels to")
    edges: dict = Field(
        default_factory=lambda: {
            "top": [255, 0, 0],
            "right": [0, 255, 0],
            "bottom": [0, 100, 255],
            "left": [255, 255, 0],
        },
        description="Map of edge names to RGB colors. Empty dict = exit test mode.",
    )
