"""
Data Validation Utilities for AniWorld.

This module provides Pydantic validators and business rule validation
utilities for ensuring data integrity across the application.
"""

import re
import warnings
from pathlib import Path
from typing import Any, Dict, List, Optional


class ValidationError(Exception):
    """Custom validation error."""

    pass


class ValidatorMixin:
    """Mixin class providing common validation utilities."""

    @staticmethod
    def validate_password_strength(password: str) -> str:
        """
        Validate password meets security requirements.

        Args:
            password: Password to validate

        Returns:
            Validated password

        Raises:
            ValueError: If password doesn't meet requirements
        """
        if len(password) < 8:
            raise ValueError("Password must be at least 8 characters long")

        if not re.search(r"[A-Z]", password):
            raise ValueError(
                "Password must contain at least one uppercase letter"
            )

        if not re.search(r"[a-z]", password):
            raise ValueError(
                "Password must contain at least one lowercase letter"
            )

        if not re.search(r"[0-9]", password):
            raise ValueError("Password must contain at least one digit")

        if not re.search(r"[!@#$%^&*(),.?\":{}|<>]", password):
            raise ValueError(
                "Password must contain at least one special character"
            )

        return password

    @staticmethod
    def validate_file_path(path: str, must_exist: bool = False) -> str:
        """
        Validate file path.

        Args:
            path: File path to validate
            must_exist: Whether the path must exist

        Returns:
            Validated path

        Raises:
            ValueError: If path is invalid
        """
        if not path or not isinstance(path, str):
            raise ValueError("Path must be a non-empty string")

        # Check for path traversal attempts
        if ".." in path or path.startswith("/"):
            raise ValueError("Invalid path: path traversal not allowed")

        path_obj = Path(path)

        if must_exist and not path_obj.exists():
            raise ValueError(f"Path does not exist: {path}")

        return path

    @staticmethod
    def validate_url(url: str) -> str:
        """
        Validate URL format.

        Args:
            url: URL to validate

        Returns:
            Validated URL

        Raises:
            ValueError: If URL is invalid
        """
        if not url or not isinstance(url, str):
            raise ValueError("URL must be a non-empty string")

        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}\.?|"
            r"localhost|"  # localhost
            r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"  # IP address
            r"(?::\d+)?"  # optional port
            r"(?:/?|[/?]\S+)$",
            re.IGNORECASE,
        )

        if not url_pattern.match(url):
            raise ValueError(f"Invalid URL format: {url}")

        return url

    @staticmethod
    def validate_email(email: str) -> str:
        """
        Validate email address format.

        Args:
            email: Email to validate

        Returns:
            Validated email

        Raises:
            ValueError: If email is invalid
        """
        if not email or not isinstance(email, str):
            raise ValueError("Email must be a non-empty string")

        email_pattern = re.compile(
            r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
        )

        if not email_pattern.match(email):
            raise ValueError(f"Invalid email format: {email}")

        return email

    @staticmethod
    def validate_port(port: int) -> int:
        """
        Validate port number.

        Args:
            port: Port number to validate

        Returns:
            Validated port

        Raises:
            ValueError: If port is invalid
        """
        if not isinstance(port, int):
            raise ValueError("Port must be an integer")

        if port < 1 or port > 65535:
            raise ValueError("Port must be between 1 and 65535")

        return port

    @staticmethod
    def validate_positive_integer(value: int, name: str = "Value") -> int:
        """
        Validate positive integer.

        Args:
            value: Value to validate
            name: Name for error messages

        Returns:
            Validated value

        Raises:
            ValueError: If value is invalid
        """
        if not isinstance(value, int):
            raise ValueError(f"{name} must be an integer")

        if value <= 0:
            raise ValueError(f"{name} must be positive")

        return value

    @staticmethod
    def validate_non_negative_integer(value: int, name: str = "Value") -> int:
        """
        Validate non-negative integer.

        Args:
            value: Value to validate
            name: Name for error messages

        Returns:
            Validated value

        Raises:
            ValueError: If value is invalid
        """
        if not isinstance(value, int):
            raise ValueError(f"{name} must be an integer")

        if value < 0:
            raise ValueError(f"{name} cannot be negative")

        return value

    @staticmethod
    def validate_string_length(
        value: str, min_length: int = 0, max_length: Optional[int] = None, name: str = "Value"
    ) -> str:
        """
        Validate string length.

        Args:
            value: String to validate
            min_length: Minimum length
            max_length: Maximum length (None for no limit)
            name: Name for error messages

        Returns:
            Validated string

        Raises:
            ValueError: If string length is invalid
        """
        if not isinstance(value, str):
            raise ValueError(f"{name} must be a string")

        if len(value) < min_length:
            raise ValueError(
                f"{name} must be at least {min_length} characters long"
            )

        if max_length is not None and len(value) > max_length:
            raise ValueError(
                f"{name} must be at most {max_length} characters long"
            )

        return value

    @staticmethod
    def validate_choice(value: Any, choices: List[Any], name: str = "Value") -> Any:
        """
        Validate value is in allowed choices.

        Args:
            value: Value to validate
            choices: List of allowed values
            name: Name for error messages

        Returns:
            Validated value

        Raises:
            ValueError: If value not in choices
        """
        if value not in choices:
            raise ValueError(f"{name} must be one of: {', '.join(map(str, choices))}")

        return value

    @staticmethod
    def validate_dict_keys(
        data: Dict[str, Any], required_keys: List[str], name: str = "Data"
    ) -> Dict[str, Any]:
        """
        Validate dictionary contains required keys.

        Args:
            data: Dictionary to validate
            required_keys: List of required keys
            name: Name for error messages

        Returns:
            Validated dictionary

        Raises:
            ValueError: If required keys are missing
        """
        if not isinstance(data, dict):
            raise ValueError(f"{name} must be a dictionary")

        missing_keys = [key for key in required_keys if key not in data]
        if missing_keys:
            raise ValueError(
                f"{name} missing required keys: {', '.join(missing_keys)}"
            )

        return data


