"""Core Pagination

This module contains custom pagination classes for the Adtlas project.
It provides various pagination styles for both web views and API responses.
"""

import math
from typing import Any, Dict, List, Optional
from urllib.parse import urlencode

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.http import Http404
from django.utils.translation import gettext_lazy as _
from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination
from rest_framework.response import Response
from rest_framework.utils.urls import replace_query_param, remove_query_param


# ============================================================================
# Django REST Framework Pagination Classes
# ============================================================================

class StandardResultsSetPagination(PageNumberPagination):
    """Standard pagination class for API responses."""
    
    page_size = 20
    page_size_query_param = 'page_size'
    max_page_size = 100
    page_query_param = 'page'
    
    def get_paginated_response(self, data):
        """Return paginated response with metadata.
        
        Args:
            data: Serialized data
            
        Returns:
            Response with pagination metadata
        """
        return Response({
            'pagination': {
                'next': self.get_next_link(),
                'previous': self.get_previous_link(),
                'count': self.page.paginator.count,
                'total_pages': self.page.paginator.num_pages,
                'current_page': self.page.number,
                'page_size': self.get_page_size(self.request),
                'has_next': self.page.has_next(),
                'has_previous': self.page.has_previous(),
                'start_index': self.page.start_index(),
                'end_index': self.page.end_index()
            },
            'results': data
        })


class LargeResultsSetPagination(PageNumberPagination):
    """Pagination class for large datasets."""
    
    page_size = 50
    page_size_query_param = 'page_size'
    max_page_size = 200
    page_query_param = 'page'
    
    def get_paginated_response(self, data):
        """Return paginated response with metadata."""
        return Response({
            'pagination': {
                'next': self.get_next_link(),
                'previous': self.get_previous_link(),
                'count': self.page.paginator.count,
                'total_pages': self.page.paginator.num_pages,
                'current_page': self.page.number,
                'page_size': self.get_page_size(self.request),
                'has_next': self.page.has_next(),
                'has_previous': self.page.has_previous()
            },
            'results': data
        })


class SmallResultsSetPagination(PageNumberPagination):
    """Pagination class for small datasets."""
    
    page_size = 10
    page_size_query_param = 'page_size'
    max_page_size = 50
    page_query_param = 'page'
    
    def get_paginated_response(self, data):
        """Return paginated response with metadata."""
        return Response({
            'pagination': {
                'next': self.get_next_link(),
                'previous': self.get_previous_link(),
                'count': self.page.paginator.count,
                'total_pages': self.page.paginator.num_pages,
                'current_page': self.page.number,
                'page_size': self.get_page_size(self.request),
                'has_next': self.page.has_next(),
                'has_previous': self.page.has_previous()
            },
            'results': data
        })


class CustomLimitOffsetPagination(LimitOffsetPagination):
    """Custom limit/offset pagination class."""
    
    default_limit = 20
    limit_query_param = 'limit'
    offset_query_param = 'offset'
    max_limit = 100
    
    def get_paginated_response(self, data):
        """Return paginated response with metadata."""
        return Response({
            'pagination': {
                'next': self.get_next_link(),
                'previous': self.get_previous_link(),
                'count': self.count,
                'limit': self.get_limit(self.request),
                'offset': self.get_offset(self.request)
            },
            'results': data
        })


class CursorPaginationWithCount(PageNumberPagination):
    """Cursor pagination with count for performance."""
    
    page_size = 20
    page_size_query_param = 'page_size'
    max_page_size = 100
    ordering = '-created_at'  # Default ordering
    
    def get_paginated_response(self, data):
        """Return paginated response with metadata."""
        return Response({
            'pagination': {
                'next': self.get_next_link(),
                'previous': self.get_previous_link(),
                'count': self.page.paginator.count,
                'current_page': self.page.number,
                'total_pages': self.page.paginator.num_pages,
                'page_size': self.get_page_size(self.request)
            },
            'results': data
        })


# ============================================================================
# Django Template Pagination Classes
# ============================================================================

