"""
Security utilities for SLTK API.

Provides centralized security functions for:
- Path validation and sanitization
- Input size limits
- File type validation
- Error message sanitization
- Security constants and configuration
"""

from __future__ import annotations

import logging
import os
import re
from pathlib import Path
from typing import Any

from fastapi import HTTPException

logger = logging.getLogger(__name__)

# ============================================================================
# Security Constants
# ============================================================================

# Maximum sizes for various inputs
MAX_SEGMENT_COUNT = 10000  # Maximum segments in a single request
MAX_FILE_PATH_LENGTH = 4096  # Maximum path length
MAX_LABEL_LENGTH = 500  # Maximum segment label length
MAX_TIER_NAME_LENGTH = 100  # Maximum tier name length
MAX_H5_PATHS_PER_REQUEST = 50  # Maximum H5 files to analyze at once
MAX_ELAN_PATHS_PER_REQUEST = 100  # Maximum ELAN files in a request
MAX_UPLOAD_SIZE_MB = 500  # Maximum upload file size in MB
MAX_SEARCH_GLOSSES = 100  # Maximum glosses in batch search

# Allowed file extensions by category
ALLOWED_VIDEO_EXTENSIONS = frozenset({".mp4", ".mov", ".avi", ".mkv", ".webm"})
ALLOWED_ELAN_EXTENSIONS = frozenset({".eaf", ".pfsx"})
ALLOWED_POSE_EXTENSIONS = frozenset({".h5", ".hdf5"})
ALLOWED_FEATURE_EXTENSIONS = frozenset({".h5", ".hdf5", ".pt", ".lzma", ".npy", ".npz"})
ALLOWED_OUTPUT_EXTENSIONS = frozenset({".csv", ".json", ".jsonl", ".parquet", ".eaf"})

# Protected directories that should never be written to
PROTECTED_DIRECTORIES = frozenset(
    {
        "/",
        "/bin",
        "/boot",
        "/dev",
        "/etc",
        "/lib",
        "/lib64",
        "/opt",
        "/proc",
        "/root",
        "/sbin",
        "/sys",
        "/usr",
        "/var",
    }
)

# Dangerous filename patterns
DANGEROUS_FILENAME_PATTERNS = [
    r"^\.",  # Hidden files
    r"\.\.+",  # Multiple dots (potential traversal)
    r"[<>:\"|?*]",  # Characters illegal on some filesystems
    r"\x00",  # Null bytes
]

# ============================================================================
# Path Validation Utilities
# ============================================================================


def is_path_safe(path: str | Path) -> tuple[bool, str | None]:
    """
    Check if a path is safe for file operations.

    Performs comprehensive validation including:
    - Length checks
    - Null byte detection
    - Path traversal detection
    - URL encoding detection
    - Dangerous character detection

    Args:
        path: Path to validate

    Returns:
        Tuple of (is_safe, error_message)
    """
    path_str = str(path)

    # Check length
    if len(path_str) > MAX_FILE_PATH_LENGTH:
        return False, f"Path exceeds maximum length ({MAX_FILE_PATH_LENGTH} chars)"

    # Check for null bytes
    if "\x00" in path_str:
        return False, "Path contains null bytes"

    # Check for URL-encoded traversal attempts
    if "%2e" in path_str.lower() or "%2f" in path_str.lower():
        return False, "Path contains URL-encoded characters"

    # Check for path traversal using Path components
    path_obj = Path(path_str)
    if ".." in path_obj.parts:
        return False, "Path traversal not allowed"

    # Check for backslash (could be used for Windows-style traversal on Unix)
    if "\\" in path_str and os.name != "nt":
        return False, "Backslashes not allowed in paths"

    return True, None


def validate_file_extension(
    path: str | Path,
    allowed_extensions: frozenset[str],
    category: str = "file",
) -> Path:
    """
    Validate that a file has an allowed extension.

    Args:
        path: Path to validate
        allowed_extensions: Set of allowed extensions (lowercase, with dot)
        category: Category name for error messages

    Returns:
        Path object

    Raises:
        HTTPException: If extension is not allowed
    """
    path_obj = Path(path)
    ext = path_obj.suffix.lower()

    if ext not in allowed_extensions:
        allowed_list = ", ".join(sorted(allowed_extensions))
        raise HTTPException(
            400, f"Invalid {category} file extension: {ext}. Allowed: {allowed_list}"
        )

    return path_obj