def validate_episode_range(start: int, end: int) -> tuple[int, int]:
    """
    Validate episode range.

    Args:
        start: Start episode number
        end: End episode number

    Returns:
        Tuple of (start, end)

    Raises:
        ValueError: If range is invalid
    """
    if start < 1:
        raise ValueError("Start episode must be at least 1")

    if end < start:
        raise ValueError("End episode must be greater than or equal to start")

    if end - start > 1000:
        raise ValueError("Episode range too large (max 1000 episodes)")

    return start, end


def validate_download_quality(quality: str) -> str:
    """
    Validate download quality setting.

    Args:
        quality: Quality setting

    Returns:
        Validated quality

    Raises:
        ValueError: If quality is invalid
    """
    valid_qualities = ["360p", "480p", "720p", "1080p", "best", "worst"]
    if quality not in valid_qualities:
        raise ValueError(
            f"Invalid quality: {quality}. Must be one of: {', '.join(valid_qualities)}"
        )
    return quality


def validate_language(language: str) -> str:
    """
    Validate language code.

    Args:
        language: Language code

    Returns:
        Validated language

    Raises:
        ValueError: If language is invalid
    """
    valid_languages = ["ger-sub", "ger-dub", "eng-sub", "eng-dub", "jpn"]
    if language not in valid_languages:
        raise ValueError(
            f"Invalid language: {language}. Must be one of: {', '.join(valid_languages)}"
        )
    return language


def validate_download_priority(priority: int) -> int:
    """
    Validate download priority.

    Args:
        priority: Priority value

    Returns:
        Validated priority

    Raises:
        ValueError: If priority is invalid
    """
    if priority < 0 or priority > 10:
        raise ValueError("Priority must be between 0 and 10")
    return priority


