# -*- coding: utf-8 -*-
"""
token handlers base module.
"""

import time

from abc import abstractmethod

import jwt

import pyrin.configuration.services as config_services
import pyrin.security.utils.services as security_utils_services

from pyrin.core.structs import DTO
from pyrin.core.exceptions import CoreNotImplementedError
from pyrin.security.enumerations import TokenTypeEnum
from pyrin.security.token.exceptions import TokenVerificationError, TokenDecodingError
from pyrin.security.token.interface import AbstractTokenBase
from pyrin.settings.static import APPLICATION_ENCODING
from pyrin.utils import unique_id


class TokenBase(AbstractTokenBase):
    """
    token base class.
    """

    def __init__(self, name, **options):
        """
        initializes an instance of TokenBase.

        :param str name: token handler name.
        """

        super().__init__()

        self._set_name(name)
        self._encoding = APPLICATION_ENCODING

    def generate_access_token(self, payload, **options):
        """
        generates an access token from the given inputs and returns it.

        the generated token is in the form of `header_hash.payload_hash.signature_hash`
        and each part is encoded using a signing key.

        :param dict payload: a dictionary containing key/values as payload.
                             note that for better performance, keep payload
                             as small as possible.

        :keyword dict custom_headers: a dictionary containing custom headers.

        :keyword bool is_fresh: indicates that this token is fresh.
                                being fresh means that token is generated by
                                providing user credentials to server.
                                if not provided, defaults to False.

        :returns: token.
        :rtype: str
        """

        updated_payload = payload or DTO()
        updated_payload.update(**self._get_access_token_required_claims())

        return self._generate_token(updated_payload, **options)

    def generate_refresh_token(self, payload, **options):
        """
        generates a refresh token from the given inputs and returns it.

        the generated token is in the form of `header_hash.payload_hash.signature_hash`
        and each part is encoded using a signing key.

        :param dict payload: a dictionary containing key/values as payload.
                             note that for better performance, keep payload
                             as small as possible.

        :keyword dict custom_headers: a dictionary containing custom headers.

        :returns: token.
        :rtype: str
        """

        updated_payload = payload or DTO()
        updated_payload.update(**self._get_refresh_token_required_claims())

        # refresh tokens never could be fresh.
        updated_payload.update(is_fresh=False)

        return self._generate_token(updated_payload, **options)

    def _generate_token(self, payload, **options):
        """
        generates a token from the given inputs and returns it.

        :param dict payload: a dictionary containing key/values as payload.
                             note that for better performance, keep payload
                             as small as possible.

        :keyword dict custom_headers: a dictionary containing custom headers.

        :keyword bool is_fresh: indicates that this token is fresh.
                                being fresh means that token is generated by
                                providing user credentials to server.
                                if not provided, defaults to False.

        :returns: token.
        :rtype: str
        """

        is_fresh = options.get('is_fresh', False)
        updated_payload = payload or DTO()
        updated_payload.update(is_fresh=is_fresh)

        custom_headers = options.get('custom_headers', DTO())
        custom_headers.update(**self._get_common_required_headers())

        return jwt.encode(updated_payload,
                          self._get_encoding_key(),
                          self._get_algorithm(),
                          custom_headers)

    def get_payload(self, token, **options):
        """
        decodes token and gets the payload data.

        if token signature could not be verified, it will raise an error.

        :param str token: token to get it's payload.

        :raises TokenVerificationError: token verification error.

        :rtype: dict
        """

        try:
            return self._get_payload(token, verify=True)
        except Exception as error:
            raise TokenVerificationError(error) from error

    def _get_payload(self, token, **options):
        """
        decodes token and gets the payload data.

        :param str token: token to get it's payload.

        :keyword bool verify: specifies that token must be verified before getting
                              the payload. if not provided, defaults to True.

        :rtype: dict
        """

        default_options = self._get_default_options()
        verify = options.get('verify', True)
        if verify is False:
            default_options = dict(verify_signature=False)

        return jwt.decode(token,
                          self._get_decoding_key(),
                          self._get_algorithm(**options),
                          default_options)

    def get_unverified_payload(self, token, **options):
        """
        decodes token and gets the payload data without verifying the signature.

        note that returned payload must not be trusted for any critical operations.

        :param str token: token to get it's payload.

        :raises TokenDecodingError: token decoding error.

        :rtype: dict
        """

        try:
            return self._get_payload(token, verify=False)
        except Exception as error:
            raise TokenDecodingError(error) from error

    def get_unverified_header(self, token, **options):
        """
        gets the header dict of token without verifying the signature.

        note that returned header must not be trusted for critical operations.

        :param str token: token to get it's header.

        :rtype: dict
        """

        return jwt.get_unverified_header(token)

    def _get_common_required_headers(self):
        """
        gets a dictionary containing common required headers.

        :returns: dict[str kid: str kid]
        :rtype: dict
        """

        return DTO(kid=self.get_kid())

    def _get_common_required_claims(self):
        """
        gets a dictionary containing common required claims for access and refresh tokens.

        :returns: dict(str jti: jwt id,
                       float iat: issued time)

        :rtype: dict
        """

        now = time.time()

        return DTO(jti=str(unique_id.generate_uuid4()),
                   iat=now)

    def _get_access_token_required_claims(self):
        """
        gets a dictionary containing required claims for access token.

        :returns: dict(str jti: jwt id,
                       float iat: issued time,
                       float exp: expire time)

        :rtype: dict
        """

        common_required = self._get_common_required_claims()
        expire_duration = self._get_access_token_lifetime()
        expiration = common_required['iat'] + expire_duration
        common_required.update(exp=expiration,
                               type=TokenTypeEnum.ACCESS)

        return common_required

    def _get_refresh_token_required_claims(self):
        """
        gets a dictionary containing required claims for refresh token.

        :returns: dict(str jti: jwt id,
                       float iat: issued time,
                       float exp: expire time)

        :rtype: dict
        """

        common_required = self._get_common_required_claims()
        expire_duration = self._get_refresh_token_lifetime()
        expiration = common_required['iat'] + expire_duration
        common_required.update(exp=expiration,
                               type=TokenTypeEnum.REFRESH)

        return common_required

    def _get_default_options(self):
        """
        gets a dictionary containing default options.

        :returns: dict(bool verify_signature: verify signature,
                       bool verify_exp: verify expire time,
                       bool verify_nbf: verify not before,
                       bool verify_iat: verify issued at,
                       bool verify_aud: verify audience,
                       bool verify_iss: verify issuer,
                       bool require_exp: requires expire time,
                       bool require_iat: requires issued time,
                       bool require_nbf: requires not before time)

        :rtype: dict
        """

        return DTO(verify_signature=True,
                   verify_exp=True,
                   verify_nbf=True,
                   verify_iat=True,
                   verify_aud=True,
                   verify_iss=True,
                   require_exp=True,
                   require_iat=True,
                   require_nbf=False)

    @abstractmethod
    def _get_encoding_key(self, **options):
        """
        gets the signing key for encoding.

        :raises CoreNotImplementedError: core not implemented error.

        :rtype: str
        """

        raise CoreNotImplementedError()

    @abstractmethod
    def _get_decoding_key(self, **options):
        """
        gets the signing key for decoding.

        :raises CoreNotImplementedError: core not implemented error.

        :rtype: str
        """

        raise CoreNotImplementedError()

    def _get_access_token_lifetime(self, **options):
        """
        gets access token lifetime.

        :rtype: int
        """

        return config_services.get('security', 'token', 'access_token_lifetime', **options)

    def _get_refresh_token_lifetime(self, **options):
        """
        gets refresh token lifetime.

        :rtype: int
        """

        return config_services.get('security', 'token', 'refresh_token_lifetime', **options)

    @abstractmethod
    def _get_algorithm(self, **options):
        """
        gets the algorithm for signing the token.

        :raises CoreNotImplementedError: core not implemented error.

        :rtype: str
        """

        raise CoreNotImplementedError()


