import asyncio
import time

from mariner import exceptions
from mariner.clients import http
from mariner.models import tracker, url
from mariner.services import templater
from mariner.types import HTML


class Service:
    """Service to fetch search results from trackers."""

    def __init__(
        self,
        client: http.AsyncClientProtocol,
        tracker: tracker.Definition,
        server: url.Site,
    ) -> None:
        self._client = client
        self._tracker = tracker
        self.server = server
        self._request_delay = tracker.spec.request_delay
        self._last_request: float = 0

    @classmethod
    async def create(
        cls,
        client: http.AsyncClientProtocol,
        tracker: tracker.Definition,
    ) -> "Service":
        """Create a fetcher service by probing for a working server.

        This is the primary way to construct a Service. Concurrent calls for the
        same tracker are not guarded by a lock, since the CLI processes one
        search at a time.

        Args:
            client: A shared async HTTP client.
            tracker: The tracker definition.

        Returns:
            A fully initialized Service with a working server.

        Raises:
            exceptions.NoServerAvailableError: If no server is reachable.

        """
        server = await cls._choose_server(client, tracker)
        return cls(client=client, tracker=tracker, server=server)

    @staticmethod
    async def _choose_server(
        client: http.AsyncClientProtocol,
        tracker: tracker.Definition,
    ) -> url.Site:
        """Choose a working server from the tracker's list of links.

        Raises:
            exceptions.NoServerAvailableError: If no server is reachable.

        """
        for server in tracker.spec.links:
            try:
                await client.get(server)
                return server
            except exceptions.HTTPError:
                continue
        raise exceptions.NoServerAvailableError(tracker.metadata.name)

    async def fetch(self, keywords: str) -> HTML:
        """Fetch search results from the tracker for the given query.

        Respects the tracker's ``requestDelay`` by sleeping if the
        previous request was made less than ``request_delay`` seconds ago.

        Args:
            keywords: The search query.

        Returns:
            The HTML content of the search results.

        """
        await self._throttle()
        search_url = self._format_url(keywords)
        return HTML(await self._client.get(search_url.full))

    async def _throttle(self) -> None:
        """Sleep if needed to respect the tracker's request delay."""
        if self._request_delay <= 0:
            return
        elapsed = time.monotonic() - self._last_request
        remaining = self._request_delay - elapsed
        if remaining > 0:
            await asyncio.sleep(remaining)
        self._last_request = time.monotonic()

    def _format_url(self, keywords: str) -> url.URL:
        """Format the search query according to the tracker's specification.

        Args:
            keywords: The raw search keywords.

        Returns:
            The formatted search url.

        """
        context: dict[str, object] = {
            "Keywords": keywords,
            "Config": self._tracker.spec.config,
        }

        inputs = self._tracker.spec.paths[0].inputs
        query_params = {
            k: templater.render(v, context) if isinstance(v, str) else str(v)
            for k, v in inputs.items()
        }
        query = url.Query(query_params) if query_params else None
        path = self._tracker.spec.paths[0].path
        pathname = url.Pathname(templater.render(path, context))
        return url.URL(self.server, pathname, query)
