"""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 (
    AudioSource,
    CaptureAudioSource,
    ProcessedAudioSource,
)
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 chain to its physical device + filter templates.

    Walking the chain from a ProcessedAudioSource to the terminal CaptureAudioSource
    collects audio_processing_template_ids in chain order (outermost first).
    """

    device_index: int
    is_loopback: bool
    audio_template_id: Optional[str]
    audio_processing_template_ids: List[str]  # ordered list of template IDs along the chain


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 create_source(
        self,
        name: str,
        source_type: str,
        device_index: Optional[int] = None,
        is_loopback: Optional[bool] = None,
        audio_source_id: Optional[str] = None,
        description: Optional[str] = None,
        audio_template_id: Optional[str] = None,
        tags: Optional[List[str]] = None,
        audio_processing_template_id: Optional[str] = None,
    ) -> AudioSource:
        self._check_name_unique(name)

        if source_type not in ("capture", "processed"):
            raise ValueError(f"Invalid source type: {source_type}")

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

        if source_type == "processed":
            if not audio_source_id:
                raise ValueError("Processed sources require audio_source_id")
            if not audio_processing_template_id:
                raise ValueError("Processed sources require audio_processing_template_id")
            # Validate parent source exists
            parent = self._items.get(audio_source_id)
            if not parent:
                raise ValueError(f"Parent audio source not found: {audio_source_id}")
            # Check for cycles (new source doesn't exist yet, just validate parent chain)
            self._check_no_cycle_for_new(audio_source_id)

            source: AudioSource = ProcessedAudioSource(
                id=sid,
                name=name,
                source_type="processed",
                created_at=now,
                updated_at=now,
                description=description,
                tags=tags or [],
                audio_source_id=audio_source_id,
                audio_processing_template_id=audio_processing_template_id,
            )
        else:
            source = CaptureAudioSource(
                id=sid,
                name=name,
                source_type="capture",
                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,
        description: Optional[str] = None,
        audio_template_id: Optional[str] = None,
        tags: Optional[List[str]] = None,
        audio_processing_template_id: Optional[str] = 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, CaptureAudioSource):
            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, ProcessedAudioSource):
            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 audio_processing_template_id is not None:
                source.audio_processing_template_id = resolve_ref(
                    audio_processing_template_id, source.audio_processing_template_id
                )

        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 ProcessedAudioSource children
        for other in self._items.values():
            if other.id == source_id:
                continue
            if isinstance(other, ProcessedAudioSource) and other.audio_source_id == source_id:
                raise ValueError(
                    f"Cannot delete '{source.name}': referenced by processed 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 + ordered processing template IDs.

        Walks the chain: ProcessedAudioSource → ... → CaptureAudioSource.
        Collects audio_processing_template_ids in chain order (outermost first).

        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, CaptureAudioSource):
            return ResolvedAudioSource(
                device_index=source.device_index,
                is_loopback=source.is_loopback,
                audio_template_id=source.audio_template_id,
                audio_processing_template_ids=[],
            )

        if isinstance(source, ProcessedAudioSource):
            if not source.audio_source_id:
                raise ValueError(f"Processed source {source_id} has no audio_source_id")
            parent_resolved = self._resolve(source.audio_source_id, visited)
            # Prepend this source's template to the list (outermost first)
            template_ids = [source.audio_processing_template_id] + list(
                parent_resolved.audio_processing_template_ids
            )
            return ResolvedAudioSource(
                device_index=parent_resolved.device_index,
                is_loopback=parent_resolved.is_loopback,
                audio_template_id=parent_resolved.audio_template_id,
                audio_processing_template_ids=template_ids,
            )

        raise ValueError(f"Audio source {source_id} has unknown type: {source.source_type}")

    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 _check_no_cycle_for_new(self, parent_id: str) -> None:
        """Validate that parent chain doesn't already contain a cycle (for new source creation)."""
        visited: Set[str] = set()
        current = parent_id
        while current:
            if current in visited:
                raise ValueError("Cycle detected in parent audio source chain")
            visited.add(current)
            item = self._items.get(current)
            if item is None:
                break
            current = getattr(item, "audio_source_id", None)

    # ── Reference query helpers ──────────────────────────────────────

    def get_sources_referencing_template(self, template_id: str) -> List[ProcessedAudioSource]:
        """Return all ProcessedAudioSources that reference a given audio processing template."""
        return [
            s
            for s in self._items.values()
            if isinstance(s, ProcessedAudioSource) and s.audio_processing_template_id == template_id
        ]
