"""Audio source storage using SQLite."""

import uuid
from datetime import datetime, timezone
from typing import List, NamedTuple, Optional, Set

from wled_controller.storage.audio_source import (
    BAND_PRESETS,
    AudioSource,
    BandExtractAudioSource,
    MonoAudioSource,
    MultichannelAudioSource,
)
from wled_controller.storage.base_sqlite_store import BaseSqliteStore
from wled_controller.storage.database import Database
from wled_controller.storage.utils import resolve_ref
from wled_controller.utils import get_logger

logger = get_logger(__name__)


class ResolvedAudioSource(NamedTuple):
    """Result of resolving an audio source to its physical device + band info."""

    device_index: int
    is_loopback: bool
    channel: str
    audio_template_id: Optional[str]
    freq_low: Optional[float] = None   # None = full range (no band filtering)
    freq_high: Optional[float] = None


class AudioSourceStore(BaseSqliteStore[AudioSource]):
    """Persistent storage for audio sources."""

    _table_name = "audio_sources"
    _entity_name = "Audio source"

    def __init__(self, db: Database):
        super().__init__(db, AudioSource.from_dict)

    # Backward-compatible aliases
    get_all_sources = BaseSqliteStore.get_all
    get_source = BaseSqliteStore.get

    def get_mono_sources(self) -> List[MonoAudioSource]:
        """Return only mono audio sources (for CSS dropdown)."""
        return [s for s in self._items.values() if isinstance(s, MonoAudioSource)]

    def create_source(
        self,
        name: str,
        source_type: str,
        device_index: Optional[int] = None,
        is_loopback: Optional[bool] = None,
        audio_source_id: Optional[str] = None,
        channel: Optional[str] = None,
        description: Optional[str] = None,
        audio_template_id: Optional[str] = None,
        tags: Optional[List[str]] = None,
        band: Optional[str] = None,
        freq_low: Optional[float] = None,
        freq_high: Optional[float] = None,
    ) -> AudioSource:
        self._check_name_unique(name)

        if source_type not in ("multichannel", "mono", "band_extract"):
            raise ValueError(f"Invalid source type: {source_type}")

        sid = f"as_{uuid.uuid4().hex[:8]}"
        now = datetime.now(timezone.utc)

        if source_type == "mono":
            if not audio_source_id:
                raise ValueError("Mono sources require audio_source_id")
            # Validate parent exists and is multichannel
            parent = self._items.get(audio_source_id)
            if not parent:
                raise ValueError(f"Parent audio source not found: {audio_source_id}")
            if not isinstance(parent, MultichannelAudioSource):
                raise ValueError("Mono sources must reference a multichannel source")

            source: AudioSource = MonoAudioSource(
                id=sid, name=name, source_type="mono",
                created_at=now, updated_at=now, description=description, tags=tags or [],
                audio_source_id=audio_source_id,
                channel=channel or "mono",
            )
        elif source_type == "band_extract":
            if not audio_source_id:
                raise ValueError("Band extract sources require audio_source_id")
            parent = self._items.get(audio_source_id)
            if not parent:
                raise ValueError(f"Parent audio source not found: {audio_source_id}")

            band_val = band or "bass"
            fl, fh = _resolve_band_freqs(band_val, freq_low, freq_high)

            source = BandExtractAudioSource(
                id=sid, name=name, source_type="band_extract",
                created_at=now, updated_at=now, description=description, tags=tags or [],
                audio_source_id=audio_source_id,
                band=band_val, freq_low=fl, freq_high=fh,
            )
        else:
            source = MultichannelAudioSource(
                id=sid, name=name, source_type="multichannel",
                created_at=now, updated_at=now, description=description, tags=tags or [],
                device_index=device_index if device_index is not None else -1,
                is_loopback=bool(is_loopback) if is_loopback is not None else True,
                audio_template_id=audio_template_id,
            )

        self._items[sid] = source
        self._save_item(sid, source)

        logger.info(f"Created audio source: {name} ({sid}, type={source_type})")
        return source

    def update_source(
        self,
        source_id: str,
        name: Optional[str] = None,
        device_index: Optional[int] = None,
        is_loopback: Optional[bool] = None,
        audio_source_id: Optional[str] = None,
        channel: Optional[str] = None,
        description: Optional[str] = None,
        audio_template_id: Optional[str] = None,
        tags: Optional[List[str]] = None,
        band: Optional[str] = None,
        freq_low: Optional[float] = None,
        freq_high: Optional[float] = None,
    ) -> AudioSource:
        source = self.get(source_id)

        if name is not None:
            self._check_name_unique(name, exclude_id=source_id)
            source.name = name

        if description is not None:
            source.description = description
        if tags is not None:
            source.tags = tags

        if isinstance(source, MultichannelAudioSource):
            if device_index is not None:
                source.device_index = device_index
            if is_loopback is not None:
                source.is_loopback = bool(is_loopback)
            if audio_template_id is not None:
                source.audio_template_id = resolve_ref(audio_template_id, source.audio_template_id)
        elif isinstance(source, MonoAudioSource):
            if audio_source_id is not None:
                resolved = resolve_ref(audio_source_id, source.audio_source_id)
                if resolved is not None:
                    # Validate parent exists and is multichannel
                    parent = self._items.get(resolved)
                    if not parent:
                        raise ValueError(f"Parent audio source not found: {resolved}")
                    if not isinstance(parent, MultichannelAudioSource):
                        raise ValueError("Mono sources must reference a multichannel source")
                source.audio_source_id = resolved
            if channel is not None:
                source.channel = channel
        elif isinstance(source, BandExtractAudioSource):
            if audio_source_id is not None:
                resolved = resolve_ref(audio_source_id, source.audio_source_id)
                if resolved is not None:
                    parent = self._items.get(resolved)
                    if not parent:
                        raise ValueError(f"Parent audio source not found: {resolved}")
                    # Check for cycles
                    self._check_no_cycle(source_id, resolved)
                source.audio_source_id = resolved
            if band is not None:
                fl, fh = _resolve_band_freqs(band, freq_low, freq_high)
                source.band = band
                source.freq_low = fl
                source.freq_high = fh
            elif freq_low is not None or freq_high is not None:
                # Update custom freq range without changing band preset
                if freq_low is not None:
                    source.freq_low = freq_low
                if freq_high is not None:
                    source.freq_high = freq_high

        source.updated_at = datetime.now(timezone.utc)
        self._save_item(source_id, source)

        logger.info(f"Updated audio source: {source_id}")
        return source

    def delete_source(self, source_id: str) -> None:
        if source_id not in self._items:
            raise ValueError(f"{self._entity_name} not found: {source_id}")

        source = self._items[source_id]

        # Prevent deleting sources referenced by children (mono or band_extract)
        for other in self._items.values():
            if other.id == source_id:
                continue
            parent_ref = getattr(other, "audio_source_id", None)
            if parent_ref == source_id:
                raise ValueError(
                    f"Cannot delete '{source.name}': referenced by {other.source_type} source '{other.name}'"
                )

        del self._items[source_id]
        self._delete_item(source_id)

        logger.info(f"Deleted audio source: {source_id}")

    # ── Resolution ───────────────────────────────────────────────────

    def resolve_audio_source(self, source_id: str) -> ResolvedAudioSource:
        """Resolve any audio source to its physical device, channel, and band info.

        Follows the reference chain: band_extract → mono/multichannel → device.
        For band_extract sources, intersects frequency ranges when chained.

        Raises:
            ValueError: If source not found, chain is broken, or cycle detected
        """
        return self._resolve(source_id, visited=set())

    def _resolve(self, source_id: str, visited: Set[str]) -> ResolvedAudioSource:
        if source_id in visited:
            raise ValueError(f"Cycle detected in audio source chain: {source_id}")
        visited.add(source_id)

        source = self.get_source(source_id)

        if isinstance(source, MultichannelAudioSource):
            return ResolvedAudioSource(
                source.device_index, source.is_loopback, "mono", source.audio_template_id,
            )

        if isinstance(source, MonoAudioSource):
            parent = self.get_source(source.audio_source_id)
            if not isinstance(parent, MultichannelAudioSource):
                raise ValueError(
                    f"Mono source {source_id} references non-multichannel source {source.audio_source_id}"
                )
            return ResolvedAudioSource(
                parent.device_index, parent.is_loopback, source.channel, parent.audio_template_id,
            )

        if isinstance(source, BandExtractAudioSource):
            parent_resolved = self._resolve(source.audio_source_id, visited)
            # Intersect frequency ranges if parent also has band filtering
            fl = source.freq_low
            fh = source.freq_high
            if parent_resolved.freq_low is not None:
                fl = max(fl, parent_resolved.freq_low)
            if parent_resolved.freq_high is not None:
                fh = min(fh, parent_resolved.freq_high)
            if fl >= fh:
                raise ValueError(
                    f"Band extract '{source.name}' has empty frequency intersection: {fl}-{fh} Hz"
                )
            return ResolvedAudioSource(
                parent_resolved.device_index,
                parent_resolved.is_loopback,
                parent_resolved.channel,
                parent_resolved.audio_template_id,
                fl, fh,
            )

        raise ValueError(f"Audio source {source_id} is not a valid audio source")

    def _check_no_cycle(self, source_id: str, new_parent_id: str) -> None:
        """Ensure setting new_parent_id as parent of source_id won't create a cycle."""
        visited: Set[str] = {source_id}
        current = new_parent_id
        while current:
            if current in visited:
                raise ValueError("Cannot set parent: would create a circular reference")
            visited.add(current)
            item = self._items.get(current)
            if item is None:
                break
            current = getattr(item, "audio_source_id", None)


def _resolve_band_freqs(
    band: str,
    freq_low: Optional[float],
    freq_high: Optional[float],
) -> tuple[float, float]:
    """Resolve band preset or custom range to (freq_low, freq_high)."""
    if band in BAND_PRESETS:
        return BAND_PRESETS[band]
    if band != "custom":
        raise ValueError(f"Invalid band: {band}. Must be one of: bass, mid, treble, custom")
    fl = float(freq_low) if freq_low is not None else 20.0
    fh = float(freq_high) if freq_high is not None else 20000.0
    if not (20.0 <= fl < fh <= 20000.0):
        raise ValueError(f"Invalid frequency range: {fl}-{fh} Hz (must be 20-20000, low < high)")
    return fl, fh