def validate_anime_url(url: str) -> str:
    """
    Validate anime URL format.

    Args:
        url: Anime URL

    Returns:
        Validated URL

    Raises:
        ValueError: If URL is invalid
    """
    if not url:
        raise ValueError("URL cannot be empty")

    # Check if it's a valid aniworld.to URL
    if "aniworld.to" not in url and "s.to" not in url:
        raise ValueError("URL must be from aniworld.to or s.to")

    # Basic URL validation
    ValidatorMixin.validate_url(url)

    return url


def validate_series_name(name: str) -> str:
    """
    Validate series name.

    Args:
        name: Series name

    Returns:
        Validated name

    Raises:
        ValueError: If name is invalid
    """
    if not name or not name.strip():
        raise ValueError("Series name cannot be empty")

    if len(name) > 200:
        raise ValueError("Series name too long (max 200 characters)")

    # Check for invalid characters
    invalid_chars = ['<', '>', ':', '"', '/', '\\', '|', '?', '*']
    for char in invalid_chars:
        if char in name:
            raise ValueError(
                f"Series name contains invalid character: {char}"
            )

    return name.strip()


def validate_series_key(key: str) -> str:
    """
    Validate series key format.

    Series keys are unique, provider-assigned, URL-safe identifiers.
    They should be lowercase, use hyphens for word separation, and contain
    only alphanumeric characters and hyphens.

    Valid examples:
        - "attack-on-titan"
        - "one-piece"
        - "naruto"

    Invalid examples:
        - "Attack On Titan" (uppercase, spaces)
        - "attack_on_titan" (underscores)
        - "attack on titan" (spaces)
        - "" (empty)

    Args:
        key: Series key to validate

    Returns:
        Validated key (trimmed)

    Raises:
        ValueError: If key is invalid
    """
    if not key or not isinstance(key, str):
        raise ValueError("Series key must be a non-empty string")

    key = key.strip()

    if not key:
        raise ValueError("Series key cannot be empty")

    if len(key) > 255:
        raise ValueError("Series key must be 255 characters or less")

    # Key must be lowercase, alphanumeric with hyphens only
    # Pattern: starts with letter/number, can contain letters, numbers, hyphens
    # Cannot start or end with hyphen, no consecutive hyphens
    if not re.match(r'^[a-z0-9]+(?:-[a-z0-9]+)*$', key):
        raise ValueError(
            "Series key must be lowercase, URL-safe, and use hyphens "
            "for word separation (e.g., 'attack-on-titan'). "
            "No spaces, underscores, or uppercase letters allowed."
        )

    return key


def validate_series_key_or_folder(
    identifier: str, allow_folder: bool = True
) -> tuple[str, bool]:
    """
    Validate an identifier that could be either a series key or folder.

    .. deprecated:: 2.0.0
        Folder-based identification is deprecated. Use series `key` only.
        This function will require key format only in v3.0.0.

    This function provides backward compatibility during the transition
    from folder-based to key-based identification.

    Args:
        identifier: The identifier to validate (key or folder)
        allow_folder: Whether to allow folder-style identifiers (default: True)

    Returns:
        Tuple of (validated_identifier, is_key) where is_key indicates
        whether the identifier is a valid key format.

    Raises:
        ValueError: If identifier is empty or invalid
    """
    if not identifier or not isinstance(identifier, str):
        raise ValueError("Identifier must be a non-empty string")

    identifier = identifier.strip()

    if not identifier:
        raise ValueError("Identifier cannot be empty")

    # Try to validate as key first
    try:
        validate_series_key(identifier)
        return identifier, True
    except ValueError:
        pass

    # If not a valid key, check if folder format is allowed
    if not allow_folder:
        raise ValueError(
            f"Invalid series key format: '{identifier}'. "
            "Keys must be lowercase with hyphens (e.g., 'attack-on-titan')."
        )

    # Emit deprecation warning for folder-based identification
    warnings.warn(
        f"Folder-based identification for '{identifier}' is deprecated. "
        "Use series key (lowercase with hyphens) instead. "
        "Folder-based identification will be removed in v3.0.0.",
        DeprecationWarning,
        stacklevel=2
    )

    # Validate as folder (more permissive)
    if len(identifier) > 1000:
        raise ValueError("Identifier too long (max 1000 characters)")

    return identifier, False


