"""Color strip source storage using SQLite."""

import uuid
from datetime import datetime, timezone
from typing import List

from ledgrab.storage.base_sqlite_store import BaseSqliteStore
from ledgrab.storage.database import Database
from ledgrab.storage.utils import resolve_ref
from ledgrab.storage.color_strip_source import (
    ColorStripSource,
    CompositeColorStripSource,
    MappedColorStripSource,
    ProcessedColorStripSource,
)
from ledgrab.utils import get_logger

logger = get_logger(__name__)

MAX_COMPOSITE_DEPTH = 4


class ColorStripStore(BaseSqliteStore[ColorStripSource]):
    """Persistent storage for color strip sources."""

    _table_name = "color_strip_sources"
    _entity_name = "Color strip source"

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

    # Backward-compatible aliases
    get_all_sources = BaseSqliteStore.get_all
    delete_source = BaseSqliteStore.delete

    def get_source(self, source_id: str) -> ColorStripSource:
        """Get a color strip source by ID (alias for get())."""
        return self.get(source_id)

    def create_source(self, name: str, source_type: str = "picture", **kwargs) -> ColorStripSource:
        """Create a new color strip source.

        All type-specific parameters are passed as keyword arguments and
        forwarded to the subclass ``create_from_kwargs`` factory method.

        Raises:
            ValueError: If validation fails
        """
        if not name or not name.strip():
            raise ValueError("Name is required")

        for source in self._items.values():
            if source.name == name:
                raise ValueError(f"Color strip source with name '{name}' already exists")

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

        tags = kwargs.pop("tags", None) or []

        source = ColorStripSource.create_instance(
            source_type,
            id=source_id,
            name=name,
            created_at=now,
            updated_at=now,
            tags=tags,
            **kwargs,
        )

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

        logger.info(f"Created color strip source: {name} ({source_id}, type={source_type})")
        return source

    def update_source(self, source_id: str, **kwargs) -> ColorStripSource:
        """Update an existing color strip source.

        All type-specific parameters are passed as keyword arguments and
        forwarded to the subclass ``apply_update`` method.

        Raises:
            ValueError: If source not found
        """
        if source_id not in self._items:
            raise ValueError(f"Color strip source not found: {source_id}")

        source = self._items[source_id]

        # -- Common fields handled here (before dispatching to subclass) --
        name = kwargs.pop("name", None)
        if name is not None:
            for other in self._items.values():
                if other.id != source_id and other.name == name:
                    raise ValueError(f"Color strip source with name '{name}' already exists")
            source.name = name

        description = kwargs.pop("description", None)
        if description is not None:
            source.description = description

        clock_id = kwargs.pop("clock_id", None)
        if clock_id is not None:
            source.clock_id = resolve_ref(clock_id, source.clock_id)

        tags = kwargs.pop("tags", None)
        if tags is not None:
            source.tags = tags

        # -- Type-specific fields --
        source.apply_update(**kwargs)

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

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

    def get_composites_referencing(self, source_id: str) -> List[str]:
        """Return names of composite sources that reference a given source as a layer."""
        names = []
        for source in self._items.values():
            if isinstance(source, CompositeColorStripSource):
                for layer in source.layers:
                    if layer.get("source_id") == source_id:
                        names.append(source.name)
                        break
        return names

    def get_mapped_referencing(self, source_id: str) -> List[str]:
        """Return names of mapped sources that reference a given source as a zone."""
        names = []
        for source in self._items.values():
            if isinstance(source, MappedColorStripSource):
                for zone in source.zones:
                    if zone.get("source_id") == source_id:
                        names.append(source.name)
                        break
        return names

    def get_transitive_dependencies(self, source_id: str) -> set[str]:
        """Return the set of all source IDs that *source_id* transitively depends on.

        Walks composite layers and mapped zones to build the full dependency graph.
        """
        visited: set[str] = set()
        stack = [source_id]
        while stack:
            current = stack.pop()
            if current in visited:
                continue
            visited.add(current)
            source = self._items.get(current)
            if source is None:
                continue
            child_ids: list[str] = []
            if isinstance(source, CompositeColorStripSource):
                child_ids = [
                    layer.get("source_id", "") for layer in source.layers if layer.get("source_id")
                ]
            elif isinstance(source, MappedColorStripSource):
                child_ids = [
                    zone.get("source_id", "") for zone in source.zones if zone.get("source_id")
                ]
            elif isinstance(source, ProcessedColorStripSource):
                if source.input_source_id:
                    child_ids = [source.input_source_id]
            stack.extend(cid for cid in child_ids if cid not in visited)
        visited.discard(source_id)  # exclude self from dependency set
        return visited

    def get_nesting_depth(self, source_id: str) -> int:
        """Return the maximum nesting depth reachable from *source_id*.

        Leaf sources have depth 0.  A composite with only leaf layers has depth 1.
        """
        cache: dict[str, int] = {}

        def _depth(sid: str, path: frozenset[str]) -> int:
            if sid in cache:
                return cache[sid]
            source = self._items.get(sid)
            if source is None:
                return 0
            child_ids: list[str] = []
            if isinstance(source, CompositeColorStripSource):
                child_ids = [ly.get("source_id", "") for ly in source.layers if ly.get("source_id")]
            elif isinstance(source, MappedColorStripSource):
                child_ids = [z.get("source_id", "") for z in source.zones if z.get("source_id")]
            elif isinstance(source, ProcessedColorStripSource):
                if source.input_source_id:
                    child_ids = [source.input_source_id]
            if not child_ids:
                cache[sid] = 0
                return 0
            max_child = 0
            for cid in child_ids:
                if cid in path:
                    continue  # skip cycles (validated separately)
                max_child = max(max_child, _depth(cid, path | {cid}))
            depth = 1 + max_child
            cache[sid] = depth
            return depth

        return _depth(source_id, frozenset({source_id}))

    def validate_nesting(
        self,
        parent_id: str,
        child_source_ids: list[str],
    ) -> None:
        """Validate that adding *child_source_ids* as layers/zones of *parent_id* is safe.

        Raises ValueError if:
        - Any child is the parent itself (self-reference)
        - Any child transitively depends on the parent (would create a cycle)
        - The resulting nesting depth exceeds MAX_COMPOSITE_DEPTH
        """
        for child_id in child_source_ids:
            if not child_id:
                continue
            if child_id == parent_id:
                child_name = getattr(self._items.get(child_id), "name", child_id)
                raise ValueError(f"Cannot add '{child_name}' as its own layer (self-reference)")
            child_deps = self.get_transitive_dependencies(child_id)
            if parent_id in child_deps:
                child_name = getattr(self._items.get(child_id), "name", child_id)
                parent_name = getattr(self._items.get(parent_id), "name", parent_id)
                raise ValueError(
                    f"Cannot add '{child_name}' as a layer of '{parent_name}': "
                    f"it would create a circular reference"
                )

        # Check depth: temporarily compute what the depth would be
        max_child_depth = 0
        for child_id in child_source_ids:
            if not child_id:
                continue
            max_child_depth = max(max_child_depth, self.get_nesting_depth(child_id))
        resulting_depth = 1 + max_child_depth
        if resulting_depth > MAX_COMPOSITE_DEPTH:
            raise ValueError(
                f"Nesting depth {resulting_depth} exceeds maximum of {MAX_COMPOSITE_DEPTH}"
            )

    def get_processed_referencing(self, source_id: str) -> List[str]:
        """Return names of processed sources that reference a given source as input."""
        names = []
        for source in self._items.values():
            if isinstance(source, ProcessedColorStripSource):
                if source.input_source_id == source_id:
                    names.append(source.name)
        return names
