"""Core Utility Functions

This module provides common utility functions and helpers for the Adtlas Core module:
- Data validation and sanitization
- Date and time utilities
- File and path operations
- Encryption and security helpers
- Performance monitoring utilities
- Cache management helpers
- Configuration utilities
- Error handling helpers

Usage:
    from apps.core.utils import (
        validate_email,
        sanitize_filename,
        generate_uuid,
        encrypt_data,
        measure_performance,
        cache_result,
        get_client_ip,
        format_file_size,
    )

Security:
    All utility functions implement security best practices:
    - Input validation
    - Output sanitization
    - Safe file operations
    - Secure random generation
"""

import os
import re
import uuid
import hashlib
import secrets
import mimetypes
from datetime import datetime, timedelta
from decimal import Decimal, InvalidOperation
from typing import Any, Dict, List, Optional, Union, Tuple
from pathlib import Path
from functools import wraps
import time
import json
import base64
from urllib.parse import urlparse, quote, unquote

from django.conf import settings
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.core.validators import validate_email as django_validate_email
from django.utils import timezone
from django.utils.text import slugify
from django.utils.html import strip_tags
from django.utils.encoding import force_str
from django.http import HttpRequest
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

from apps.core.logging import get_logger

logger = get_logger('core.utils')


# =============================================================================
# Data Validation Utilities
# =============================================================================

def validate_email(email: str) -> bool:
    """
    Validate email address format.
    
    Args:
        email: Email address to validate
        
    Returns:
        True if email is valid, False otherwise
    """
    try:
        django_validate_email(email)
        return True
    except ValidationError:
        return False


def validate_phone(phone: str, country_code: str = 'US') -> bool:
    """
    Validate phone number format.
    
    Args:
        phone: Phone number to validate
        country_code: Country code for validation
        
    Returns:
        True if phone is valid, False otherwise
    """
    # Basic phone validation - can be enhanced with phonenumbers library
    phone_clean = re.sub(r'[^\d+]', '', phone)
    
    if country_code == 'US':
        # US phone number validation
        pattern = r'^(\+1)?[2-9]\d{2}[2-9]\d{2}\d{4}$'
        return bool(re.match(pattern, phone_clean))
    
    # Generic international format
    return len(phone_clean) >= 7 and len(phone_clean) <= 15


def validate_url(url: str) -> bool:
    """
    Validate URL format.
    
    Args:
        url: URL to validate
        
    Returns:
        True if URL is valid, False otherwise
    """
    try:
        result = urlparse(url)
        return all([result.scheme, result.netloc])
    except Exception:
        return False


def validate_json(json_str: str) -> bool:
    """
    Validate JSON string format.
    
    Args:
        json_str: JSON string to validate
        
    Returns:
        True if JSON is valid, False otherwise
    """
    try:
        json.loads(json_str)
        return True
    except (json.JSONDecodeError, TypeError):
        return False


def sanitize_input(text: str, max_length: int = 1000) -> str:
    """
    Sanitize user input by removing HTML tags and limiting length.
    
    Args:
        text: Text to sanitize
        max_length: Maximum allowed length
        
    Returns:
        Sanitized text
    """
    if not text:
        return ''
    
    # Remove HTML tags
    sanitized = strip_tags(force_str(text))
    
    # Limit length
    if len(sanitized) > max_length:
        sanitized = sanitized[:max_length]
    
    return sanitized.strip()


def sanitize_filename(filename: str) -> str:
    """
    Sanitize filename for safe file operations.
    
    Args:
        filename: Original filename
        
    Returns:
        Sanitized filename
    """
    if not filename:
        return 'unnamed_file'
    
    # Remove path separators and dangerous characters
    sanitized = re.sub(r'[<>:"/\\|?*]', '_', filename)
    
    # Remove leading/trailing dots and spaces
    sanitized = sanitized.strip('. ')
    
    # Ensure filename is not empty
    if not sanitized:
        sanitized = 'unnamed_file'
    
    # Limit length
    if len(sanitized) > 255:
        name, ext = os.path.splitext(sanitized)
        sanitized = name[:255-len(ext)] + ext
    
    return sanitized


