from grpclib import GRPCError, Status
from blickfeld_qb2.base.grpc.channel import Channel

from blickfeld_qb2.secure.services import Session, Authentication, AuthenticationLoginRequestApplication


class TokenFactory:
    """Callable Token Factory which holds the current JWT access token.

    The token is checked for validity before returning it by requesting the current session-nonce.
    The token is re-generated by authenticating with the application-key-secret when it is expired or unset.

    Example Usage with `blickfeld_qb2` public API module:

        import blickfeld_qb2

        # inject `blickfeld_qb2.TokenFactory`-instance into grpc-channel for authentication
        channel = blickfeld_qb2.Channel(
            fqdn_or_ip=my_qb2_fqdn_or_ip,
            token=blickfeld_qb2.TokenFactory(application_key_secret="my-application-key")
        )

        # get session nonce
        session_service = blickfeld_qb2.secure.services.Session(channel=channel)
        nonce = session_service.get_nonce()
        print(f"session-nonce: {nonce}")

        # get account information
        account_service = blickfeld_qb2.secure.services.Account(channel=channel)
        account = account_service.get()
        print(f"account: {account}")

    """

    current_token: str = None

    def __init__(self, application_key_secret: str) -> None:
        self.application_key_secret = application_key_secret

    async def __call__(self, channel: Channel) -> str:
        """Awaitable callable interface"""
        return await self.create_or_renew_token(channel)

    async def create_or_renew_token(self, channel: Channel) -> str:
        """Awaitable handler which is used for automatic token generation with Channel from blickfeld_qb2.base."""
        if self.current_token:
            # Check if current token is still valid
            try:
                async with channel.clone(token=self.current_token) as cloned_channel:
                    _ = await Session(channel=cloned_channel).async_get_nonce()
            except GRPCError as ex:
                if ex.status == Status.PERMISSION_DENIED:
                    self.current_token = None
                else:
                    raise

            # Token is still valid, return it
            if self.current_token:
                return self.current_token

        # Authenticate to renew token
        async with channel.clone(token=None) as cloned_channel:
            login_response = await Authentication(channel=cloned_channel).async_login(
                application=AuthenticationLoginRequestApplication(key=self.application_key_secret)
            )
            self.current_token = login_response.token

        return self.current_token
