from __future__ import annotations

from dataclasses import dataclass
from typing import Protocol

from yt_dlp import YoutubeDL
from yt_dlp.utils import DownloadError

from youtube_cli_agent.errors import ToolExecutionError
from youtube_cli_agent.models import VideoCandidate


class SearchBackend(Protocol):
    def search(self, query: str, limit: int) -> list[dict]:
        ...


@dataclass(slots=True)
class YtDlpSearchBackend:
    timeout_seconds: int = 30

    def search(self, query: str, limit: int) -> list[dict]:
        ydl_opts = {
            "quiet": True,
            "no_warnings": True,
            "skip_download": True,
            "noplaylist": True,
            "extract_flat": "in_playlist",
            "socket_timeout": self.timeout_seconds,
        }
        search_term = f"ytsearch{limit}:{query}"

        try:
            with YoutubeDL(ydl_opts) as ydl:
                payload = ydl.extract_info(search_term, download=False)
        except DownloadError as exc:
            message = str(exc)
            if "429" in message or "Too Many Requests" in message:
                raise ToolExecutionError(
                    code="RATE_LIMITED",
                    stage="Search",
                    message="YouTube rate-limited the search request.",
                ) from None
            if "timed out" in message.lower():
                raise ToolExecutionError(
                    code="NETWORK_TIMEOUT",
                    stage="Search",
                    message="Search request timed out.",
                ) from None
            raise ToolExecutionError(
                code="SEARCH_FAILED",
                stage="Search",
                message=f"Search failed: {message}",
            ) from None
        except Exception as exc:  # pragma: no cover - safety net
            raise ToolExecutionError(
                code="SEARCH_FAILED",
                stage="Search",
                message=f"Search failed: {exc}",
            ) from None

        entries = payload.get("entries") if isinstance(payload, dict) else None
        if not entries:
            return []
        return [entry for entry in entries if isinstance(entry, dict)]


@dataclass(slots=True)
class YouTubeSearchTool:
    backend: SearchBackend

    def run(self, query: str, limit: int = 10) -> list[VideoCandidate]:
        raw_entries = self.backend.search(query=query, limit=limit)
        if not raw_entries:
            raise ToolExecutionError(
                code="NO_SEARCH_RESULTS",
                stage="Search",
                message="No videos were returned for the query.",
            )

        deduped: dict[str, dict] = {}
        ordered_ids: list[str] = []
        for raw in raw_entries:
            video_id = str(raw.get("id") or "").strip()
            if not video_id:
                continue
            if video_id not in deduped:
                deduped[video_id] = raw
                ordered_ids.append(video_id)

        results: list[VideoCandidate] = []
        for rank, video_id in enumerate(ordered_ids[:limit], start=1):
            raw = deduped[video_id]
            title = str(raw.get("title") or "").strip() or "Untitled"
            channel = str(raw.get("channel") or raw.get("uploader") or "Unknown Channel").strip()
            url = str(raw.get("url") or f"https://www.youtube.com/watch?v={video_id}").strip()
            if not url.startswith("http"):
                url = f"https://www.youtube.com/watch?v={video_id}"

            duration = raw.get("duration")
            duration_seconds = int(duration) if isinstance(duration, (int, float)) else None
            publish_date = raw.get("upload_date")
            publish_date_str = str(publish_date) if publish_date else None
            description = str(raw.get("description") or "").strip() or None

            results.append(
                VideoCandidate(
                    rank=rank,
                    video_id=video_id,
                    title=title,
                    channel=channel,
                    url=url,
                    duration_seconds=duration_seconds,
                    publish_date=publish_date_str,
                    description=description,
                )
            )

        if not results:
            raise ToolExecutionError(
                code="NO_VALID_SEARCH_RESULTS",
                stage="Search",
                message="Search returned entries, but none had valid video IDs.",
            )

        return results
