"""
Validation utilities for NFOGuard
Provides runtime validation and type checking for critical paths
"""
import re
from pathlib import Path
from typing import Any, Dict, List, Optional, Union, Callable, TypeVar, Type
from datetime import datetime

from utils.exceptions import ConfigurationError, NFOGuardException


T = TypeVar('T')


def validate_imdb_id(imdb_id: str) -> bool:
    """
    Validate IMDb ID format
    
    Args:
        imdb_id: IMDb ID to validate
        
    Returns:
        True if valid, False otherwise
    """
    if not imdb_id or not isinstance(imdb_id, str):
        return False
    
    # Must start with 'tt' followed by 7+ digits
    return bool(re.match(r'^tt\d{7,}$', imdb_id))


def validate_tmdb_id(tmdb_id: str) -> bool:
    """
    Validate TMDB ID format
    
    Args:
        tmdb_id: TMDB ID to validate
        
    Returns:
        True if valid, False otherwise
    """
    if not tmdb_id or not isinstance(tmdb_id, str):
        return False
    
    # Can be numeric or have tmdb- prefix
    if tmdb_id.startswith('tmdb-'):
        return tmdb_id[5:].isdigit()
    return tmdb_id.isdigit()


def validate_season_episode(season: int, episode: int) -> bool:
    """
    Validate season and episode numbers
    
    Args:
        season: Season number
        episode: Episode number
        
    Returns:
        True if valid, False otherwise
    """
    return (
        isinstance(season, int) and season >= 0 and
        isinstance(episode, int) and episode >= 1
    )


def validate_date_string(date_str: str) -> bool:
    """
    Validate date string format (ISO format)
    
    Args:
        date_str: Date string to validate
        
    Returns:
        True if valid ISO date, False otherwise
    """
    if not date_str or not isinstance(date_str, str):
        return False
    
    try:
        datetime.fromisoformat(date_str.replace('Z', '+00:00'))
        return True
    except ValueError:
        return False


def validate_path_exists(path: Union[str, Path]) -> bool:
    """
    Validate that a path exists
    
    Args:
        path: Path to validate
        
    Returns:
        True if path exists, False otherwise
    """
    try:
        return Path(path).exists()
    except (OSError, ValueError):
        return False


def validate_video_file(file_path: Union[str, Path]) -> bool:
    """
    Validate that a file is a video file
    
    Args:
        file_path: Path to validate
        
    Returns:
        True if valid video file, False otherwise
    """
    try:
        path = Path(file_path)
        video_extensions = {'.mkv', '.mp4', '.avi', '.m4v', '.mov', '.ts'}
        return path.is_file() and path.suffix.lower() in video_extensions
    except (OSError, ValueError):
        return False


def validate_webhook_payload(payload: Dict[str, Any], required_fields: List[str]) -> List[str]:
    """
    Validate webhook payload has required fields
    
    Args:
        payload: Webhook payload to validate
        required_fields: List of required field names
        
    Returns:
        List of missing field names (empty if all present)
    """
    missing_fields = []
    for field in required_fields:
        if field not in payload or payload[field] is None:
            missing_fields.append(field)
    return missing_fields


def validate_config_paths(paths: List[Union[str, Path]], path_type: str) -> None:
    """
    Validate configuration paths exist and are directories
    
    Args:
        paths: List of paths to validate
        path_type: Type of paths for error messages (e.g., "TV", "Movie")
        
    Raises:
        ConfigurationError: If any paths are invalid
    """
    invalid_paths = []
    
    for path in paths:
        try:
            path_obj = Path(path)
            if not path_obj.exists():
                invalid_paths.append(f"{path} (does not exist)")
            elif not path_obj.is_dir():
                invalid_paths.append(f"{path} (not a directory)")
        except (OSError, ValueError) as e:
            invalid_paths.append(f"{path} (invalid: {e})")
    
    if invalid_paths:
        raise ConfigurationError(
            f"{path_type.lower()}_paths",
            f"Invalid {path_type} paths found",
            {"invalid_paths": invalid_paths}
        )


def require_type(value: Any, expected_type: Type[T], name: str) -> T:
    """
    Require value to be of specific type
    
    Args:
        value: Value to check
        expected_type: Expected type
        name: Name of the value for error messages
        
    Returns:
        The value if it matches the type
        
    Raises:
        TypeError: If value is not of expected type
    """
    if not isinstance(value, expected_type):
        raise TypeError(
            f"{name} must be {expected_type.__name__}, got {type(value).__name__}"
        )
    return value


