"""
File Upload Security Utilities for ThrillWiki.

This module provides comprehensive file validation and security checks for
file uploads, including:
- File type validation (MIME type and magic number verification)
- File size validation
- Filename sanitization
- Image-specific validation

Security Note:
    Always validate uploaded files before saving them. Never trust user-provided
    file extensions or Content-Type headers alone.

Usage:
    from apps.core.utils.file_scanner import validate_image_upload, sanitize_filename

    # In view
    try:
        validate_image_upload(uploaded_file)
        # Safe to save file
    except FileValidationError as e:
        return JsonResponse({'error': str(e)}, status=400)

    # Sanitize filename
    safe_name = sanitize_filename(uploaded_file.name)
"""

import os
import re
import uuid
from io import BytesIO

from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import UploadedFile


class FileValidationError(ValidationError):
    """Custom exception for file validation errors."""

    pass


# =============================================================================
# Image Magic Number Signatures
# =============================================================================

# Magic number signatures for common image formats
# Format: (magic_bytes, offset, description)
IMAGE_SIGNATURES = {
    "jpeg": [
        (b"\xff\xd8\xff\xe0", 0, "JPEG (JFIF)"),
        (b"\xff\xd8\xff\xe1", 0, "JPEG (EXIF)"),
        (b"\xff\xd8\xff\xe2", 0, "JPEG (ICC)"),
        (b"\xff\xd8\xff\xe3", 0, "JPEG (Samsung)"),
        (b"\xff\xd8\xff\xe8", 0, "JPEG (SPIFF)"),
        (b"\xff\xd8\xff\xdb", 0, "JPEG (Raw)"),
    ],
    "png": [
        (b"\x89PNG\r\n\x1a\n", 0, "PNG"),
    ],
    "gif": [
        (b"GIF87a", 0, "GIF87a"),
        (b"GIF89a", 0, "GIF89a"),
    ],
    "webp": [
        (b"RIFF", 0, "RIFF"),  # WebP starts with RIFF header
    ],
    "bmp": [
        (b"BM", 0, "BMP"),
    ],
}

# All allowed MIME types
ALLOWED_IMAGE_MIME_TYPES: set[str] = frozenset(
    {
        "image/jpeg",
        "image/png",
        "image/gif",
        "image/webp",
    }
)

# Allowed file extensions
ALLOWED_IMAGE_EXTENSIONS: set[str] = frozenset(
    {
        ".jpg",
        ".jpeg",
        ".png",
        ".gif",
        ".webp",
    }
)

# Maximum file size (10MB)
MAX_FILE_SIZE = 10 * 1024 * 1024

# Minimum file size (prevent empty files)
MIN_FILE_SIZE = 100  # 100 bytes


# =============================================================================
# File Validation Functions
# =============================================================================


def validate_image_upload(
    file: UploadedFile,
    max_size: int = MAX_FILE_SIZE,
    allowed_types: set[str] | None = None,
    allowed_extensions: set[str] | None = None,
) -> bool:
    """
    Validate an uploaded image file for security.

    Performs multiple validation checks:
    1. File size validation
    2. File extension validation
    3. MIME type validation (from Content-Type header)
    4. Magic number validation (actual file content check)
    5. Image integrity validation (using PIL)

    Args:
        file: The uploaded file object
        max_size: Maximum allowed file size in bytes
        allowed_types: Set of allowed MIME types
        allowed_extensions: Set of allowed file extensions

    Returns:
        True if all validations pass

    Raises:
        FileValidationError: If any validation fails
    """
    if allowed_types is None:
        allowed_types = ALLOWED_IMAGE_MIME_TYPES
    if allowed_extensions is None:
        allowed_extensions = ALLOWED_IMAGE_EXTENSIONS

    # 1. Check if file exists
    if not file:
        raise FileValidationError("No file provided")

    # 2. Check file size
    if file.size > max_size:
        raise FileValidationError(f"File too large. Maximum size is {max_size // (1024 * 1024)}MB")

    if file.size < MIN_FILE_SIZE:
        raise FileValidationError("File too small or empty")

    # 3. Check file extension
    filename = file.name or ""
    ext = os.path.splitext(filename)[1].lower()
    if ext not in allowed_extensions:
        raise FileValidationError(f"Invalid file extension '{ext}'. Allowed: {', '.join(allowed_extensions)}")

    # 4. Check Content-Type header
    content_type = getattr(file, "content_type", "")
    if content_type and content_type not in allowed_types:
        raise FileValidationError(f"Invalid file type '{content_type}'. Allowed: {', '.join(allowed_types)}")

    # 5. Validate magic numbers (actual file content)
    if not _validate_magic_number(file):
        raise FileValidationError("File content doesn't match file extension. File may be corrupted or malicious.")

    # 6. Validate image integrity using PIL
    if not _validate_image_integrity(file):
        raise FileValidationError("Invalid or corrupted image file")

    return True