# =============================================================================
# ID and Token Generation
# =============================================================================

def generate_uuid() -> str:
    """
    Generate a UUID4 string.
    
    Returns:
        UUID4 string
    """
    return str(uuid.uuid4())


def generate_short_id(length: int = 8) -> str:
    """
    Generate a short random ID.
    
    Args:
        length: Length of the ID
        
    Returns:
        Random ID string
    """
    alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
    return ''.join(secrets.choice(alphabet) for _ in range(length))


def generate_token(length: int = 32) -> str:
    """
    Generate a secure random token.
    
    Args:
        length: Length of the token
        
    Returns:
        Random token string
    """
    return secrets.token_urlsafe(length)


def generate_api_key() -> str:
    """
    Generate an API key.
    
    Returns:
        API key string
    """
    prefix = 'ak_'
    random_part = secrets.token_urlsafe(32)
    return f"{prefix}{random_part}"


# =============================================================================
# Encryption and Hashing
# =============================================================================

def hash_password(password: str, salt: Optional[str] = None) -> Tuple[str, str]:
    """
    Hash a password with salt.
    
    Args:
        password: Password to hash
        salt: Optional salt (generated if not provided)
        
    Returns:
        Tuple of (hashed_password, salt)
    """
    if salt is None:
        salt = secrets.token_hex(16)
    
    # Use PBKDF2 with SHA256
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=32,
        salt=salt.encode(),
        iterations=100000,
    )
    
    hashed = base64.b64encode(kdf.derive(password.encode())).decode()
    return hashed, salt


def verify_password(password: str, hashed_password: str, salt: str) -> bool:
    """
    Verify a password against its hash.
    
    Args:
        password: Password to verify
        hashed_password: Stored hash
        salt: Salt used for hashing
        
    Returns:
        True if password matches, False otherwise
    """
    try:
        computed_hash, _ = hash_password(password, salt)
        return secrets.compare_digest(computed_hash, hashed_password)
    except Exception:
        return False


def encrypt_data(data: str, key: Optional[str] = None) -> str:
    """
    Encrypt data using Fernet symmetric encryption.
    
    Args:
        data: Data to encrypt
        key: Encryption key (uses settings.SECRET_KEY if not provided)
        
    Returns:
        Encrypted data as base64 string
    """
    if key is None:
        key = settings.SECRET_KEY
    
    # Derive key from secret
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=32,
        salt=b'adtlas_salt',  # Use a fixed salt for consistency
        iterations=100000,
    )
    derived_key = base64.urlsafe_b64encode(kdf.derive(key.encode()))
    
    fernet = Fernet(derived_key)
    encrypted = fernet.encrypt(data.encode())
    return base64.b64encode(encrypted).decode()


def decrypt_data(encrypted_data: str, key: Optional[str] = None) -> str:
    """
    Decrypt data using Fernet symmetric encryption.
    
    Args:
        encrypted_data: Encrypted data as base64 string
        key: Decryption key (uses settings.SECRET_KEY if not provided)
        
    Returns:
        Decrypted data
    """
    if key is None:
        key = settings.SECRET_KEY
    
    try:
        # Derive key from secret
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=b'adtlas_salt',
            iterations=100000,
        )
        derived_key = base64.urlsafe_b64encode(kdf.derive(key.encode()))
        
        fernet = Fernet(derived_key)
        encrypted_bytes = base64.b64decode(encrypted_data.encode())
        decrypted = fernet.decrypt(encrypted_bytes)
        return decrypted.decode()
    except Exception as e:
        logger.error(f"Failed to decrypt data: {str(e)}")
        raise ValueError("Invalid encrypted data")


def hash_string(text: str, algorithm: str = 'sha256') -> str:
    """
    Hash a string using specified algorithm.
    
    Args:
        text: Text to hash
        algorithm: Hash algorithm (md5, sha1, sha256, sha512)
        
    Returns:
        Hexadecimal hash string
    """
    hash_obj = hashlib.new(algorithm)
    hash_obj.update(text.encode('utf-8'))
    return hash_obj.hexdigest()


