# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Video Extractor using yt-dlp.

Supports downloading from 1000+ sites including:
- YouTube
- Vimeo
- Twitter/X
- TikTok
- Reddit
- Twitch
- And many more
"""

import asyncio
import json
import logging
import re
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)


@dataclass
class VideoFormat:
    """Available video format."""

    format_id: str
    ext: str
    resolution: str
    filesize: Optional[int]
    vcodec: Optional[str]
    acodec: Optional[str]
    tbr: Optional[float]  # Total bitrate

    @property
    def has_video(self) -> bool:
        return self.vcodec and self.vcodec != "none"

    @property
    def has_audio(self) -> bool:
        return self.acodec and self.acodec != "none"


@dataclass
class VideoInfo:
    """Video information."""

    url: str
    title: str
    description: Optional[str]
    duration: int  # seconds
    thumbnail: Optional[str]
    uploader: Optional[str]
    uploader_id: Optional[str]
    upload_date: Optional[str]
    view_count: Optional[int]
    like_count: Optional[int]
    formats: List[VideoFormat]
    chapters: List[Dict[str, Any]]
    subtitles: Dict[str, List[Dict[str, str]]]
    extractor: str  # e.g., "youtube", "vimeo"

    @classmethod
    def from_yt_dlp(cls, data: Dict[str, Any]) -> "VideoInfo":
        """Create from yt-dlp info dict."""
        formats = []
        for f in data.get("formats", []):
            formats.append(
                VideoFormat(
                    format_id=f.get("format_id", ""),
                    ext=f.get("ext", ""),
                    resolution=f.get("resolution", "unknown"),
                    filesize=f.get("filesize"),
                    vcodec=f.get("vcodec"),
                    acodec=f.get("acodec"),
                    tbr=f.get("tbr"),
                )
            )

        chapters = []
        for ch in data.get("chapters", []):
            chapters.append(
                {
                    "start": ch.get("start_time", 0),
                    "end": ch.get("end_time"),
                    "title": ch.get("title", ""),
                }
            )

        return cls(
            url=data.get("webpage_url") or data.get("url", ""),
            title=data.get("title", "Unknown"),
            description=data.get("description"),
            duration=data.get("duration", 0) or 0,
            thumbnail=data.get("thumbnail"),
            uploader=data.get("uploader"),
            uploader_id=data.get("uploader_id"),
            upload_date=data.get("upload_date"),
            view_count=data.get("view_count"),
            like_count=data.get("like_count"),
            formats=formats,
            chapters=chapters,
            subtitles=data.get("subtitles", {}),
            extractor=data.get("extractor", "unknown"),
        )

    def get_best_format(self, max_height: int = 720) -> Optional[VideoFormat]:
        """Get best format up to specified height."""
        video_formats = [f for f in self.formats if f.has_video and f.has_audio]

        # Parse resolution to height
        def get_height(f):
            match = re.search(r"(\d+)p", f.resolution)
            if match:
                return int(match.group(1))
            match = re.search(r"(\d+)x(\d+)", f.resolution)
            if match:
                return int(match.group(2))
            return 0

        # Filter by max height and sort by quality
        suitable = [f for f in video_formats if get_height(f) <= max_height]
        if suitable:
            suitable.sort(key=get_height, reverse=True)
            return suitable[0]

        # Fallback to any format
        return video_formats[0] if video_formats else None


# Quality presets
QUALITY_PRESETS = {
    "360p": 360,
    "480p": 480,
    "720p": 720,
    "1080p": 1080,
    "1440p": 1440,
    "2160p": 2160,
    "4k": 2160,
    "best": 9999,
}


class VideoExtractor:
    """
    Video extraction using yt-dlp.

    yt-dlp is a fork of youtube-dl with additional features
    and support for many more sites.
    """

    def __init__(self, config):
        self.config = config
        self._check_yt_dlp()

    def _check_yt_dlp(self):
        """Check if yt-dlp is available."""
        try:
            result = subprocess.run(
                ["yt-dlp", "--version"],
                capture_output=True,
                text=True,
            )
            if result.returncode == 0:
                logger.info(f"yt-dlp version: {result.stdout.strip()}")
            else:
                logger.warning("yt-dlp not found")
        except FileNotFoundError:
            logger.warning("yt-dlp not found")

    async def get_info(self, url: str) -> VideoInfo:
        """
        Get video information without downloading.

        Args:
            url: Video URL

        Returns:
            VideoInfo object
        """
        cmd = [
            "yt-dlp",
            "--dump-json",
            "--no-download",
            "--no-warnings",
            url,
        ]

        result = await self._run_command(cmd)

        if result["returncode"] != 0:
            raise Exception(f"Failed to get video info: {result['stderr']}")

        # Parse JSON (might be multiple lines for playlists)
        lines = result["stdout"].strip().split("\n")
        data = json.loads(lines[0])

        return VideoInfo.from_yt_dlp(data)

    async def download(
        self,
        url: str,
        output_path: str,
        quality: str = "720p",
        subtitles: bool = True,
        audio_only: bool = False,
    ) -> Dict[str, Any]:
        """
        Download a video.

        Args:
            url: Video URL
            output_path: Output file path
            quality: Quality preset (360p, 480p, 720p, 1080p, best)
            subtitles: Download subtitles
            audio_only: Download audio only

        Returns:
            Download result info
        """
        # Build format selector
        max_height = QUALITY_PRESETS.get(quality, 720)

        if audio_only:
            format_selector = "bestaudio/best"
        else:
            format_selector = (
                f"bestvideo[height<={max_height}]+bestaudio/best[height<={max_height}]/best"
            )

        cmd = [
            "yt-dlp",
            "-f",
            format_selector,
            "-o",
            output_path,
            "--merge-output-format",
            "mp4",
            "--no-warnings",
        ]

        # Add subtitle options
        if subtitles:
            langs = ",".join(self.config.subtitle_languages)
            cmd.extend(
                [
                    "--write-subs",
                    "--write-auto-subs",
                    "--sub-langs",
                    langs,
                    "--convert-subs",
                    "srt",
                ]
            )

        # Add the URL
        cmd.append(url)

        logger.info(f"Downloading: {url} ({quality})")

        result = await self._run_command(cmd)

        if result["returncode"] != 0:
            raise Exception(f"Download failed: {result['stderr']}")

        return {
            "success": True,
            "output_path": output_path,
            "quality": quality,
        }

    async def download_audio(
        self,
        url: str,
        output_path: str,
        format: str = "mp3",
    ) -> Dict[str, Any]:
        """
        Download audio only.

        Args:
            url: Video URL
            output_path: Output file path
            format: Audio format (mp3, m4a, opus, etc.)

        Returns:
            Download result info
        """
        cmd = [
            "yt-dlp",
            "-x",  # Extract audio
            "--audio-format",
            format,
            "-o",
            output_path,
            "--no-warnings",
            url,
        ]

        result = await self._run_command(cmd)

        if result["returncode"] != 0:
            raise Exception(f"Audio download failed: {result['stderr']}")

        return {
            "success": True,
            "output_path": output_path,
            "format": format,
        }

    async def get_subtitles(
        self,
        url: str,
        languages: Optional[List[str]] = None,
    ) -> Dict[str, str]:
        """
        Download subtitles for a video.

        Args:
            url: Video URL
            languages: Language codes to download

        Returns:
            Dict of language code to subtitle text
        """
        languages = languages or self.config.subtitle_languages

        # Create temp directory
        import tempfile

        with tempfile.TemporaryDirectory() as tmpdir:
            cmd = [
                "yt-dlp",
                "--skip-download",
                "--write-subs",
                "--write-auto-subs",
                "--sub-langs",
                ",".join(languages),
                "--convert-subs",
                "srt",
                "-o",
                f"{tmpdir}/subs",
                url,
            ]

            result = await self._run_command(cmd)
            if result.returncode != 0:
                logger.warning(f"yt-dlp subtitle download failed: {result.stderr}")
                return {}

            # Read subtitle files
            subtitles = {}
            for file in Path(tmpdir).glob("*.srt"):
                # Extract language from filename
                parts = file.stem.split(".")
                if len(parts) >= 2:
                    lang = parts[-1]
                    subtitles[lang] = file.read_text()

            return subtitles

    async def get_playlist_info(
        self,
        url: str,
        max_videos: int = 50,
    ) -> List[VideoInfo]:
        """
        Get information for all videos in a playlist.

        Args:
            url: Playlist URL
            max_videos: Maximum videos to retrieve

        Returns:
            List of VideoInfo objects
        """
        cmd = [
            "yt-dlp",
            "--dump-json",
            "--no-download",
            "--flat-playlist",
            "--playlist-end",
            str(max_videos),
            url,
        ]

        result = await self._run_command(cmd)

        if result["returncode"] != 0:
            raise Exception(f"Failed to get playlist info: {result['stderr']}")

        videos = []
        for line in result["stdout"].strip().split("\n"):
            if line:
                data = json.loads(line)
                videos.append(VideoInfo.from_yt_dlp(data))

        return videos

    async def _run_command(self, cmd: List[str]) -> Dict[str, Any]:
        """Run a command asynchronously."""
        loop = asyncio.get_event_loop()

        def run():
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
            )
            return {
                "returncode": result.returncode,
                "stdout": result.stdout,
                "stderr": result.stderr,
            }

        return await loop.run_in_executor(None, run)

    @staticmethod
    def is_supported(url: str) -> bool:
        """Check if a URL is supported by yt-dlp."""
        # Common supported domains
        supported_patterns = [
            r"youtube\.com",
            r"youtu\.be",
            r"vimeo\.com",
            r"twitter\.com",
            r"x\.com",
            r"tiktok\.com",
            r"reddit\.com",
            r"twitch\.tv",
            r"dailymotion\.com",
            r"facebook\.com",
            r"instagram\.com",
            r"soundcloud\.com",
            r"bandcamp\.com",
        ]

        for pattern in supported_patterns:
            if re.search(pattern, url, re.IGNORECASE):
                return True

        return False

    @staticmethod
    def extract_video_id(url: str) -> Optional[str]:
        """Extract video ID from URL."""
        # YouTube
        patterns = [
            r"youtube\.com/watch\?v=([a-zA-Z0-9_-]+)",
            r"youtu\.be/([a-zA-Z0-9_-]+)",
            r"youtube\.com/embed/([a-zA-Z0-9_-]+)",
        ]

        for pattern in patterns:
            match = re.search(pattern, url)
            if match:
                return match.group(1)

        return None
