"""
Security features and input validation for CutySoup
"""
import re
import time
import threading
from typing import Dict, List, Set, Optional, Union, Callable, Any
from dataclasses import dataclass
from collections import defaultdict, deque
from urllib.parse import urlparse
from .exceptions import ValidationError, SecurityError


@dataclass
class SecurityConfig:
    """Security configuration settings."""
    max_document_size: int = 10 * 1024 * 1024  # 10MB
    max_parse_time: float = 30.0  # seconds
    max_tree_depth: int = 1000
    max_attributes_per_tag: int = 100
    max_attribute_length: int = 8192
    max_tag_name_length: int = 100
    enable_xxe_protection: bool = True
    enable_html_sanitization: bool = False
    blocked_tag_names: Set[str] = None
    blocked_attributes: Set[str] = None
    allowed_protocols: Set[str] = None
    enable_rate_limiting: bool = False
    rate_limit_requests: int = 100
    rate_limit_window: int = 60  # seconds
    
    def __post_init__(self):
        if self.blocked_tag_names is None:
            self.blocked_tag_names = {
                'script', 'object', 'embed', 'applet', 'link', 'meta'
            }
        
        if self.blocked_attributes is None:
            self.blocked_attributes = {
                'onload', 'onclick', 'onerror', 'onmouseover', 'onfocus',
                'onblur', 'onchange', 'onsubmit', 'onreset', 'onselect',
                'onabort', 'onkeydown', 'onkeypress', 'onkeyup',
                'onmousedown', 'onmouseup', 'onmousemove', 'onmouseout',
                'style'  # Potentially dangerous CSS
            }
        
        if self.allowed_protocols is None:
            self.allowed_protocols = {'http', 'https', 'ftp', 'mailto'}


class InputSanitizer:
    """HTML input sanitization and validation."""
    
    def __init__(self, config: SecurityConfig):
        self.config = config
        
        # Compile regex patterns for efficiency
        self._tag_name_pattern = re.compile(r'^[a-zA-Z][a-zA-Z0-9\-]*$')
        self._attribute_name_pattern = re.compile(r'^[a-zA-Z][a-zA-Z0-9\-_:]*$')
        self._dangerous_patterns = [
            re.compile(r'javascript:', re.IGNORECASE),
            re.compile(r'vbscript:', re.IGNORECASE),
            re.compile(r'data:text/html', re.IGNORECASE),
            re.compile(r'<script[^>]*>', re.IGNORECASE),
            re.compile(r'</script>', re.IGNORECASE),
            re.compile(r'on\w+\s*=', re.IGNORECASE),
        ]
    
    def validate_document_size(self, content: Union[str, bytes]) -> None:
        """Validate document size against limits."""
        if isinstance(content, bytes):
            size = len(content)
        else:
            size = len(content.encode('utf-8'))
        
        if size > self.config.max_document_size:
            raise SecurityError(
                f"Document size {size} exceeds maximum allowed size {self.config.max_document_size}"
            )
    
    def validate_tag_name(self, tag_name: str) -> str:
        """Validate and sanitize tag name."""
        if not isinstance(tag_name, str):
            raise ValidationError("Tag name must be string")
        
        tag_name = tag_name.strip().lower()
        
        if len(tag_name) > self.config.max_tag_name_length:
            raise SecurityError(f"Tag name too long: {len(tag_name)} > {self.config.max_tag_name_length}")
        
        if not self._tag_name_pattern.match(tag_name):
            raise SecurityError(f"Invalid tag name format: {tag_name}")
        
        if tag_name in self.config.blocked_tag_names:
            raise SecurityError(f"Blocked tag name: {tag_name}")
        
        return tag_name
    
    def validate_attribute_name(self, attr_name: str) -> str:
        """Validate and sanitize attribute name."""
        if not isinstance(attr_name, str):
            raise ValidationError("Attribute name must be string")
        
        attr_name = attr_name.strip().lower()
        
        if not self._attribute_name_pattern.match(attr_name):
            raise SecurityError(f"Invalid attribute name format: {attr_name}")
        
        if attr_name in self.config.blocked_attributes:
            raise SecurityError(f"Blocked attribute name: {attr_name}")
        
        return attr_name
    
    def validate_attribute_value(self, attr_name: str, attr_value: str) -> str:
        """Validate and sanitize attribute value."""
        if not isinstance(attr_value, str):
            attr_value = str(attr_value)
        
        if len(attr_value) > self.config.max_attribute_length:
            raise SecurityError(f"Attribute value too long: {len(attr_value)} > {self.config.max_attribute_length}")
        
        # Check for dangerous patterns
        for pattern in self._dangerous_patterns:
            if pattern.search(attr_value):
                raise SecurityError(f"Dangerous pattern detected in attribute value: {attr_name}")
        
        # Special validation for URL attributes
        if attr_name in ['href', 'src', 'action', 'formaction', 'data']:
            self._validate_url(attr_value)
        
        return attr_value
    
    def _validate_url(self, url: str) -> None:
        """Validate URL for security issues."""
        if not url.strip():
            return
        
        try:
            parsed = urlparse(url)
            
            # Check protocol
            if parsed.scheme and parsed.scheme.lower() not in self.config.allowed_protocols:
                raise SecurityError(f"Blocked URL protocol: {parsed.scheme}")
            
            # Check for dangerous patterns
            if any(pattern.search(url) for pattern in self._dangerous_patterns):
                raise SecurityError(f"Dangerous pattern detected in URL: {url}")
            
        except Exception as e:
            if isinstance(e, SecurityError):
                raise
            raise SecurityError(f"Invalid URL format: {url}")
    
    def sanitize_text_content(self, text: str) -> str:
        """Sanitize text content."""
        if not isinstance(text, str):
            text = str(text)
        
        # Remove potential script content
        text = re.sub(r'<script[^>]*>.*?</script>', '', text, flags=re.IGNORECASE | re.DOTALL)
        text = re.sub(r'<style[^>]*>.*?</style>', '', text, flags=re.IGNORECASE | re.DOTALL)
        
        # Remove dangerous patterns
        for pattern in self._dangerous_patterns:
            text = pattern.sub('', text)
        
        return text
    
    def validate_tree_depth(self, depth: int) -> None:
        """Validate tree depth against limits."""
        if depth > self.config.max_tree_depth:
            raise SecurityError(f"Tree depth {depth} exceeds maximum allowed depth {self.config.max_tree_depth}")
    
    def validate_attributes_count(self, attrs_count: int) -> None:
        """Validate number of attributes per tag."""
        if attrs_count > self.config.max_attributes_per_tag:
            raise SecurityError(f"Too many attributes: {attrs_count} > {self.config.max_attributes_per_tag}")