# =============================================================================
# Date and Time Utilities
# =============================================================================

def get_current_timestamp() -> datetime:
    """
    Get current timestamp in UTC.
    
    Returns:
        Current UTC datetime
    """
    return timezone.now()


def format_datetime(dt: datetime, format_str: str = '%Y-%m-%d %H:%M:%S') -> str:
    """
    Format datetime to string.
    
    Args:
        dt: Datetime object
        format_str: Format string
        
    Returns:
        Formatted datetime string
    """
    if dt is None:
        return ''
    return dt.strftime(format_str)


def parse_datetime(date_str: str, format_str: str = '%Y-%m-%d %H:%M:%S') -> Optional[datetime]:
    """
    Parse datetime string.
    
    Args:
        date_str: Date string
        format_str: Format string
        
    Returns:
        Parsed datetime object or None if invalid
    """
    try:
        dt = datetime.strptime(date_str, format_str)
        return timezone.make_aware(dt)
    except (ValueError, TypeError):
        return None


def get_time_ago(dt: datetime) -> str:
    """
    Get human-readable time ago string.
    
    Args:
        dt: Datetime object
        
    Returns:
        Time ago string (e.g., '2 hours ago')
    """
    if dt is None:
        return 'Unknown'
    
    now = timezone.now()
    diff = now - dt
    
    if diff.days > 0:
        return f"{diff.days} day{'s' if diff.days != 1 else ''} ago"
    elif diff.seconds > 3600:
        hours = diff.seconds // 3600
        return f"{hours} hour{'s' if hours != 1 else ''} ago"
    elif diff.seconds > 60:
        minutes = diff.seconds // 60
        return f"{minutes} minute{'s' if minutes != 1 else ''} ago"
    else:
        return "Just now"


def is_business_hours(dt: Optional[datetime] = None) -> bool:
    """
    Check if given datetime is within business hours.
    
    Args:
        dt: Datetime to check (defaults to now)
        
    Returns:
        True if within business hours
    """
    if dt is None:
        dt = timezone.now()
    
    # Business hours: Monday-Friday, 9 AM - 5 PM
    weekday = dt.weekday()  # 0 = Monday, 6 = Sunday
    hour = dt.hour
    
    return weekday < 5 and 9 <= hour < 17


# =============================================================================
# File and Path Utilities
# =============================================================================

def get_file_extension(filename: str) -> str:
    """
    Get file extension from filename.
    
    Args:
        filename: Filename
        
    Returns:
        File extension (without dot)
    """
    return os.path.splitext(filename)[1].lstrip('.')


def get_mime_type(filename: str) -> str:
    """
    Get MIME type for filename.
    
    Args:
        filename: Filename
        
    Returns:
        MIME type string
    """
    mime_type, _ = mimetypes.guess_type(filename)
    return mime_type or 'application/octet-stream'


def format_file_size(size_bytes: int) -> str:
    """
    Format file size in human-readable format.
    
    Args:
        size_bytes: Size in bytes
        
    Returns:
        Formatted size string
    """
    if size_bytes < 1024:
        return f"{size_bytes} B"
    elif size_bytes < 1024 * 1024:
        return f"{size_bytes / 1024:.1f} KB"
    elif size_bytes < 1024 * 1024 * 1024:
        return f"{size_bytes / (1024 * 1024):.1f} MB"
    else:
        return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"


def ensure_directory(path: str) -> bool:
    """
    Ensure directory exists, create if necessary.
    
    Args:
        path: Directory path
        
    Returns:
        True if directory exists or was created
    """
    try:
        Path(path).mkdir(parents=True, exist_ok=True)
        return True
    except Exception as e:
        logger.error(f"Failed to create directory {path}: {str(e)}")
        return False


def safe_join(*paths: str) -> str:
    """
    Safely join file paths.
    
    Args:
        *paths: Path components
        
    Returns:
        Joined path
    """
    return os.path.join(*[sanitize_filename(p) for p in paths])


# =============================================================================
# Network and Request Utilities
# =============================================================================

