"""
Provides RestFramework auth backend for signed tokens.
"""

import logging
from lclazy import LazyLoader
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import pyseto
else:
    pyseto = LazyLoader('pyseto', globals(), 'pyseto')

from rest_framework import exceptions
from rest_framework.authentication import get_authorization_header, BaseAuthentication
from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as tr
from rest_framework.request import Request

from encipher import get_setting
from encipher.tokens import AccessToken, UnverifiedToken, UnverifiedRegistrationToken
from encipher.typehints import VerifiedAuth
from encipher.utils import lookup_user_record

logger = logging.getLogger(__name__)
User = get_user_model()


# ---------------------------------------------------------------------------
class PasetoTokenMixin:
    """
    Paseto token authentication mixin for Django Rest Framework.
    """

    TokenClass = UnverifiedToken
    header_method_key = 'AUTH_METHOD'

    def authenticate(self, request: Request):
        # Ensure this auth header was meant for us (it has the Paseto Token auth method).
        auth = get_authorization_header(request).split()

        method = self.authenticate_header(request)
        if not auth or auth[0] != method.encode():
            return None

        try:
            header_token = auth[1].decode()
            return self.authenticate_credentials(header_token, request)

        except IndexError:
            error_msg = tr('Invalid signed token header. No credentials provided.')
        except UnicodeError:
            error_msg = tr('Invalid signed token header. Token string contains invalid characters.')

        raise exceptions.AuthenticationFailed(error_msg)

    def lookup_user_record(self, token: AccessToken, request: Request) -> VerifiedAuth:
        """
        Lookup user record from token claims
        """
        user = lookup_user_record(token)
        if not user:
            raise exceptions.AuthenticationFailed(tr('User not found for provided token.'))
        if not user.is_active:
            raise exceptions.AuthenticationFailed(tr('User inactive or deleted.'))

        # Add the token to the request so we can access
        # the data later for further authorization uses
        request.access_token = token

        # Add subject and subject_cid so these properties
        # can be used for query params
        if subject := token.payload.get('sub'):
            user.subject = subject
        if subject_cid := token.payload.get('sub_cid'):
            user.subject_cid = subject_cid

        return VerifiedAuth(user, token)

    def authenticate_header(self, request: Request) -> str:  # noqa
        if not self.header_method_key:
            return ''
        return get_setting(self.header_method_key)

    def authenticate_credentials(self, token: str, request):
        raise NotImplementedError('Override on subclasses')


# ---------------------------------------------------------------------------
class PasetoAuthentication(PasetoTokenMixin, BaseAuthentication):
    """
    Clients should authenticate by passing the token key in the "Authorization"
    HTTP header, prepended with the string "Paseto ".  For example:

        Authorization: Paseto 401f7ac837da42b97f613d789819ff93537bee6a
    """

    def authenticate_credentials(self, token: str, request: Request) -> VerifiedAuth:
        """
        Authenticate user by comparing signed token with any
        EncryptionKey records linked to the specified user.
        """
        try:
            access_token = self.TokenClass(token).verify()
            access_token.verify()
            return self.lookup_user_record(access_token, request)

        except (ValueError, pyseto.PysetoError) as e:
            error = str(e).lower()
            msg = 'Invalid token error: %s'
            if 'token expired' in error or 'invalid exp' in error or 'invalid nbf' in error:
                msg = f'{msg} - check time on both client and server'
            logger.error(msg, e)
            raise exceptions.AuthenticationFailed(tr(msg) % e) from None


# ---------------------------------------------------------------------------
class PasetoPinnedAuthentication(PasetoTokenMixin, BaseAuthentication):
    """
    This class should only be done in specific situations, such as on
    pre-defined acceptable end-points.

    Tokens verified by this class will be signed by one of two methods:
       * A pre-generated, pinned EncryptionKey included in installer packages.
       * A public key in the POST body as a value of the `api_key` key.

    Do NOT include this authentication method on all API classes!
    Each of these techniques has security ramifications and should
    be used only for specific endpoints.

    Validates Paseto tokens for Django Rest Framework, to be used
    in registration situations, such as the creation of the FIRST
    EncryptionKey for an Appliance or Device.

        Authorization: Pinned_Paseto 401f7ac837da42b97f613d789819ff93537bee6a
    """

    header_method_key = 'PINNED_AUTH_METHOD'
    TokenClass = UnverifiedRegistrationToken

    def authenticate_credentials(self, token: str, request: Request) -> VerifiedAuth:
        """
        Allow HEAD method so requests can query whether the key exists or not.
        """
        # Allow HEAD method so requests can query whether the key exists or not.
        if request.method not in ('POST', 'PUT', 'PATCH', 'HEAD'):
            raise exceptions.AuthenticationFailed('Invalid registration method')

        request_data = {}
        if pem := request.data.get('api_key', ''):
            request_data['pem'] = pem
        if cid := request.data.get('api_key_cid', ''):
            request_data['cid'] = cid

        try:
            access_token = self.TokenClass(token, request_data).verify()
            access_token.verify()
            return self.lookup_user_record(access_token, request)

        except (ValueError, pyseto.PysetoError) as e:
            logger.error('Invalid token error: %s', e)
            raise exceptions.AuthenticationFailed(tr('Invalid token error: %s') % e) from None
