"""Value source data model with inheritance-based source types.

A ValueSource produces either a scalar float (0.0–1.0) or an RGB color [R,G,B]
depending on ``return_type`` ("float" or "color").

Float types (return_type="float"):
  StaticValueSource    — constant float value
  AnimatedValueSource  — periodic waveform (sine, triangle, square, sawtooth)
  AudioValueSource     — audio-reactive scalar (RMS, peak, beat detection)
  AdaptiveValueSource  — adapts to external conditions:
      adaptive_time  — interpolates brightness along a 24-hour schedule
      adaptive_scene — derives brightness from a picture source's frame luminance
  DaylightValueSource  — brightness based on simulated or real-time daylight cycle

Color types (return_type="color"):
  StaticColorValueSource       — constant RGB color
  AnimatedColorValueSource     — cycles through a color list over time
  AdaptiveTimeColorValueSource — 24-hour schedule of RGB colors
"""

from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Dict, List, Optional, Type


@dataclass
class ValueSource:
    """Base class for value source configurations."""

    id: str
    name: str
    source_type: (
        str  # "static" | "animated" | "audio" | "adaptive_time" | "adaptive_scene" | "daylight"
    )
    created_at: datetime
    updated_at: datetime
    description: Optional[str] = None
    tags: List[str] = field(default_factory=list)

    def to_dict(self) -> dict:
        """Convert source to dictionary. Subclasses extend this."""
        return {
            "id": self.id,
            "name": self.name,
            "source_type": self.source_type,
            "created_at": self.created_at.isoformat(),
            "updated_at": self.updated_at.isoformat(),
            "description": self.description,
            "tags": self.tags,
            # Subclass fields default to None for forward compat
            "value": None,
            "waveform": None,
            "speed": None,
            "min_value": None,
            "max_value": None,
            "audio_source_id": None,
            "mode": None,
            "sensitivity": None,
            "smoothing": None,
            "auto_gain": None,
            "schedule": None,
            "picture_source_id": None,
            "scene_behavior": None,
            "use_real_time": None,
            "latitude": None,
        }

    @staticmethod
    def from_dict(data: dict) -> "ValueSource":
        """Factory: dispatch to the correct subclass based on source_type."""
        source_type = data.get("source_type", "static") or "static"
        subcls = _VALUE_SOURCE_MAP.get(source_type, StaticValueSource)
        return subcls.from_dict(data)


def _parse_common_fields(data: dict) -> dict:
    """Extract common fields shared by all value source types."""
    raw_created = data.get("created_at")
    created_at = (
        datetime.fromisoformat(raw_created)
        if isinstance(raw_created, str)
        else raw_created if isinstance(raw_created, datetime) else datetime.now(timezone.utc)
    )
    raw_updated = data.get("updated_at")
    updated_at = (
        datetime.fromisoformat(raw_updated)
        if isinstance(raw_updated, str)
        else raw_updated if isinstance(raw_updated, datetime) else datetime.now(timezone.utc)
    )
    return dict(
        id=data["id"],
        name=data["name"],
        description=data.get("description"),
        tags=data.get("tags", []),
        created_at=created_at,
        updated_at=updated_at,
    )


@dataclass
class StaticValueSource(ValueSource):
    """Value source that outputs a constant float.

    Useful as a simple per-target brightness override.
    """

    value: float = 1.0  # 0.0–1.0

    def to_dict(self) -> dict:
        d = super().to_dict()
        d["value"] = self.value
        return d

    @classmethod
    def from_dict(cls, data: dict) -> "StaticValueSource":
        common = _parse_common_fields(data)
        return cls(
            **common,
            source_type="static",
            value=float(data["value"]) if data.get("value") is not None else 1.0,
        )


