"""Pagination helpers — page-based and cursor-based.

Framework-agnostic; works with any iterable or Django QuerySet.

Usage (page-based)::

    from simorgh.shared.utils.pagination import paginate_queryset

    result = paginate_queryset(MyModel.objects.all(), page=2, page_size=25)
    # result.items  → sliced queryset
    # result.total  → total count
    # result.pages  → total pages
    # result.has_next / .has_prev

Usage (cursor-based)::

    from simorgh.shared.utils.pagination import CursorPage, encode_cursor, decode_cursor

    cursor = encode_cursor({"id": 42, "created_at": "2024-01-01T00:00:00"})
    params = decode_cursor(cursor)   # {"id": 42, "created_at": "..."}
"""
from __future__ import annotations

import base64
import json
import math
from dataclasses import dataclass, field
from typing import Any, Generic, Sequence, TypeVar

__all__ = [
    "PageResult",
    "paginate_queryset",
    "encode_cursor",
    "decode_cursor",
    "CursorPage",
]

T = TypeVar("T")

DEFAULT_PAGE_SIZE = 25
MAX_PAGE_SIZE = 200


# ---------------------------------------------------------------------------
# Page-based
# ---------------------------------------------------------------------------

@dataclass
class PageResult(Generic[T]):
    """Result of a page-based pagination operation."""

    items: Sequence[T]
    total: int
    page: int
    page_size: int
    pages: int = field(init=False)
    has_next: bool = field(init=False)
    has_prev: bool = field(init=False)

    def __post_init__(self) -> None:
        self.pages = math.ceil(self.total / self.page_size) if self.page_size else 1
        self.has_next = self.page < self.pages
        self.has_prev = self.page > 1

    def as_dict(self) -> dict[str, Any]:
        return {
            "total": self.total,
            "page": self.page,
            "page_size": self.page_size,
            "pages": self.pages,
            "has_next": self.has_next,
            "has_prev": self.has_prev,
        }


def paginate_queryset(
    queryset: Any,
    page: int = 1,
    page_size: int = DEFAULT_PAGE_SIZE,
) -> PageResult:
    """Slice *queryset* for the given *page* / *page_size*.

    Works with any object that supports ``len()`` and slicing (Django QuerySet,
    list, tuple, etc.).

    :param queryset:  A Django QuerySet or sequence.
    :param page:      1-based page number (clamped to ≥ 1).
    :param page_size: Items per page (clamped to 1–:data:`MAX_PAGE_SIZE`).
    """
    page = max(1, page)
    page_size = max(1, min(page_size, MAX_PAGE_SIZE))
    offset = (page - 1) * page_size

    # Prefer QuerySet.count() to avoid loading all rows.
    if hasattr(queryset, "count"):
        total = queryset.count()
    else:
        total = len(queryset)

    items = queryset[offset: offset + page_size]
    return PageResult(items=items, total=total, page=page, page_size=page_size)


# ---------------------------------------------------------------------------
# Cursor-based
# ---------------------------------------------------------------------------

@dataclass
class CursorPage(Generic[T]):
    """Result of a cursor-based pagination operation."""

    items: Sequence[T]
    next_cursor: str | None
    prev_cursor: str | None
    has_next: bool
    has_prev: bool


def encode_cursor(params: dict[str, Any]) -> str:
    """Encode a dict of cursor params to a URL-safe opaque token."""
    raw = json.dumps(params, separators=(",", ":"), sort_keys=True, default=str)
    return base64.urlsafe_b64encode(raw.encode()).decode()


def decode_cursor(cursor: str) -> dict[str, Any]:
    """Decode a cursor token produced by :func:`encode_cursor`.

    Returns an empty dict if the token is malformed.
    """
    try:
        raw = base64.urlsafe_b64decode(cursor.encode() + b"==")
        return json.loads(raw)
    except Exception:
        return {}


def paginate_with_cursor(
    queryset: Any,
    *,
    cursor: str | None = None,
    page_size: int = DEFAULT_PAGE_SIZE,
    ordering_field: str = "pk",
    direction: str = "next",
) -> CursorPage:
    """Apply cursor-based pagination to a Django QuerySet.

    Only supports single-field ordering by *ordering_field* (ascending).
    Use :func:`encode_cursor` / :func:`decode_cursor` to pass cursors between
    requests.

    :param queryset:       A Django QuerySet (must be ordered by *ordering_field*).
    :param cursor:         Opaque cursor from a previous response, or ``None``
                           for the first page.
    :param page_size:      Max items per page (clamped to :data:`MAX_PAGE_SIZE`).
    :param ordering_field: The field to paginate on (default ``"pk"``).
    :param direction:      ``"next"`` or ``"prev"``.
    """
    page_size = max(1, min(page_size, MAX_PAGE_SIZE))
    params = decode_cursor(cursor) if cursor else {}
    last_value = params.get(ordering_field)

    qs = queryset
    if last_value is not None:
        lookup = f"{ordering_field}__gt" if direction == "next" else f"{ordering_field}__lt"
        qs = qs.filter(**{lookup: last_value})

    items = list(qs.order_by(ordering_field)[: page_size + 1])
    has_next = len(items) > page_size
    if has_next:
        items = items[:page_size]

    next_cursor: str | None = None
    prev_cursor: str | None = None
    if has_next and items:
        last = items[-1]
        next_cursor = encode_cursor({ordering_field: getattr(last, ordering_field)})
    if items:
        first = items[0]
        prev_cursor = encode_cursor({ordering_field: getattr(first, ordering_field)})

    return CursorPage(
        items=items,
        next_cursor=next_cursor,
        prev_cursor=prev_cursor,
        has_next=has_next,
        has_prev=last_value is not None,
    )
