"""Filename sanitization for file uploads.

Wraps werkzeug's secure_filename with additional safety checks.
All file upload endpoints should use sanitize_filename() instead of
importing secure_filename directly.
"""

from __future__ import annotations

from typing import Optional

from werkzeug.utils import secure_filename

# Maximum filename length (including extension)
MAX_FILENAME_LENGTH = 255


class UnsafeFilenameError(ValueError):
    """Raised when a filename cannot be sanitized to a safe value."""


def sanitize_filename(
    filename: Optional[str],
    *,
    allowed_extensions: Optional[set[str]] = None,
    max_length: int = MAX_FILENAME_LENGTH,
) -> str:
    """Sanitize an uploaded filename for safe filesystem storage.

    Args:
        filename: Raw filename from the upload.
        allowed_extensions: Optional set of allowed extensions
            (lowercase, with dot, e.g. {".pdf", ".txt"}).
            If None, all extensions are allowed.
        max_length: Maximum allowed filename length.

    Returns:
        Sanitized filename safe for filesystem use.

    Raises:
        UnsafeFilenameError: If the filename is empty, becomes empty
            after sanitization, or has a disallowed extension.
    """
    if not filename:
        raise UnsafeFilenameError("No filename provided")

    # Strip null bytes before passing to secure_filename
    cleaned = filename.replace("\x00", "")

    # Apply werkzeug's path traversal protection
    safe_name = secure_filename(cleaned)

    if not safe_name:
        raise UnsafeFilenameError(
            "Filename contains no safe characters after sanitization"
        )

    # Enforce length limit
    if len(safe_name) > max_length:
        # Preserve extension when truncating
        dot_idx = safe_name.rfind(".")
        if dot_idx > 0:
            ext = safe_name[dot_idx:]
            safe_name = safe_name[: max_length - len(ext)] + ext
        else:
            safe_name = safe_name[:max_length]

    # Validate extension if allowlist provided
    if allowed_extensions is not None:
        dot_idx = safe_name.rfind(".")
        ext = safe_name[dot_idx:].lower() if dot_idx > 0 else ""
        # Normalize allowlist for case-insensitive comparison
        normalized = {e.lower() for e in allowed_extensions}
        if ext not in normalized:
            raise UnsafeFilenameError("File type not allowed")

    return safe_name
