import logging
from abc import ABC, abstractmethod
from base64 import b64decode
from typing import Any, Optional, Tuple
from urllib.parse import unquote

from django.conf import settings
from django.http import HttpRequest

from ninja.security.base import AuthBase

__all__ = ["HttpAuthBase", "HttpBearer", "DecodeError", "HttpBasicAuth"]


logger = logging.getLogger("django")


class HttpAuthBase(AuthBase, ABC):
    openapi_type: str = "http"


class HttpBearer(HttpAuthBase, ABC):
    openapi_scheme: str = "bearer"
    header: str = "Authorization"

    def __call__(self, request: HttpRequest) -> Optional[Any]:
        headers = request.headers
        auth_value = headers.get(self.header)
        if not auth_value:
            return None
        parts = auth_value.split(" ")

        if parts[0].lower() != self.openapi_scheme:
            if settings.DEBUG:
                logger.error(f"Unexpected auth - '{auth_value}'")
            return None
        token = " ".join(parts[1:])
        return self.authenticate(request, token)

    @abstractmethod
    def authenticate(self, request: HttpRequest, token: str) -> Optional[Any]:
        pass  # pragma: no cover


class DecodeError(Exception):
    pass


class HttpBasicAuth(HttpAuthBase, ABC):  # TODO: maybe HttpBasicAuthBase
    openapi_scheme = "basic"
    header = "Authorization"

    def __call__(self, request: HttpRequest) -> Optional[Any]:
        headers = request.headers
        auth_value = headers.get(self.header)
        if not auth_value:
            return None

        try:
            username, password = self.decode_authorization(auth_value)
        except DecodeError as e:
            if settings.DEBUG:
                logger.exception(e)
            return None
        return self.authenticate(request, username, password)

    @abstractmethod
    def authenticate(
        self, request: HttpRequest, username: str, password: str
    ) -> Optional[Any]:
        pass  # pragma: no cover

    def decode_authorization(self, value: str) -> Tuple[str, str]:
        parts = value.split(" ")
        if len(parts) == 1:
            user_pass_encoded = parts[0]
        elif len(parts) == 2 and parts[0].lower() == "basic":
            user_pass_encoded = parts[1]
        else:
            raise DecodeError("Invalid Authorization header")

        try:
            username, password = b64decode(user_pass_encoded).decode().split(":", 1)
            return unquote(username), unquote(password)
        except Exception as e:  # dear contributors please do not change to valueerror - here can be multiple exceptions
            raise DecodeError("Invalid Authorization header") from e