def require_non_empty(value: Optional[str], name: str) -> str:
    """
    Require string value to be non-empty
    
    Args:
        value: String value to check
        name: Name of the value for error messages
        
    Returns:
        The value if it's non-empty
        
    Raises:
        ValueError: If value is None or empty
    """
    if not value:
        raise ValueError(f"{name} cannot be None or empty")
    return value


def validate_and_clean_imdb_id(imdb_id: Optional[str]) -> Optional[str]:
    """
    Validate and clean IMDb ID
    
    Args:
        imdb_id: IMDb ID to validate and clean
        
    Returns:
        Cleaned IMDb ID or None if invalid
    """
    if not imdb_id:
        return None
    
    # Clean the ID
    cleaned = imdb_id.strip().lower()
    
    # Remove common prefixes
    if cleaned.startswith('imdb-'):
        cleaned = cleaned[5:]
    elif cleaned.startswith('imdb_'):
        cleaned = cleaned[5:]
    
    # Ensure it starts with 'tt'
    if not cleaned.startswith('tt'):
        cleaned = f'tt{cleaned}'
    
    # Validate format
    if validate_imdb_id(cleaned):
        return cleaned
    
    return None


def create_validator(validation_func: Callable[[Any], bool], error_message: str) -> Callable:
    """
    Create a validator decorator
    
    Args:
        validation_func: Function that returns True if value is valid
        error_message: Error message to raise if validation fails
        
    Returns:
        Decorator function
    """
    def decorator(func: Callable) -> Callable:
        def wrapper(*args, **kwargs):
            # Apply validation to first argument
            if args and not validation_func(args[0]):
                raise ValueError(error_message.format(args[0]))
            return func(*args, **kwargs)
        return wrapper
    return decorator


# Common validators
validate_imdb_required = create_validator(
    lambda x: validate_imdb_id(x),
    "Invalid IMDb ID format: {}"
)

validate_path_required = create_validator(
    lambda x: validate_path_exists(x),
    "Path does not exist: {}"
)

validate_date_required = create_validator(
    lambda x: validate_date_string(x),
    "Invalid date format: {}"
)


class ValidationError(NFOGuardException):
    """Raised when validation fails"""
    
    def __init__(self, field_name: str, value: Any, reason: str):
        details = {
            "field_name": field_name,
            "value": str(value),
            "reason": reason
        }
        message = f"Validation failed for {field_name}: {reason}"
        super().__init__(message, details)


def validate_episode_file_pattern(filename: str) -> Optional[Dict[str, int]]:
    """
    Validate and extract episode information from filename
    
    Args:
        filename: Filename to validate
        
    Returns:
        Dictionary with season and episode if valid, None otherwise
    """
    # Episode patterns
    patterns = [
        r'[sS](\d{1,2})[eE](\d{1,3})',  # S01E01
        r'(\d{1,2})x(\d{1,3})',         # 1x01
        r'[sS](\d{1,2})\.?[eE](\d{1,3})', # S01.E01
    ]
    
    for pattern in patterns:
        match = re.search(pattern, filename)
        if match:
            season = int(match.group(1))
            episode = int(match.group(2))
            if validate_season_episode(season, episode):
                return {"season": season, "episode": episode}
    
    return None


def sanitize_filename(filename: str) -> str:
    """
    Sanitize filename by removing invalid characters
    
    Args:
        filename: Filename to sanitize
        
    Returns:
        Sanitized filename
    """
    # Remove invalid characters for most filesystems
    invalid_chars = r'<>:"/\|?*'
    for char in invalid_chars:
        filename = filename.replace(char, '_')
    
    # Remove leading/trailing dots and spaces
    filename = filename.strip('. ')
    
    # Ensure it's not empty
    if not filename:
        filename = 'unnamed'
    
    return filename


def validate_url_format(url: str) -> bool:
    """
    Validate URL format
    
    Args:
        url: URL to validate
        
    Returns:
        True if valid URL format, False otherwise
    """
    if not url or not isinstance(url, str):
        return False
    
    # Basic URL validation
    url_pattern = re.compile(
        r'^https?://'  # http:// or https://
        r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|'  # domain...
        r'localhost|'  # localhost...
        r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'  # ...or ip
        r'(?::\d+)?'  # optional port
        r'(?:/?|[/?]\S+)$', re.IGNORECASE)
    
    return bool(url_pattern.match(url))