@dataclass
class AnimatedValueSource(ValueSource):
    """Value source that cycles through a periodic waveform.

    Produces a smooth animation between min_value and max_value
    at the configured speed (cycles per minute).
    """

    waveform: str = "sine"  # sine | triangle | square | sawtooth
    speed: float = 10.0  # cycles per minute (1.0–120.0)
    min_value: float = 0.0  # minimum output (0.0–1.0)
    max_value: float = 1.0  # maximum output (0.0–1.0)

    def to_dict(self) -> dict:
        d = super().to_dict()
        d["waveform"] = self.waveform
        d["speed"] = self.speed
        d["min_value"] = self.min_value
        d["max_value"] = self.max_value
        return d

    @classmethod
    def from_dict(cls, data: dict) -> "AnimatedValueSource":
        common = _parse_common_fields(data)
        return cls(
            **common,
            source_type="animated",
            waveform=data.get("waveform") or "sine",
            speed=float(data.get("speed") or 10.0),
            min_value=float(data.get("min_value") or 0.0),
            max_value=float(data["max_value"]) if data.get("max_value") is not None else 1.0,
        )


@dataclass
class AudioValueSource(ValueSource):
    """Value source driven by audio input.

    Converts audio analysis (RMS level, peak, or beat detection)
    into a scalar value for brightness modulation.
    """

    audio_source_id: str = ""  # references an AudioSource (capture or processed)
    mode: str = "rms"  # rms | peak | beat
    sensitivity: float = 1.0  # gain multiplier (0.1–20.0)
    smoothing: float = 0.3  # temporal smoothing (0.0–1.0)
    min_value: float = 0.0  # minimum output (0.0–1.0)
    max_value: float = 1.0  # maximum output (0.0–1.0)
    auto_gain: bool = False  # auto-normalize audio levels to full range

    def to_dict(self) -> dict:
        d = super().to_dict()
        d["audio_source_id"] = self.audio_source_id
        d["mode"] = self.mode
        d["sensitivity"] = self.sensitivity
        d["smoothing"] = self.smoothing
        d["min_value"] = self.min_value
        d["max_value"] = self.max_value
        d["auto_gain"] = self.auto_gain
        return d

    @classmethod
    def from_dict(cls, data: dict) -> "AudioValueSource":
        common = _parse_common_fields(data)
        return cls(
            **common,
            source_type="audio",
            audio_source_id=data.get("audio_source_id") or "",
            mode=data.get("mode") or "rms",
            sensitivity=float(data.get("sensitivity") or 1.0),
            smoothing=float(data.get("smoothing") or 0.3),
            min_value=float(data.get("min_value") or 0.0),
            max_value=float(data["max_value"]) if data.get("max_value") is not None else 1.0,
            auto_gain=bool(data.get("auto_gain", False)),
        )


@dataclass
class AdaptiveValueSource(ValueSource):
    """Value source that adapts to external conditions.

    source_type determines the sub-mode:
      adaptive_time  — interpolates brightness along a 24-hour schedule
      adaptive_scene — derives brightness from a picture source's frame luminance
    """

    schedule: List[dict] = field(default_factory=list)  # [{time: "HH:MM", value: 0.0-1.0}]
    picture_source_id: str = ""  # for scene mode
    scene_behavior: str = "complement"  # "complement" | "match"
    sensitivity: float = 1.0  # gain multiplier (0.1-5.0)
    smoothing: float = 0.3  # temporal smoothing (0.0-1.0)
    min_value: float = 0.0  # output range min
    max_value: float = 1.0  # output range max

    def to_dict(self) -> dict:
        d = super().to_dict()
        d["schedule"] = self.schedule
        d["picture_source_id"] = self.picture_source_id
        d["scene_behavior"] = self.scene_behavior
        d["sensitivity"] = self.sensitivity
        d["smoothing"] = self.smoothing
        d["min_value"] = self.min_value
        d["max_value"] = self.max_value
        return d

    @classmethod
    def from_dict(cls, data: dict) -> "AdaptiveValueSource":
        common = _parse_common_fields(data)
        source_type = data.get("source_type", "adaptive_time")
        return cls(
            **common,
            source_type=source_type,
            schedule=data.get("schedule") or [],
            picture_source_id=data.get("picture_source_id") or "",
            scene_behavior=data.get("scene_behavior") or "complement",
            sensitivity=float(data.get("sensitivity") or 1.0),
            smoothing=float(data.get("smoothing") or 0.3),
            min_value=float(data.get("min_value") or 0.0),
            max_value=float(data["max_value"]) if data.get("max_value") is not None else 1.0,
        )


