from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Generic, List, Optional, Tuple, TypeVar
from uuid import UUID

T = TypeVar("T")
C = TypeVar("C", int, str, UUID)


@dataclass
class ClassicPagination(Generic[T]):
    """Container for data returned using limit/offset pagination."""

    __slots__ = ("items", "page_size", "current_page", "total_pages")

    items: List[T]
    """List of data being sent as part of the response."""
    page_size: int
    """Number of items per page."""
    current_page: int
    """Current page number."""
    total_pages: int
    """Total number of pages."""


@dataclass
class OffsetPagination(Generic[T]):
    """Container for data returned using limit/offset pagination."""

    __slots__ = ("items", "limit", "offset", "total")

    items: List[T]
    """List of data being sent as part of the response."""
    limit: int
    """Maximal number of items to send."""
    offset: int
    """Offset from the beginning of the query.

    Identical to an index.
    """
    total: int
    """Total number of items."""


@dataclass
class CursorPagination(Generic[C, T]):
    """Container for data returned using cursor pagination."""

    __slots__ = ("items", "results_per_page", "cursor", "next_cursor")

    items: List[T]
    """List of data being sent as part of the response."""
    results_per_page: int
    """Maximal number of items to send."""
    cursor: Optional[C]
    """Unique ID, designating the last identifier in the given data set.

    This value can be used to request the "next" batch of records.
    """


class AbstractSyncClassicPaginator(ABC, Generic[T]):
    """Base paginator class for sync classic pagination.

    Implement this class to return paginated result sets using the classic pagination scheme.
    """

    @abstractmethod
    def get_total(self, page_size: int) -> int:
        """Return the total number of records.

        Args:
            page_size: Maximal number of records to return.

        Returns:
            An integer.
        """
        raise NotImplementedError

    @abstractmethod
    def get_items(self, page_size: int, current_page: int) -> List[T]:
        """Return a list of items of the given size 'page_size' correlating with 'current_page'.

        Args:
            page_size: Maximal number of records to return.
            current_page: The current page of results to return.

        Returns:
            A list of items.
        """
        raise NotImplementedError

    def __call__(self, page_size: int, current_page: int) -> ClassicPagination[T]:
        """Return a paginated result set.

        Args:
            page_size: Maximal number of records to return.
            current_page: The current page of results to return.

        Returns:
            A paginated result set.
        """
        total_pages = self.get_total(page_size=page_size)

        items = self.get_items(page_size=page_size, current_page=current_page)

        return ClassicPagination[T](
            items=items, total_pages=total_pages, page_size=page_size, current_page=current_page
        )


class AbstractAsyncClassicPaginator(ABC, Generic[T]):
    """Base paginator class for async classic pagination.

    Implement this class to return paginated result sets using the classic pagination scheme.
    """

    @abstractmethod
    async def get_total(self, page_size: int) -> int:
        """Return the total number of records.

        Args:
            page_size: Maximal number of records to return.

        Returns:
            An integer.
        """
        raise NotImplementedError

    @abstractmethod
    async def get_items(self, page_size: int, current_page: int) -> List[T]:
        """Return a list of items of the given size 'page_size' correlating with 'current_page'.

        Args:
            page_size: Maximal number of records to return.
            current_page: The current page of results to return.

        Returns:
            A list of items.
        """
        raise NotImplementedError

    async def __call__(self, page_size: int, current_page: int) -> ClassicPagination[T]:
        """Return a paginated result set.

        Args:
            page_size: Maximal number of records to return.
            current_page: The current page of results to return.

        Returns:
            A paginated result set.
        """
        total_pages = await self.get_total(page_size=page_size)

        items = await self.get_items(page_size=page_size, current_page=current_page)

        return ClassicPagination[T](
            items=items, total_pages=total_pages, page_size=page_size, current_page=current_page
        )