class XXEProtection:
    """Protection against XML External Entity (XXE) attacks."""
    
    @staticmethod
    def scan_for_xxe_patterns(content: str) -> List[str]:
        """Scan content for potential XXE attack patterns."""
        xxe_patterns = [
            r'<!ENTITY\s+\w+\s+SYSTEM',
            r'<!ENTITY\s+\w+\s+PUBLIC',
            r'&\w+;',
            r'<!DOCTYPE\s+\w+\s+\[',
            r'SYSTEM\s+["\'][^"\']*["\']',
            r'PUBLIC\s+["\'][^"\']*["\']',
        ]
        
        detected_patterns = []
        for pattern in xxe_patterns:
            if re.search(pattern, content, re.IGNORECASE):
                detected_patterns.append(pattern)
        
        return detected_patterns
    
    @staticmethod
    def remove_xxe_content(content: str) -> str:
        """Remove potential XXE content from input."""
        # Remove DOCTYPE declarations
        content = re.sub(r'<!DOCTYPE[^>]*>', '', content, flags=re.IGNORECASE | re.DOTALL)
        
        # Remove ENTITY declarations
        content = re.sub(r'<!ENTITY[^>]*>', '', content, flags=re.IGNORECASE)
        
        # Remove suspicious entity references
        content = re.sub(r'&[a-zA-Z][a-zA-Z0-9]*;', '', content)
        
        return content