@dataclass
class DaylightValueSource(ValueSource):
    """Value source that outputs brightness based on a daylight cycle.

    Uses the same daylight LUT as the CSS daylight stream to derive a
    scalar brightness from the simulated (or real-time) sky color luminance.
    """

    speed: float = 1.0  # simulation speed (ignored when use_real_time)
    use_real_time: bool = False  # use wall clock instead of simulation
    latitude: float = 50.0  # affects sunrise/sunset in real-time mode
    min_value: float = 0.0  # output range min
    max_value: float = 1.0  # output range max

    def to_dict(self) -> dict:
        d = super().to_dict()
        d["speed"] = self.speed
        d["use_real_time"] = self.use_real_time
        d["latitude"] = self.latitude
        d["min_value"] = self.min_value
        d["max_value"] = self.max_value
        return d

    @classmethod
    def from_dict(cls, data: dict) -> "DaylightValueSource":
        common = _parse_common_fields(data)
        return cls(
            **common,
            source_type="daylight",
            speed=float(data.get("speed") or 1.0),
            use_real_time=bool(data.get("use_real_time", False)),
            latitude=float(data.get("latitude") or 50.0),
            min_value=float(data.get("min_value") or 0.0),
            max_value=float(data["max_value"]) if data.get("max_value") is not None else 1.0,
        )


# =====================================================================
# Color value sources (return_type="color")
# =====================================================================


@dataclass
class StaticColorValueSource(ValueSource):
    """Color value source that outputs a constant RGB color."""

    color: list = field(default_factory=lambda: [255, 255, 255])  # [R, G, B]

    def to_dict(self) -> dict:
        d = super().to_dict()
        d["color"] = list(self.color)
        d["return_type"] = "color"
        return d

    @classmethod
    def from_dict(cls, data: dict) -> "StaticColorValueSource":
        common = _parse_common_fields(data)
        raw = data.get("color", [255, 255, 255])
        color = list(raw) if isinstance(raw, list) and len(raw) == 3 else [255, 255, 255]
        return cls(**common, source_type="static_color", color=color)


@dataclass
class AnimatedColorValueSource(ValueSource):
    """Color value source that cycles through a list of colors over time."""

    colors: list = field(default_factory=lambda: [[255, 0, 0], [0, 255, 0], [0, 0, 255]])
    speed: float = 10.0  # cycles per minute
    easing: str = "linear"  # linear | step

    def to_dict(self) -> dict:
        d = super().to_dict()
        d["colors"] = [list(c) for c in self.colors]
        d["speed"] = self.speed
        d["easing"] = self.easing
        d["return_type"] = "color"
        return d

    @classmethod
    def from_dict(cls, data: dict) -> "AnimatedColorValueSource":
        common = _parse_common_fields(data)
        raw_colors = data.get("colors")
        colors = (
            [list(c) for c in raw_colors]
            if isinstance(raw_colors, list) and len(raw_colors) >= 2
            else [[255, 0, 0], [0, 255, 0], [0, 0, 255]]
        )
        return cls(
            **common,
            source_type="animated_color",
            colors=colors,
            speed=float(data.get("speed") or 10.0),
            easing=data.get("easing") or "linear",
        )


@dataclass
class AdaptiveTimeColorValueSource(ValueSource):
    """Color value source with a 24-hour schedule of RGB colors."""

    schedule: List[dict] = field(default_factory=list)  # [{"time": "HH:MM", "color": [R,G,B]}, ...]

    def to_dict(self) -> dict:
        d = super().to_dict()
        d["schedule"] = self.schedule
        d["return_type"] = "color"
        return d

    @classmethod
    def from_dict(cls, data: dict) -> "AdaptiveTimeColorValueSource":
        common = _parse_common_fields(data)
        return cls(
            **common,
            source_type="adaptive_time_color",
            schedule=data.get("schedule") or [],
        )


