import functools
from functools import wraps
from inspect import iscoroutinefunction
from typing import Callable, Union, Optional
from urllib.request import Request

from flask import request
from flask_cors import cross_origin

from api.errors import unauthorized
from api.route_user_extension import RouteUserExtension
from core.environment import Environment
from data.schemas.administration.api_key import ApiKey
from data.schemas.administration.api_key_dao import apiKeyDao
from data.schemas.administration.user import User


class Route(RouteUserExtension):
    registered_routes = {}

    @classmethod
    async def get_api_key(cls) -> ApiKey:
        auth_header = request.headers.get("Authorization", None)
        api_key = auth_header.split(" ")[1]
        return await apiKeyDao.find_by_key(api_key)

    @classmethod
    async def _verify_api_key(cls, req: Request) -> bool:
        auth_header = req.headers.get("Authorization", None)
        if not auth_header or not auth_header.startswith("API-Key "):
            return False

        api_key = auth_header.split(" ")[1]
        api_key_from_db = await apiKeyDao.find_by_key(api_key)
        return api_key_from_db is not None and not api_key_from_db.deleted

    @classmethod
    async def _get_auth_type(cls, auth_header: str) -> Optional[Union[User, ApiKey]]:
        if auth_header.startswith("Bearer "):
            return await cls.get_user()
        elif auth_header.startswith("API-Key "):
            return await cls.get_api_key()
        elif (
            auth_header.startswith("DEV-User ")
            and Environment.get_environment() == "development"
        ):
            return await cls.get_dev_user()
        return None

    @classmethod
    async def get_authenticated_user_or_api_key(cls) -> Union[User, ApiKey]:
        auth_header = request.headers.get("Authorization", None)
        if not auth_header:
            raise Exception("No Authorization header found")

        user_or_api_key = await cls._get_auth_type(auth_header)
        if user_or_api_key is None:
            raise Exception("Invalid Authorization header")
        return user_or_api_key

    @classmethod
    async def get_authenticated_user_or_api_key_or_default(
        cls,
    ) -> Optional[Union[User, ApiKey]]:
        auth_header = request.headers.get("Authorization", None)
        if not auth_header:
            return None

        return await cls._get_auth_type(auth_header)

    @classmethod
    async def is_authorized(cls) -> bool:
        auth_header = request.headers.get("Authorization", None)
        if not auth_header:
            return False

        if auth_header.startswith("Bearer "):
            return await cls.verify_login(request)
        elif auth_header.startswith("API-Key "):
            return await cls._verify_api_key(request)
        elif (
            auth_header.startswith("DEV-User ")
            and Environment.get_environment() == "development"
        ):
            user = await cls.get_dev_user()
            return user is not None
        return False

    @classmethod
    def authorize(
        cls,
        f: Callable = None,
        skip_in_dev=False,
        by_api_key=False,
    ):
        if f is None:
            return functools.partial(
                cls.authorize, skip_in_dev=skip_in_dev, by_api_key=by_api_key
            )

        @wraps(f)
        async def decorator(*args, **kwargs):
            if skip_in_dev and Environment.get_environment() == "development":
                if iscoroutinefunction(f):
                    return await f(*args, **kwargs)
                return f(*args, **kwargs)

            if not await cls.is_authorized():
                return unauthorized()

            if iscoroutinefunction(f):
                return await f(*args, **kwargs)
            return f(*args, **kwargs)

        return decorator

    @classmethod
    def route(cls, path=None, **kwargs):
        def inner(fn):
            cross_origin(fn)
            cls.registered_routes[path] = (fn, kwargs)
            return fn

        return inner

    @classmethod
    def get(cls, path=None, **kwargs):
        return cls.route(path, methods=["GET"], **kwargs)

    @classmethod
    def post(cls, path=None, **kwargs):
        return cls.route(path, methods=["POST"], **kwargs)

    @classmethod
    def head(cls, path=None, **kwargs):
        return cls.route(path, methods=["HEAD"], **kwargs)

    @classmethod
    def put(cls, path=None, **kwargs):
        return cls.route(path, methods=["PUT"], **kwargs)

    @classmethod
    def delete(cls, path=None, **kwargs):
        return cls.route(path, methods=["DELETE"], **kwargs)
