"""
Enterprise-Ready Django Mixins

This module provides comprehensive mixin classes for the common application,
offering reusable functionality for views, forms, and models.

Features:
- AJAX response handling
- Message framework integration
- Pagination utilities
- Permission checking
- Filtering and searching
- Caching mechanisms
- JSON response handling
- Logging and monitoring
- Database transaction handling

Author: Focus Development Team
Version: 2.0.0
License: Proprietary
"""

import json
import logging
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Union

from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin as DjangoPermissionRequiredMixin
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.db import transaction
from django.db.models import Q, QuerySet
from django.http import JsonResponse, HttpRequest, HttpResponse, HttpResponseRedirect
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import cache_page
from django.views.decorators.csrf import csrf_exempt
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse_lazy
from django.views.generic import View
from django.views.generic.detail import SingleObjectMixin
from django.contrib.auth import get_user_model

from .utils import create_api_response, get_client_ip, get_user_agent

logger = logging.getLogger(__name__)
User = get_user_model()


# ============================================================================
# MODEL MIXINS
# ============================================================================

class TimestampMixin:
    """
    Mixin to add created_at and updated_at fields to models.
    
    Automatically sets timestamps when saving model instances.
    Should be used with models, not views.
    """
    def save(self, *args, **kwargs):
        """Override save to set timestamps."""
        if not self.pk:
            self.created_at = timezone.now()
        self.updated_at = timezone.now()
        super().save(*args, **kwargs)


# ============================================================================
# RESPONSE MIXINS
# ============================================================================

class AjaxResponseMixin:
    """
    Mixin to handle AJAX requests with proper JSON responses.
    
    Provides methods to detect AJAX requests and return appropriate
    JSON responses for both success and error cases.
    """
    
    def dispatch(self, request, *args, **kwargs):
        """Store request for later use."""
        self.request = request
        return super().dispatch(request, *args, **kwargs)
    
    def is_ajax(self):
        """Check if request is AJAX."""
        return (
            self.request.headers.get('X-Requested-With') == 'XMLHttpRequest' or
            self.request.content_type == 'application/json'
        )
    
    def form_valid(self, form):
        """Handle valid form submission for AJAX requests."""
        response = super().form_valid(form)
        
        if self.is_ajax():
            data = {
                'success': True,
                'message': getattr(self, 'success_message', _('Operation completed successfully.')),
                'redirect_url': self.get_success_url() if hasattr(self, 'get_success_url') else None,
            }
            
            # Add object data if available
            if hasattr(self, 'object') and self.object:
                data['object'] = {
                    'id': self.object.pk,
                    'str': str(self.object),
                }
            
            return JsonResponse(create_api_response(success=True, data=data))
        
        return response
    
    def form_invalid(self, form):
        """Handle invalid form submission for AJAX requests."""
        response = super().form_invalid(form)
        
        if self.is_ajax():
            errors = {}
            for field, field_errors in form.errors.items():
                errors[field] = [str(error) for error in field_errors]
            
            return JsonResponse(
                create_api_response(
                    success=False,
                    message=_('Please correct the errors below.'),
                    data={'errors': errors},
                    status_code=400
                ),
                status=400
            )
        
        return response


class JSONResponseMixin:
    """
    Mixin to add JSON response capabilities to views.
    
    Provides methods to render JSON responses with proper
    content types and status codes.
    """
    
    def render_json_response(self, data: Dict[str, Any], status: int = 200) -> JsonResponse:
        """Render JSON response with proper headers."""
        return JsonResponse(
            data,
            status=status,
            json_dumps_params={'ensure_ascii': False, 'indent': 2}
        )
    
    def render_json_error(self, message: str, status: int = 400, **kwargs) -> JsonResponse:
        """Render JSON error response."""
        data = create_api_response(
            success=False,
            message=message,
            status_code=status,
            **kwargs
        )
        return self.render_json_response(data, status=status)
    
    def render_json_success(self, message: str = None, data: Any = None, **kwargs) -> JsonResponse:
        """Render JSON success response."""
        response_data = create_api_response(
            success=True,
            message=message,
            data=data,
            **kwargs
        )
        return self.render_json_response(response_data)
    
    def render_to_json_response(self, context, **response_kwargs):
        """
        Return a JSON response containing 'context' as payload.
        """
        return JsonResponse(self.get_data(context), **response_kwargs)
    
    def get_data(self, context):
        """
        Return an object that will be serialized as JSON.
        """
        return context


# ============================================================================
# MESSAGE MIXINS
# ============================================================================