def validate_backup_name(name: str) -> str:
    """
    Validate backup file name.

    Args:
        name: Backup name

    Returns:
        Validated name

    Raises:
        ValueError: If name is invalid
    """
    if not name or not name.strip():
        raise ValueError("Backup name cannot be empty")

    # Must be a valid filename
    if not re.match(r"^[a-zA-Z0-9_\-\.]+$", name):
        raise ValueError(
            "Backup name can only contain letters, numbers, underscores, hyphens, and dots"
        )

    if not name.endswith(".json"):
        raise ValueError("Backup name must end with .json")

    return name


def validate_config_data(data: Dict[str, Any]) -> Dict[str, Any]:
    """
    Validate configuration data structure.

    Args:
        data: Configuration data

    Returns:
        Validated data

    Raises:
        ValueError: If data is invalid
    """
    required_keys = ["download_directory", "concurrent_downloads"]
    ValidatorMixin.validate_dict_keys(data, required_keys, "Configuration")

    # Validate download directory
    if not isinstance(data["download_directory"], str):
        raise ValueError("download_directory must be a string")

    # Validate concurrent downloads
    concurrent = data["concurrent_downloads"]
    if not isinstance(concurrent, int) or concurrent < 1 or concurrent > 10:
        raise ValueError("concurrent_downloads must be between 1 and 10")

    # Validate quality if present
    if "quality" in data:
        validate_download_quality(data["quality"])

    # Validate language if present
    if "language" in data:
        validate_language(data["language"])

    return data


def sanitize_filename(filename: str) -> str:
    """
    Sanitize filename for safe filesystem use.

    Args:
        filename: Original filename

    Returns:
        Sanitized filename
    """
    # Remove or replace invalid characters
    invalid_chars = ['<', '>', ':', '"', '/', '\\', '|', '?', '*']
    for char in invalid_chars:
        filename = filename.replace(char, '_')

    # Remove leading/trailing spaces and dots
    filename = filename.strip('. ')

    # Ensure not empty
    if not filename:
        filename = "unnamed"

    # Limit length
    if len(filename) > 255:
        name, ext = filename.rsplit('.', 1) if '.' in filename else (filename, '')
        max_name_len = 255 - len(ext) - 1 if ext else 255
        filename = name[:max_name_len] + ('.' + ext if ext else '')

    return filename


def validate_jwt_token(token: str) -> str:
    """
    Validate JWT token format.

    Args:
        token: JWT token

    Returns:
        Validated token

    Raises:
        ValueError: If token format is invalid
    """
    if not token or not isinstance(token, str):
        raise ValueError("Token must be a non-empty string")

    # JWT tokens have 3 parts separated by dots
    parts = token.split(".")
    if len(parts) != 3:
        raise ValueError("Invalid JWT token format")

    # Each part should be base64url encoded (alphanumeric + - and _)
    for part in parts:
        if not re.match(r"^[A-Za-z0-9_-]+$", part):
            raise ValueError("Invalid JWT token encoding")

    return token


def validate_ip_address(ip: str) -> str:
    """
    Validate IP address format.

    Args:
        ip: IP address

    Returns:
        Validated IP address

    Raises:
        ValueError: If IP is invalid
    """
    if not ip or not isinstance(ip, str):
        raise ValueError("IP address must be a non-empty string")

    # IPv4 pattern
    ipv4_pattern = re.compile(
        r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}"
        r"(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
    )

    # IPv6 pattern (simplified)
    ipv6_pattern = re.compile(
        r"^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$"
    )

    if not ipv4_pattern.match(ip) and not ipv6_pattern.match(ip):
        raise ValueError(f"Invalid IP address format: {ip}")

    return ip


