"""
Async token lifecycle manager.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import httpx

from fortytwo.core.auth.tokens import Tokens, parse_token_response
from fortytwo.exceptions import FortyTwoAuthException
from fortytwo.logger import logger


if TYPE_CHECKING:
    from fortytwo.core.auth.providers import AuthProvider, TokenRequest
    from fortytwo.core.config import Config


class AsyncAuthenticator:
    """
    Manages the OAuth2 token lifecycle asynchronously.

    Args:
        http_client: Shared ``httpx.AsyncClient`` used for token requests.
        provider: The auth-grant strategy that builds request payloads.
        endpoint: The OAuth2 token endpoint URL.
    """

    def __init__(
        self,
        config: Config,
        provider: AuthProvider,
        http_client: httpx.AsyncClient,
    ) -> None:
        self.config = config
        self.__provider = provider
        self.__http_client = http_client
        self.__token: Tokens | None = None

    @property
    def provider(self) -> AuthProvider:
        """
        The underlying auth-grant provider.
        """
        return self.__provider

    @provider.setter
    def provider(self, provider: AuthProvider) -> None:
        """
        Update the auth-grant provider and clear existing tokens.
        """
        self.__provider = provider
        self.__token = None

    async def get_tokens(self) -> Tokens:
        """
        Return the current access token, refreshing automatically if expired.

        Returns:
            A valid :class:`Tokens` instance.
        """
        if self.__token is None or self.__token.is_expired():
            await self.refresh_tokens()

        return self.__token

    def set_tokens(self, tokens: Tokens) -> None:
        """
        Manually inject tokens (e.g. restored from external storage).

        Args:
            tokens: The :class:`Tokens` to use for subsequent requests.
        """
        self.__token = tokens

    async def refresh_tokens(self) -> Tokens:
        """
        Force a token refresh (or initial fetch).

        If the current token carries a refresh token **and** the provider
        supports refresh, a refresh-grant request is made.  Otherwise a
        full token acquisition is performed.

        Returns:
            The newly acquired :class:`Tokens`.

        Raises:
            FortyTwoAuthException: If the token request fails.
        """
        if self.__token is not None and self.__token.refresh_token is not None:
            request = self.__provider.build_refresh_request(
                self.__token.refresh_token,
            )
            self.__token = await self.__post(request)
            return self.__token

        request = self.__provider.build_token_request()
        self.__token = await self.__post(request)
        return self.__token

    async def __post(self, request: TokenRequest) -> Tokens:
        """
        Execute the OAuth POST and parse the response.
        """
        try:
            logger.info("Requesting OAuth token")
            response = await self.__http_client.post(
                url=self.config.request_endpoint_oauth,
                data=request.data,
                timeout=120,
            )
            response.raise_for_status()

            tokens = parse_token_response(response)
            logger.info("OAuth token acquired")
            return tokens

        except httpx.HTTPStatusError as e:
            logger.error("OAuth token request failed: %s", e)
            raise FortyTwoAuthException from e