class MessageMixin:
    """
    Mixin to add message framework support to views.
    
    Automatically adds success/error messages based on
    form validation and view operations.
    """
    
    success_message = None
    error_message = None
    
    def get_success_message(self, cleaned_data: Dict[str, Any] = None) -> str:
        """Get success message with optional formatting."""
        if self.success_message:
            if cleaned_data:
                return self.success_message % cleaned_data
            return self.success_message
        return _('Operation completed successfully.')
    
    def get_error_message(self) -> str:
        """Get error message."""
        return self.error_message or _('An error occurred. Please try again.')
    
    def form_valid(self, form):
        """Add success message on valid form."""
        response = super().form_valid(form)
        
        if not self.is_ajax() if hasattr(self, 'is_ajax') else True:
            success_message = self.get_success_message(form.cleaned_data)
            messages.success(self.request, success_message)
        
        return response
    
    def form_invalid(self, form):
        """Add error message on invalid form."""
        response = super().form_invalid(form)
        
        if not self.is_ajax() if hasattr(self, 'is_ajax') else True:
            error_message = self.get_error_message()
            messages.error(self.request, error_message)
        
        return response
    
    def delete(self, request, *args, **kwargs):
        """Add success message on delete."""
        response = super().delete(request, *args, **kwargs)
        
        success_message = getattr(self, 'success_message', _('Item deleted successfully.'))
        messages.success(request, success_message)
        
        return response


# ============================================================================
# PAGINATION MIXINS
# ============================================================================

