"""Base classes for the audio filter system."""

from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Dict, List, Optional

from wled_controller.core.audio.analysis import AudioAnalysis


@dataclass
class AudioFilterOptionDef:
    """Describes a single configurable option for an audio filter."""

    key: str
    label: str
    option_type: str  # "float" | "int" | "bool" | "select" | "string"
    default: Any
    min_value: Any
    max_value: Any
    step: Any
    choices: Optional[List[Dict[str, str]]] = None  # for "select": [{value, label}]
    max_length: Optional[int] = None  # for "string" type

    def to_dict(self) -> dict:
        d = {
            "key": self.key,
            "label": self.label,
            "type": self.option_type,
            "default": self.default,
            "min_value": self.min_value,
            "max_value": self.max_value,
            "step": self.step,
        }
        if self.choices is not None:
            d["choices"] = self.choices
        if self.max_length is not None:
            d["max_length"] = self.max_length
        return d


class AudioFilter(ABC):
    """Base class for all audio filters.

    Each filter operates on an AudioAnalysis snapshot and returns
    a new (possibly transformed) AudioAnalysis.

    Stateful filters (e.g. peak hold, envelope follower) must override
    ``is_stateful`` to return True and implement ``reset()``.
    """

    filter_id: str = ""
    filter_name: str = ""

    def __init__(self, options: Dict[str, Any]):
        """Initialize filter with validated options."""
        self.options = self.validate_options(options)

    @property
    def is_stateful(self) -> bool:
        """Whether this filter maintains internal state across calls.

        Stateful filters need per-stream instances and reset() support.
        """
        return False

    def reset(self) -> None:
        """Reset internal state. Override in stateful filters."""

    @classmethod
    @abstractmethod
    def get_options_schema(cls) -> List[AudioFilterOptionDef]:
        """Return the list of configurable options for this filter type."""
        ...

    @abstractmethod
    def process(self, analysis: AudioAnalysis) -> AudioAnalysis:
        """Process an audio analysis snapshot.

        Args:
            analysis: Input AudioAnalysis snapshot.

        Returns:
            New AudioAnalysis with transformations applied.
        """
        ...

    @classmethod
    def validate_options(cls, options: dict) -> dict:
        """Validate and clamp options against the schema. Returns cleaned dict."""
        schema = cls.get_options_schema()
        cleaned = {}
        for opt_def in schema:
            raw = options.get(opt_def.key, opt_def.default)
            if opt_def.option_type == "float":
                val = float(raw)
            elif opt_def.option_type == "int":
                val = int(raw)
            elif opt_def.option_type == "bool":
                val = bool(raw) if not isinstance(raw, bool) else raw
            elif opt_def.option_type == "select":
                val = str(raw) if raw is not None else opt_def.default
            elif opt_def.option_type == "string":
                val = str(raw) if raw is not None else opt_def.default
            else:
                val = raw
            # Clamp to range (skip for non-numeric types)
            if opt_def.option_type not in ("bool", "select", "string"):
                if opt_def.min_value is not None and val < opt_def.min_value:
                    val = opt_def.min_value
                if opt_def.max_value is not None and val > opt_def.max_value:
                    val = opt_def.max_value
            cleaned[opt_def.key] = val
        return cleaned

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}({self.options})"
