"""Core Decorators

This module provides common decorators for the Adtlas Core module:
- Authentication and authorization decorators
- Caching decorators
- Rate limiting decorators
- Validation decorators
- Performance monitoring decorators
- Error handling decorators
- Logging decorators
- Permission checking decorators

Usage:
    from apps.core.decorators import (
        require_authentication,
        require_permission,
        cache_response,
        rate_limit,
        validate_json,
        log_execution,
        handle_exceptions,
        require_ajax,
        require_post,
    )

Security:
    All decorators implement security best practices:
    - Proper authentication checks
    - Permission validation
    - Rate limiting protection
    - Input validation
    - Secure error handling
"""

import json
import time
from functools import wraps
from typing import Any, Callable, Dict, List, Optional, Union
from datetime import datetime, timedelta

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required, user_passes_test
from django.core.cache import cache
from django.core.exceptions import PermissionDenied, ValidationError
from django.http import (
    HttpRequest, HttpResponse, JsonResponse, 
    HttpResponseBadRequest, HttpResponseForbidden
)
from django.shortcuts import redirect
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.views.decorators.cache import cache_page
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType

from apps.core.logging import get_logger
from apps.core.utils import get_client_ip, hash_string

User = get_user_model()
logger = get_logger('core.decorators')


# =============================================================================
# Authentication and Authorization Decorators
# =============================================================================

def require_authentication(redirect_url: str = '/login/'):
    """
    Decorator to require user authentication.
    
    Args:
        redirect_url: URL to redirect unauthenticated users
        
    Returns:
        Decorated function
    """
    def decorator(func):
        @wraps(func)
        def wrapper(request, *args, **kwargs):
            if not request.user.is_authenticated:
                if request.is_ajax() or 'application/json' in request.META.get('HTTP_ACCEPT', ''):
                    return JsonResponse(
                        {'error': 'Authentication required'}, 
                        status=401
                    )
                return redirect(redirect_url)
            return func(request, *args, **kwargs)
        return wrapper
    return decorator


def require_permission(permission: str, raise_exception: bool = True):
    """
    Decorator to require specific permission.
    
    Args:
        permission: Permission string (e.g., 'app.view_model')
        raise_exception: Whether to raise exception or return 403
        
    Returns:
        Decorated function
    """
    def decorator(func):
        @wraps(func)
        def wrapper(request, *args, **kwargs):
            if not request.user.is_authenticated:
                if raise_exception:
                    raise PermissionDenied("Authentication required")
                return HttpResponseForbidden("Authentication required")
            
            if not request.user.has_perm(permission):
                if raise_exception:
                    raise PermissionDenied(f"Permission '{permission}' required")
                return HttpResponseForbidden(f"Permission '{permission}' required")
            
            return func(request, *args, **kwargs)
        return wrapper
    return decorator


def require_staff(func):
    """
    Decorator to require staff status.
    
    Args:
        func: Function to decorate
        
    Returns:
        Decorated function
    """
    @wraps(func)
    def wrapper(request, *args, **kwargs):
        if not request.user.is_authenticated or not request.user.is_staff:
            raise PermissionDenied("Staff access required")
        return func(request, *args, **kwargs)
    return wrapper


def require_superuser(func):
    """
    Decorator to require superuser status.
    
    Args:
        func: Function to decorate
        
    Returns:
        Decorated function
    """
    @wraps(func)
    def wrapper(request, *args, **kwargs):
        if not request.user.is_authenticated or not request.user.is_superuser:
            raise PermissionDenied("Superuser access required")
        return func(request, *args, **kwargs)
    return wrapper


def require_group(group_name: str):
    """
    Decorator to require membership in specific group.
    
    Args:
        group_name: Name of the required group
        
    Returns:
        Decorated function
    """
    def decorator(func):
        @wraps(func)
        def wrapper(request, *args, **kwargs):
            if not request.user.is_authenticated:
                raise PermissionDenied("Authentication required")
            
            if not request.user.groups.filter(name=group_name).exists():
                raise PermissionDenied(f"Group '{group_name}' membership required")
            
            return func(request, *args, **kwargs)
        return wrapper
    return decorator


def require_owner_or_staff(owner_field: str = 'user'):
    """
    Decorator to require object ownership or staff status.
    
    Args:
        owner_field: Field name that contains the owner
        
    Returns:
        Decorated function
    """
    def decorator(func):
        @wraps(func)
        def wrapper(request, *args, **kwargs):
            if not request.user.is_authenticated:
                raise PermissionDenied("Authentication required")
            
            # If staff, allow access
            if request.user.is_staff:
                return func(request, *args, **kwargs)
            
            # Check ownership (this is a simplified check)
            # In practice, you'd need to get the object and check ownership
            obj_id = kwargs.get('pk') or kwargs.get('id')
            if obj_id:
                # This would need to be customized based on your models
                logger.info(f"Checking ownership for object {obj_id}")
            
            return func(request, *args, **kwargs)
        return wrapper
    return decorator