@dataclass
class HAEntityValueSource(ValueSource):
    """Value source that reads numeric values from a Home Assistant entity.

    Reads state or attribute from an HA entity, normalizes the raw value
    to 0.0–1.0 using the configured min/max range, and applies EMA smoothing.
    """

    ha_source_id: str = ""  # references a HomeAssistantSource
    entity_id: str = ""  # HA entity ID (e.g. "sensor.temperature")
    attribute: str = ""  # optional attribute name (empty = use state)
    min_ha_value: float = 0.0  # raw HA value mapped to output 0.0
    max_ha_value: float = 100.0  # raw HA value mapped to output 1.0
    smoothing: float = 0.0  # EMA smoothing factor (0.0–1.0)

    def to_dict(self) -> dict:
        d = super().to_dict()
        d["ha_source_id"] = self.ha_source_id
        d["entity_id"] = self.entity_id
        d["attribute"] = self.attribute
        d["min_ha_value"] = self.min_ha_value
        d["max_ha_value"] = self.max_ha_value
        d["smoothing"] = self.smoothing
        return d

    @classmethod
    def from_dict(cls, data: dict) -> "HAEntityValueSource":
        common = _parse_common_fields(data)
        return cls(
            **common,
            source_type="ha_entity",
            ha_source_id=data.get("ha_source_id") or "",
            entity_id=data.get("entity_id") or "",
            attribute=data.get("attribute") or "",
            min_ha_value=float(data.get("min_ha_value") or 0.0),
            max_ha_value=float(
                data.get("max_ha_value") if data.get("max_ha_value") is not None else 100.0
            ),
            smoothing=float(data.get("smoothing") or 0.0),
        )


@dataclass
class GradientMapValueSource(ValueSource):
    """Color value source that maps a float value source through a gradient.

    Takes a float-returning value source (0..1), interpolates the color at
    that position in the gradient referenced by gradient_id.
    """

    value_source_id: str = ""  # references a float-returning ValueSource
    gradient_id: str = ""  # references a Gradient entity
    easing: str = "linear"  # linear | step

    def to_dict(self) -> dict:
        d = super().to_dict()
        d["value_source_id"] = self.value_source_id
        d["gradient_id"] = self.gradient_id
        d["easing"] = self.easing
        d["return_type"] = "color"
        return d

    @classmethod
    def from_dict(cls, data: dict) -> "GradientMapValueSource":
        common = _parse_common_fields(data)
        return cls(
            **common,
            source_type="gradient_map",
            value_source_id=data.get("value_source_id") or "",
            gradient_id=data.get("gradient_id") or "",
            easing=data.get("easing") or "linear",
        )


@dataclass
class CSSExtractValueSource(ValueSource):
    """Color value source that extracts a single color from a color strip source.

    Averages the colors in the specified LED range of a color strip source
    to produce a single RGB color output.
    """

    color_strip_source_id: str = ""  # references a ColorStripSource
    led_start: int = 0  # start of LED range (0-based)
    led_end: int = -1  # end of LED range (-1 = whole strip)

    def to_dict(self) -> dict:
        d = super().to_dict()
        d["color_strip_source_id"] = self.color_strip_source_id
        d["led_start"] = self.led_start
        d["led_end"] = self.led_end
        d["return_type"] = "color"
        return d

    @classmethod
    def from_dict(cls, data: dict) -> "CSSExtractValueSource":
        common = _parse_common_fields(data)
        return cls(
            **common,
            source_type="css_extract",
            color_strip_source_id=data.get("color_strip_source_id") or "",
            led_start=int(data.get("led_start") or 0),
            led_end=int(data.get("led_end") if data.get("led_end") is not None else -1),
        )


VALID_SYSTEM_METRICS = (
    "cpu_load",
    "cpu_temp",
    "gpu_load",
    "gpu_temp",
    "ram_usage",
    "disk_usage",
    "network_rx",
    "network_tx",
    "battery_level",
    "fan_speed",
)


