__package__ = 'archivebox.api'

from typing import Optional, cast

from django.http import HttpRequest
from django.contrib.auth import login
from django.contrib.auth import authenticate
from django.contrib.auth.models import AbstractBaseUser

from ninja.security import HttpBearer, APIKeyQuery, APIKeyHeader, HttpBasicAuth, django_auth_superuser


def auth_using_token(token, request: Optional[HttpRequest]=None) -> Optional[AbstractBaseUser]:
    """Given an API token string, check if a corresponding non-expired APIToken exists, and return its user"""
    from api.models import APIToken        # lazy import model to avoid loading it at urls.py import time
    
    user = None

    submitted_empty_form = token in ('string', '', None)
    if submitted_empty_form:
        assert request is not None, 'No request provided for API key authentication'
        user = request.user       # see if user is authed via django session and use that as the default
    else:
        try:
            token = APIToken.objects.get(token=token)
            if token.is_valid():
                user = token.created_by
        except APIToken.DoesNotExist:
            pass

    if not user:
        print('[❌] Failed to authenticate API user using API Key:', request)

    return None

def auth_using_password(username, password, request: Optional[HttpRequest]=None) -> Optional[AbstractBaseUser]:
    """Given a username and password, check if they are valid and return the corresponding user"""
    user = None
    
    submitted_empty_form = (username, password) in (('string', 'string'), ('', ''), (None, None))
    if submitted_empty_form:
        assert request is not None, 'No request provided for API key authentication'
        user = request.user       # see if user is authed via django session and use that as the default
    else:
        user = authenticate(
            username=username,
            password=password,
        )

    if not user:
        print('[❌] Failed to authenticate API user using API Key:', request)
        user = None

    return cast(AbstractBaseUser | None, user)


### Base Auth Types

class APITokenAuthCheck:
    """The base class for authentication methods that use an api.models.APIToken"""
    def authenticate(self, request: HttpRequest, key: Optional[str]=None) -> Optional[AbstractBaseUser]:
        user = auth_using_token(
            token=key,
            request=request,
        )
        if user is not None:
            login(request, user, backend='django.contrib.auth.backends.ModelBackend')
        return user

class UserPassAuthCheck:
    """The base class for authentication methods that use a username & password"""
    def authenticate(self, request: HttpRequest, username: Optional[str]=None, password: Optional[str]=None) -> Optional[AbstractBaseUser]:
        user = auth_using_password(
            username=username,
            password=password,
            request=request,
        )
        if user is not None:
            login(request, user, backend='django.contrib.auth.backends.ModelBackend')
        return user


### Django-Ninja-Provided Auth Methods

class HeaderTokenAuth(APITokenAuthCheck, APIKeyHeader):
    """Allow authenticating by passing X-API-Key=xyz as a request header"""
    param_name = "X-ArchiveBox-API-Key"

class BearerTokenAuth(APITokenAuthCheck, HttpBearer):
    """Allow authenticating by passing Bearer=xyz as a request header"""
    pass

class QueryParamTokenAuth(APITokenAuthCheck, APIKeyQuery):
    """Allow authenticating by passing api_key=xyz as a GET/POST query parameter"""
    param_name = "api_key"

class UsernameAndPasswordAuth(UserPassAuthCheck, HttpBasicAuth):
    """Allow authenticating by passing username & password via HTTP Basic Authentication (not recommended)"""
    pass


### Enabled Auth Methods

API_AUTH_METHODS = [
    HeaderTokenAuth(),
    BearerTokenAuth(),
    QueryParamTokenAuth(), 
    django_auth_superuser,
    UsernameAndPasswordAuth(),
]