# =============================================================================
# Rate Limiting Decorators
# =============================================================================

def rate_limit(requests: int = 100, window: int = 3600, per_ip: bool = True, per_user: bool = False):
    """
    Decorator to implement rate limiting.
    
    Args:
        requests: Number of requests allowed
        window: Time window in seconds
        per_ip: Apply rate limit per IP address
        per_user: Apply rate limit per user
        
    Returns:
        Decorated function
    """
    def decorator(func):
        @wraps(func)
        def wrapper(request, *args, **kwargs):
            # Generate rate limit key
            key_parts = ['rate_limit', func.__name__]
            
            if per_ip:
                key_parts.append(f"ip_{get_client_ip(request)}")
            
            if per_user and request.user.is_authenticated:
                key_parts.append(f"user_{request.user.id}")
            
            cache_key = '_'.join(key_parts)
            
            # Check current count
            current_count = cache.get(cache_key, 0)
            
            if current_count >= requests:
                logger.warning(
                    f"Rate limit exceeded for {cache_key}: {current_count}/{requests}"
                )
                return JsonResponse(
                    {
                        'error': 'Rate limit exceeded',
                        'retry_after': window
                    },
                    status=429
                )
            
            # Increment counter
            cache.set(cache_key, current_count + 1, window)
            
            return func(request, *args, **kwargs)
        return wrapper
    return decorator


def throttle_user(requests: int = 60, window: int = 60):
    """
    Decorator to throttle requests per user.
    
    Args:
        requests: Number of requests allowed
        window: Time window in seconds
        
    Returns:
        Decorated function
    """
    return rate_limit(requests=requests, window=window, per_ip=False, per_user=True)


def throttle_ip(requests: int = 100, window: int = 3600):
    """
    Decorator to throttle requests per IP.
    
    Args:
        requests: Number of requests allowed
        window: Time window in seconds
        
    Returns:
        Decorated function
    """
    return rate_limit(requests=requests, window=window, per_ip=True, per_user=False)


# =============================================================================
# Caching Decorators
# =============================================================================

def cache_response(timeout: int = 300, vary_on: Optional[List[str]] = None, key_prefix: str = ''):
    """
    Decorator to cache response.
    
    Args:
        timeout: Cache timeout in seconds
        vary_on: List of request attributes to vary cache on
        key_prefix: Prefix for cache key
        
    Returns:
        Decorated function
    """
    def decorator(func):
        @wraps(func)
        def wrapper(request, *args, **kwargs):
            # Generate cache key
            key_parts = [key_prefix, func.__name__, request.path]
            
            if vary_on:
                for attr in vary_on:
                    if hasattr(request, attr):
                        key_parts.append(f"{attr}_{getattr(request, attr)}")
                    elif attr in request.GET:
                        key_parts.append(f"{attr}_{request.GET[attr]}")
            
            if request.user.is_authenticated:
                key_parts.append(f"user_{request.user.id}")
            
            cache_key = hash_string('_'.join(key_parts))
            
            # Try to get from cache
            cached_response = cache.get(cache_key)
            if cached_response is not None:
                logger.debug(f"Cache hit for {func.__name__}")
                return cached_response
            
            # Execute function and cache response
            logger.debug(f"Cache miss for {func.__name__}")
            response = func(request, *args, **kwargs)
            
            # Only cache successful responses
            if hasattr(response, 'status_code') and response.status_code == 200:
                cache.set(cache_key, response, timeout)
            
            return response
        return wrapper
    return decorator


def cache_per_user(timeout: int = 300):
    """
    Decorator to cache response per user.
    
    Args:
        timeout: Cache timeout in seconds
        
    Returns:
        Decorated function
    """
    return cache_response(timeout=timeout, vary_on=['user'])