def _validate_magic_number(file: UploadedFile) -> bool:
    """
    Validate file content against known magic number signatures.

    This is more reliable than checking the file extension or Content-Type
    header, which can be easily spoofed.

    Args:
        file: The uploaded file object

    Returns:
        True if magic number matches an allowed image type
    """
    # Read the file header
    file.seek(0)
    header = file.read(16)
    file.seek(0)

    # Check against known signatures
    for format_name, signatures in IMAGE_SIGNATURES.items():
        for magic, offset, _description in signatures:
            if len(header) >= offset + len(magic) and header[offset : offset + len(magic)] == magic:
                # Special handling for WebP (must also have WEBP marker)
                if format_name == "webp":
                    if len(header) >= 12 and header[8:12] == b"WEBP":
                        return True
                else:
                    return True

    return False


def _validate_image_integrity(file: UploadedFile) -> bool:
    """
    Validate image integrity using PIL.

    This catches corrupted images and various image-related attacks.

    Args:
        file: The uploaded file object

    Returns:
        True if image can be opened and verified
    """
    try:
        from PIL import Image

        file.seek(0)
        # Read into BytesIO to avoid issues with file-like objects
        img_data = BytesIO(file.read())
        file.seek(0)

        with Image.open(img_data) as img:
            # Verify the image is not truncated or corrupted
            img.verify()

            # Re-open for size check (verify() can only be called once)
            img_data.seek(0)
            with Image.open(img_data) as img2:
                # Check for reasonable image dimensions
                # Prevent decompression bombs
                max_dimension = 10000
                if img2.width > max_dimension or img2.height > max_dimension:
                    raise FileValidationError(f"Image dimensions too large. Maximum is {max_dimension}x{max_dimension}")

                # Check for very small dimensions (might be suspicious)
                if img2.width < 1 or img2.height < 1:
                    raise FileValidationError("Invalid image dimensions")

        return True

    except FileValidationError:
        raise
    except Exception:
        return False


# =============================================================================
# Filename Sanitization
# =============================================================================


def sanitize_filename(filename: str, max_length: int = 100) -> str:
    """
    Sanitize a filename to prevent directory traversal and other attacks.

    This function:
    - Removes path separators and directory traversal attempts
    - Removes special characters
    - Truncates to maximum length
    - Ensures the filename is not empty

    Args:
        filename: The original filename
        max_length: Maximum length for the filename

    Returns:
        Sanitized filename
    """
    if not filename:
        return f"file_{uuid.uuid4().hex[:8]}"

    # Get just the filename (remove any path components)
    filename = os.path.basename(filename)

    # Split into name and extension
    name, ext = os.path.splitext(filename)

    # Remove or replace dangerous characters from name
    # Allow alphanumeric, hyphens, underscores, dots
    name = re.sub(r"[^\w\-.]", "_", name)

    # Remove leading dots and underscores (hidden file prevention)
    name = name.lstrip("._")

    # Collapse multiple underscores
    name = re.sub(r"_+", "_", name)

    # Ensure name is not empty
    if not name:
        name = f"file_{uuid.uuid4().hex[:8]}"

    # Sanitize extension
    ext = ext.lower()
    ext = re.sub(r"[^\w.]", "", ext)

    # Combine and truncate
    result = f"{name[:max_length - len(ext)]}{ext}"

    return result