class RateLimiter:
    """Rate limiting for parsing operations."""
    
    def __init__(self, max_requests: int = 100, window_seconds: int = 60):
        self.max_requests = max_requests
        self.window_seconds = window_seconds
        self._requests: Dict[str, deque] = defaultdict(deque)
        self._lock = threading.Lock()
    
    def is_allowed(self, client_id: str) -> bool:
        """Check if request is allowed for client."""
        current_time = time.time()
        window_start = current_time - self.window_seconds
        
        with self._lock:
            # Clean old requests
            client_requests = self._requests[client_id]
            while client_requests and client_requests[0] < window_start:
                client_requests.popleft()
            
            # Check if under limit
            if len(client_requests) >= self.max_requests:
                return False
            
            # Add current request
            client_requests.append(current_time)
            return True
    
    def get_remaining_requests(self, client_id: str) -> int:
        """Get remaining requests for client in current window."""
        current_time = time.time()
        window_start = current_time - self.window_seconds
        
        with self._lock:
            client_requests = self._requests[client_id]
            # Count requests in current window
            recent_requests = sum(1 for req_time in client_requests if req_time >= window_start)
            return max(0, self.max_requests - recent_requests)
    
    def reset_client(self, client_id: str) -> None:
        """Reset rate limit for specific client."""
        with self._lock:
            if client_id in self._requests:
                del self._requests[client_id]


class SecurityValidator:
    """Main security validation orchestrator."""
    
    def __init__(self, config: Optional[SecurityConfig] = None):
        self.config = config or SecurityConfig()
        self.sanitizer = InputSanitizer(self.config)
        self.xxe_protection = XXEProtection()
        self.rate_limiter = RateLimiter(
            self.config.rate_limit_requests,
            self.config.rate_limit_window
        ) if self.config.enable_rate_limiting else None
        
        # Audit log
        self._audit_log: List[Dict[str, Any]] = []
        self._audit_lock = threading.Lock()
    
    def validate_input(self, content: Union[str, bytes], 
                      client_id: Optional[str] = None) -> str:
        """Comprehensive input validation."""
        start_time = time.time()
        
        try:
            # Rate limiting
            if self.rate_limiter and client_id:
                if not self.rate_limiter.is_allowed(client_id):
                    raise SecurityError(f"Rate limit exceeded for client {client_id}")
            
            # Convert to string if bytes
            if isinstance(content, bytes):
                try:
                    content = content.decode('utf-8', errors='replace')
                except Exception as e:
                    raise ValidationError(f"Failed to decode content: {e}")
            
            # Size validation
            self.sanitizer.validate_document_size(content)
            
            # XXE protection
            if self.config.enable_xxe_protection:
                xxe_patterns = self.xxe_protection.scan_for_xxe_patterns(content)
                if xxe_patterns:
                    self._log_security_event("XXE_ATTEMPT", {
                        "patterns": xxe_patterns,
                        "client_id": client_id,
                        "content_size": len(content)
                    })
                    if self.config.enable_html_sanitization:
                        content = self.xxe_protection.remove_xxe_content(content)
                    else:
                        raise SecurityError("Potential XXE attack detected")
            
            # Content sanitization
            if self.config.enable_html_sanitization:
                content = self.sanitizer.sanitize_text_content(content)
            
            # Log successful validation
            self._log_security_event("INPUT_VALIDATED", {
                "content_size": len(content),
                "client_id": client_id,
                "validation_time": time.time() - start_time
            })
            
            return content
            
        except Exception as e:
            self._log_security_event("VALIDATION_FAILED", {
                "error": str(e),
                "client_id": client_id,
                "content_size": len(content) if isinstance(content, str) else len(content),
                "validation_time": time.time() - start_time
            })
            raise
    
    def validate_tag_creation(self, tag_name: str, attributes: Optional[Dict[str, str]] = None) -> None:
        """Validate tag creation parameters."""
        # Validate tag name
        self.sanitizer.validate_tag_name(tag_name)
        
        # Validate attributes
        if attributes:
            self.sanitizer.validate_attributes_count(len(attributes))
            
            for attr_name, attr_value in attributes.items():
                validated_name = self.sanitizer.validate_attribute_name(attr_name)
                self.sanitizer.validate_attribute_value(validated_name, attr_value)
    
    def validate_tree_operation(self, operation: str, **kwargs) -> None:
        """Validate tree manipulation operations."""
        # Check for suspicious operation patterns
        if operation in ['script_injection', 'style_injection']:
            raise SecurityError(f"Blocked operation: {operation}")
        
        # Log operation for audit
        self._log_security_event("TREE_OPERATION", {
            "operation": operation,
            "parameters": str(kwargs)[:200]  # Truncate for safety
        })
    
    def _log_security_event(self, event_type: str, data: Dict[str, Any]) -> None:
        """Log security event for audit purposes."""
        with self._audit_lock:
            event = {
                "timestamp": time.time(),
                "event_type": event_type,
                "data": data,
                "thread_id": threading.get_ident()
            }
            self._audit_log.append(event)
            
            # Keep only last 1000 events
            if len(self._audit_log) > 1000:
                self._audit_log = self._audit_log[-1000:]
    
    def get_audit_log(self, event_type: Optional[str] = None) -> List[Dict[str, Any]]:
        """Get security audit log."""
        with self._audit_lock:
            if event_type:
                return [event for event in self._audit_log if event["event_type"] == event_type]
            return self._audit_log.copy()
    
    def get_security_stats(self) -> Dict[str, Any]:
        """Get security statistics."""
        with self._audit_lock:
            total_events = len(self._audit_log)
            event_counts = defaultdict(int)
            
            for event in self._audit_log:
                event_counts[event["event_type"]] += 1
            
            stats = {
                "total_events": total_events,
                "event_counts": dict(event_counts),
                "rate_limiting_enabled": self.config.enable_rate_limiting,
                "xxe_protection_enabled": self.config.enable_xxe_protection,
                "html_sanitization_enabled": self.config.enable_html_sanitization
            }
            
            if self.rate_limiter:
                stats["active_clients"] = len(self.rate_limiter._requests)
            
            return stats
    
    def clear_audit_log(self) -> None:
        """Clear security audit log."""
        with self._audit_lock:
            self._audit_log.clear()