def validate_output_path(path: str | Path, allow_overwrite: bool = False) -> Path:
    """
    Validate an output path for file creation.

    Checks:
    - Path is within allowed directories
    - Parent directory exists
    - Not in protected directories
    - File doesn't exist (unless allow_overwrite is True)

    Args:
        path: Output path to validate
        allow_overwrite: Whether to allow overwriting existing files

    Returns:
        Validated Path object

    Raises:
        HTTPException: If validation fails
    """
    from sltk.api.dependencies import validate_path

    # Use existing validation
    validated = validate_path(str(path), must_exist=False, allow_creation=True)

    # Additional check: not in protected directories
    resolved_str = str(validated.resolve())
    for protected in PROTECTED_DIRECTORIES:
        if resolved_str == protected or resolved_str.startswith(protected + "/"):
            # Allow subdirectories of /home, /tmp, etc.
            if protected in {"/home", "/tmp", "/var"}:
                continue
            raise HTTPException(403, f"Cannot write to protected directory: {protected}")

    # Check if file exists
    if validated.exists() and not allow_overwrite:
        raise HTTPException(
            409, f"File already exists: {path}. Use allow_overwrite=True to overwrite."
        )

    return validated


def is_in_protected_directory(path: Path) -> bool:
    """
    Check if a path is in a protected system directory.

    Args:
        path: Path to check

    Returns:
        True if path is in a protected directory
    """
    resolved = path.resolve()
    resolved_str = str(resolved)

    for protected in PROTECTED_DIRECTORIES:
        if resolved_str == protected:
            return True
        if resolved_str.startswith(protected + "/"):
            # Allow user-writable subdirectories
            if protected in {"/home", "/tmp", "/var"}:
                return False
            return True

    return False


# ============================================================================
# Input Validation Utilities
# ============================================================================


def validate_segment_count(segments: list, max_count: int = MAX_SEGMENT_COUNT) -> None:
    """
    Validate that segment count doesn't exceed limits.

    Args:
        segments: List of segments
        max_count: Maximum allowed count

    Raises:
        HTTPException: If count exceeds limit
    """
    if len(segments) > max_count:
        raise HTTPException(400, f"Segment count ({len(segments)}) exceeds maximum ({max_count})")


def validate_segment_data(segment: dict | Any) -> dict:
    """
    Validate and sanitize segment data.

    Args:
        segment: Segment data to validate

    Returns:
        Sanitized segment dict

    Raises:
        HTTPException: If validation fails
    """
    # Handle Pydantic models
    if hasattr(segment, "model_dump"):
        segment = segment.model_dump()
    elif hasattr(segment, "dict"):
        segment = segment.dict()

    # Validate required fields
    if "start" not in segment or "end" not in segment:
        raise HTTPException(400, "Segment must have start and end times")

    start = segment.get("start", 0)
    end = segment.get("end", 0)

    # Validate numeric types
    if not isinstance(start, (int, float)) or not isinstance(end, (int, float)):
        raise HTTPException(400, "Segment start and end must be numeric")

    # Validate time values
    if start < 0 or end < 0:
        raise HTTPException(400, "Segment times cannot be negative")

    if start > end:
        raise HTTPException(400, "Segment start must be less than or equal to end")

    # Validate and truncate label
    label = segment.get("label", "")
    if label and len(str(label)) > MAX_LABEL_LENGTH:
        logger.warning(
            f"Truncating segment label from {len(str(label))} to {MAX_LABEL_LENGTH} chars"
        )
        label = str(label)[:MAX_LABEL_LENGTH]

    # Validate tier name
    tier = segment.get("tier", "default")
    if tier and len(str(tier)) > MAX_TIER_NAME_LENGTH:
        raise HTTPException(400, f"Tier name exceeds maximum length ({MAX_TIER_NAME_LENGTH} chars)")

    return {
        "start": float(start),
        "end": float(end),
        "label": str(label) if label else None,
        "confidence": (
            float(segment.get("confidence", 1.0)) if segment.get("confidence") is not None else None
        ),
        "tier": str(tier) if tier else "default",
    }


def validate_path_list(
    paths: list[str],
    max_count: int,
    must_exist: bool = True,
    category: str = "file",
) -> list[Path]:
    """
    Validate a list of file paths.

    Args:
        paths: List of paths to validate
        max_count: Maximum number of paths allowed
        must_exist: Whether files must exist
        category: Category name for error messages

    Returns:
        List of validated Path objects

    Raises:
        HTTPException: If validation fails
    """
    from sltk.api.dependencies import validate_path

    if len(paths) > max_count:
        raise HTTPException(400, f"Too many {category} paths ({len(paths)}). Maximum: {max_count}")

    validated_paths = []
    for path_str in paths:
        try:
            validated = validate_path(path_str, must_exist=must_exist)
            validated_paths.append(validated)
        except HTTPException:
            if must_exist:
                raise
            # For optional files, skip invalid paths
            continue

    return validated_paths


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