class SymmetricTokenBase(TokenBase):
    """
    symmetric token base class.

    this token type uses a single symmetric key for decoding and encoding.
    """

    def __init__(self, **options):
        """
        initializes an instance of SymmetricTokenBase.
        """

        # we pass the algorithm of token handler as the name of it.
        super().__init__(self._get_algorithm(**options), **options)

    def _get_decoding_key(self, **options):
        """
        gets the signing key for decoding.

        :rtype: str
        """

        return self._get_encoding_key(**options)


class AsymmetricTokenBase(TokenBase):
    """
    asymmetric token base class.

    this token type uses a pair of public/private asymmetric keys for decoding and encoding.
    """

    def __init__(self, **options):
        """
        initializes an instance of AsymmetricTokenBase.
        """

        # we pass the algorithm of token handler as the name of it.
        super().__init__(self._get_algorithm(**options), **options)


class RSTokenBase(AsymmetricTokenBase):
    """
    rs token base class.

    this token type uses a pair of public/private asymmetric keys for decoding and encoding.
    """

    def __init__(self, **options):
        """
        initializes an instance of AsymmetricTokenBase.
        """

        super().__init__(**options)

        self._private_key = None
        self._public_key = None

        self._load_keys(**options)

    def generate_key(self, **options):
        """
        generates a valid public/private key for this handler and returns it.

        :keyword int length: the length of generated key in bits.
                             if not provided, it uses default value
                             from relevant config.

        :returns: tuple[str public_key, str private_key]
        :rtype: tuple[str, str]
        """

        return security_utils_services.generate_rsa_key(**options)

    @abstractmethod
    def _load_keys(self, **options):
        """
        loads public/private keys into this class's relevant attributes.

        :raises CoreNotImplementedError: core not implemented error.
        """

        raise CoreNotImplementedError()
