# 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

"""
Familiar Video Skill

Privacy-focused video handling with:
- YouTube/Vimeo playback without tracking (Invidious proxy)
- Video downloading with yt-dlp (1000+ sites)
- Local transcription with Whisper
- AI-powered summarization and search
- Encrypted video archive
- Chapter generation from transcripts
"""

import asyncio  # noqa: F401
import hashlib
import json
import logging
import os  # noqa: F401
import re  # noqa: F401
import uuid  # noqa: F401
from dataclasses import asdict, dataclass, field
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple  # noqa: F401

logger = logging.getLogger(__name__)

from .ai_video import VideoAI  # noqa: E402
from .extractor import VideoExtractor, VideoInfo  # noqa: E402
from .player import PrivacyPlayer  # noqa: E402
from .transcribe import Transcriber, Transcript  # noqa: E402


@dataclass
class VideoConfig:
    """Video skill configuration."""

    # Storage
    storage_path: str = "~/.familiar/video"
    max_storage_gb: int = 50
    encrypt_downloads: bool = True

    # Download settings
    default_quality: str = "720p"  # 360p, 480p, 720p, 1080p, best
    download_subtitles: bool = True
    subtitle_languages: List[str] = field(default_factory=lambda: ["en"])

    # Transcription
    whisper_model: str = "base"  # tiny, base, small, medium, large
    transcribe_on_download: bool = True

    # Privacy
    use_invidious: bool = True
    invidious_instance: str = "https://invidious.snopyta.org"

    # AI
    auto_summarize: bool = False
    auto_chapters: bool = True


@dataclass
class VideoMetadata:
    """Stored video metadata."""

    id: str
    source_url: str
    title: str
    description: str
    duration_seconds: int
    thumbnail_url: Optional[str]
    uploader: Optional[str]
    upload_date: Optional[str]
    view_count: Optional[int]
    local_path: Optional[str]
    transcript_path: Optional[str]
    summary: Optional[str]
    chapters: List[Dict[str, Any]]
    downloaded_at: Optional[str]
    file_size_bytes: Optional[int]

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "VideoMetadata":
        return cls(**data)

    @property
    def duration_formatted(self) -> str:
        """Format duration as HH:MM:SS."""
        hours, remainder = divmod(self.duration_seconds, 3600)
        minutes, seconds = divmod(remainder, 60)
        if hours:
            return f"{hours}:{minutes:02d}:{seconds:02d}"
        return f"{minutes}:{seconds:02d}"