def sanitize_filename(filename: str, max_length: int = 255) -> str:
    """
    Sanitize a filename for safe file operations.

    Args:
        filename: Original filename
        max_length: Maximum allowed length

    Returns:
        Sanitized filename
    """
    # Remove path components
    filename = Path(filename).name

    # Remove null bytes
    filename = filename.replace("\x00", "")

    # Remove/replace dangerous characters
    filename = re.sub(r'[<>:"|?*]', "_", filename)
    filename = re.sub(r"[\x00-\x1f]", "", filename)  # Control characters

    # Prevent leading dots (hidden files)
    while filename.startswith("."):
        filename = filename[1:]

    # Prevent empty filename
    if not filename:
        filename = "unnamed_file"

    # Truncate if too long
    if len(filename) > max_length:
        # Keep extension if present
        parts = filename.rsplit(".", 1)
        if len(parts) == 2 and len(parts[1]) < 10:
            ext = "." + parts[1]
            base = parts[0][: max_length - len(ext)]
            filename = base + ext
        else:
            filename = filename[:max_length]

    return filename


# ============================================================================
# Error Message Sanitization
# ============================================================================


def sanitize_error_message(error: Exception | str, include_type: bool = True) -> str:
    """
    Sanitize an error message for safe exposure to clients.

    Removes potentially sensitive information like:
    - Full file paths
    - Internal stack traces
    - System information

    Args:
        error: Exception or error string
        include_type: Whether to include exception type

    Returns:
        Sanitized error message
    """
    if isinstance(error, Exception):
        msg = str(error)
        type_name = type(error).__name__
    else:
        msg = str(error)
        type_name = "Error"

    # Remove full paths - keep only filename
    # Pattern matches paths like /home/user/path/to/file.ext
    msg = re.sub(r"/[^\s:]+/([^/\s:]+)", r"\1", msg)

    # Remove potential credentials
    msg = re.sub(
        r"(password|secret|key|token)[:=]\s*\S+", r"\1=[REDACTED]", msg, flags=re.IGNORECASE
    )

    # Limit length
    max_length = 500
    if len(msg) > max_length:
        msg = msg[:max_length] + "..."

    if include_type:
        return f"{type_name}: {msg}"
    return msg


def create_safe_error_response(
    error: Exception,
    status_code: int = 500,
    context: str | None = None,
) -> HTTPException:
    """
    Create an HTTPException with a sanitized error message.

    Args:
        error: Original exception
        status_code: HTTP status code
        context: Optional context for the error

    Returns:
        HTTPException with safe message
    """
    safe_msg = sanitize_error_message(error)

    if context:
        safe_msg = f"{context}: {safe_msg}"

    # Log the full error for debugging
    logger.error(f"API Error [{status_code}]: {error}", exc_info=True)

    return HTTPException(status_code, safe_msg)


# ============================================================================
# Request Validation Decorators
# ============================================================================


def validate_request_size(max_items: int, item_name: str = "items"):
    """
    Decorator factory for validating request size.

    Usage:
        @validate_request_size(100, "paths")
        async def my_endpoint(paths: list[str]):
            ...
    """

    def decorator(func):
        async def wrapper(*args, **kwargs):
            # Find list arguments
            for arg in args:
                if isinstance(arg, list) and len(arg) > max_items:
                    raise HTTPException(
                        400, f"Too many {item_name} ({len(arg)}). Maximum: {max_items}"
                    )
            for value in kwargs.values():
                if isinstance(value, list) and len(value) > max_items:
                    raise HTTPException(
                        400, f"Too many {item_name} ({len(value)}). Maximum: {max_items}"
                    )
            return await func(*args, **kwargs)

        return wrapper

    return decorator


# ============================================================================
# Audit Logging
# ============================================================================


def log_security_event(
    event_type: str,
    details: dict[str, Any],
    severity: str = "WARNING",
) -> None:
    """
    Log a security-relevant event.

    Args:
        event_type: Type of security event
        details: Event details
        severity: Log severity (DEBUG, INFO, WARNING, ERROR)
    """
    log_func = getattr(logger, severity.lower(), logger.warning)
    log_func(f"[SECURITY] {event_type}: {details}")