# Security context manager
class SecurityContext:
    """Context manager for scoped security settings."""
    
    def __init__(self, validator: SecurityValidator, **overrides):
        self.validator = validator
        self.overrides = overrides
        self.original_config = None
    
    def __enter__(self):
        # Save original config
        self.original_config = self.validator.config
        
        # Create new config with overrides
        config_dict = self.original_config.__dict__.copy()
        config_dict.update(self.overrides)
        
        new_config = SecurityConfig(**config_dict)
        self.validator.config = new_config
        self.validator.sanitizer = InputSanitizer(new_config)
        
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        # Restore original config
        if self.original_config:
            self.validator.config = self.original_config
            self.validator.sanitizer = InputSanitizer(self.original_config)


# Global security validator
_global_security_validator: Optional[SecurityValidator] = None


def get_security_validator(config: Optional[SecurityConfig] = None) -> SecurityValidator:
    """Get global security validator."""
    global _global_security_validator
    if _global_security_validator is None:
        _global_security_validator = SecurityValidator(config)
    return _global_security_validator


def configure_security(**kwargs) -> None:
    """Configure global security settings."""
    global _global_security_validator
    config = SecurityConfig(**kwargs)
    _global_security_validator = SecurityValidator(config)


def enable_strict_security() -> None:
    """Enable strict security mode."""
    configure_security(
        enable_xxe_protection=True,
        enable_html_sanitization=True,
        enable_rate_limiting=True,
        max_document_size=1024 * 1024,  # 1MB
        max_tree_depth=500,
        max_attributes_per_tag=50
    )


def disable_security() -> None:
    """Disable security features (not recommended for production)."""
    configure_security(
        enable_xxe_protection=False,
        enable_html_sanitization=False,
        enable_rate_limiting=False,
        max_document_size=100 * 1024 * 1024,  # 100MB
        max_tree_depth=10000,
        max_attributes_per_tag=1000
    )


# Decorators for security
def secure_operation(operation_name: str = None):
    """Decorator to add security validation to operations."""
    def decorator(func):
        op_name = operation_name or f"{func.__module__}.{func.__name__}"
        
        def wrapper(*args, **kwargs):
            validator = get_security_validator()
            validator.validate_tree_operation(op_name, args=args, kwargs=kwargs)
            return func(*args, **kwargs)
        
        return wrapper
    return decorator


def rate_limited(client_id_func: Optional[Callable] = None):
    """Decorator to add rate limiting to functions."""
    def decorator(func):
        def wrapper(*args, **kwargs):
            validator = get_security_validator()
            
            if validator.rate_limiter:
                # Determine client ID
                if client_id_func:
                    client_id = client_id_func(*args, **kwargs)
                else:
                    client_id = f"thread_{threading.get_ident()}"
                
                if not validator.rate_limiter.is_allowed(client_id):
                    raise SecurityError(f"Rate limit exceeded for {client_id}")
            
            return func(*args, **kwargs)
        
        return wrapper
    return decorator