"""Picture source storage using SQLite."""

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

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.storage.picture_source import (
    PictureSource,
    ProcessedPictureSource,
    ScreenCapturePictureSource,
    StaticImagePictureSource,
    VideoCaptureSource,
)
from wled_controller.utils import get_logger

logger = get_logger(__name__)


class PictureSourceStore(BaseSqliteStore[PictureSource]):
    """Storage for picture sources.

    Supports raw and processed stream types with cycle detection
    for processed streams that reference other streams.
    """

    _table_name = "picture_sources"
    _entity_name = "Picture source"

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

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

    # Legacy aliases (old code used "stream" naming)
    get_all_streams = BaseSqliteStore.get_all
    get_stream = BaseSqliteStore.get

    # ── Helpers ───────────────────────────────────────────────────────

    def _detect_cycle(self, source_stream_id: str, exclude_stream_id: Optional[str] = None) -> bool:
        """Detect if following the source chain from source_stream_id would create a cycle.

        Args:
            source_stream_id: The source stream ID to start walking from
            exclude_stream_id: Stream ID to exclude (the stream being created/updated)

        Returns:
            True if a cycle would be created
        """
        visited: Set[str] = set()
        if exclude_stream_id:
            visited.add(exclude_stream_id)

        current_id = source_stream_id
        while current_id:
            if current_id in visited:
                return True
            visited.add(current_id)

            current_stream = self._items.get(current_id)
            if not current_stream:
                break
            if not isinstance(current_stream, ProcessedPictureSource):
                break
            current_id = current_stream.source_stream_id

        return False

    # ── CRUD ──────────────────────────────────────────────────────────

    def create_stream(
        self,
        name: str,
        stream_type: str,
        display_index: Optional[int] = None,
        capture_template_id: Optional[str] = None,
        target_fps: Optional[int] = None,
        source_stream_id: Optional[str] = None,
        postprocessing_template_id: Optional[str] = None,
        image_asset_id: Optional[str] = None,
        description: Optional[str] = None,
        tags: Optional[List[str]] = None,
        # Video fields
        video_asset_id: Optional[str] = None,
        loop: bool = True,
        playback_speed: float = 1.0,
        start_time: Optional[float] = None,
        end_time: Optional[float] = None,
        resolution_limit: Optional[int] = None,
        clock_id: Optional[str] = None,
    ) -> PictureSource:
        """Create a new picture source.

        Raises:
            ValueError: If validation fails or cycle detected
        """
        if stream_type not in ("raw", "processed", "static_image", "video"):
            raise ValueError(f"Invalid stream type: {stream_type}")

        if stream_type == "raw":
            if display_index is None:
                raise ValueError("Raw streams require display_index")
            if not capture_template_id:
                raise ValueError("Raw streams require capture_template_id")
            if target_fps is None:
                raise ValueError("Raw streams require target_fps")
        elif stream_type == "processed":
            if not source_stream_id:
                raise ValueError("Processed streams require source_stream_id")
            if not postprocessing_template_id:
                raise ValueError("Processed streams require postprocessing_template_id")
            # Validate source stream exists
            if source_stream_id not in self._items:
                raise ValueError(f"Source stream not found: {source_stream_id}")
            # Check for cycles
            if self._detect_cycle(source_stream_id):
                raise ValueError("Cycle detected in stream chain")
        elif stream_type == "static_image":
            if not image_asset_id:
                raise ValueError("Static image streams require image_asset_id")
        elif stream_type == "video":
            if not video_asset_id:
                raise ValueError("Video streams require video_asset_id")

        # Check for duplicate name
        self._check_name_unique(name)

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

        common = dict(
            id=stream_id, name=name, stream_type=stream_type,
            created_at=now, updated_at=now, description=description,
            tags=tags or [],
        )

        stream: PictureSource
        if stream_type == "raw":
            stream = ScreenCapturePictureSource(
                **common,
                display_index=display_index,  # type: ignore[arg-type]
                capture_template_id=capture_template_id,  # type: ignore[arg-type]
                target_fps=target_fps,  # type: ignore[arg-type]
            )
        elif stream_type == "processed":
            stream = ProcessedPictureSource(
                **common,
                source_stream_id=source_stream_id,  # type: ignore[arg-type]
                postprocessing_template_id=postprocessing_template_id,  # type: ignore[arg-type]
            )
        elif stream_type == "video":
            stream = VideoCaptureSource(
                **common,
                video_asset_id=video_asset_id,
                loop=loop,
                playback_speed=playback_speed,
                start_time=start_time,
                end_time=end_time,
                resolution_limit=resolution_limit,
                clock_id=clock_id,
                target_fps=target_fps or 30,
            )
        else:
            stream = StaticImagePictureSource(
                **common,
                image_asset_id=image_asset_id,
            )

        self._items[stream_id] = stream
        self._save_item(stream_id, stream)

        logger.info(f"Created picture source: {name} ({stream_id}, type={stream_type})")
        return stream

    def update_stream(
        self,
        stream_id: str,
        name: Optional[str] = None,
        display_index: Optional[int] = None,
        capture_template_id: Optional[str] = None,
        target_fps: Optional[int] = None,
        source_stream_id: Optional[str] = None,
        postprocessing_template_id: Optional[str] = None,
        image_asset_id: Optional[str] = None,
        description: Optional[str] = None,
        tags: Optional[List[str]] = None,
        # Video fields
        video_asset_id: Optional[str] = None,
        loop: Optional[bool] = None,
        playback_speed: Optional[float] = None,
        start_time: Optional[float] = None,
        end_time: Optional[float] = None,
        resolution_limit: Optional[int] = None,
        clock_id: Optional[str] = None,
    ) -> PictureSource:
        """Update an existing picture source.

        Raises:
            ValueError: If stream not found, validation fails, or cycle detected
        """
        stream = self.get(stream_id)

        # If changing source_stream_id on a processed stream, check for cycles
        # (skip validation when clearing via empty string)
        if source_stream_id is not None and source_stream_id != "" and isinstance(stream, ProcessedPictureSource):
            if source_stream_id not in self._items:
                raise ValueError(f"Source stream not found: {source_stream_id}")
            if self._detect_cycle(source_stream_id, exclude_stream_id=stream_id):
                raise ValueError("Cycle detected in stream chain")

        if name is not None:
            self._check_name_unique(name, exclude_id=stream_id)
            stream.name = name
        if description is not None:
            stream.description = description
        if tags is not None:
            stream.tags = tags

        if isinstance(stream, ScreenCapturePictureSource):
            if display_index is not None:
                stream.display_index = display_index
            if capture_template_id is not None:
                stream.capture_template_id = resolve_ref(capture_template_id, stream.capture_template_id)
            if target_fps is not None:
                stream.target_fps = target_fps
        elif isinstance(stream, ProcessedPictureSource):
            if source_stream_id is not None:
                stream.source_stream_id = resolve_ref(source_stream_id, stream.source_stream_id)
            if postprocessing_template_id is not None:
                stream.postprocessing_template_id = resolve_ref(postprocessing_template_id, stream.postprocessing_template_id)
        elif isinstance(stream, StaticImagePictureSource):
            if image_asset_id is not None:
                stream.image_asset_id = resolve_ref(image_asset_id, stream.image_asset_id)
        elif isinstance(stream, VideoCaptureSource):
            if video_asset_id is not None:
                stream.video_asset_id = resolve_ref(video_asset_id, stream.video_asset_id)
            if loop is not None:
                stream.loop = loop
            if playback_speed is not None:
                stream.playback_speed = playback_speed
            if start_time is not None:
                stream.start_time = start_time if start_time > 0 else None
            if end_time is not None:
                stream.end_time = end_time if end_time > 0 else None
            if resolution_limit is not None:
                stream.resolution_limit = resolution_limit if resolution_limit > 0 else None
            if clock_id is not None:
                stream.clock_id = resolve_ref(clock_id, stream.clock_id)
            if target_fps is not None:
                stream.target_fps = target_fps

        stream.updated_at = datetime.now(timezone.utc)

        self._save_item(stream_id, stream)

        logger.info(f"Updated picture source: {stream_id}")
        return stream

    def delete_stream(self, stream_id: str) -> None:
        """Delete a picture source.

        Raises:
            ValueError: If stream not found or is referenced by another stream
        """
        if stream_id not in self._items:
            raise ValueError(f"Picture source not found: {stream_id}")

        # Check if any other stream references this one as source
        for other_stream in self._items.values():
            if isinstance(other_stream, ProcessedPictureSource) and other_stream.source_stream_id == stream_id:
                raise ValueError(
                    f"Cannot delete stream '{self._items[stream_id].name}': "
                    f"it is referenced by stream '{other_stream.name}'"
                )

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

        logger.info(f"Deleted picture source: {stream_id}")

    # Also expose as delete_source for consistency
    def delete_source(self, source_id: str) -> None:
        """Alias for delete_stream with reference checking."""
        self.delete_stream(source_id)

    # ── Query helpers ─────────────────────────────────────────────────

    def get_targets_referencing(self, stream_id: str, target_store) -> List[str]:
        """Return names of targets that reference this stream."""
        return target_store.get_targets_referencing_source(stream_id)

    def resolve_stream_chain(self, stream_id: str) -> dict:
        """Resolve a stream chain to get the terminal stream and collected postprocessing templates.

        Walks the chain from the given stream to a terminal stream (raw or static_image),
        collecting postprocessing template IDs along the way.

        Args:
            stream_id: Starting stream ID

        Returns:
            Dict with:
                - raw_stream: The terminal PictureSource (raw or static_image)
                - postprocessing_template_ids: List of PP template IDs (in chain order)

        Raises:
            ValueError: If stream not found or chain is broken
        """
        postprocessing_template_ids = []
        visited = set()
        current_id = stream_id

        while True:
            if current_id in visited:
                raise ValueError(f"Cycle detected in stream chain at {current_id}")
            visited.add(current_id)

            stream = self.get(current_id)

            if not isinstance(stream, ProcessedPictureSource):
                return {
                    "raw_stream": stream,
                    "postprocessing_template_ids": postprocessing_template_ids,
                }

            # Processed stream — collect PP template and follow source
            if stream.postprocessing_template_id:
                postprocessing_template_ids.append(stream.postprocessing_template_id)

            if not stream.source_stream_id:
                raise ValueError(f"Processed stream {current_id} has no source_stream_id")

            current_id = stream.source_stream_id