class PaginationMixin:
    """
    Enhanced pagination mixin with additional features.
    
    Provides pagination with customizable page sizes,
    page range display, and AJAX support.
    """
    
    paginate_by = 25
    paginate_orphans = 5
    page_size_options = [10, 25, 50, 100]
    max_page_size = 100
    page_kwarg = 'page'
    
    def get_paginate_by(self, queryset: QuerySet) -> int:
        """Get pagination size from request or default."""
        page_size = self.request.GET.get('page_size')
        
        if page_size:
            try:
                page_size = int(page_size)
                if page_size in self.page_size_options and page_size <= self.max_page_size:
                    return page_size
            except (ValueError, TypeError):
                pass
        
        return self.paginate_by
    
    def paginate_queryset(self, queryset, page_size):
        """
        Paginate the queryset, if needed.
        """
        paginator = Paginator(queryset, page_size, orphans=self.paginate_orphans)
        page = self.request.GET.get(self.page_kwarg)
        
        try:
            page_obj = paginator.page(page)
        except PageNotAnInteger:
            page_obj = paginator.page(1)
        except EmptyPage:
            page_obj = paginator.page(paginator.num_pages)
            
        return paginator, page_obj, page_obj.object_list, page_obj.has_other_pages()
    
    def get_context_data(self, **kwargs):
        """Add pagination context."""
        context = super().get_context_data(**kwargs)
        
        if 'page_obj' in context:
            page_obj = context['page_obj']
            
            # Add page range for display
            page_range = self.get_page_range(page_obj)
            context.update({
                'page_range': page_range,
                'page_size_options': self.page_size_options,
                'current_page_size': self.get_paginate_by(None),
                'total_items': page_obj.paginator.count,
            })
        
        return context
    
    def get_page_range(self, page_obj, window: int = 5) -> List[int]:
        """Get page range for pagination display."""
        current_page = page_obj.number
        total_pages = page_obj.paginator.num_pages
        
        start_page = max(1, current_page - window // 2)
        end_page = min(total_pages, start_page + window - 1)
        
        # Adjust start if we're near the end
        if end_page - start_page < window - 1:
            start_page = max(1, end_page - window + 1)
        
        return list(range(start_page, end_page + 1))



# ============================================================================
# PERMISSION MIXINS
# ============================================================================

class PermissionRequiredMixin(DjangoPermissionRequiredMixin):
    """
    Enhanced permission required mixin with additional features.
    
    Provides object-level permissions, AJAX-aware error handling,
    and detailed permission checking.
    """
    permission_denied_message = _("You don't have permission to access this resource.")
    
    def get_permission_required(self):
        """
        Override this method to override the permission_required attribute.
        """
        if self.permission_required is None:
            raise NotImplementedError(
                'PermissionRequiredMixin requires either a definition of '
                'permission_required or an implementation of get_permission_required()'
            )
        if isinstance(self.permission_required, str):
            perms = (self.permission_required,)
        else:
            perms = self.permission_required
        return perms
    
    def has_permission(self) -> bool:
        """Check if user has required permissions."""
        perms = self.get_permission_required()
        return self.request.user.has_perms(perms)
    
    def has_object_permission(self, obj: Any) -> bool:
        """Check object-level permissions."""
        # Override in subclasses for object-specific permissions
        return True
    
    def dispatch(self, request, *args, **kwargs):
        """Check permissions before dispatching."""
        if not self.has_permission():
            return self.handle_no_permission()
        
        # Check object permissions for detail views
        if hasattr(self, 'get_object'):
            try:
                obj = self.get_object()
                if not self.has_object_permission(obj):
                    return self.handle_no_permission()
            except AttributeError:
                # get_object not available yet
                pass
        
        return super().dispatch(request, *args, **kwargs)
    
    def handle_no_permission(self):
        """Handle permission denied with AJAX support."""
        if hasattr(self, 'is_ajax') and self.is_ajax():
            return JsonResponse(
                create_api_response(
                    success=False,
                    message=self.permission_denied_message,
                    status_code=403
                ),
                status=403
            )
        
        return super().handle_no_permission()


class OwnerRequiredMixin:
    """
    Mixin to ensure that only the owner of an object can access it.
    
    Checks ownership based on a configurable field and provides
    proper error handling for unauthorized access.
    """
    owner_field = 'user'  # Field name that contains the owner
    
    def get_owner_field(self):
        """Get the field name that contains the owner."""
        return self.owner_field
    
    def get_object(self, queryset=None):
        """Get object and check ownership."""
        obj = super().get_object(queryset)
        owner_field = self.get_owner_field()
        
        if hasattr(obj, owner_field):
            if getattr(obj, owner_field) != self.request.user:
                raise PermissionDenied(_("You can only access your own objects."))
        else:
            raise AttributeError(f"Object has no attribute '{owner_field}'")
        
        return obj


class MultiplePermissionsRequiredMixin:
    """
    Mixin to check multiple permissions with AND/OR logic.
    
    Allows checking multiple permissions with configurable logic
    for complex permission requirements.
    """
    permissions_required = []
    permission_logic = 'AND'  # 'AND' or 'OR'
    
    def get_permissions_required(self):
        """Get list of required permissions."""
        return self.permissions_required
    
    def has_permissions(self):
        """Check if user has required permissions based on logic."""
        perms = self.get_permissions_required()
        if not perms:
            return True
        
        user = self.request.user
        
        if self.permission_logic == 'AND':
            return user.has_perms(perms)
        elif self.permission_logic == 'OR':
            return any(user.has_perm(perm) for perm in perms)
        
        return False
    
    def dispatch(self, request, *args, **kwargs):
        """Check permissions before dispatching."""
        if not self.has_permissions():
            raise PermissionDenied(_("Insufficient permissions."))
        return super().dispatch(request, *args, **kwargs)


class SuperUserRequiredMixin:
    """
    Mixin to restrict access to superusers only.
    
    Ensures only superusers can access the view.
    """
    def dispatch(self, request, *args, **kwargs):
        """Check superuser status."""
        if not request.user.is_superuser:
            raise PermissionDenied(_("Superuser access required."))
        return super().dispatch(request, *args, **kwargs)


class StaffRequiredMixin:
    """
    Mixin to restrict access to staff users only.
    
    Ensures only staff users can access the view.
    """
    def dispatch(self, request, *args, **kwargs):
        """Check staff status."""
        if not request.user.is_staff:
            raise PermissionDenied(_("Staff access required."))
        return super().dispatch(request, *args, **kwargs)


# ============================================================================
# FILTERING AND SEARCH MIXINS
# ============================================================================

class FilterMixin:
    """
    Mixin to add filtering capabilities to list views.
    
    Supports multiple filter types including exact match,
    range filters, and choice filters.
    """
    
    filter_fields = []
    date_filter_fields = []
    
    def get_filter_fields(self):
        """Get list of filterable fields."""
        return self.filter_fields
    
    def get_queryset(self):
        """Apply filters to queryset."""
        queryset = super().get_queryset()
        return self.apply_filters(queryset)
    
    def apply_filters(self, queryset: QuerySet) -> QuerySet:
        """Apply filters based on request parameters."""
        for field in self.get_filter_fields():
            value = self.request.GET.get(field)
            if value:
                # Handle different filter types
                if '__' in field:
                    # Complex filter (e.g., 'created_at__gte')
                    queryset = queryset.filter(**{field: value})
                else:
                    # Simple filter
                    queryset = queryset.filter(**{field: value})
        
        # Apply date range filters
        for field in self.date_filter_fields:
            start_date = self.request.GET.get(f'{field}_start')
            end_date = self.request.GET.get(f'{field}_end')
            
            if start_date:
                try:
                    start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
                    queryset = queryset.filter(**{f'{field}__gte': start_date})
                except ValueError:
                    pass
            
            if end_date:
                try:
                    end_date = datetime.strptime(end_date, '%Y-%m-%d').date()
                    queryset = queryset.filter(**{f'{field}__lte': end_date})
                except ValueError:
                    pass
        
        return queryset
    
    def filter_queryset(self, queryset):
        """
        Filter the queryset based on GET parameters.
        """
        return self.apply_filters(queryset)
    
    def get_context_data(self, **kwargs):
        """Add filter context."""
        context = super().get_context_data(**kwargs)
        
        # Add current filters to context
        current_filters = {}
        for field in self.get_filter_fields() + self.date_filter_fields:
            value = self.request.GET.get(field)
            if value:
                current_filters[field] = value
        
        context['current_filters'] = current_filters
        return context


class SearchMixin:
    """
    Mixin to add search capabilities to list views.
    
    Supports full-text search across multiple fields
    with configurable search behavior.
    """
    
    search_fields = []
    search_param = 'q'
    
    def get_search_fields(self):
        """Get list of searchable fields."""
        return self.search_fields
    
    def get_search_query(self):
        """Get search query from request."""
        return self.request.GET.get(self.search_param, '').strip()
    
    def get_queryset(self):
        """Apply search to queryset."""
        queryset = super().get_queryset()
        return self.apply_search(queryset)
    
    def apply_search(self, queryset: QuerySet) -> QuerySet:
        """Apply search filter to queryset."""
        query = self.get_search_query()
        
        if query and self.get_search_fields():
            search_query = Q()
            
            for field in self.get_search_fields():
                # Support different search types
                if field.endswith('__exact'):
                    search_query |= Q(**{field: query})
                elif field.endswith('__icontains'):
                    search_query |= Q(**{field: query})
                else:
                    # Default to icontains
                    search_query |= Q(**{f'{field}__icontains': query})
            
            queryset = queryset.filter(search_query).distinct()
        
        return queryset
    
    def search_queryset(self, queryset):
        """
        Filter the queryset based on search query.
        """
        return self.apply_search(queryset)
    
    def get_context_data(self, **kwargs):
        """Add search context."""
        context = super().get_context_data(**kwargs)
        context['search_query'] = self.get_search_query()
        return context


# ============================================================================
# CACHING MIXINS
# ============================================================================

class CacheMixin:
    """
    Mixin to add caching capabilities to views.
    
    Provides automatic caching of view responses with
    configurable cache keys and timeouts.
    """
    
    cache_timeout = 300  # 5 minutes
    cache_key_prefix = 'view'
    
    def get_cache_key(self) -> str:
        """Generate cache key for the view."""
        key_parts = [
            self.cache_key_prefix,
            self.__class__.__name__.lower(),
            str(self.request.user.id) if self.request.user.is_authenticated else 'anonymous',
        ]
        
        # Add query parameters to cache key
        if self.request.GET:
            query_string = '&'.join([
                f'{k}={v}' for k, v in sorted(self.request.GET.items())
            ])
            key_parts.append(query_string)
        
        return ':'.join(key_parts)
    
    def get_cache_timeout(self):
        """Get cache timeout."""
        return self.cache_timeout
    
    def get_cached_response(self):
        """Get cached response if available."""
        cache_key = self.get_cache_key()
        return cache.get(cache_key)
    
    def set_cached_response(self, response):
        """Cache the response."""
        cache_key = self.get_cache_key()
        cache.set(cache_key, response, self.get_cache_timeout())
    
    def dispatch(self, request, *args, **kwargs):
        """Check cache before processing request."""
        # Only cache GET requests
        if request.method == 'GET':
            cached_response = self.get_cached_response()
            if cached_response:
                return cached_response
        
        response = super().dispatch(request, *args, **kwargs)
        
        # Cache successful GET responses
        if request.method == 'GET' and response.status_code == 200:
            self.set_cached_response(response)
        
        return response


# ============================================================================
# LOGGING MIXINS
# ============================================================================

class LoggingMixin:
    """
    Mixin to add comprehensive logging to views.
    
    Logs view access, performance metrics, and errors
    with detailed context information.
    """
    
    def dispatch(self, request, *args, **kwargs):
        """Log request and measure performance."""
        start_time = timezone.now()
        
        # Log request start
        logger.info(
            f"View {self.__class__.__name__} accessed",
            extra={
                'user': request.user.username if request.user.is_authenticated else 'anonymous',
                'ip_address': get_client_ip(request),
                'user_agent': get_user_agent(request),
                'method': request.method,
                'path': request.path,
                'query_params': dict(request.GET),
            }
        )
        
        try:
            response = super().dispatch(request, *args, **kwargs)
            
            # Log successful response
            duration = (timezone.now() - start_time).total_seconds()
            logger.info(
                f"View {self.__class__.__name__} completed successfully",
                extra={
                    'duration': duration,
                    'status_code': response.status_code,
                }
            )
            
            return response
            
        except Exception as e:
            # Log error
            duration = (timezone.now() - start_time).total_seconds()
            logger.error(
                f"View {self.__class__.__name__} failed",
                extra={
                    'duration': duration,
                    'error': str(e),
                    'error_type': type(e).__name__,
                },
                exc_info=True
            )
            raise


# ============================================================================
# TRANSACTION MIXINS
# ============================================================================

class AtomicTransactionMixin:
    """
    Mixin to wrap view operations in database transactions.
    
    Ensures data consistency by wrapping form processing
    and other operations in atomic transactions.
    """
    
    @method_decorator(transaction.atomic)
    def dispatch(self, request, *args, **kwargs):
        """Wrap entire view in transaction."""
        return super().dispatch(request, *args, **kwargs)
    
    def form_valid(self, form):
        """Process form in transaction."""
        try:
            with transaction.atomic():
                return super().form_valid(form)
        except Exception as e:
            logger.error(f"Transaction failed in {self.__class__.__name__}: {e}")
            if hasattr(self, 'form_invalid'):
                form.add_error(None, _('An error occurred while saving. Please try again.'))
                return self.form_invalid(form)
            raise


# ============================================================================
# SOFT DELETE MIXINS
# ============================================================================

class SoftDeleteMixin:
    """
    Mixin to handle soft deletion of model instances.
    
    Instead of actually deleting objects, marks them
    as deleted with a timestamp.
    """
    
    def delete(self, request, *args, **kwargs):
        """Soft delete the object."""
        self.object = self.get_object()
        
        if hasattr(self.object, 'is_deleted'):
            self.object.is_deleted = True
            if hasattr(self.object, 'deleted_at'):
                self.object.deleted_at = timezone.now()
            if hasattr(self.object, 'deleted_by'):
                self.object.deleted_by = request.user
            
            self.object.save(update_fields=['is_deleted', 'deleted_at', 'deleted_by'])
        else:
            # Fallback to hard delete
            self.object.delete()
        
        return HttpResponseRedirect(self.get_success_url())

    def get_queryset(self):
        """
        Exclude soft deleted objects by default.
        """
        queryset = super().get_queryset()
        if hasattr(queryset.model, 'is_deleted'):
            return queryset.filter(is_deleted=False)
        return queryset


# ============================================================================
# UTILITY MIXINS
# ============================================================================

class UserTrackingMixin:
    """
    Mixin to automatically set user tracking fields.
    
    Sets created_by and updated_by fields based on
    the current request user.
    """
    
    def form_valid(self, form):
        """Set user tracking fields."""
        if hasattr(form.instance, 'created_by') and not form.instance.pk:
            form.instance.created_by = self.request.user
        
        if hasattr(form.instance, 'updated_by'):
            form.instance.updated_by = self.request.user
        
        return super().form_valid(form)


class FormValidationMixin:
    """
    Mixin to add custom form validation logic.
    
    Provides hooks for additional validation beyond
    standard form validation.
    """
    
    def form_valid(self, form):
        """
        Add custom validation logic here.
        """
        # Perform additional validation
        if hasattr(self, 'extra_form_valid'):
            extra_valid = self.extra_form_valid(form)
            if not extra_valid:
                return self.form_invalid(form)
        
        return super().form_valid(form)
    
    def extra_form_valid(self, form):
        """
        Override this method to add custom validation.
        Return False to make form invalid.
        """
        return True


class RedirectMixin:
    """
    Mixin to handle redirects based on conditions.
    
    Provides conditional redirects before view processing.
    """
    
    def get_redirect_url(self):
        """
        Override this method to provide redirect URL.
        """
        return None
    
    def dispatch(self, request, *args, **kwargs):
        """Check for redirect before processing."""
        redirect_url = self.get_redirect_url()
        if redirect_url:
            return redirect(redirect_url)
        return super().dispatch(request, *args, **kwargs)