"""Pagination utility functions."""

from collections.abc import Collection, Iterator
from typing import Generic, Literal, TypeVar, cast

from httpcore import ConnectError as HttpCoreConnectError
from httpx import Client, HTTPError
from httpx import ConnectError as HttpxConnectError
from httpx._types import QueryParamTypes
from msgspec import Struct, defstruct, field
from msgspec.json import Decoder as JsonDecoder

from slices_clientlib_core.base.exceptions import ApiConnectError, from_httpx_exception


class Link(Struct):
    """Link to related page."""

    href: str
    rel: Literal["next", "prev"]


T = TypeVar("T")


class Page(Struct, Generic[T]):
    """Page of items."""

    total: int
    items: list[T]
    links: list[Link]


def create_page_class(item_name: str, item_type: type[T]) -> type[Page[T]]:
    """Create Page class for the given item type."""

    item_list_type = list[item_type]

    page_class = defstruct(
        item_name.capitalize() + "Page",
        [
            ("total", int),
            (
                "items",
                item_list_type,
                field(
                    name=item_name + "s",
                ),
            ),
            (
                "links",
                list[Link],
                field(name=item_name + "_links"),
            ),
        ],
    )

    return cast("type[Page[T]]", page_class)


class PagedCollection(Collection[T], Generic[T]):
    """A collection that maps to multiple pages of items."""

    _page_class: type[Page[T]]

    def __init__(
        self,
        item_name: str,
        item_type: type[T],
        url: str,
        client: Client,
        params: QueryParamTypes | None = None,
    ) -> None:
        """Initialize PagedCollection."""
        self._item_name = item_name
        self._item_type = item_type
        self._page_class = create_page_class(item_name, item_type)

        self._decoder = JsonDecoder(self._page_class)

        self._client = client
        self._params = params
        try:
            response = self._client.get(url, params=params)
        except HttpCoreConnectError as err:
            raise ApiConnectError(url) from err
        except HttpxConnectError as err:
            raise ApiConnectError(url) from err
        try:
            response.raise_for_status()
            self._first_page = self._decoder.decode(response.content)
        except HTTPError as err:
            raise from_httpx_exception(err) from err

    def __iter__(self) -> Iterator[T]:
        """Create iterator over items."""
        return PagedIterator(self._first_page, self._decoder, self._client)

    def __contains__(self, x: object) -> bool:
        """Check if the item exists in the collection."""
        return any(x == item for item in self.__iter__())

    def __len__(self) -> int:
        """Get total number of items."""
        return self._first_page.total


class PagedIterator(Iterator[T]):
    """Paged iterator."""

    _current_page: Page[T] | None
    _decoder: JsonDecoder[Page[T]]
    _client: Client

    def __init__(
        self,
        first_page: Page,
        decoder: JsonDecoder[Page[T]],
        client: Client,
    ) -> None:
        """Initialize PagedIterator."""

        self._current_page = first_page
        self._decoder = decoder
        self._client = client
        self._iterator = iter(first_page.items)

    def _next_page(self) -> Page | None:
        if not self._current_page:
            return None

        for link in self._current_page.links:
            if link.rel == "next":
                try:
                    response = self._client.get(link.href)
                except HttpCoreConnectError as err:
                    raise ApiConnectError(link.href) from err
                except HttpxConnectError as err:
                    raise ApiConnectError(link.href) from err
                try:
                    response.raise_for_status()
                except HTTPError as err:
                    raise from_httpx_exception(err) from err
                return self._decoder.decode(response.content)

        return None

    def __next__(self) -> T:
        """Get next item."""
        try:
            return next(self._iterator)
        except StopIteration:
            self._current_page = self._next_page()
            if self._current_page:
                self._iterator = iter(self._current_page.items)
                return next(self._iterator)
            raise