class CustomPaginator:
    """Custom paginator for Django templates."""
    
    def __init__(self, queryset, per_page=20, orphans=0, allow_empty_first_page=True):
        """Initialize paginator.
        
        Args:
            queryset: QuerySet to paginate
            per_page: Number of items per page
            orphans: Minimum number of items on last page
            allow_empty_first_page: Allow empty first page
        """
        self.paginator = Paginator(
            queryset, 
            per_page, 
            orphans=orphans, 
            allow_empty_first_page=allow_empty_first_page
        )
        self.per_page = per_page
    
    def get_page(self, page_number):
        """Get page object.
        
        Args:
            page_number: Page number to retrieve
            
        Returns:
            Page object
            
        Raises:
            Http404: If page doesn't exist
        """
        try:
            page = self.paginator.page(page_number)
        except PageNotAnInteger:
            # If page is not an integer, deliver first page
            page = self.paginator.page(1)
        except EmptyPage:
            # If page is out of range, deliver last page
            page = self.paginator.page(self.paginator.num_pages)
        
        return page
    
    def get_page_info(self, page_number):
        """Get comprehensive page information.
        
        Args:
            page_number: Page number
            
        Returns:
            Dictionary with page information
        """
        page = self.get_page(page_number)
        
        return {
            'page': page,
            'paginator': self.paginator,
            'page_range': self.get_elided_page_range(page.number),
            'has_previous': page.has_previous(),
            'has_next': page.has_next(),
            'previous_page_number': page.previous_page_number() if page.has_previous() else None,
            'next_page_number': page.next_page_number() if page.has_next() else None,
            'start_index': page.start_index(),
            'end_index': page.end_index(),
            'total_count': self.paginator.count,
            'num_pages': self.paginator.num_pages,
            'current_page': page.number
        }
    
    def get_elided_page_range(self, current_page, on_each_side=3, on_ends=2):
        """Get elided page range for pagination display.
        
        Args:
            current_page: Current page number
            on_each_side: Pages to show on each side of current
            on_ends: Pages to show at the ends
            
        Returns:
            Generator of page numbers and ellipsis
        """
        try:
            return self.paginator.get_elided_page_range(
                current_page, 
                on_each_side=on_each_side, 
                on_ends=on_ends
            )
        except AttributeError:
            # Fallback for older Django versions
            return self._get_elided_page_range_fallback(
                current_page, on_each_side, on_ends
            )
    
    def _get_elided_page_range_fallback(self, current_page, on_each_side, on_ends):
        """Fallback implementation for elided page range."""
        num_pages = self.paginator.num_pages
        
        if num_pages <= (on_each_side + on_ends) * 2:
            return range(1, num_pages + 1)
        
        # Calculate ranges
        start_range = range(1, min(on_ends + 1, num_pages + 1))
        end_range = range(max(num_pages - on_ends + 1, 1), num_pages + 1)
        
        middle_start = max(current_page - on_each_side, 1)
        middle_end = min(current_page + on_each_side + 1, num_pages + 1)
        middle_range = range(middle_start, middle_end)
        
        # Combine ranges
        page_range = []
        
        # Add start range
        page_range.extend(start_range)
        
        # Add ellipsis if needed
        if middle_start > max(start_range) + 1:
            page_range.append('…')
        
        # Add middle range
        for page in middle_range:
            if page not in page_range:
                page_range.append(page)
        
        # Add ellipsis if needed
        if middle_end < min(end_range):
            page_range.append('…')
        
        # Add end range
        for page in end_range:
            if page not in page_range:
                page_range.append(page)
        
        return page_range


# ============================================================================
# Pagination Utilities
# ============================================================================

def paginate_queryset(queryset, page_number, per_page=20):
    """Paginate a queryset.
    
    Args:
        queryset: QuerySet to paginate
        page_number: Page number to retrieve
        per_page: Number of items per page
        
    Returns:
        Tuple of (page_object, paginator)
    """
    paginator = CustomPaginator(queryset, per_page)
    page = paginator.get_page(page_number)
    
    return page, paginator


def get_pagination_context(request, queryset, per_page=20):
    """Get pagination context for templates.
    
    Args:
        request: HTTP request object
        queryset: QuerySet to paginate
        per_page: Number of items per page
        
    Returns:
        Dictionary with pagination context
    """
    page_number = request.GET.get('page', 1)
    paginator = CustomPaginator(queryset, per_page)
    page_info = paginator.get_page_info(page_number)
    
    # Add query parameters for pagination links
    query_params = request.GET.copy()
    if 'page' in query_params:
        del query_params['page']
    
    page_info['query_string'] = query_params.urlencode()
    
    return page_info


def build_pagination_url(base_url, page_number, query_params=None):
    """Build pagination URL.
    
    Args:
        base_url: Base URL
        page_number: Page number
        query_params: Additional query parameters
        
    Returns:
        Complete pagination URL
    """
    params = query_params.copy() if query_params else {}
    params['page'] = page_number
    
    query_string = urlencode(params)
    return f"{base_url}?{query_string}"