class VideoSkill:
    """
    Privacy-focused video skill.

    Provides:
    - Private playback (YouTube without tracking via Invidious)
    - Video downloading (yt-dlp supports 1000+ sites)
    - Local transcription (Whisper)
    - AI summarization and search within videos
    - Automatic chapter generation
    - Encrypted video storage
    """

    name = "video"
    description = "Privacy-focused video playback, download, and AI analysis"

    def __init__(self, agent, config: Dict[str, Any]):
        self.agent = agent
        self.config = VideoConfig(**config.get("video", {}))

        # Storage paths
        self.storage_path = Path(self.config.storage_path).expanduser()
        self.videos_path = self.storage_path / "videos"
        self.transcripts_path = self.storage_path / "transcripts"
        self.thumbnails_path = self.storage_path / "thumbnails"

        # Create directories
        for path in [self.videos_path, self.transcripts_path, self.thumbnails_path]:
            path.mkdir(parents=True, exist_ok=True)

        # Components
        self.extractor = VideoExtractor(self.config)
        self.player = PrivacyPlayer(self.config)
        self.transcriber = Transcriber(self.config)
        self.ai = VideoAI(agent) if agent else None

        # Video index
        self._index: Dict[str, VideoMetadata] = {}
        self._index_file = self.storage_path / "index.json"

    async def start(self):
        """Initialize video skill."""
        await self._load_index()
        logger.info(f"Video skill initialized ({len(self._index)} videos indexed)")

    async def stop(self):
        """Cleanup."""
        await self._save_index()

    async def _load_index(self):
        """Load video index from disk."""
        if self._index_file.exists():
            try:
                data = json.loads(self._index_file.read_text())
                self._index = {k: VideoMetadata.from_dict(v) for k, v in data.items()}
            except Exception as e:
                logger.error(f"Error loading video index: {e}")
                self._index = {}

    async def _save_index(self):
        """Save video index to disk."""
        data = {k: v.to_dict() for k, v in self._index.items()}
        self._index_file.write_text(json.dumps(data, indent=2))

    # ═══════════════════════════════════════════════════════════════
    # Video Information
    # ═══════════════════════════════════════════════════════════════

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

        Args:
            url: Video URL (YouTube, Vimeo, etc.)

        Returns:
            VideoInfo with title, duration, formats, etc.
        """
        return await self.extractor.get_info(url)

    async def search(
        self,
        query: str,
        max_results: int = 10,
    ) -> List[Dict[str, Any]]:
        """
        Search for videos privately.

        Uses Invidious API to search YouTube without tracking.

        Args:
            query: Search query
            max_results: Maximum results to return

        Returns:
            List of video results
        """
        return await self.player.search(query, max_results)

    # ═══════════════════════════════════════════════════════════════
    # Privacy Playback
    # ═══════════════════════════════════════════════════════════════

    async def get_stream_url(
        self,
        url: str,
        quality: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Get a privacy-friendly stream URL.

        For YouTube, uses Invidious to avoid Google tracking.

        Args:
            url: Original video URL
            quality: Desired quality (360p, 480p, 720p, 1080p, best)

        Returns:
            {
                'stream_url': str,
                'title': str,
                'duration': int,
                'thumbnail': str,
            }
        """
        quality = quality or self.config.default_quality
        return await self.player.get_stream(url, quality)

    async def get_embed_html(
        self,
        url: str,
        width: int = 640,
        height: int = 360,
        autoplay: bool = False,
    ) -> str:
        """
        Get embeddable HTML for privacy playback.

        Args:
            url: Video URL
            width: Player width
            height: Player height
            autoplay: Auto-start playback

        Returns:
            HTML string for embedding
        """
        return await self.player.get_embed_html(url, width, height, autoplay)

    # ═══════════════════════════════════════════════════════════════
    # Download & Storage
    # ═══════════════════════════════════════════════════════════════

    async def download(
        self,
        url: str,
        quality: Optional[str] = None,
        transcribe: Optional[bool] = None,
        summarize: bool = False,
    ) -> VideoMetadata:
        """
        Download a video for offline viewing.

        Args:
            url: Video URL
            quality: Download quality
            transcribe: Generate transcript (None = use config default)
            summarize: Generate AI summary

        Returns:
            VideoMetadata for the downloaded video
        """
        quality = quality or self.config.default_quality
        do_transcribe = transcribe if transcribe is not None else self.config.transcribe_on_download

        # Get video info
        info = await self.extractor.get_info(url)

        # Generate ID
        video_id = hashlib.sha256(url.encode()).hexdigest()[:12]

        # Download
        logger.info(f"Downloading: {info.title}")

        local_path = self.videos_path / f"{video_id}.mp4"
        await self.extractor.download(
            url,
            str(local_path),
            quality=quality,
            subtitles=self.config.download_subtitles,
        )

        # Get file size
        file_size = local_path.stat().st_size if local_path.exists() else 0

        # Create metadata
        metadata = VideoMetadata(
            id=video_id,
            source_url=url,
            title=info.title,
            description=info.description or "",
            duration_seconds=info.duration,
            thumbnail_url=info.thumbnail,
            uploader=info.uploader,
            upload_date=info.upload_date,
            view_count=info.view_count,
            local_path=str(local_path),
            transcript_path=None,
            summary=None,
            chapters=info.chapters or [],
            downloaded_at=datetime.utcnow().isoformat(),
            file_size_bytes=file_size,
        )

        # Transcribe if requested
        if do_transcribe:
            transcript = await self.transcribe_video(video_id)
            metadata.transcript_path = str(self.transcripts_path / f"{video_id}.json")

            # Generate chapters from transcript if none exist
            if self.config.auto_chapters and not metadata.chapters:
                metadata.chapters = await self._generate_chapters(transcript)

        # Summarize if requested
        if summarize and self.ai:
            transcript = await self.get_transcript(video_id)
            if transcript:
                metadata.summary = await self.ai.summarize_video(
                    info.title,
                    transcript.full_text,
                    info.duration,
                )

        # Save to index
        self._index[video_id] = metadata
        await self._save_index()

        logger.info(f"Downloaded: {info.title} ({metadata.duration_formatted})")

        return metadata

    async def delete_video(self, video_id: str) -> bool:
        """Delete a downloaded video."""
        if video_id not in self._index:
            return False

        metadata = self._index[video_id]

        # Delete files
        if metadata.local_path:
            path = Path(metadata.local_path)
            if path.exists():
                path.unlink()

        if metadata.transcript_path:
            path = Path(metadata.transcript_path)
            if path.exists():
                path.unlink()

        # Remove from index
        del self._index[video_id]
        await self._save_index()

        return True

    async def list_videos(
        self,
        limit: int = 50,
        search: Optional[str] = None,
    ) -> List[VideoMetadata]:
        """List downloaded videos."""
        videos = list(self._index.values())

        # Filter by search
        if search:
            search_lower = search.lower()
            videos = [
                v
                for v in videos
                if search_lower in v.title.lower() or search_lower in v.description.lower()
            ]

        # Sort by download date (newest first)
        videos.sort(key=lambda v: v.downloaded_at or "", reverse=True)

        return videos[:limit]

    async def get_video(self, video_id: str) -> Optional[VideoMetadata]:
        """Get video metadata by ID."""
        return self._index.get(video_id)

    # ═══════════════════════════════════════════════════════════════
    # Transcription
    # ═══════════════════════════════════════════════════════════════

    async def transcribe_video(
        self,
        video_id: str,
        force: bool = False,
    ) -> Transcript:
        """
        Transcribe a downloaded video.

        Args:
            video_id: Video ID
            force: Re-transcribe even if transcript exists

        Returns:
            Transcript object
        """
        if video_id not in self._index:
            raise ValueError(f"Video {video_id} not found")

        metadata = self._index[video_id]
        transcript_path = self.transcripts_path / f"{video_id}.json"

        # Check for existing transcript
        if transcript_path.exists() and not force:
            return Transcript.from_file(transcript_path)

        # Transcribe
        logger.info(f"Transcribing: {metadata.title}")

        transcript = await self.transcriber.transcribe(
            metadata.local_path,
            metadata.title,
        )

        # Save transcript
        transcript.save(transcript_path)

        # Update metadata
        metadata.transcript_path = str(transcript_path)
        await self._save_index()

        logger.info(f"Transcription complete: {len(transcript.segments)} segments")

        return transcript

    async def get_transcript(
        self,
        video_id: str,
    ) -> Optional[Transcript]:
        """Get transcript for a video."""
        if video_id not in self._index:
            return None

        metadata = self._index[video_id]

        if not metadata.transcript_path:
            return None

        path = Path(metadata.transcript_path)
        if not path.exists():
            return None

        return Transcript.from_file(path)

    async def search_transcript(
        self,
        video_id: str,
        query: str,
    ) -> List[Dict[str, Any]]:
        """
        Search within a video's transcript.

        Args:
            video_id: Video ID
            query: Search query

        Returns:
            List of matching segments with timestamps
        """
        transcript = await self.get_transcript(video_id)
        if not transcript:
            return []

        return transcript.search(query)

    # ═══════════════════════════════════════════════════════════════
    # AI Features
    # ═══════════════════════════════════════════════════════════════

    async def summarize(
        self,
        video_id: str,
        max_length: int = 500,
    ) -> str:
        """
        Generate AI summary of a video.

        Args:
            video_id: Video ID
            max_length: Maximum summary length

        Returns:
            Summary text
        """
        if not self.ai:
            return "AI not available"

        metadata = self._index.get(video_id)
        if not metadata:
            return "Video not found"

        transcript = await self.get_transcript(video_id)
        if not transcript:
            # Try to transcribe first
            transcript = await self.transcribe_video(video_id)

        summary = await self.ai.summarize_video(
            metadata.title,
            transcript.full_text,
            metadata.duration_seconds,
            max_length,
        )

        # Cache summary
        metadata.summary = summary
        await self._save_index()

        return summary

    async def ask_about_video(
        self,
        video_id: str,
        question: str,
    ) -> str:
        """
        Ask AI a question about a video.

        Args:
            video_id: Video ID
            question: Question to ask

        Returns:
            Answer with timestamp references
        """
        if not self.ai:
            return "AI not available"

        metadata = self._index.get(video_id)
        if not metadata:
            return "Video not found"

        transcript = await self.get_transcript(video_id)
        if not transcript:
            return "No transcript available. Please transcribe the video first."

        return await self.ai.answer_question(
            question,
            metadata.title,
            transcript,
        )

    async def find_moment(
        self,
        video_id: str,
        description: str,
    ) -> Optional[Dict[str, Any]]:
        """
        Find a specific moment in a video.

        Args:
            video_id: Video ID
            description: Description of the moment to find

        Returns:
            {
                'timestamp': float,
                'timestamp_formatted': str,
                'text': str,
                'confidence': float,
            }
        """
        if not self.ai:
            return None

        transcript = await self.get_transcript(video_id)
        if not transcript:
            return None

        return await self.ai.find_moment(description, transcript)

    async def generate_chapters(
        self,
        video_id: str,
    ) -> List[Dict[str, Any]]:
        """
        Generate chapters from video transcript.

        Args:
            video_id: Video ID

        Returns:
            List of chapters with timestamps and titles
        """
        transcript = await self.get_transcript(video_id)
        if not transcript:
            return []

        chapters = await self._generate_chapters(transcript)

        # Cache chapters
        if video_id in self._index:
            self._index[video_id].chapters = chapters
            await self._save_index()

        return chapters

    async def _generate_chapters(
        self,
        transcript: Transcript,
    ) -> List[Dict[str, Any]]:
        """Generate chapters from transcript."""
        if self.ai:
            return await self.ai.generate_chapters(transcript)

        # Fallback: simple time-based chapters
        duration = transcript.duration
        chapter_length = 300  # 5 minutes

        chapters = []
        for i, start in enumerate(range(0, int(duration), chapter_length)):
            chapters.append(
                {
                    "start": start,
                    "title": f"Part {i + 1}",
                }
            )

        return chapters

    # ═══════════════════════════════════════════════════════════════
    # Utilities
    # ═══════════════════════════════════════════════════════════════

    async def get_stats(self) -> Dict[str, Any]:
        """Get video skill statistics."""
        total_size = 0
        total_duration = 0

        for metadata in self._index.values():
            if metadata.file_size_bytes:
                total_size += metadata.file_size_bytes
            total_duration += metadata.duration_seconds

        return {
            "videos_downloaded": len(self._index),
            "total_size_bytes": total_size,
            "total_size_gb": round(total_size / (1024**3), 2),
            "total_duration_seconds": total_duration,
            "total_duration_formatted": str(timedelta(seconds=total_duration)),
            "transcripts_available": sum(1 for v in self._index.values() if v.transcript_path),
            "storage_path": str(self.storage_path),
            "whisper_model": self.config.whisper_model,
            "invidious_enabled": self.config.use_invidious,
        }

    async def cleanup_storage(
        self,
        max_age_days: Optional[int] = None,
        max_size_gb: Optional[int] = None,
    ) -> int:
        """
        Clean up old videos to free storage.

        Args:
            max_age_days: Delete videos older than this
            max_size_gb: Delete oldest videos until under this size

        Returns:
            Number of videos deleted
        """
        deleted = 0

        if max_age_days:
            cutoff = datetime.utcnow() - timedelta(days=max_age_days)
            cutoff_str = cutoff.isoformat()

            for video_id, metadata in list(self._index.items()):
                if metadata.downloaded_at and metadata.downloaded_at < cutoff_str:
                    await self.delete_video(video_id)
                    deleted += 1

        if max_size_gb:
            max_bytes = max_size_gb * (1024**3)

            # Get videos sorted by age (oldest first)
            videos = sorted(
                self._index.items(),
                key=lambda x: x[1].downloaded_at or "",
            )

            total_size = sum(v.file_size_bytes or 0 for _, v in videos)

            for video_id, metadata in videos:
                if total_size <= max_bytes:
                    break

                total_size -= metadata.file_size_bytes or 0
                await self.delete_video(video_id)
                deleted += 1

        return deleted


# Skill registration
def register(agent, config):
    """Register the video skill."""
    return VideoSkill(agent, config)


__all__ = ["VideoSkill", "VideoConfig", "VideoMetadata", "register"]
