import json
from flask import request, _request_ctx_stack, abort
from functools import wraps
import jose
from jose import jwt
from urllib.request import urlopen
import os
from dotenv import load_dotenv

basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '.env'))


AUTH0_DOMAIN = os.environ.get('AUTH0_TENANT_DOMAIN')
ALGORITHMS = os.environ.get('AUTH0_ALGORITHMS')
API_AUDIENCE = os.environ.get('AUTH0_API_AUDIENCE')


class AuthError(Exception):
    '''
    AuthError Exception
    A standardized way to communicate auth failure modes
    '''

    def __init__(self, error, status_code):
        self.error = error
        self.status_code = status_code


def get_token_auth_header():
    """
    Returns the access token from the authorization header
    """
    auth_header = request.headers.get("Authorization", None)

    if not auth_header:
        raise AuthError({
            "code": 'authorization_header_missing',
            "description": "Authorization header is expected."
        }, 401)

    parts = auth_header.split()

    if len(parts) is not 2:
        raise AuthError({
            'code': 'invalid_header',
            'description': 'Authorization header must be a valid bearer token.'
        }, 401)

    if parts[0].lower() != 'bearer':
        print('wrong header', parts[0].lower())
        raise AuthError({
            "code": "invalid_header",
            "description": "Authorization header must start with 'Bearer'"
        }, 401)

    token = parts[1]
    return token


def check_permissions(permission, payload):
    """
    Returns True if requested permission string is included in the payload, otherwise it raises an AuthError
    """

    if 'permissions' not in payload:
        raise AuthError({
            "code": "invalid_claims",
            "description": "Permissions not included in JWT"
        }, 403)

    if permission not in payload["permissions"]:
        raise AuthError({
            "code": "unauthorized",
            "description": "Permission not found."
        }, 403)

    return True


def verify_decode_jwt(token):
    """
    Validates a JWT and returns the decoded payload.
    """
    jsonurl = urlopen(f"https://{AUTH0_DOMAIN}/.well-known/jwks.json")
    jwks = json.loads(jsonurl.read())

    unverified_header = None

    try:
        unverified_header = jwt.get_unverified_header(token)

    except jose.JWTError:
        raise AuthError({
            'code': 'invalid_header',
            'description': 'Error decoding token headers.'
        }, 401)

    rsa_key = {}

    if 'kid' not in unverified_header:
        raise AuthError({
            'code': 'invalid_header',
            'description': 'Authorization malformed.'
        }, 401)

    for key in jwks["keys"]:
        if key["kid"] == unverified_header["kid"]:
            rsa_key = {
                "kty": key["kty"],
                "kid": key["kid"],
                "use": key["use"],
                "n": key["n"],
                "e": key["e"]
            }

    if rsa_key:
        try:
            payload = jwt.decode(
                token,
                rsa_key,
                algorithms=ALGORITHMS,
                audience=API_AUDIENCE,
                issuer=f"https://{AUTH0_DOMAIN}/"
            )

            return payload

        except jwt.ExpiredSignatureError:
            raise AuthError({
                'code': 'token_expired',
                'description': 'Token expired.'
            }, 401)

        except jwt.JWTClaimsError:
            raise AuthError({
                'code': 'invalid_claims',
                'description': 'Incorrect claims. Please, check the audience and issuer.'
            }, 401)

        except Exception:
            raise AuthError({
                'code': 'invalid_header',
                'description': 'Unable to parse authentication token.'
            }, 401)

    raise AuthError({
        'code': 'invalid_header',
                'description': 'Unable to find the appropriate key.'
    }, 401)


def requires_auth(permission=''):
    def requires_auth_decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            try:
                token = get_token_auth_header()
                payload = verify_decode_jwt(token)
                check_permissions(permission, payload)

            except AuthError as auth_error:
                print(auth_error)
                if auth_error.status_code == 400:
                    abort(400)

                if auth_error.status_code == 401:
                    abort(401)

                if auth_error.status_code == 403:
                    abort(403)

            return f(payload, *args, **kwargs)

        return wrapper
    return requires_auth_decorator


def get_jwt_subject():
    """Returns a the subject from a valid access tokekn"""
    token = get_token_auth_header()
    payload = verify_decode_jwt(token)

    if "sub" not in payload:
        abort(401)

    return payload["sub"]

import json
from flask import request, _request_ctx_stack, abort
from functools import wraps
import jose
from jose import jwt
from urllib.request import urlopen
import os
from dotenv import load_dotenv

basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '.env'))


AUTH0_DOMAIN = os.environ.get('AUTH0_TENANT_DOMAIN')
ALGORITHMS = os.environ.get('AUTH0_ALGORITHMS')
API_AUDIENCE = os.environ.get('AUTH0_API_AUDIENCE')


class AuthError(Exception):
    '''
    AuthError Exception
    A standardized way to communicate auth failure modes
    '''

    def __init__(self, error, status_code):
        self.error = error
        self.status_code = status_code


def get_token_auth_header():
    """
    Returns the access token from the authorization header
    """
    auth_header = request.headers.get("Authorization", None)

    if not auth_header:
        raise AuthError({
            "code": 'authorization_header_missing',
            "description": "Authorization header is expected."
        }, 401)

    parts = auth_header.split()

    if len(parts) is not 2:
        raise AuthError({
            'code': 'invalid_header',
            'description': 'Authorization header must be a valid bearer token.'
        }, 401)

    if parts[0].lower() != 'bearer':
        print('wrong header', parts[0].lower())
        raise AuthError({
            "code": "invalid_header",
            "description": "Authorization header must start with 'Bearer'"
        }, 401)

    token = parts[1]
    return token


def check_permissions(permission, payload):
    """
    Returns True if requested permission string is included in the payload, otherwise it raises an AuthError
    """

    if 'permissions' not in payload:
        raise AuthError({
            "code": "invalid_claims",
            "description": "Permissions not included in JWT"
        }, 403)

    if permission not in payload["permissions"]:
        raise AuthError({
            "code": "unauthorized",
            "description": "Permission not found."
        }, 403)

    return True


def verify_decode_jwt(token):
    """
    Validates a JWT and returns the decoded payload.
    """
    jsonurl = urlopen(f"https://{AUTH0_DOMAIN}/.well-known/jwks.json")
    jwks = json.loads(jsonurl.read())

    unverified_header = None

    try:
        unverified_header = jwt.get_unverified_header(token)

    except jose.JWTError:
        raise AuthError({
            'code': 'invalid_header',
            'description': 'Error decoding token headers.'
        }, 401)

    rsa_key = {}

    if 'kid' not in unverified_header:
        raise AuthError({
            'code': 'invalid_header',
            'description': 'Authorization malformed.'
        }, 401)

    for key in jwks["keys"]:
        if key["kid"] == unverified_header["kid"]:
            rsa_key = {
                "kty": key["kty"],
                "kid": key["kid"],
                "use": key["use"],
                "n": key["n"],
                "e": key["e"]
            }

    if rsa_key:
        try:
            payload = jwt.decode(
                token,
                rsa_key,
                algorithms=ALGORITHMS,
                audience=API_AUDIENCE,
                issuer=f"https://{AUTH0_DOMAIN}/"
            )

            return payload

        except jwt.ExpiredSignatureError:
            raise AuthError({
                'code': 'token_expired',
                'description': 'Token expired.'
            }, 401)

        except jwt.JWTClaimsError:
            raise AuthError({
                'code': 'invalid_claims',
                'description': 'Incorrect claims. Please, check the audience and issuer.'
            }, 401)

        except Exception:
            raise AuthError({
                'code': 'invalid_header',
                'description': 'Unable to parse authentication token.'
            }, 401)

    raise AuthError({
        'code': 'invalid_header',
                'description': 'Unable to find the appropriate key.'
    }, 401)


def requires_auth(permission=''):
    def requires_auth_decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            try:
                token = get_token_auth_header()
                payload = verify_decode_jwt(token)
                check_permissions(permission, payload)

            except AuthError as auth_error:
                print(auth_error)
                if auth_error.status_code == 400:
                    abort(400)

                if auth_error.status_code == 401:
                    abort(401)

                if auth_error.status_code == 403:
                    abort(403)

            return f(payload, *args, **kwargs)

        return wrapper
    return requires_auth_decorator


def get_jwt_subject():
    """Returns a the subject from a valid access tokekn"""
    token = get_token_auth_header()
    payload = verify_decode_jwt(token)

    if "sub" not in payload:
        abort(401)

    return payload["sub"]

AuthError Exception     A standardized way to communicate auth failure modes Returns the access token from the authorization header Returns True if requested permission string is included in the payload, otherwise it raises an AuthError Validates a JWT and returns the decoded payload. Returns a the subject from a valid access tokekn