@dataclass
class GameEventValueSource(ValueSource):
    """Value source driven by game events via the GameEventBus.

    Exposes game metrics (health, ammo, mana, etc.) as 0.0-1.0 scalar values.
    Incoming raw game values are normalized using min/max mapping, with optional
    EMA smoothing for smooth transitions. Reverts to default_value when no
    events are received within the timeout period.
    """

    game_integration_id: str = ""  # references a GameIntegration config
    event_type: str = "health"  # standard event vocabulary type
    min_game_value: float = 0.0  # raw game value mapped to 0.0
    max_game_value: float = 100.0  # raw game value mapped to 1.0
    smoothing: float = 0.0  # EMA smoothing factor (0.0-1.0)
    default_value: float = 0.5  # value when timed out or no events
    timeout: float = 5.0  # seconds before reverting to default

    def to_dict(self) -> dict:
        d = super().to_dict()
        d["game_integration_id"] = self.game_integration_id
        d["event_type"] = self.event_type
        d["min_game_value"] = self.min_game_value
        d["max_game_value"] = self.max_game_value
        d["smoothing"] = self.smoothing
        d["default_value"] = self.default_value
        d["timeout"] = self.timeout
        return d

    @classmethod
    def from_dict(cls, data: dict) -> "GameEventValueSource":
        common = _parse_common_fields(data)
        return cls(
            **common,
            source_type="game_event",
            game_integration_id=data.get("game_integration_id") or "",
            event_type=data.get("event_type") or "health",
            min_game_value=float(data.get("min_game_value") or 0.0),
            max_game_value=float(
                data.get("max_game_value") if data.get("max_game_value") is not None else 100.0
            ),
            smoothing=float(data.get("smoothing") or 0.0),
            default_value=float(
                data.get("default_value") if data.get("default_value") is not None else 0.5
            ),
            timeout=float(data.get("timeout") if data.get("timeout") is not None else 5.0),
        )


@dataclass
class SystemMetricsValueSource(ValueSource):
    """Value source that reads system hardware metrics.

    Reads CPU/GPU/RAM/disk/network/battery/fan metrics via psutil/pynvml,
    normalizes to 0.0–1.0.
    """

    metric: str = "cpu_load"
    min_value: float = 0.0
    max_value: float = 100.0
    max_rate: float = 125_000_000.0  # bytes/sec for network (default ~1Gbps)
    disk_path: str = ""  # for disk_usage (empty = root)
    sensor_label: str = ""  # for cpu_temp/fan_speed (empty = first available)
    poll_interval: float = 1.0  # seconds between reads
    smoothing: float = 0.0  # EMA smoothing factor

    def to_dict(self) -> dict:
        d = super().to_dict()
        d["metric"] = self.metric
        d["min_value"] = self.min_value
        d["max_value"] = self.max_value
        d["max_rate"] = self.max_rate
        d["disk_path"] = self.disk_path
        d["sensor_label"] = self.sensor_label
        d["poll_interval"] = self.poll_interval
        d["smoothing"] = self.smoothing
        return d

    @classmethod
    def from_dict(cls, data: dict) -> "SystemMetricsValueSource":
        common = _parse_common_fields(data)
        return cls(
            **common,
            source_type="system_metrics",
            metric=data.get("metric") or "cpu_load",
            min_value=float(data.get("min_value") or 0.0),
            max_value=float(data.get("max_value") if data.get("max_value") is not None else 100.0),
            max_rate=float(
                data.get("max_rate") if data.get("max_rate") is not None else 125_000_000.0
            ),
            disk_path=data.get("disk_path") or "",
            sensor_label=data.get("sensor_label") or "",
            poll_interval=float(data.get("poll_interval") or 1.0),
            smoothing=float(data.get("smoothing") or 0.0),
        )


# -- Source type registry --
# Maps source_type string to its subclass for factory dispatch.
_VALUE_SOURCE_MAP: Dict[str, Type[ValueSource]] = {
    "static": StaticValueSource,
    "animated": AnimatedValueSource,
    "audio": AudioValueSource,
    "adaptive_time": AdaptiveValueSource,
    "adaptive_scene": AdaptiveValueSource,
    "daylight": DaylightValueSource,
    "static_color": StaticColorValueSource,
    "animated_color": AnimatedColorValueSource,
    "adaptive_time_color": AdaptiveTimeColorValueSource,
    "ha_entity": HAEntityValueSource,
    "gradient_map": GradientMapValueSource,
    "css_extract": CSSExtractValueSource,
    "system_metrics": SystemMetricsValueSource,
    "game_event": GameEventValueSource,
}