def get_page_range_info(paginator, current_page, window_size=5):
    """Get page range information for pagination display.
    
    Args:
        paginator: Paginator object
        current_page: Current page number
        window_size: Size of page window
        
    Returns:
        Dictionary with page range information
    """
    num_pages = paginator.num_pages
    
    # Calculate start and end of page window
    start_page = max(1, current_page - window_size // 2)
    end_page = min(num_pages, start_page + window_size - 1)
    
    # Adjust start_page if end_page is at the limit
    if end_page - start_page < window_size - 1:
        start_page = max(1, end_page - window_size + 1)
    
    return {
        'start_page': start_page,
        'end_page': end_page,
        'page_range': range(start_page, end_page + 1),
        'show_first': start_page > 1,
        'show_last': end_page < num_pages,
        'show_prev_ellipsis': start_page > 2,
        'show_next_ellipsis': end_page < num_pages - 1
    }


# ============================================================================
# AJAX Pagination
# ============================================================================

class AjaxPaginationMixin:
    """Mixin for AJAX pagination in class-based views."""
    
    paginate_by = 20
    ajax_template_name = None
    
    def get_context_data(self, **kwargs):
        """Add pagination context."""
        context = super().get_context_data(**kwargs)
        
        if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
            context['is_ajax'] = True
        
        return context
    
    def get_template_names(self):
        """Return appropriate template for AJAX requests."""
        if (self.request.headers.get('X-Requested-With') == 'XMLHttpRequest' 
            and self.ajax_template_name):
            return [self.ajax_template_name]
        
        return super().get_template_names()


# ============================================================================
# Infinite Scroll Pagination
# ============================================================================

class InfiniteScrollPagination(PageNumberPagination):
    """Pagination class for infinite scroll."""
    
    page_size = 20
    page_size_query_param = 'page_size'
    max_page_size = 50
    
    def get_paginated_response(self, data):
        """Return response optimized for infinite scroll."""
        return Response({
            'has_next': self.page.has_next(),
            'next_page': self.page.next_page_number() if self.page.has_next() else None,
            'count': self.page.paginator.count,
            'results': data
        })


# ============================================================================
# Search Pagination
# ============================================================================

class SearchPagination(StandardResultsSetPagination):
    """Pagination class optimized for search results."""
    
    page_size = 15
    max_page_size = 50
    
    def get_paginated_response(self, data):
        """Return search-optimized pagination response."""
        response = super().get_paginated_response(data)
        
        # Add search-specific metadata
        response.data['search_info'] = {
            'total_results': self.page.paginator.count,
            'results_per_page': self.get_page_size(self.request),
            'search_time': getattr(self, 'search_time', None)
        }
        
        return response


# ============================================================================
# Mobile Pagination
# ============================================================================

class MobilePagination(PageNumberPagination):
    """Pagination class optimized for mobile devices."""
    
    page_size = 10  # Smaller page size for mobile
    page_size_query_param = 'page_size'
    max_page_size = 25
    
    def get_paginated_response(self, data):
        """Return mobile-optimized pagination response."""
        return Response({
            'pagination': {
                'next': self.get_next_link(),
                'previous': self.get_previous_link(),
                'has_next': self.page.has_next(),
                'has_previous': self.page.has_previous(),
                'current_page': self.page.number,
                'total_pages': self.page.paginator.num_pages,
                'count': self.page.paginator.count
            },
            'results': data
        })


# ============================================================================
# Export Pagination
# ============================================================================

class ExportPagination:
    """Pagination class for data export."""
    
    def __init__(self, queryset, chunk_size=1000):
        """Initialize export pagination.
        
        Args:
            queryset: QuerySet to export
            chunk_size: Size of each chunk
        """
        self.queryset = queryset
        self.chunk_size = chunk_size
        self.total_count = queryset.count()
        self.total_chunks = math.ceil(self.total_count / chunk_size)
    
    def get_chunk(self, chunk_number):
        """Get specific chunk of data.
        
        Args:
            chunk_number: Chunk number (1-based)
            
        Returns:
            QuerySet chunk
        """
        if chunk_number < 1 or chunk_number > self.total_chunks:
            return self.queryset.none()
        
        start = (chunk_number - 1) * self.chunk_size
        end = start + self.chunk_size
        
        return self.queryset[start:end]
    
    def get_all_chunks(self):
        """Generator that yields all chunks.
        
        Yields:
            QuerySet chunks
        """
        for chunk_number in range(1, self.total_chunks + 1):
            yield self.get_chunk(chunk_number)
    
    def get_chunk_info(self, chunk_number):
        """Get information about a specific chunk.
        
        Args:
            chunk_number: Chunk number
            
        Returns:
            Dictionary with chunk information
        """
        start = (chunk_number - 1) * self.chunk_size + 1
        end = min(chunk_number * self.chunk_size, self.total_count)
        
        return {
            'chunk_number': chunk_number,
            'total_chunks': self.total_chunks,
            'start_index': start,
            'end_index': end,
            'chunk_size': end - start + 1,
            'total_count': self.total_count
        }