"""Anime Pydantic models for the Aniworld web application.

This module defines request/response models used by the anime API
and services. Models are focused on serialization, validation, 
and OpenAPI documentation.

Note on identifiers:
- key: Primary identifier (provider-assigned, URL-safe, e.g., 'attack-on-titan')
- folder: Filesystem folder name (metadata only, e.g., 'Attack on Titan (2013)')
"""
from __future__ import annotations

import re
from datetime import datetime
from typing import List, Optional

from pydantic import BaseModel, Field, HttpUrl, field_validator

# Regex pattern for valid series keys (URL-safe, lowercase with hyphens)
KEY_PATTERN = re.compile(r'^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$')


class EpisodeInfo(BaseModel):
    """Information about a single episode."""

    episode_number: int = Field(..., ge=1, description="Episode index (1-based)")
    title: Optional[str] = Field(None, description="Optional episode title")
    aired_at: Optional[datetime] = Field(None, description="Air date/time if known")
    duration_seconds: Optional[int] = Field(None, ge=0, description="Duration in seconds")
    available: bool = Field(True, description="Whether the episode is available for download")
    sources: List[HttpUrl] = Field(default_factory=list, description="List of known streaming/download source URLs")


class MissingEpisodeInfo(BaseModel):
    """Represents a gap in the episode list for a series."""

    from_episode: int = Field(..., ge=1, description="Starting missing episode number")
    to_episode: int = Field(..., ge=1, description="Ending missing episode number (inclusive)")
    reason: Optional[str] = Field(None, description="Optional explanation why episodes are missing")

    @property
    def count(self) -> int:
        """Number of missing episodes in the range."""
        return max(0, self.to_episode - self.from_episode + 1)


class AnimeSeriesResponse(BaseModel):
    """Response model for a series with metadata and episodes.
    
    Note on identifiers:
    - key: Primary identifier (provider-assigned, URL-safe, e.g., 'attack-on-titan')
      This is the unique key used for all lookups and operations.
    - folder: Filesystem folder name (metadata only, e.g., 'Attack on Titan (2013)')
      Used only for display and filesystem operations.
    """

    key: str = Field(
        ...,
        description=(
            "Series key (primary identifier) - provider-assigned URL-safe "
            "key (e.g., 'attack-on-titan'). Used for lookups/identification."
        )
    )
    title: str = Field(..., description="Series title")
    folder: Optional[str] = Field(
        None,
        description=(
            "Series folder name on disk (metadata only) "
            "(e.g., 'Attack on Titan (2013)'). For display/filesystem ops only."
        )
    )
    alt_titles: List[str] = Field(default_factory=list, description="Alternative titles")
    episodes: List[EpisodeInfo] = Field(default_factory=list, description="Known episodes information")
    missing_episodes: List[MissingEpisodeInfo] = Field(default_factory=list, description="Detected missing episode ranges")
    thumbnail: Optional[HttpUrl] = Field(None, description="Optional thumbnail image URL")

    @field_validator('key', mode='before')
    @classmethod
    def normalize_key(cls, v: str) -> str:
        """Normalize key to lowercase."""
        if isinstance(v, str):
            return v.lower().strip()
        return v


class AnimeMetadataUpdate(BaseModel):
    """Request model for updating anime metadata (key, tmdb_id, tvdb_id)."""

    key: Optional[str] = Field(None, description="New series key (URL-safe, lowercase)")
    tmdb_id: Optional[int] = Field(None, ge=1, description="TMDB ID (positive integer)")
    tvdb_id: Optional[int] = Field(None, ge=1, description="TVDB ID (positive integer)")

    @field_validator('key', mode='before')
    @classmethod
    def validate_key_format(cls, v: Optional[str]) -> Optional[str]:
        """Validate key is URL-safe lowercase with hyphens only."""
        if v is None:
            return v
        v = v.strip().lower()
        if not v:
            raise ValueError("Key cannot be empty")
        if not KEY_PATTERN.match(v):
            raise ValueError(
                "Key must contain only lowercase letters, numbers, and hyphens. "
                "Cannot start or end with a hyphen."
            )
        return v


class SearchRequest(BaseModel):
    """Request payload for searching series."""

    query: str = Field(..., min_length=1, description="Search query string")
    limit: int = Field(10, ge=1, le=100, description="Maximum number of results")
    include_adult: bool = Field(False, description="Include adult content in results")


class SearchResult(BaseModel):
    """Search result item for a series discovery endpoint.
    
    Note on identifiers:
    - key: Primary identifier (provider-assigned, URL-safe, e.g., 'attack-on-titan')
      This is the unique key used for all lookups and operations.
    - folder: Filesystem folder name (metadata only, e.g., 'Attack on Titan (2013)')
      Used only for display and filesystem operations.
    """

    key: str = Field(
        ...,
        description=(
            "Series key (primary identifier) - provider-assigned URL-safe "
            "key (e.g., 'attack-on-titan'). Used for lookups/identification."
        )
    )
    title: str = Field(..., description="Series title")
    folder: Optional[str] = Field(
        None,
        description=(
            "Series folder name on disk (metadata only) "
            "(e.g., 'Attack on Titan (2013)'). For display/filesystem ops only."
        )
    )
    snippet: Optional[str] = Field(None, description="Short description or snippet")
    thumbnail: Optional[HttpUrl] = Field(None, description="Thumbnail image URL")
    score: Optional[float] = Field(None, ge=0.0, le=1.0, description="Search relevance score (0-1)")

    @field_validator('key', mode='before')
    @classmethod
    def normalize_key(cls, v: str) -> str:
        """Normalize key to lowercase."""
        if isinstance(v, str):
            return v.lower().strip()
        return v
