import logging
from abc import abstractmethod
from functools import partial
from time import sleep
from typing import Callable, Iterator, Optional, Type

from pydantic import BaseModel

logger = logging.getLogger(__name__)


class PaginationModel(BaseModel):
    """
    Base abstract class defining a pagination model

    By implementing the 3 abstract methods below, enables
    to fetch all elements of a Paginated API by using the
    `fetch_all_pages` method
    """

    current_page_payload: Optional["dict"] = None

    @abstractmethod
    def is_last(self) -> bool:
        """Stopping condition for the pagination"""
        pass

    @abstractmethod
    def next_page_payload(self) -> dict:
        """Payload enabling to generate the request for the next page"""
        pass

    @abstractmethod
    def page_results(self) -> list:
        """List of results of the current page"""
        pass


def fetch_all_pages(
    request: Callable,
    pagination_model: Type[PaginationModel],
    rate_limit: Optional[int] = None,
) -> Iterator:
    """
    Method to return all results of a Paginated API based on the
    pagination model and the first request call
    """
    page_number = 1
    response_payload = request()
    paginated_response = pagination_model(**response_payload)
    while not paginated_response.is_last():
        logger.info(f"Fetching page number {page_number}")
        yield from paginated_response.page_results()
        next_payload = paginated_response.next_page_payload()
        new_request = partial(request, pagination_params=next_payload)
        if rate_limit:
            sleep(rate_limit)

        paginated_response = pagination_model(
            current_page_payload=next_payload, **new_request()
        )
        page_number += 1

    # send last page's results
    logger.info(f"Fetching page number {page_number}")
    yield from paginated_response.page_results()
