"""Security utilities for input validation and sanitization.

Provides comprehensive security functions for validating and sanitizing
user inputs, file paths, URLs, and other security-sensitive data.
"""

from __future__ import annotations

import hashlib
import html
import os
import re
from pathlib import Path
from typing import ClassVar, Optional, Pattern
from urllib.parse import urlparse


class InputValidator:
    """Comprehensive input validation and sanitization utilities."""

    # Email validation pattern
    EMAIL_PATTERN: Pattern = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")

    # Phone number validation pattern (international format)
    PHONE_PATTERN: Pattern = re.compile(r"^\+?[1-9]\d{1,14}$")

    # URL validation pattern
    URL_PATTERN: 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,
    )

    # Safe filename pattern (allows letters, numbers, dots, underscores, hyphens)
    SAFE_FILENAME_PATTERN: Pattern = re.compile(r"^[a-zA-Z0-9._-]+$")

    # Dangerous path traversal patterns
    DANGEROUS_PATTERNS: ClassVar[list[str]] = ["..", "../", "..\\", "/", "\\"]

    @classmethod
    def validate_email(cls, email: str) -> bool:
        """Validate email format.

        Args:
            email: Email address to validate

        Returns
        -------
            True if email format is valid, False otherwise
        """
        return bool(cls.EMAIL_PATTERN.match(email))

    @classmethod
    def validate_phone(cls, phone: str) -> bool:
        """Validate phone number format.

        Args:
            phone: Phone number to validate

        Returns
        -------
            True if phone format is valid, False otherwise
        """
        # Strip whitespace and check minimum length
        clean_phone = phone.strip()
        if len(clean_phone) < 3:
            return False
        return bool(cls.PHONE_PATTERN.match(clean_phone))

    @classmethod
    def validate_url(cls, url: str) -> Optional[str]:
        """Validate and normalize URL.

        Args:
            url: URL to validate

        Returns
        -------
            Normalized URL string if valid, None otherwise
        """
        if not cls.URL_PATTERN.match(url):
            return None

        parsed = urlparse(url)

        # Only allow http and https protocols
        if parsed.scheme not in ("http", "https"):
            return None

        # Reconstruct URL to ensure proper formatting
        normalized = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
        if parsed.query:
            normalized += f"?{parsed.query}"
        if parsed.fragment:
            normalized += f"#{parsed.fragment}"

        return normalized

    @classmethod
    def sanitize_html(cls, text: str) -> str:
        """Sanitize HTML to prevent XSS attacks.

        Args:
            text: Text to sanitize

        Returns
        -------
            Sanitized text with HTML entities escaped
        """
        return html.escape(text)

    @classmethod
    def validate_filename(cls, filename: str) -> bool:
        """Validate filename for security.

        Args:
            filename: Filename to validate

        Returns
        -------
            True if filename is safe, False if it contains dangerous patterns
        """
        # Check for path traversal attacks
        if any(pattern in filename for pattern in cls.DANGEROUS_PATTERNS):
            return False

        # Check if filename matches safe pattern
        return bool(cls.SAFE_FILENAME_PATTERN.match(filename))

    @classmethod
    def sanitize_filename(cls, filename: str) -> str:
        """Sanitize filename by removing dangerous characters.

        Args:
            filename: Filename to sanitize

        Returns
        -------
            Sanitized filename with only safe characters
        """
        # Remove path components
        clean_name = Path(filename).name

        # Remove HTML tags and dangerous characters
        # First remove HTML-like content
        clean_name = re.sub(r"<[^>]+>", "_", clean_name)

        # Remove dangerous characters, keep only alphanumeric and safe symbols
        safe_chars = re.compile(r"[^a-zA-Z0-9._-]")
        sanitized = safe_chars.sub("_", clean_name)

        # Ensure it's not empty
        if not sanitized:
            sanitized = "unnamed_file"

        return sanitized

    @classmethod
    def validate_file_path(cls, base_dir: Path, user_path: str) -> Optional[Path]:
        """Validate file path to prevent directory traversal attacks.

        Args:
            base_dir: Base directory that user paths should be within
            user_path: User-provided path component

        Returns
        -------
            Resolved safe path if valid, None if it's a security risk
        """
        try:
            # Sanitize the user path first
            clean_path = cls.sanitize_filename(user_path)

            # Construct the full path
            full_path = (base_dir / clean_path).resolve()

            # Ensure the resolved path is within the base directory
            if not full_path.is_relative_to(base_dir):
                return None

            return full_path

        except Exception:
            return None

    @classmethod
    def generate_secure_token(cls, length: int = 32) -> str:
        """Generate a cryptographically secure random token.

        Args:
            length: Length of the token in bytes

        Returns
        -------
            Hexadecimal string representation of the secure token
        """
        return os.urandom(length).hex()

    @classmethod
    def hash_password(cls, password: str, salt: Optional[str] = None) -> tuple[str, str]:
        """Hash password with salt using SHA-256.

        Args:
            password: Password to hash
            salt: Optional salt. If None, generates a random salt

        Returns
        -------
            Tuple of (hashed_password, salt_used)
        """
        if salt is None:
            salt = cls.generate_secure_token(16)

        # Combine password and salt
        salted_password = password + salt

        # Hash using SHA-256
        hashed = hashlib.sha256(salted_password.encode("utf-8")).hexdigest()

        return hashed, salt

    @classmethod
    def verify_password(cls, password: str, hashed: str, salt: str) -> bool:
        """Verify password against stored hash and salt.

        Args:
            password: Password to verify
            hashed: Stored hashed password
            salt: Salt used for hashing

        Returns
        -------
            True if password matches, False otherwise
        """
        test_hash, _ = cls.hash_password(password, salt)
        return test_hash == hashed