def validate_websocket_message(message: Dict[str, Any]) -> Dict[str, Any]:
    """
    Validate WebSocket message structure.

    Args:
        message: WebSocket message

    Returns:
        Validated message

    Raises:
        ValueError: If message structure is invalid
    """
    required_keys = ["type"]
    ValidatorMixin.validate_dict_keys(message, required_keys, "WebSocket message")

    valid_types = [
        "download_progress",
        "download_complete",
        "download_failed",
        "queue_update",
        "error",
        "system_message",
    ]

    if message["type"] not in valid_types:
        raise ValueError(
            f"Invalid message type. Must be one of: {', '.join(valid_types)}"
        )

    return message


def validate_sql_injection(value: str, param_name: str = "parameter") -> None:
    """
    Validate input for SQL injection patterns.
    
    Checks for dangerous patterns that could be used for SQL injection attacks.
    This is a defense-in-depth measure; proper parameterized queries should
    be the primary defense.
    
    Args:
        value: The input string to validate
        param_name: Name of the parameter being validated (for error messages)
    
    Raises:
        ValueError: If dangerous patterns are detected
    
    Example:
        >>> validate_sql_injection("normal_value", "filter")
        >>> validate_sql_injection("value; DROP TABLE", "filter")  # Raises ValueError
    """
    if not value:
        return
    
    # Comprehensive list of dangerous SQL patterns
    dangerous_patterns = [
        ";", "--", "/*", "*/",  # SQL comment/statement separators
        "xp_", "sp_",  # SQL Server extended/stored procedures
        "exec", "execute",  # SQL execution commands
        "union", "select", "insert", "update", "delete", "drop",  # SQL DML/DDL
        "create", "alter", "truncate",  # SQL DDL
        "sleep", "waitfor", "benchmark",  # Time-based attacks
        " or ", "||", " and ", "&&"  # Logical operators for condition manipulation
    ]
    
    lower_value = value.lower()
    for pattern in dangerous_patterns:
        if pattern in lower_value:
            raise ValueError(
                f"Invalid {param_name}: dangerous pattern '{pattern}' detected"
            )


def validate_search_query(query: str) -> str:
    """
    Validate and normalize a search query string.
    
    Strips whitespace, normalizes spacing, and checks for SQL injection patterns.
    
    Args:
        query: The search query to validate
    
    Returns:
        Normalized and validated query string
    
    Raises:
        ValueError: If the query contains dangerous patterns
    
    Example:
        >>> validate_search_query("  Attack   on  Titan  ")
        'Attack on Titan'
        >>> validate_search_query("anime' OR '1'='1")  # Raises ValueError
    """
    if not query:
        raise ValueError("Search query cannot be empty")
    
    # Strip and normalize whitespace
    normalized = " ".join(query.strip().split())
    
    # Check for SQL injection patterns
    try:
        validate_sql_injection(normalized, "search query")
    except ValueError as e:
        raise ValueError(f"Invalid search query: {str(e)}")
    
    return normalized


def validate_filter_value(filter_value: str, allowed_filters: List[str]) -> None:
    """
    Validate a filter parameter against allowed values and dangerous patterns.
    
    Args:
        filter_value: The filter value to validate
        allowed_filters: List of allowed filter values
    
    Raises:
        ValueError: If filter contains dangerous patterns or is not in allowed list
    
    Example:
        >>> validate_filter_value("no_episodes", ["no_episodes", "complete"])
        >>> validate_filter_value("invalid", ["no_episodes"])  # Raises ValueError
    """
    if not filter_value:
        return
    
    # Check for SQL injection patterns first
    validate_sql_injection(filter_value, "filter")
    
    # Then check if value is in allowed list
    if filter_value not in allowed_filters:
        allowed = ", ".join(allowed_filters)
        raise ValueError(
            f"Invalid filter value '{filter_value}'. Allowed: {allowed}"
        )