def invalidate_cache_on_change(cache_keys: List[str]):
    """
    Decorator to invalidate cache keys when function is called.
    
    Args:
        cache_keys: List of cache keys to invalidate
        
    Returns:
        Decorated function
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            result = func(*args, **kwargs)
            
            # Invalidate cache keys
            for key in cache_keys:
                cache.delete(key)
                logger.debug(f"Invalidated cache key: {key}")
            
            return result
        return wrapper
    return decorator


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

def validate_json(required_fields: Optional[List[str]] = None, optional_fields: Optional[List[str]] = None):
    """
    Decorator to validate JSON request data.
    
    Args:
        required_fields: List of required field names
        optional_fields: List of optional field names
        
    Returns:
        Decorated function
    """
    def decorator(func):
        @wraps(func)
        def wrapper(request, *args, **kwargs):
            if request.content_type != 'application/json':
                return JsonResponse(
                    {'error': 'Content-Type must be application/json'},
                    status=400
                )
            
            try:
                data = json.loads(request.body)
            except json.JSONDecodeError:
                return JsonResponse(
                    {'error': 'Invalid JSON data'},
                    status=400
                )
            
            # Validate required fields
            if required_fields:
                missing_fields = [field for field in required_fields if field not in data]
                if missing_fields:
                    return JsonResponse(
                        {
                            'error': 'Missing required fields',
                            'missing_fields': missing_fields
                        },
                        status=400
                    )
            
            # Add validated data to request
            request.json = data
            
            return func(request, *args, **kwargs)
        return wrapper
    return decorator


def validate_params(**param_validators):
    """
    Decorator to validate request parameters.
    
    Args:
        **param_validators: Parameter validators
        
    Returns:
        Decorated function
    """
    def decorator(func):
        @wraps(func)
        def wrapper(request, *args, **kwargs):
            errors = {}
            
            for param_name, validator in param_validators.items():
                value = request.GET.get(param_name) or request.POST.get(param_name)
                
                if value is not None:
                    try:
                        if callable(validator):
                            validator(value)
                    except (ValueError, ValidationError) as e:
                        errors[param_name] = str(e)
            
            if errors:
                return JsonResponse(
                    {
                        'error': 'Parameter validation failed',
                        'validation_errors': errors
                    },
                    status=400
                )
            
            return func(request, *args, **kwargs)
        return wrapper
    return decorator


def require_fields(*field_names):
    """
    Decorator to require specific fields in POST data.
    
    Args:
        *field_names: Required field names
        
    Returns:
        Decorated function
    """
    def decorator(func):
        @wraps(func)
        def wrapper(request, *args, **kwargs):
            missing_fields = []
            
            for field_name in field_names:
                if field_name not in request.POST:
                    missing_fields.append(field_name)
            
            if missing_fields:
                return JsonResponse(
                    {
                        'error': 'Missing required fields',
                        'missing_fields': missing_fields
                    },
                    status=400
                )
            
            return func(request, *args, **kwargs)
        return wrapper
    return decorator


# =============================================================================
# HTTP Method and Request Type Decorators
# =============================================================================

def require_ajax(func):
    """
    Decorator to require AJAX requests.
    
    Args:
        func: Function to decorate
        
    Returns:
        Decorated function
    """
    @wraps(func)
    def wrapper(request, *args, **kwargs):
        if not request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest':
            return JsonResponse(
                {'error': 'AJAX request required'},
                status=400
            )
        return func(request, *args, **kwargs)
    return wrapper


def require_post(func):
    """
    Decorator to require POST requests.
    
    Args:
        func: Function to decorate
        
    Returns:
        Decorated function
    """
    @wraps(func)
    def wrapper(request, *args, **kwargs):
        if request.method != 'POST':
            return JsonResponse(
                {'error': 'POST request required'},
                status=405
            )
        return func(request, *args, **kwargs)
    return wrapper


def require_get(func):
    """
    Decorator to require GET requests.
    
    Args:
        func: Function to decorate
        
    Returns:
        Decorated function
    """
    @wraps(func)
    def wrapper(request, *args, **kwargs):
        if request.method != 'GET':
            return JsonResponse(
                {'error': 'GET request required'},
                status=405
            )
        return func(request, *args, **kwargs)
    return wrapper


def allow_methods(*methods):
    """
    Decorator to allow specific HTTP methods.
    
    Args:
        *methods: Allowed HTTP methods
        
    Returns:
        Decorated function
    """
    def decorator(func):
        @wraps(func)
        def wrapper(request, *args, **kwargs):
            if request.method not in methods:
                return JsonResponse(
                    {
                        'error': f'Method {request.method} not allowed',
                        'allowed_methods': list(methods)
                    },
                    status=405
                )
            return func(request, *args, **kwargs)
        return wrapper
    return decorator


# =============================================================================
# Logging and Monitoring Decorators
# =============================================================================

def log_execution(include_args: bool = False, include_result: bool = False):
    """
    Decorator to log function execution.
    
    Args:
        include_args: Whether to log function arguments
        include_result: Whether to log function result
        
    Returns:
        Decorated function
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            start_time = time.time()
            
            log_data = {
                'function': func.__name__,
                'timestamp': timezone.now().isoformat(),
            }
            
            if include_args:
                log_data['args'] = str(args)
                log_data['kwargs'] = str(kwargs)
            
            logger.info(f"Executing {func.__name__}", extra=log_data)
            
            try:
                result = func(*args, **kwargs)
                execution_time = time.time() - start_time
                
                log_data.update({
                    'status': 'success',
                    'execution_time': execution_time,
                })
                
                if include_result:
                    log_data['result'] = str(result)[:1000]  # Limit result size
                
                logger.info(
                    f"Completed {func.__name__} in {execution_time:.4f}s",
                    extra=log_data
                )
                
                return result
                
            except Exception as e:
                execution_time = time.time() - start_time
                
                log_data.update({
                    'status': 'error',
                    'execution_time': execution_time,
                    'error': str(e),
                    'error_type': type(e).__name__,
                })
                
                logger.error(
                    f"Failed {func.__name__} after {execution_time:.4f}s: {str(e)}",
                    extra=log_data
                )
                
                raise
        return wrapper
    return decorator