def get_client_ip(request: HttpRequest) -> str:
    """
    Get client IP address from request.
    
    Args:
        request: HTTP request object
        
    Returns:
        Client IP address
    """
    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
    if x_forwarded_for:
        ip = x_forwarded_for.split(',')[0].strip()
    else:
        ip = request.META.get('REMOTE_ADDR', '')
    
    return ip


def get_user_agent(request: HttpRequest) -> str:
    """
    Get user agent from request.
    
    Args:
        request: HTTP request object
        
    Returns:
        User agent string
    """
    return request.META.get('HTTP_USER_AGENT', '')


def is_ajax_request(request: HttpRequest) -> bool:
    """
    Check if request is AJAX.
    
    Args:
        request: HTTP request object
        
    Returns:
        True if AJAX request
    """
    return request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest'


def get_request_info(request: HttpRequest) -> Dict[str, Any]:
    """
    Get comprehensive request information.
    
    Args:
        request: HTTP request object
        
    Returns:
        Dictionary with request information
    """
    return {
        'method': request.method,
        'path': request.path,
        'query_string': request.META.get('QUERY_STRING', ''),
        'ip_address': get_client_ip(request),
        'user_agent': get_user_agent(request),
        'is_ajax': is_ajax_request(request),
        'is_secure': request.is_secure(),
        'timestamp': timezone.now().isoformat(),
    }


# =============================================================================
# Performance and Monitoring
# =============================================================================

def measure_performance(func):
    """
    Decorator to measure function execution time.
    
    Args:
        func: Function to measure
        
    Returns:
        Decorated function
    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        try:
            result = func(*args, **kwargs)
            execution_time = time.time() - start_time
            logger.info(
                f"Function {func.__name__} executed in {execution_time:.4f} seconds"
            )
            return result
        except Exception as e:
            execution_time = time.time() - start_time
            logger.error(
                f"Function {func.__name__} failed after {execution_time:.4f} seconds: {str(e)}"
            )
            raise
    return wrapper


class PerformanceTimer:
    """
    Context manager for measuring execution time.
    
    Usage:
        with PerformanceTimer('operation_name') as timer:
            # Your code here
            pass
        print(f"Execution time: {timer.elapsed_time}")
    """
    
    def __init__(self, name: str = 'operation'):
        self.name = name
        self.start_time = None
        self.end_time = None
        self.elapsed_time = None
    
    def __enter__(self):
        self.start_time = time.time()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.end_time = time.time()
        self.elapsed_time = self.end_time - self.start_time
        
        if exc_type is None:
            logger.info(f"{self.name} completed in {self.elapsed_time:.4f} seconds")
        else:
            logger.error(f"{self.name} failed after {self.elapsed_time:.4f} seconds")


# =============================================================================
# Cache Utilities
# =============================================================================

def cache_result(timeout: int = 300, key_prefix: str = ''):
    """
    Decorator to cache function results.
    
    Args:
        timeout: Cache timeout in seconds
        key_prefix: Prefix for cache key
        
    Returns:
        Decorated function
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # Generate cache key
            key_parts = [key_prefix, func.__name__]
            key_parts.extend([str(arg) for arg in args])
            key_parts.extend([f"{k}={v}" for k, v in sorted(kwargs.items())])
            cache_key = hashlib.md5('_'.join(key_parts).encode()).hexdigest()
            
            # Try to get from cache
            result = cache.get(cache_key)
            if result is not None:
                logger.debug(f"Cache hit for {func.__name__}")
                return result
            
            # Execute function and cache result
            logger.debug(f"Cache miss for {func.__name__}")
            result = func(*args, **kwargs)
            cache.set(cache_key, result, timeout)
            return result
        return wrapper
    return decorator


def invalidate_cache_pattern(pattern: str):
    """
    Invalidate cache keys matching pattern.
    
    Args:
        pattern: Cache key pattern
    """
    try:
        # This is a simplified implementation
        # In production, you might want to use Redis SCAN or similar
        cache.delete_many([pattern])
        logger.info(f"Invalidated cache pattern: {pattern}")
    except Exception as e:
        logger.error(f"Failed to invalidate cache pattern {pattern}: {str(e)}")


