"""Cursor-based pagination for the Sika SDK.

Provides :class:`PageIterator` for automatic multi-page traversal of list
endpoints, and :class:`Page` as a lightweight container for a single page
of results.

The iterator fetches pages lazily — no HTTP call is made until iteration
begins. Pages are fetched automatically as items are consumed. A
configurable safety limit (default 10,000 items) prevents unbounded
iteration in scripts.

Field names for the response envelope (``items_key``, ``token_key``) and
the query parameter (``token_param``) are configurable per-iterator so
that different API surfaces (Recon uses ``items``/``next_token``; Core
Platform uses ``tokens``/``nextCursor``/``cursor``) work with the same
pagination machinery.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Generic, TypeVar

from pydantic import BaseModel

if TYPE_CHECKING:
    from collections.abc import Generator, Iterator

    from sika._http import SikaHTTPClient

T = TypeVar("T", bound=BaseModel)

_DEFAULT_MAX_ITEMS = 10_000


class Page(Generic[T]):
    """A single page of paginated results.

    Attributes:
        items: The parsed model instances on this page.
        next_token: Opaque cursor for fetching the next page, or ``None``
            if this is the last page.
        count: Number of items on this page (equal to ``len(items)``).
    """

    __slots__ = ("count", "items", "next_token")

    def __init__(
        self,
        items: list[T],
        next_token: str | None,
    ) -> None:
        self.items = items
        self.next_token = next_token
        self.count = len(items)

    def __repr__(self) -> str:
        return (
            f"Page(count={self.count}, "
            f"next_token={self.next_token!r})"
        )


class PageIterator(Generic[T]):
    """Lazy iterator that auto-fetches pages from a paginated endpoint.

    Iterating over a ``PageIterator`` yields individual model instances.
    Use :meth:`pages` to iterate page-by-page instead.

    Args:
        http: The SDK HTTP client used to make requests.
        path: API path (e.g. ``/recon/v1/scans``).
        params: Base query parameters (filters, limit, etc.).
        model: Pydantic model class to parse each item with.
        items_key: JSON key for the items array in the response body.
            Defaults to ``"items"`` (Recon API standard).
        token_key: JSON key for the next-page cursor in the response body.
            Defaults to ``"next_token"`` (Recon API standard).
        token_param: Query parameter name for passing the cursor on the
            next request. Defaults to ``"next_token"``.
        max_items: Safety limit — iteration stops after this many items.
            Defaults to 10,000. Set to ``0`` to disable.

    Example::

        iterator = PageIterator(http, "/recon/v1/scans", {}, Scan)
        for scan in iterator:
            print(scan.domain)

        # Page-by-page:
        for page in iterator.pages():
            print(f"{page.count} items on this page")
    """

    def __init__(
        self,
        http: SikaHTTPClient,
        path: str,
        params: dict[str, object] | None,
        model: type[T],
        *,
        items_key: str = "items",
        token_key: str = "next_token",
        token_param: str = "next_token",
        max_items: int = _DEFAULT_MAX_ITEMS,
    ) -> None:
        self._http = http
        self._path = path
        self._params = dict(params) if params else {}
        self._model = model
        self._items_key = items_key
        self._token_key = token_key
        self._token_param = token_param
        self._max_items = max_items

    def __iter__(self) -> Iterator[T]:
        """Iterate over individual items across all pages.

        Yields:
            Parsed model instances, one at a time.

        Raises:
            StopIteration: When all pages are exhausted or the safety
                limit is reached.
        """
        yielded = 0
        for page in self.pages():
            for item in page.items:
                if self._max_items and yielded >= self._max_items:
                    return
                yield item
                yielded += 1

    def pages(self) -> Generator[Page[T], None, None]:
        """Iterate page-by-page, yielding :class:`Page` objects.

        Each page is fetched lazily when the previous page is consumed.
        Stops when the API returns no ``next_token`` or the safety limit
        is reached.

        Yields:
            :class:`Page` instances containing parsed model items.
        """
        next_token: str | None = None
        total_yielded = 0

        while True:
            params = dict(self._params)
            if next_token is not None:
                params[self._token_param] = next_token

            body = self._http.get(self._path, params=params)

            raw_items: list[object] = body.get(self._items_key, [])
            next_token = body.get(self._token_key)

            # Apply safety limit at the page level.
            if self._max_items:
                remaining = self._max_items - total_yielded
                if remaining <= 0:
                    return
                raw_items = raw_items[:remaining]

            items = [self._model.model_validate(item) for item in raw_items]
            page = Page(items=items, next_token=next_token)
            total_yielded += page.count

            yield page

            if next_token is None:
                return

            # Stop if we've reached the safety limit.
            if self._max_items and total_yielded >= self._max_items:
                return