def log_user_action(action: str, object_type: str = ''):
    """
    Decorator to log user actions.
    
    Args:
        action: Action description
        object_type: Type of object being acted upon
        
    Returns:
        Decorated function
    """
    def decorator(func):
        @wraps(func)
        def wrapper(request, *args, **kwargs):
            result = func(request, *args, **kwargs)
            
            if request.user.is_authenticated:
                log_data = {
                    'user_id': request.user.id,
                    'username': request.user.username,
                    'action': action,
                    'object_type': object_type,
                    'ip_address': get_client_ip(request),
                    'user_agent': request.META.get('HTTP_USER_AGENT', ''),
                    'timestamp': timezone.now().isoformat(),
                }
                
                # Add object ID if available
                obj_id = kwargs.get('pk') or kwargs.get('id')
                if obj_id:
                    log_data['object_id'] = obj_id
                
                logger.info(
                    f"User {request.user.username} performed {action}",
                    extra=log_data
                )
            
            return result
        return wrapper
    return decorator


# =============================================================================
# Error Handling Decorators
# =============================================================================

def handle_exceptions(default_response=None, log_errors: bool = True):
    """
    Decorator to handle exceptions gracefully.
    
    Args:
        default_response: Default response for exceptions
        log_errors: Whether to log errors
        
    Returns:
        Decorated function
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except Exception as e:
                if log_errors:
                    logger.error(
                        f"Exception in {func.__name__}: {str(e)}",
                        exc_info=True
                    )
                
                if default_response is not None:
                    return default_response
                
                # Return JSON error response for AJAX requests
                if len(args) > 0 and hasattr(args[0], 'META'):
                    request = args[0]
                    if request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest':
                        return JsonResponse(
                            {
                                'error': 'An error occurred',
                                'message': str(e) if settings.DEBUG else 'Internal server error'
                            },
                            status=500
                        )
                
                # Re-raise exception if no default response
                raise
        return wrapper
    return decorator


def retry_on_failure(max_retries: int = 3, delay: float = 1.0, backoff: float = 2.0):
    """
    Decorator to retry function on failure.
    
    Args:
        max_retries: Maximum number of retries
        delay: Initial delay between retries
        backoff: Backoff multiplier for delay
        
    Returns:
        Decorated function
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            current_delay = delay
            
            for attempt in range(max_retries + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    
                    if attempt < max_retries:
                        logger.warning(
                            f"Attempt {attempt + 1} failed for {func.__name__}: {str(e)}. "
                            f"Retrying in {current_delay}s..."
                        )
                        time.sleep(current_delay)
                        current_delay *= backoff
                    else:
                        logger.error(
                            f"All {max_retries + 1} attempts failed for {func.__name__}: {str(e)}"
                        )
            
            # Re-raise the last exception
            raise last_exception
        return wrapper
    return decorator


# =============================================================================
# Class-based View Decorators
# =============================================================================

class DecoratorMixin:
    """
    Mixin to apply decorators to class-based views.
    
    Usage:
        class MyView(DecoratorMixin, View):
            decorators = [require_authentication(), rate_limit(100, 3600)]
            
            def get(self, request):
                return HttpResponse('Hello')
    """
    
    decorators = []
    
    @classmethod
    def as_view(cls, **initkwargs):
        view = super().as_view(**initkwargs)
        
        for decorator in cls.decorators:
            view = decorator(view)
        
        return view


# =============================================================================
# Export all decorators
# =============================================================================

__all__ = [
    # Authentication
    'require_authentication',
    'require_permission',
    'require_staff',
    'require_superuser',
    'require_group',
    'require_owner_or_staff',
    
    # Rate Limiting
    'rate_limit',
    'throttle_user',
    'throttle_ip',
    
    # Caching
    'cache_response',
    'cache_per_user',
    'invalidate_cache_on_change',
    
    # Validation
    'validate_json',
    'validate_params',
    'require_fields',
    
    # HTTP Methods
    'require_ajax',
    'require_post',
    'require_get',
    'allow_methods',
    
    # Logging
    'log_execution',
    'log_user_action',
    
    # Error Handling
    'handle_exceptions',
    'retry_on_failure',
    
    # Class-based Views
    'DecoratorMixin',
]