class AbstractSyncOffsetPaginator(ABC, Generic[T]):
    """Base paginator class for limit / offset pagination.

    Implement this class to return paginated result sets using the limit / offset pagination scheme.
    """

    @abstractmethod
    def get_total(self) -> int:
        """Return the total number of records.

        Returns:
            An integer.
        """
        raise NotImplementedError

    @abstractmethod
    def get_items(self, limit: int, offset: int) -> List[T]:
        """Return a list of items of the given size 'limit' starting from position 'offset'.

        Args:
            limit: Maximal number of records to return.
            offset: Starting position within the result set (assume index 0 as starting position).

        Returns:
            A list of items.
        """
        raise NotImplementedError

    def __call__(self, limit: int, offset: int) -> OffsetPagination[T]:
        """Return a paginated result set.

        Args:
            limit: Maximal number of records to return.
            offset: Starting position within the result set (assume index 0 as starting position).

        Returns:
            A paginated result set.
        """
        total = self.get_total()

        items = self.get_items(limit=limit, offset=offset)

        return OffsetPagination[T](items=items, total=total, offset=offset, limit=limit)


class AbstractAsyncOffsetPaginator(ABC, Generic[T]):
    """Base paginator class for limit / offset pagination.

    Implement this class to return paginated result sets using the limit / offset pagination scheme.
    """

    @abstractmethod
    async def get_total(self) -> int:
        """Return the total number of records.

        Returns:
            An integer.
        """
        raise NotImplementedError

    @abstractmethod
    async def get_items(self, limit: int, offset: int) -> List[T]:
        """Return a list of items of the given size 'limit' starting from position 'offset'.

        Args:
            limit: Maximal number of records to return.
            offset: Starting position within the result set (assume index 0 as starting position).

        Returns:
            A list of items.
        """
        raise NotImplementedError

    async def __call__(self, limit: int, offset: int) -> OffsetPagination[T]:
        """Return a paginated result set.

        Args:
            limit: Maximal number of records to return.
            offset: Starting position within the result set (assume index 0 as starting position).

        Returns:
            A paginated result set.
        """
        total = await self.get_total()
        items = await self.get_items(limit=limit, offset=offset)

        return OffsetPagination[T](items=items, total=total, offset=offset, limit=limit)


class AbstractSyncCursorPaginator(ABC, Generic[C, T]):
    """Base paginator class for sync cursor pagination.

    Implement this class to return paginated result sets using the cursor pagination scheme.
    """

    @abstractmethod
    def get_items(self, cursor: Optional[C], results_per_page: int) -> Tuple[List[T], Optional[C]]:
        """Return a list of items of the size 'results_per_page' following the given cursor, if any,

        Args:
            cursor: A unique identifier that acts as the 'cursor' after which results should be given.
            results_per_page: A maximal number of results to return.

        Returns:
            A tuple containing the result set and a new cursor that marks the last record retrieved.
            The new cursor can be used to ask for the 'next_cursor' batch of results.
        """
        raise NotImplementedError

    def __call__(self, cursor: Optional[C], results_per_page: int) -> CursorPagination[C, T]:
        """Return a paginated result set given an optional cursor (unique ID) and a maximal number of results to return.

        Args:
            cursor: A unique identifier that acts as the 'cursor' after which results should be given.
            results_per_page: A maximal number of results to return.

        Returns:
            A paginated result set.
        """
        items, new_cursor = self.get_items(cursor=cursor, results_per_page=results_per_page)

        return CursorPagination[C, T](
            items=items,
            results_per_page=results_per_page,
            cursor=new_cursor,
        )


class AbstractAsyncCursorPaginator(ABC, Generic[C, T]):
    """Base paginator class for async cursor pagination.

    Implement this class to return paginated result sets using the cursor pagination scheme.
    """

    @abstractmethod
    async def get_items(self, cursor: Optional[C], results_per_page: int) -> Tuple[List[T], Optional[C]]:
        """Return a list of items of the size 'results_per_page' following the given cursor, if any,

        Args:
            cursor: A unique identifier that acts as the 'cursor' after which results should be given.
            results_per_page: A maximal number of results to return.

        Returns:
            A tuple containing the result set and a new cursor that marks the last record retrieved.
            The new cursor can be used to ask for the 'next_cursor' batch of results.
        """
        raise NotImplementedError

    async def __call__(self, cursor: Optional[C], results_per_page: int) -> CursorPagination[C, T]:
        """Return a paginated result set given an optional cursor (unique ID) and a maximal number of results to return.

        Args:
            cursor: A unique identifier that acts as the 'cursor' after which results should be given.
            results_per_page: A maximal number of results to return.

        Returns:
            A paginated result set.
        """
        items, new_cursor = await self.get_items(cursor=cursor, results_per_page=results_per_page)

        return CursorPagination[C, T](
            items=items,
            results_per_page=results_per_page,
            cursor=new_cursor,
        )