# =============================================================================
# Data Conversion Utilities
# =============================================================================

def to_decimal(value: Any, default: Decimal = Decimal('0')) -> Decimal:
    """
    Convert value to Decimal safely.
    
    Args:
        value: Value to convert
        default: Default value if conversion fails
        
    Returns:
        Decimal value
    """
    try:
        return Decimal(str(value))
    except (InvalidOperation, TypeError, ValueError):
        return default


def to_int(value: Any, default: int = 0) -> int:
    """
    Convert value to integer safely.
    
    Args:
        value: Value to convert
        default: Default value if conversion fails
        
    Returns:
        Integer value
    """
    try:
        return int(value)
    except (TypeError, ValueError):
        return default


def to_float(value: Any, default: float = 0.0) -> float:
    """
    Convert value to float safely.
    
    Args:
        value: Value to convert
        default: Default value if conversion fails
        
    Returns:
        Float value
    """
    try:
        return float(value)
    except (TypeError, ValueError):
        return default


def to_bool(value: Any) -> bool:
    """
    Convert value to boolean safely.
    
    Args:
        value: Value to convert
        
    Returns:
        Boolean value
    """
    if isinstance(value, bool):
        return value
    if isinstance(value, str):
        return value.lower() in ('true', '1', 'yes', 'on')
    return bool(value)


# =============================================================================
# Configuration Utilities
# =============================================================================

def get_setting(key: str, default: Any = None) -> Any:
    """
    Get Django setting with default value.
    
    Args:
        key: Setting key
        default: Default value
        
    Returns:
        Setting value or default
    """
    return getattr(settings, key, default)


def is_debug_mode() -> bool:
    """
    Check if Django is in debug mode.
    
    Returns:
        True if in debug mode
    """
    return getattr(settings, 'DEBUG', False)


def get_environment() -> str:
    """
    Get current environment name.
    
    Returns:
        Environment name (development, staging, production)
    """
    return getattr(settings, 'ENVIRONMENT', 'development')


# =============================================================================
# Error Handling Utilities
# =============================================================================

def safe_execute(func, *args, default=None, **kwargs):
    """
    Execute function safely with error handling.
    
    Args:
        func: Function to execute
        *args: Function arguments
        default: Default value if function fails
        **kwargs: Function keyword arguments
        
    Returns:
        Function result or default value
    """
    try:
        return func(*args, **kwargs)
    except Exception as e:
        logger.error(f"Safe execution failed for {func.__name__}: {str(e)}")
        return default


def log_exception(exc: Exception, context: Dict[str, Any] = None):
    """
    Log exception with context information.
    
    Args:
        exc: Exception to log
        context: Additional context information
    """
    context = context or {}
    logger.error(
        f"Exception occurred: {type(exc).__name__}: {str(exc)}",
        extra={'context': context}
    )


# =============================================================================
# Export all utility functions
# =============================================================================

__all__ = [
    # Validation
    'validate_email',
    'validate_phone', 
    'validate_url',
    'validate_json',
    'sanitize_input',
    'sanitize_filename',
    
    # ID Generation
    'generate_uuid',
    'generate_short_id',
    'generate_token',
    'generate_api_key',
    
    # Encryption
    'hash_password',
    'verify_password',
    'encrypt_data',
    'decrypt_data',
    'hash_string',
    
    # Date/Time
    'get_current_timestamp',
    'format_datetime',
    'parse_datetime',
    'get_time_ago',
    'is_business_hours',
    
    # File/Path
    'get_file_extension',
    'get_mime_type',
    'format_file_size',
    'ensure_directory',
    'safe_join',
    
    # Network
    'get_client_ip',
    'get_user_agent',
    'is_ajax_request',
    'get_request_info',
    
    # Performance
    'measure_performance',
    'PerformanceTimer',
    
    # Cache
    'cache_result',
    'invalidate_cache_pattern',
    
    # Conversion
    'to_decimal',
    'to_int',
    'to_float',
    'to_bool',
    
    # Configuration
    'get_setting',
    'is_debug_mode',
    'get_environment',
    
    # Error Handling
    'safe_execute',
    'log_exception',
]