"""authlib.oauth2.rfc9068.token_validator.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Implementation of Validating JWT Access Tokens per `Section 4`_.

.. _`Section 7`: https://www.rfc-editor.org/rfc/rfc9068.html#name-validating-jwt-access-token
"""

from joserfc import jwt
from joserfc.errors import JoseError

from authlib._joserfc_helpers import import_any_key
from authlib.oauth2.claims import ClaimsOption
from authlib.oauth2.rfc6750.errors import InsufficientScopeError
from authlib.oauth2.rfc6750.errors import InvalidTokenError
from authlib.oauth2.rfc6750.validator import BearerTokenValidator

from .claims import JWTAccessTokenClaims


class JWTBearerTokenValidator(BearerTokenValidator):
    """JWTBearerTokenValidator can protect your resource server endpoints.

    :param issuer: The issuer from which tokens will be accepted.
    :param resource_server: An identifier for the current resource server,
        which must appear in the JWT ``aud`` claim.

    Developers needs to implement the missing methods::

        class MyJWTBearerTokenValidator(JWTBearerTokenValidator):
            def get_jwks(self): ...


        require_oauth = ResourceProtector()
        require_oauth.register_token_validator(
            MyJWTBearerTokenValidator(
                issuer="https://authorization-server.example.org",
                resource_server="https://resource-server.example.org",
            )
        )

    You can then protect resources depending on the JWT `scope`, `groups`,
    `roles` or `entitlements` claims::

        @require_oauth(
            scope="profile",
            groups="admins",
            roles="student",
            entitlements="captain",
        )
        def resource_endpoint(): ...
    """

    def __init__(self, issuer, resource_server, *args, **kwargs):
        self.issuer = issuer
        self.resource_server = resource_server
        super().__init__(*args, **kwargs)

    def get_jwks(self):
        """Return the JWKs that will be used to check the JWT access token signature.
        Developers MUST re-implement this method. Typically the JWKs are statically
        stored in the resource server configuration, or dynamically downloaded and
        cached using :ref:`specs/rfc8414`::

            def get_jwks(self):
                if "jwks" in cache:
                    return cache.get("jwks")

                server_metadata = get_server_metadata(self.issuer)
                jwks_uri = server_metadata.get("jwks_uri")
                cache["jwks"] = requests.get(jwks_uri).json()
                return cache["jwks"]
        """
        raise NotImplementedError()

    def validate_iss(self, claims, iss: "str") -> bool:
        # The issuer identifier for the authorization server (which is typically
        # obtained during discovery) MUST exactly match the value of the 'iss'
        # claim.
        return iss == self.issuer

    def authenticate_token(self, token_string):
        """"""
        # empty docstring avoids to display the irrelevant parent docstring

        claims_options: dict[str, ClaimsOption] = {
            "iss": {"essential": True, "validate": self.validate_iss},
            "exp": {"essential": True},
            "aud": {"essential": True, "value": self.resource_server},
            "sub": {"essential": True},
            "client_id": {"essential": True},
            "iat": {"essential": True},
            "jti": {"essential": True},
            "auth_time": {"essential": False},
            "acr": {"essential": False},
            "amr": {"essential": False},
            "scope": {"essential": False},
            "groups": {"essential": False},
            "roles": {"essential": False},
            "entitlements": {"essential": False},
        }
        key = import_any_key(self.get_jwks())

        # If the JWT access token is encrypted, decrypt it using the keys and algorithms
        # that the resource server specified during registration. If encryption was
        # negotiated with the authorization server at registration time and the incoming
        # JWT access token is not encrypted, the resource server SHOULD reject it.

        # The resource server MUST validate the signature of all incoming JWT access
        # tokens according to [RFC7515] using the algorithm specified in the JWT 'alg'
        # Header Parameter. The resource server MUST reject any JWT in which the value
        # of 'alg' is 'none'. The resource server MUST use the keys provided by the
        # authorization server.
        try:
            token = jwt.decode(token_string, key=key)
            return JWTAccessTokenClaims(token.claims, token.header, claims_options)
        except JoseError as exc:
            raise InvalidTokenError(
                realm=self.realm, extra_attributes=self.extra_attributes
            ) from exc

    def validate_token(
        self,
        token: JWTAccessTokenClaims,
        scopes,
        request,
        groups=None,
        roles=None,
        entitlements=None,
    ):
        """"""
        # empty docstring avoids to display the irrelevant parent docstring
        try:
            token.validate()
        except JoseError as exc:
            raise InvalidTokenError(
                realm=self.realm, extra_attributes=self.extra_attributes
            ) from exc

        # If an authorization request includes a scope parameter, the corresponding
        # issued JWT access token SHOULD include a 'scope' claim as defined in Section
        # 4.2 of [RFC8693]. All the individual scope strings in the 'scope' claim MUST
        # have meaning for the resources indicated in the 'aud' claim. See Section 5 for
        # more considerations about the relationship between scope strings and resources
        # indicated by the 'aud' claim.

        if self.scope_insufficient(token.get("scope", []), scopes):
            raise InsufficientScopeError()

        # Many authorization servers embed authorization attributes that go beyond the
        # delegated scenarios described by [RFC7519] in the access tokens they issue.
        # Typical examples include resource owner memberships in roles and groups that
        # are relevant to the resource being accessed, entitlements assigned to the
        # resource owner for the targeted resource that the authorization server knows
        # about, and so on. An authorization server wanting to include such attributes
        # in a JWT access token SHOULD use the 'groups', 'roles', and 'entitlements'
        # attributes of the 'User' resource schema defined by Section 4.1.2 of
        # [RFC7643]) as claim types.

        if self.scope_insufficient(token.get("groups"), groups):
            raise InvalidTokenError()

        if self.scope_insufficient(token.get("roles"), roles):
            raise InvalidTokenError()

        if self.scope_insufficient(token.get("entitlements"), entitlements):
            raise InvalidTokenError()