def generate_unique_filename(original_filename: str, prefix: str = "") -> str:
    """
    Generate a unique filename using UUID while preserving extension.

    Args:
        original_filename: The original filename
        prefix: Optional prefix for the filename

    Returns:
        Unique filename with UUID
    """
    ext = os.path.splitext(original_filename)[1].lower()

    # Sanitize extension
    ext = re.sub(r"[^\w.]", "", ext)

    # Generate unique filename
    unique_id = uuid.uuid4().hex[:12]
    if prefix:
        return f"{sanitize_filename(prefix)}_{unique_id}{ext}"
    return f"{unique_id}{ext}"


# =============================================================================
# Rate Limiting for Uploads
# =============================================================================

# Rate limiting configuration
UPLOAD_RATE_LIMITS = {
    "per_minute": 10,
    "per_hour": 100,
    "per_day": 500,
}


def check_upload_rate_limit(user_id: int, cache_backend=None) -> tuple[bool, str]:
    """
    Check if user has exceeded upload rate limits.

    Args:
        user_id: The user's ID
        cache_backend: Optional Django cache backend (uses default if not provided)

    Returns:
        Tuple of (is_allowed, reason_if_blocked)
    """
    if cache_backend is None:
        from django.core.cache import cache

        cache_backend = cache

    # Check per-minute limit
    minute_key = f"upload_rate:{user_id}:minute"
    minute_count = cache_backend.get(minute_key, 0)
    if minute_count >= UPLOAD_RATE_LIMITS["per_minute"]:
        return False, "Upload rate limit exceeded. Please wait a minute."

    # Check per-hour limit
    hour_key = f"upload_rate:{user_id}:hour"
    hour_count = cache_backend.get(hour_key, 0)
    if hour_count >= UPLOAD_RATE_LIMITS["per_hour"]:
        return False, "Hourly upload limit exceeded. Please try again later."

    # Check per-day limit
    day_key = f"upload_rate:{user_id}:day"
    day_count = cache_backend.get(day_key, 0)
    if day_count >= UPLOAD_RATE_LIMITS["per_day"]:
        return False, "Daily upload limit exceeded. Please try again tomorrow."

    return True, ""


def increment_upload_count(user_id: int, cache_backend=None) -> None:
    """
    Increment upload count for rate limiting.

    Args:
        user_id: The user's ID
        cache_backend: Optional Django cache backend
    """
    if cache_backend is None:
        from django.core.cache import cache

        cache_backend = cache

    # Increment per-minute counter (expires in 60 seconds)
    minute_key = f"upload_rate:{user_id}:minute"
    try:
        cache_backend.incr(minute_key)
    except ValueError:
        cache_backend.set(minute_key, 1, 60)

    # Increment per-hour counter (expires in 3600 seconds)
    hour_key = f"upload_rate:{user_id}:hour"
    try:
        cache_backend.incr(hour_key)
    except ValueError:
        cache_backend.set(hour_key, 1, 3600)

    # Increment per-day counter (expires in 86400 seconds)
    day_key = f"upload_rate:{user_id}:day"
    try:
        cache_backend.incr(day_key)
    except ValueError:
        cache_backend.set(day_key, 1, 86400)


# =============================================================================
# Antivirus Integration Point
# =============================================================================


def scan_file_for_malware(file: UploadedFile) -> tuple[bool, str]:
    """
    Placeholder for antivirus/malware scanning integration.

    This function should be implemented to integrate with a virus scanner
    like ClamAV. Currently it returns True (safe) for all files.

    See FUTURE_WORK.md - THRILLWIKI-110 for ClamAV integration plan.

    Args:
        file: The uploaded file object

    Returns:
        Tuple of (is_safe, reason_if_unsafe)
    """
    # ClamAV integration not yet implemented - see FUTURE_WORK.md
    # Currently returns True (safe) for all files
    return True, ""
