"""Pexels service for fetching and validating stock footage."""

import asyncio
import logging
from pathlib import Path
from typing import TYPE_CHECKING

import aiofiles
import httpx
from tenacity import (
    retry,
    retry_if_exception_type,
    stop_after_attempt,
    wait_exponential,
)

from video_composer.models import PexelsVideo, PexelsVideoFile, StockFootage

if TYPE_CHECKING:
    from video_composer.config import Settings
    from video_composer.services.openai_service import OpenAIService

logger = logging.getLogger(__name__)


class PexelsServiceError(Exception):
    """Base exception for Pexels service errors."""

    pass


class VideoNotFoundError(PexelsServiceError):
    """No suitable video found for the keyword."""

    pass


class VideoValidationError(PexelsServiceError):
    """Video failed validation."""

    pass


class PexelsService:
    """Service for fetching stock footage from Pexels API."""

    BASE_URL = "https://api.pexels.com/videos"

    def __init__(
        self,
        settings: "Settings",
        openai_service: "OpenAIService | None" = None,
    ) -> None:
        """Initialize the Pexels service.

        Args:
            settings: Application settings containing API key and configuration.
            openai_service: Optional OpenAI service for video validation.
        """
        self.settings = settings
        self.openai_service = openai_service
        self._http_client: httpx.AsyncClient | None = None

    async def __aenter__(self) -> "PexelsService":
        """Async context manager entry."""
        self._http_client = httpx.AsyncClient(
            timeout=60.0,
            headers={"Authorization": self.settings.pexels_api_key},
        )
        return self

    async def __aexit__(self, exc_type: type | None, exc_val: Exception | None, exc_tb: object) -> None:
        """Async context manager exit."""
        if self._http_client:
            await self._http_client.aclose()
            self._http_client = None

    @property
    def http_client(self) -> httpx.AsyncClient:
        """Get the HTTP client, ensuring it's initialized."""
        if self._http_client is None:
            self._http_client = httpx.AsyncClient(
                timeout=60.0,
                headers={"Authorization": self.settings.pexels_api_key},
            )
        return self._http_client

    @retry(
        retry=retry_if_exception_type((httpx.TimeoutException, httpx.NetworkError)),
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=1, min=1, max=10),
        reraise=True,
    )
    async def search_videos(
        self,
        keyword: str,
        per_page: int | None = None,
        page: int = 1,
    ) -> list[PexelsVideo]:
        """Search for videos on Pexels.

        Args:
            keyword: Search keyword/query.
            per_page: Number of results per page (defaults to settings value).
            page: Page number for pagination.

        Returns:
            List of PexelsVideo objects.

        Raises:
            PexelsServiceError: If the API request fails.
        """
        if per_page is None:
            per_page = self.settings.pexels_videos_per_query

        logger.info(f"Searching Pexels for: '{keyword}' (page {page}, per_page {per_page})")

        try:
            response = await self.http_client.get(
                f"{self.BASE_URL}/search",
                params={
                    "query": keyword,
                    "per_page": per_page,
                    "page": page,
                    "orientation": self.settings.pexels_orientation,
                    "size": "large",
                },
            )
            response.raise_for_status()
            data = response.json()

            videos = []
            for video_data in data.get("videos", []):
                # Filter by minimum duration
                if video_data.get("duration", 0) < self.settings.pexels_min_duration:
                    continue

                # Parse video files
                video_files = [
                    PexelsVideoFile(
                        id=f.get("id", 0),
                        quality=f.get("quality", ""),
                        file_type=f.get("file_type", ""),
                        width=f.get("width", 0),
                        height=f.get("height", 0),
                        link=f.get("link", ""),
                    )
                    for f in video_data.get("video_files", [])
                ]

                # Parse video pictures (preview images)
                video_pictures = [
                    p.get("picture", "")
                    for p in video_data.get("video_pictures", [])
                    if p.get("picture")
                ]

                videos.append(
                    PexelsVideo(
                        id=video_data.get("id", 0),
                        width=video_data.get("width", 0),
                        height=video_data.get("height", 0),
                        duration=video_data.get("duration", 0),
                        url=video_data.get("url", ""),
                        video_files=video_files,
                        video_pictures=video_pictures,
                    )
                )

            logger.info(f"Found {len(videos)} suitable videos for '{keyword}'")
            return videos

        except httpx.HTTPStatusError as e:
            logger.error(f"Pexels API error: {e.response.status_code} - {e.response.text}")
            raise PexelsServiceError(f"Pexels API error: {e.response.status_code}") from e
        except Exception as e:
            logger.error(f"Failed to search Pexels: {e}")
            raise PexelsServiceError(f"Failed to search videos: {e}") from e

    @retry(
        retry=retry_if_exception_type((httpx.TimeoutException, httpx.NetworkError)),
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=1, min=1, max=10),
        reraise=True,
    )
    async def download_video(
        self,
        video: PexelsVideo,
        output_path: Path,
    ) -> Path:
        """Download a video file from Pexels.

        Args:
            video: PexelsVideo object to download.
            output_path: Path where the video will be saved.

        Returns:
            Path to the downloaded video file.

        Raises:
            PexelsServiceError: If download fails.
        """
        # Get the best video file
        video_file = video.get_best_file(
            target_width=self.settings.video_width,
            target_height=self.settings.video_height,
        )

        if not video_file:
            raise PexelsServiceError(f"No suitable video file found for video {video.id}")

        logger.info(
            f"Downloading video {video.id} ({video_file.width}x{video_file.height}, "
            f"{video_file.quality})"
        )

        try:
            # Ensure parent directory exists
            output_path.parent.mkdir(parents=True, exist_ok=True)

            # Download with streaming
            async with self.http_client.stream("GET", video_file.link) as response:
                response.raise_for_status()
                async with aiofiles.open(output_path, "wb") as f:
                    async for chunk in response.aiter_bytes(chunk_size=8192):
                        await f.write(chunk)

            logger.info(f"Video downloaded to {output_path}")
            return output_path

        except Exception as e:
            logger.error(f"Failed to download video: {e}")
            # Clean up partial download
            if output_path.exists():
                output_path.unlink()
            raise PexelsServiceError(f"Failed to download video: {e}") from e

    async def get_validated_footage(
        self,
        keyword: str,
        output_dir: Path,
        validation_context: str | None = None,
        max_attempts: int | None = None,
    ) -> StockFootage:
        """Get stock footage that has been validated to match the keyword.

        This method:
        1. Searches for videos matching the keyword
        2. Downloads each candidate
        3. Extracts a frame and validates with GPT-4 Vision
        4. Returns the first video that passes validation

        Args:
            keyword: Search keyword for the footage.
            output_dir: Directory to save downloaded videos.
            validation_context: Optional context for validation.
            max_attempts: Maximum videos to try before giving up.

        Returns:
            StockFootage object with validated video.

        Raises:
            VideoNotFoundError: If no valid video could be found.
        """
        if max_attempts is None:
            max_attempts = self.settings.max_retries

        logger.info(f"Getting validated footage for keyword: '{keyword}'")

        # Search for videos
        videos = await self.search_videos(keyword)
        if not videos:
            raise VideoNotFoundError(f"No videos found for keyword: '{keyword}'")

        # Import here to avoid circular dependency
        from video_composer.utils.video_processor import VideoProcessor

        for attempt, video in enumerate(videos[:max_attempts], 1):
            logger.info(f"Trying video {video.id} (attempt {attempt}/{max_attempts})")

            # Download the video
            video_path = output_dir / f"pexels_{video.id}.mp4"
            try:
                await self.download_video(video, video_path)
            except PexelsServiceError as e:
                logger.warning(f"Failed to download video {video.id}: {e}")
                continue

            # Skip validation if no OpenAI service configured
            if self.openai_service is None:
                logger.info("No OpenAI service configured, skipping validation")
                return StockFootage(
                    keyword=keyword,
                    video=video,
                    local_path=video_path,
                    validation_score=1.0,
                    is_validated=False,
                )

            # Extract a frame for validation
            try:
                frame_position = min(
                    video.duration * self.settings.validation_frame_position,
                    video.duration - 1,
                )
                # Ensure we're in the 20-30% range as specified
                frame_position = max(
                    video.duration * 0.2,
                    min(frame_position, video.duration * 0.3),
                )

                frame_path = output_dir / f"frame_{video.id}.jpg"
                VideoProcessor.extract_frame(video_path, frame_path, frame_position)

                # Validate with Vision
                is_valid, confidence, explanation = await self.openai_service.validate_video_frame(
                    frame_path,
                    keyword,
                    validation_context,
                )

                # Clean up frame
                if frame_path.exists():
                    frame_path.unlink()

                if is_valid and confidence >= 0.6:
                    logger.info(
                        f"Video {video.id} validated successfully "
                        f"(confidence: {confidence:.2f})"
                    )
                    return StockFootage(
                        keyword=keyword,
                        video=video,
                        local_path=video_path,
                        validation_score=confidence,
                        is_validated=True,
                    )
                else:
                    logger.info(
                        f"Video {video.id} failed validation: {explanation}"
                    )
                    # Clean up the video file
                    if video_path.exists():
                        video_path.unlink()

            except Exception as e:
                logger.warning(f"Error validating video {video.id}: {e}")
                # Clean up on error
                if video_path.exists():
                    video_path.unlink()
                continue

        raise VideoNotFoundError(
            f"No valid video found for keyword '{keyword}' after {max_attempts} attempts"
        )

    async def get_multiple_footage(
        self,
        keywords: list[str],
        output_dir: Path,
        validation_context: str | None = None,
    ) -> dict[str, StockFootage]:
        """Get validated footage for multiple keywords in parallel.

        Args:
            keywords: List of keywords to search for.
            output_dir: Directory to save downloaded videos.
            validation_context: Optional context for validation.

        Returns:
            Dictionary mapping keywords to their StockFootage.

        Raises:
            VideoNotFoundError: If any keyword fails to find valid footage.
        """
        tasks = [
            self.get_validated_footage(keyword, output_dir, validation_context)
            for keyword in keywords
        ]

        results = await asyncio.gather(*tasks, return_exceptions=True)

        footage_map: dict[str, StockFootage] = {}
        errors: list[str] = []

        for keyword, result in zip(keywords, results, strict=True):
            if isinstance(result, Exception):
                errors.append(f"'{keyword}': {result}")
            else:
                footage_map[keyword] = result

        if errors:
            error_msg = "Failed to get footage for keywords: " + "; ".join(errors)
            logger.error(error_msg)
            raise VideoNotFoundError(error_msg)

        return footage_map
