"""
Input Validation and Sanitization Utilities
Provides comprehensive validation for all user inputs
"""
import re
import html
import unicodedata
from typing import Optional, List, Any, Union
from datetime import datetime
from urllib.parse import urlparse
import logging

logger = logging.getLogger(__name__)


class ValidationError(Exception):
    """Custom validation error"""
    def __init__(self, field: str, message: str):
        self.field = field
        self.message = message
        super().__init__(f"{field}: {message}")


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

    # Common regex patterns
    EMAIL_PATTERN = re.compile(
        r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    )
    PHONE_PATTERN = re.compile(
        r'^\+?[1-9]\d{1,14}$'  # E.164 format
    )
    URL_PATTERN = re.compile(
        r'^https?://[^\s<>"{}|\\^`\[\]]+$'
    )
    SLUG_PATTERN = re.compile(
        r'^[a-z0-9]+(?:-[a-z0-9]+)*$'
    )
    USERNAME_PATTERN = re.compile(
        r'^[a-zA-Z0-9_]{3,30}$'
    )
    API_KEY_PATTERN = re.compile(
        r'^[a-zA-Z0-9_-]{20,64}$'
    )
    UUID_PATTERN = re.compile(
        r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
        re.IGNORECASE
    )

    # SQL injection patterns to detect
    SQL_INJECTION_PATTERNS = [
        r"(\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER|CREATE|TRUNCATE)\b)",
        r"(--|#|/\*|\*/)",
        r"(\bOR\b\s+\d+\s*=\s*\d+)",
        r"(\bAND\b\s+\d+\s*=\s*\d+)",
        r"(;[\s]*$)",
        r"(\bEXEC\b|\bEXECUTE\b)",
        r"(\bxp_\w+)",
    ]

    # XSS patterns to detect
    XSS_PATTERNS = [
        r"<script[^>]*>",
        r"javascript:",
        r"on\w+\s*=",
        r"<iframe[^>]*>",
        r"<object[^>]*>",
        r"<embed[^>]*>",
        r"<link[^>]*>",
        r"<meta[^>]*>",
        r"expression\s*\(",
        r"url\s*\(",
    ]

    # Dangerous file extensions
    DANGEROUS_EXTENSIONS = [
        '.exe', '.bat', '.cmd', '.sh', '.ps1', '.vbs', '.js',
        '.jar', '.msi', '.dll', '.com', '.scr', '.pif'
    ]

    # ============================================================
    # SANITIZATION METHODS
    # ============================================================

    @classmethod
    def sanitize_string(
        cls,
        value: str,
        max_length: int = 1000,
        strip_html: bool = True,
        strip_special: bool = False,
        allow_newlines: bool = True
    ) -> str:
        """
        Sanitize a string input

        Args:
            value: Input string
            max_length: Maximum allowed length
            strip_html: Remove HTML tags
            strip_special: Remove special characters
            allow_newlines: Allow newline characters
        """
        if not value:
            return ""

        # Convert to string if not already
        value = str(value)

        # Normalize unicode
        value = unicodedata.normalize('NFKC', value)

        # Strip whitespace
        value = value.strip()

        # Remove null bytes
        value = value.replace('\x00', '')

        # Handle HTML
        if strip_html:
            value = html.escape(value)

        # Remove special characters if required
        if strip_special:
            value = re.sub(r'[^\w\s\-.,!?@#$%&*()+=:;\'""]', '', value)

        # Handle newlines
        if not allow_newlines:
            value = value.replace('\n', ' ').replace('\r', '')

        # Truncate to max length
        if len(value) > max_length:
            value = value[:max_length]

        return value

    @classmethod
    def sanitize_html(cls, value: str) -> str:
        """
        Sanitize HTML content - escape dangerous tags but preserve safe formatting
        """
        if not value:
            return ""

        # Escape HTML entities
        value = html.escape(value)

        return value

    @classmethod
    def sanitize_filename(cls, filename: str) -> str:
        """
        Sanitize a filename to prevent path traversal and other attacks
        """
        if not filename:
            return ""

        # Remove path separators
        filename = filename.replace('/', '').replace('\\', '')

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

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

        # Remove dangerous characters
        filename = re.sub(r'[<>:"|?*]', '', filename)

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

        return filename

    @classmethod
    def sanitize_path(cls, path: str) -> str:
        """
        Sanitize a file path to prevent directory traversal
        """
        if not path:
            return ""

        # Remove null bytes
        path = path.replace('\x00', '')

        # Normalize path separators
        path = path.replace('\\', '/')

        # Remove directory traversal attempts
        while '../' in path or '..\\' in path:
            path = path.replace('../', '').replace('..\\', '')

        # Remove leading slashes
        path = path.lstrip('/')

        return path

    @classmethod
    def sanitize_sql_input(cls, value: str) -> str:
        """
        Sanitize input that might be used in SQL context
        Note: Always use parameterized queries - this is defense in depth
        """
        if not value:
            return ""

        # Remove common SQL injection characters
        dangerous_chars = [';', '--', '/*', '*/', "'", '"', '\\']
        for char in dangerous_chars:
            value = value.replace(char, '')

        return value

    # ============================================================
    # VALIDATION METHODS
    # ============================================================

    @classmethod
    def validate_email(cls, email: str, required: bool = True) -> Optional[str]:
        """Validate email address"""
        if not email:
            if required:
                raise ValidationError("email", "Email is required")
            return None

        email = email.strip().lower()

        if len(email) > 254:
            raise ValidationError("email", "Email is too long")

        if not cls.EMAIL_PATTERN.match(email):
            raise ValidationError("email", "Invalid email format")

        return email

    @classmethod
    def validate_phone(cls, phone: str, required: bool = False) -> Optional[str]:
        """Validate phone number (E.164 format)"""
        if not phone:
            if required:
                raise ValidationError("phone", "Phone number is required")
            return None

        # Remove common formatting characters
        phone = re.sub(r'[\s\-\(\)\.]', '', phone)

        if not cls.PHONE_PATTERN.match(phone):
            raise ValidationError("phone", "Invalid phone number format. Use E.164 format (e.g., +919876543210)")

        return phone

    @classmethod
    def validate_url(
        cls,
        url: str,
        required: bool = False,
        allowed_schemes: List[str] = None
    ) -> Optional[str]:
        """Validate URL"""
        if not url:
            if required:
                raise ValidationError("url", "URL is required")
            return None

        url = url.strip()

        # Check length
        if len(url) > 2048:
            raise ValidationError("url", "URL is too long")

        # Parse URL
        try:
            parsed = urlparse(url)
        except Exception:
            raise ValidationError("url", "Invalid URL format")

        # Check scheme
        allowed = allowed_schemes or ['http', 'https']
        if parsed.scheme not in allowed:
            raise ValidationError("url", f"URL scheme must be one of: {', '.join(allowed)}")

        # Check for host
        if not parsed.netloc:
            raise ValidationError("url", "URL must have a valid host")

        # Check for basic format
        if not cls.URL_PATTERN.match(url):
            raise ValidationError("url", "Invalid URL format")

        return url

    @classmethod
    def validate_password(
        cls,
        password: str,
        min_length: int = 8,
        require_uppercase: bool = True,
        require_lowercase: bool = True,
        require_digit: bool = True,
        require_special: bool = False
    ) -> str:
        """Validate password strength"""
        if not password:
            raise ValidationError("password", "Password is required")

        errors = []

        if len(password) < min_length:
            errors.append(f"Password must be at least {min_length} characters")

        if len(password) > 128:
            errors.append("Password is too long (max 128 characters)")

        if require_uppercase and not re.search(r'[A-Z]', password):
            errors.append("Password must contain at least one uppercase letter")

        if require_lowercase and not re.search(r'[a-z]', password):
            errors.append("Password must contain at least one lowercase letter")

        if require_digit and not re.search(r'\d', password):
            errors.append("Password must contain at least one digit")

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

        # Check for common weak passwords
        weak_passwords = [
            'password', '123456', '12345678', 'qwerty', 'abc123',
            'password123', 'admin', 'letmein', 'welcome', 'monkey'
        ]
        if password.lower() in weak_passwords:
            errors.append("Password is too common. Choose a stronger password")

        if errors:
            raise ValidationError("password", "; ".join(errors))

        return password

    @classmethod
    def validate_username(cls, username: str, required: bool = True) -> Optional[str]:
        """Validate username"""
        if not username:
            if required:
                raise ValidationError("username", "Username is required")
            return None

        username = username.strip()

        if len(username) < 3:
            raise ValidationError("username", "Username must be at least 3 characters")

        if len(username) > 30:
            raise ValidationError("username", "Username must be at most 30 characters")

        if not cls.USERNAME_PATTERN.match(username):
            raise ValidationError(
                "username",
                "Username can only contain letters, numbers, and underscores"
            )

        # Check for reserved usernames
        reserved = ['admin', 'root', 'system', 'api', 'www', 'mail', 'support']
        if username.lower() in reserved:
            raise ValidationError("username", "This username is reserved")

        return username

    @classmethod
    def validate_name(
        cls,
        name: str,
        field_name: str = "name",
        min_length: int = 1,
        max_length: int = 100,
        required: bool = True
    ) -> Optional[str]:
        """Validate a name field"""
        if not name:
            if required:
                raise ValidationError(field_name, f"{field_name.title()} is required")
            return None

        name = cls.sanitize_string(name, max_length=max_length, strip_html=True)

        if len(name) < min_length:
            raise ValidationError(field_name, f"{field_name.title()} must be at least {min_length} characters")

        # Check for suspicious patterns
        if re.search(r'[<>{}]', name):
            raise ValidationError(field_name, f"Invalid characters in {field_name}")

        return name

    @classmethod
    def validate_integer(
        cls,
        value: Any,
        field_name: str = "value",
        min_value: int = None,
        max_value: int = None,
        required: bool = True
    ) -> Optional[int]:
        """Validate integer input"""
        if value is None or value == "":
            if required:
                raise ValidationError(field_name, f"{field_name} is required")
            return None

        try:
            value = int(value)
        except (ValueError, TypeError):
            raise ValidationError(field_name, f"{field_name} must be a valid integer")

        if min_value is not None and value < min_value:
            raise ValidationError(field_name, f"{field_name} must be at least {min_value}")

        if max_value is not None and value > max_value:
            raise ValidationError(field_name, f"{field_name} must be at most {max_value}")

        return value

    @classmethod
    def validate_float(
        cls,
        value: Any,
        field_name: str = "value",
        min_value: float = None,
        max_value: float = None,
        required: bool = True
    ) -> Optional[float]:
        """Validate float input"""
        if value is None or value == "":
            if required:
                raise ValidationError(field_name, f"{field_name} is required")
            return None

        try:
            value = float(value)
        except (ValueError, TypeError):
            raise ValidationError(field_name, f"{field_name} must be a valid number")

        if min_value is not None and value < min_value:
            raise ValidationError(field_name, f"{field_name} must be at least {min_value}")

        if max_value is not None and value > max_value:
            raise ValidationError(field_name, f"{field_name} must be at most {max_value}")

        return value

    @classmethod
    def validate_date(
        cls,
        value: str,
        field_name: str = "date",
        format: str = "%Y-%m-%d",
        required: bool = True
    ) -> Optional[datetime]:
        """Validate date string"""
        if not value:
            if required:
                raise ValidationError(field_name, f"{field_name} is required")
            return None

        try:
            return datetime.strptime(value, format)
        except ValueError:
            raise ValidationError(
                field_name,
                f"Invalid date format. Expected format: {format}"
            )

    @classmethod
    def validate_uuid(cls, value: str, field_name: str = "id", required: bool = True) -> Optional[str]:
        """Validate UUID format"""
        if not value:
            if required:
                raise ValidationError(field_name, f"{field_name} is required")
            return None

        value = value.strip().lower()

        if not cls.UUID_PATTERN.match(value):
            raise ValidationError(field_name, f"Invalid UUID format for {field_name}")

        return value

    @classmethod
    def validate_enum(
        cls,
        value: str,
        allowed_values: List[str],
        field_name: str = "value",
        required: bool = True
    ) -> Optional[str]:
        """Validate value is in allowed list"""
        if not value:
            if required:
                raise ValidationError(field_name, f"{field_name} is required")
            return None

        value = str(value).strip()

        if value not in allowed_values:
            raise ValidationError(
                field_name,
                f"Invalid {field_name}. Must be one of: {', '.join(allowed_values)}"
            )

        return value

    @classmethod
    def validate_json(cls, value: Any, field_name: str = "data") -> dict:
        """Validate JSON data"""
        import json

        if isinstance(value, dict):
            return value

        if isinstance(value, str):
            try:
                return json.loads(value)
            except json.JSONDecodeError:
                raise ValidationError(field_name, "Invalid JSON format")

        raise ValidationError(field_name, "Expected JSON object")

    @classmethod
    def validate_list(
        cls,
        value: Any,
        field_name: str = "items",
        min_items: int = 0,
        max_items: int = 100,
        required: bool = True
    ) -> Optional[list]:
        """Validate list input"""
        if value is None:
            if required:
                raise ValidationError(field_name, f"{field_name} is required")
            return None

        if not isinstance(value, list):
            raise ValidationError(field_name, f"{field_name} must be a list")

        if len(value) < min_items:
            raise ValidationError(field_name, f"{field_name} must have at least {min_items} items")

        if len(value) > max_items:
            raise ValidationError(field_name, f"{field_name} can have at most {max_items} items")

        return value

    # ============================================================
    # SECURITY DETECTION METHODS
    # ============================================================

    @classmethod
    def detect_sql_injection(cls, value: str) -> bool:
        """Check if string contains SQL injection patterns"""
        if not value:
            return False

        value_upper = value.upper()

        for pattern in cls.SQL_INJECTION_PATTERNS:
            if re.search(pattern, value_upper, re.IGNORECASE):
                logger.warning(f"Potential SQL injection detected: {value[:50]}...")
                return True

        return False

    @classmethod
    def detect_xss(cls, value: str) -> bool:
        """Check if string contains XSS patterns"""
        if not value:
            return False

        value_lower = value.lower()

        for pattern in cls.XSS_PATTERNS:
            if re.search(pattern, value_lower, re.IGNORECASE):
                logger.warning(f"Potential XSS detected: {value[:50]}...")
                return True

        return False

    @classmethod
    def detect_path_traversal(cls, value: str) -> bool:
        """Check if string contains path traversal patterns"""
        if not value:
            return False

        patterns = ['../', '..\\', '%2e%2e%2f', '%2e%2e/', '..%2f', '%252e%252e%252f']
        value_lower = value.lower()

        for pattern in patterns:
            if pattern in value_lower:
                logger.warning(f"Potential path traversal detected: {value[:50]}...")
                return True

        return False

    @classmethod
    def is_safe_file_extension(cls, filename: str) -> bool:
        """Check if file extension is safe"""
        if not filename:
            return False

        ext = '.' + filename.rsplit('.', 1)[-1].lower() if '.' in filename else ''

        return ext not in cls.DANGEROUS_EXTENSIONS

    @classmethod
    def validate_safe_input(cls, value: str, field_name: str = "input") -> str:
        """
        Comprehensive validation that checks for all attack patterns
        """
        if not value:
            return ""

        if cls.detect_sql_injection(value):
            raise ValidationError(field_name, "Invalid input detected")

        if cls.detect_xss(value):
            raise ValidationError(field_name, "Invalid input detected")

        if cls.detect_path_traversal(value):
            raise ValidationError(field_name, "Invalid input detected")

        return cls.sanitize_string(value)


# Convenience functions
def sanitize(value: str, **kwargs) -> str:
    """Convenience function for string sanitization"""
    return InputValidator.sanitize_string(value, **kwargs)


def validate_email(email: str, **kwargs) -> Optional[str]:
    """Convenience function for email validation"""
    return InputValidator.validate_email(email, **kwargs)


def validate_password(password: str, **kwargs) -> str:
    """Convenience function for password validation"""
    return InputValidator.validate_password(password, **kwargs)


def validate_url(url: str, **kwargs) -> Optional[str]:
    """Convenience function for URL validation"""
    return InputValidator.validate_url(url, **kwargs)


def is_safe(value: str) -> bool:
    """Check if input is safe from common attacks"""
    return not (
        InputValidator.detect_sql_injection(value) or
        InputValidator.detect_xss(value) or
        InputValidator.detect_path_traversal(value)
    )