class SecurityChecker:
    """Security analysis and vulnerability detection."""

    SQL_INJECTION_PATTERNS: ClassVar[list[str]] = [
        r"(?i)(union|select|insert|update|delete|drop|create|alter)",
        r"(?i)(\bor\b|\band\b).*['\"=]",
        r"[\"'][^\"']*(['\"].*['\"]|--|#)",
    ]

    COMMAND_INJECTION_PATTERNS: ClassVar[list[str]] = [
        r"[;&|`]",
        r"\$\([^)]+\)",
        r"`[^`]+`",
    ]

    XSS_PATTERNS: ClassVar[list[str]] = [
        r"<script[^>]*>.*?</script>",
        r"javascript:",
        r"on\w+\s*=",
        r"<iframe[^>]*>",
    ]

    def __init__(self):
        self.sql_patterns = [re.compile(pattern) for pattern in self.SQL_INJECTION_PATTERNS]
        self.cmd_patterns = [re.compile(pattern) for pattern in self.COMMAND_INJECTION_PATTERNS]
        self.xss_patterns = [re.compile(pattern) for pattern in self.XSS_PATTERNS]

    def check_sql_injection(self, input_str: str) -> list[str]:
        """Check for potential SQL injection patterns.

        Args:
            input_str: String to check

        Returns
        -------
            List of detected suspicious patterns
        """
        matches = []
        for pattern in self.sql_patterns:
            if pattern.search(input_str):
                matches.append(pattern.pattern)
        return matches

    def check_command_injection(self, input_str: str) -> list[str]:
        """Check for potential command injection patterns.

        Args:
            input_str: String to check

        Returns
        -------
            List of detected suspicious patterns
        """
        matches = []
        for pattern in self.cmd_patterns:
            if pattern.search(input_str):
                matches.append(pattern.pattern)
        return matches

    def check_xss(self, input_str: str) -> list[str]:
        """Check for potential XSS patterns.

        Args:
            input_str: String to check

        Returns
        -------
            List of detected suspicious patterns
        """
        matches = []
        for pattern in self.xss_patterns:
            if pattern.search(input_str):
                matches.append(pattern.pattern)
        return matches

    def security_scan(self, input_str: str) -> dict[str, list[str]]:
        """Perform comprehensive security scan on input.

        Args:
            input_str: String to scan

        Returns
        -------
            Dictionary with security findings categorized by type
        """
        return {
            "sql_injection": self.check_sql_injection(input_str),
            "command_injection": self.check_command_injection(input_str),
            "xss": self.check_xss(input_str),
        }


# Convenience functions
def validate_and_sanitize_email(email: str) -> Optional[str]:
    """Validate and return sanitized email address.

    Args:
        email: Email to validate

    Returns
    -------
        Validated email address or None if invalid
    """
    validator = InputValidator()
    clean_email = email.strip().lower()
    if validator.validate_email(clean_email):
        return clean_email
    return None


def safe_join_path(base_path: Path, *parts: str) -> Optional[Path]:
    """Safely join path components preventing directory traversal.

    Args:
        base_path: Base directory
        *parts: Path components to join

    Returns
    -------
        Safe resolved path or None if security violation
    """
    validator = InputValidator()
    try:
        # Sanitize each path component
        clean_parts = [validator.sanitize_filename(part) for part in parts]

        # Join and resolve
        full_path = (base_path / Path(*clean_parts)).resolve()

        # Check if within base directory
        if full_path.is_relative_to(base_path):
            return full_path
        return None

    except Exception:
        return None


def is_safe_content(content: str, max_length: int = 10000) -> bool:
    """Check if content is safe for processing.

    Args:
        content: Content to check
        max_length: Maximum allowed content length

    Returns
    -------
        True if content is safe, False otherwise
    """
    # Check length
    if len(content) > max_length:
        return False

    # Check for null bytes
    if "\x00" in content:
        return False

    # Perform security scan
    checker = SecurityChecker()
    findings = checker.security_scan(content)

    # Return True only if no security issues found
    return all(len(matches) == 0 for matches in findings.values())


__all__ = [
    "InputValidator",
    "SecurityChecker",
    "is_safe_content",
    "safe_join_path",
    "validate_and_sanitize_email",
]
