from datetime import timedelta
import json
import logging
import re
from vanilla import DeleteView, DetailView, TemplateView, UpdateView
from webauthn import (
    generate_authentication_options,
    generate_registration_options,
    options_to_json,
    verify_authentication_response,
    verify_registration_response,
)
from webauthn.helpers.exceptions import InvalidRegistrationResponse, InvalidAuthenticationResponse
from webauthn.helpers import (
    base64url_to_bytes,
    bytes_to_base64url,
    parse_authentication_credential_json,
)
from webauthn.helpers.structs import (
    AttestationConveyancePreference,
    AuthenticatorSelectionCriteria,
    PublicKeyCredentialDescriptor,
    UserVerificationRequirement,
)
from typing import Any, TypeAlias, TYPE_CHECKING

from django.conf import settings
from django.contrib import messages
from django.contrib.auth import login
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.sessions.backends.base import SessionBase
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.utils.functional import cached_property

from console_base.typehints import StrOrPromise
from console_base.views.mixins import UrlPropertiesBaseMixin
from console_base.views import (
    LCDeleteViewBase,
    LCDetailViewBase,
    LCFormViewBase,
    LCUpdateViewBase,
    LCTemplateViewBase,
    LCOpenTemplateViewBase,
)
from console_sessions import (
    CREDENTIAL_ID_SESSION_KEY,
    SESSION_LAST_MODIFIED_KEY,
    LAST_MFA_SESSION_KEY,
)
from .forms import CredentialForm
from .models import (
    User,
    Authentication,
    Credential,
    Registration,
)
from .utils import get_credential_type

if TYPE_CHECKING:
    WebAuthnMixinBase: TypeAlias = LCFormViewBase
else:
    WebAuthnMixinBase = object

logger = logging.getLogger(__name__)


# ----------------------------------------------------------------------
class WebAuthnMixin(WebAuthnMixinBase):
    """
    Mixin class to determine whether a WebAuthn authentication
    is required before authenticating the user session.
    """

    def form_valid(self, form: AuthenticationForm) -> HttpResponse:
        """
        Check if the user uses webauthn
        """
        user = form.get_user()
        if not user:
            return super().form_valid(form)

        auth_backend = getattr(user, 'backend', None)

        if auth_backend in settings.BACKENDS_REQUIRING_WEBAUTHN and user.webauthn.active().exists():
            self.request.session[settings.USER_PK_KEY] = str(user.pk)
            if auth_backend:
                self.request.session[settings.AUTH_BACKEND_KEY] = auth_backend
            url = f'{reverse("console_webauthn:login")}?next={self.request.GET.get("next")}'
            return HttpResponseRedirect(url)

        return super().form_valid(form)


# -------------------------------------------------------------------------
class WebAuthnRegister(UrlPropertiesBaseMixin, LCTemplateViewBase, TemplateView):
    permission_required = settings.NO_PERM_CHECK
    template_name = f'webauthn_register.{settings.TEMPLATE_EXTENSION}'

    def name(self) -> StrOrPromise:
        return _('Register WebAuthn Credential')

    def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
        user = request.user

        exclude_credentials = [
            PublicKeyCredentialDescriptor(id=base64url_to_bytes(credentials.credential_id or ''))
            for credentials in user.webauthn.values_list('credential_id', named=True)  # type: ignore[union-attr]
        ]

        authenticator_selection = AuthenticatorSelectionCriteria(
            user_verification=UserVerificationRequirement.PREFERRED,
        )
        public_credential_creation_options = generate_registration_options(
            rp_id=request.get_host(),
            rp_name=settings.CONSOLE_NAME,
            user_id=str(user.cid).encode(),  # type: ignore[union-attr]
            user_name=user.email,  # type: ignore[union-attr]
            user_display_name=user.get_full_name(),
            attestation=AttestationConveyancePreference.DIRECT,
            authenticator_selection=authenticator_selection,
            exclude_credentials=exclude_credentials,
        )

        webauthn_registration, _ = Registration.objects.get_or_create(user=user)

        # The options are encoded in bytes - to save them correctly
        # I use the json strings
        options_dict = json.loads(options_to_json(public_credential_creation_options))
        webauthn_registration.challenge = options_dict["challenge"]
        webauthn_registration.save()

        return render(
            request,
            self.template_name,
            context={
                "view": self,
                "public_credential_creation_options": options_to_json(
                    public_credential_creation_options
                ),
            },
        )

    def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
        webauthn_registration = Registration.objects.get(user=request.user)
        try:
            credential_body = json.loads(request.body)
            auth_verification = verify_registration_response(
                credential=credential_body,
                expected_challenge=base64url_to_bytes(webauthn_registration.challenge),
                expected_origin=f"https://{request.get_host()}",  # dont forget the ports
                expected_rp_id=request.get_host(),
            )

            credential_format = get_credential_type(
                auth_verification.fmt,
                auth_verification.attestation_object,
            )
            Credential.objects.create(
                name=credential_body.get('credential_name'),
                user=request.user,
                user_verified=auth_verification.user_verified,
                credential_public_key=bytes_to_base64url(auth_verification.credential_public_key),
                credential_id=bytes_to_base64url(auth_verification.credential_id),
                aaguid=auth_verification.aaguid,
                credential_fmt=credential_format,
                device_type=auth_verification.credential_device_type,
            )

            return HttpResponse(status=201)
        except (Exception, InvalidRegistrationResponse) as error:
            logger.exception('Registration failed: something bad went wrong: %s', error)
            messages.error(request, f"Something went wrong: {error}")
            return redirect("console_webauthn:register")


# -------------------------------------------------------------------------
class WebAuthnLogin(UrlPropertiesBaseMixin, LCOpenTemplateViewBase, TemplateView):
    template_name = f'webauthn_login.{settings.TEMPLATE_EXTENSION}'

    def name(self) -> StrOrPromise:
        return _('Login Via WebAuthn Credential')

    def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
        user = get_object_or_404(User, pk=request.session.get(settings.USER_PK_KEY))
        webauthn_authentication, _ = Authentication.objects.get_or_create(user=user)

        allowed_credentials = [
            PublicKeyCredentialDescriptor(id=base64url_to_bytes(credentials.credential_id or ''))
            for credentials in user.webauthn.values_list('credential_id', named=True)  # type: ignore[union-attr]
        ]
        authentication_options = generate_authentication_options(
            rp_id=request.get_host(),
            allow_credentials=allowed_credentials,
        )
        json_option = json.loads(options_to_json(authentication_options))
        webauthn_authentication.challenge = json_option.get("challenge")
        webauthn_authentication.save()

        get_params = request.GET
        next_url = get_params.get('next')

        # Handle URLs where the value of the "next" param contains a URL that also has GET params.
        # That "next" URL should just be the entire string / path following `next=`,
        # Python's query parsing extracts key/value pairs from query, so use regex to get "next"
        # URL when multiple values are found.
        # /webauthn/login/?next=/o/authorize/?response_type=code&scope=openid+user_name+email&client_id=560f20ed-85c6-6f15-a31d-aa1697df14d0&redirect_uri=https%3A%2F%2Fwhitespire.compassfoundation.io%2Foidc%2Fcallback%2F&state=grEULqVIOpW72xWnjLeOhaeoRY4O54zR&nonce=lNMmp81uwvx9vHrPHniT24rt7RCv1Y5t
        if next_url and len(get_params) > 1:
            match = re.search(r'next=(.*)', request.get_full_path())
            next_url = match and match.group(1)

        return render(
            request,
            self.template_name,
            context={
                "authentication_options": options_to_json(authentication_options),
                "next": next_url or reverse('latchstring:user_profile'),
                "view": self,
            },
        )

    def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
        user = get_object_or_404(User, pk=request.session.get(settings.USER_PK_KEY))

        webauthn_authentication = Authentication.objects.get(user=user)
        authentication_cred = parse_authentication_credential_json(json.loads(request.body))

        try:
            # only lookup active credentials, so that soft deletes can remove the key from use
            webauthn_cred = Credential.objects.active().get(credential_id=authentication_cred.id)
        except Credential.DoesNotExist:
            self.clear_webauthn_session_keys(request.session)
            return HttpResponse(b'No active credential found!', status=403)

        if webauthn_cred.user != user:
            logger.error(
                'WebAuth credentials %s user does not match session user %s'
                % (webauthn_cred.user, user)
            )
            self.clear_webauthn_session_keys(request.session)
            return HttpResponse(b'Credential user does not match session user!', status=403)

        try:
            authentication_verification = verify_authentication_response(
                credential=authentication_cred,
                expected_challenge=base64url_to_bytes(webauthn_authentication.challenge),
                expected_rp_id=request.get_host(),
                expected_origin=f"https://{request.get_host()}",
                credential_public_key=base64url_to_bytes(webauthn_cred.credential_public_key),
                credential_current_sign_count=webauthn_cred.current_sign_count,
            )
        except InvalidAuthenticationResponse as error:
            logger.exception(error)
            return HttpResponse(status=403)

        webauthn_cred.current_sign_count = authentication_verification.new_sign_count
        webauthn_cred.save()
        self.clear_webauthn_session_keys(request.session)

        self.save_webauthn_session_keys(
            bytes_to_base64url(authentication_verification.credential_id),
            request.session,
        )

        # if not user.is_authenticated:
        try:
            auth_backend = request.session.get(settings.AUTH_BACKEND_KEY)
            logger.info(f'XXX the auth backend retrieved from setting key {settings.AUTH_BACKEND_KEY!r} is  {auth_backend!r}')
            login(request, user, backend='latchstring.backends.rules_backend.LCObjectBackend')
        except Exception as e:
            logger.exception(e)
            return HttpResponse(status=403)

        logger.info(f'XXX return the 200 status')
        return HttpResponse(status=200)

    def clear_webauthn_session_keys(self, session: SessionBase) -> None:
        """
        Clear the session variables that the webauthn flow adds.
        """
        for key in (settings.AUTH_BACKEND_KEY, settings.USER_PK_KEY):
            try:
                del session[key]
            except KeyError:
                pass

    def save_webauthn_session_keys(self, credential_id: str, session: SessionBase) -> None:
        """
        Add credential variables to the session, so they can be checked
        when calculating View Permissions.
        """
        session[CREDENTIAL_ID_SESSION_KEY] = credential_id
        session[LAST_MFA_SESSION_KEY] = timezone.now()
        session.modified = True


# ----------------------------------------------------------------------
class CredentialUpdate(UrlPropertiesBaseMixin, LCUpdateViewBase, UpdateView):
    permission_required = 'webauthn.change_credential'
    template_name = f'record_create.{settings.TEMPLATE_EXTENSION}'
    model = Credential
    form_class = CredentialForm

    def send_optimized_success_response(self) -> bool:
        return True

    def get_unpoly_target(self) -> str:
        return f'#{self.get_object().row_pk}'

    def optimized_success_response(self) -> HttpResponse:
        return TemplateResponse(
            self.request,
            f'single_row_table.{settings.TEMPLATE_EXTENSION}',
            {
                'body_id': 'webauthn_credentials_body',
                'table_row_template': f'snippets/webauthn_credential_row.{settings.TEMPLATE_EXTENSION}',
                'credential': self.object,
                'view': self,
            },
        )


# ----------------------------------------------------------------------
class CredentialDetail(UrlPropertiesBaseMixin, LCDetailViewBase, DetailView):
    permission_required = 'webauthn.view_credential'
    template_name = f'detail.{settings.TEMPLATE_EXTENSION}'
    model = Credential


# ----------------------------------------------------------------------
class CredentialDelete(UrlPropertiesBaseMixin, LCDeleteViewBase, DeleteView):
    permission_required = 'webauthn.delete_credential'
    template_name = f'detail.{settings.TEMPLATE_EXTENSION}'
    unpoly_modal_template = f'confirmation_delete.{settings.TEMPLATE_EXTENSION}'
    model = Credential


# ----------------------------------------------------------------------
class MfaRequiredBase:
    mfa_expiration = timedelta(minutes=settings.PASSKEY_AUTH_EXPIRY_MINUTES)
    recent_activity_extension = timedelta(minutes=settings.PASSKEY_AUTH_RECENT_ACTIVITY_EXTENSION)

    @cached_property
    def mfa_event_required(self) -> bool:
        """
        Helper function to check if MFA event is required
        """
        session = self.request.session
        last_mfa = session.get(LAST_MFA_SESSION_KEY)
        if not last_mfa or last_mfa < (timezone.now() - self.mfa_expiration):
            session[settings.USER_PK_KEY] = str(self.request.user.id)
            session.modified = True
            return True

        return False

    @cached_property
    def mfa_session_is_stale(self) -> bool:
        """
        User hasn't used this session recently.
        """
        session = self.request.session
        if not (last_mfa := session.get(LAST_MFA_SESSION_KEY)):
            return True

        if last_mfa and (last_modified := session.get(SESSION_LAST_MODIFIED_KEY)):
            return last_modified < (timezone.now() - self.recent_activity_extension)

        return True

    def mfa_check_function(self) -> bool:
        raise NotImplementedError('Define on subclasses')

    def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
        return super().get_context_data(
            mfa_auth_event_required=self.mfa_check_function(),
            **kwargs,
        )


# ----------------------------------------------------------------------
class RecentMfaRequiredMixin(MfaRequiredBase):
    def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
        if request.user.is_authenticated and self.mfa_check_function():
            return redirect(f"{reverse('console_webauthn:login')}?next={request.path}")
        return super().dispatch(request, *args, **kwargs)


# ----------------------------------------------------------------------
class RecentMfaRequired(RecentMfaRequiredMixin):
    """
    Add this mixin to views that require a Multi-factor event
    within the specified time delta.
    """

    def mfa_check_function(self) -> bool:
        return self.mfa_event_required


# ----------------------------------------------------------------------
class RecentActivityOrMfaRequired(RecentMfaRequiredMixin):
    """
    Use this mixin to minimize the annoyance of Mfa events
    if recent activity is within the specified time delta.
    """

    def mfa_check_function(self) -> bool:
        return self.mfa_session_is_stale


# ----------------------------------------------------------------------
class RecentMfaWarningMixin(MfaRequiredBase):
    """
    Use in "Detail" views to disable CRUD buttons and
    show messages to user when an MFA event is required.
    """

    # -----------------------------------------------------
    def show_create_btn(self) -> bool:
        if self.mfa_check_function():
            return False
        return super().show_create_btn()

    # -----------------------------------------------------
    def show_update_btn(self) -> bool:
        if self.mfa_check_function():
            return False
        return super().show_update_btn()

    # -----------------------------------------------------
    def show_clone_btn(self) -> bool:
        if self.mfa_check_function():
            return False
        return super().show_clone_btn()

    # -----------------------------------------------------
    def show_delete_btn(self) -> bool:
        if self.mfa_check_function():
            return False
        return super().show_delete_btn()

    # -----------------------------------------------------
    def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
        """
        Display helpful messages to the user when they're on a
        detail page that hides CRUD buttons because an MFA event
        is required.
        """
        if request.user.is_authenticated and self.mfa_check_function():
            if not request.user.webauthn.exists():
                url = f"{reverse('latchstring:user_profile')}#webauthn_credentials_tab"
                messages.info(
                    request,
                    'Add passkeys or hardware tokens to '
                    f'<a href="{url}" class="alert-link">your profile</a> '
                    'to change settings here.',
                    extra_tags='safe',
                )
            else:
                url = f"{reverse('console_webauthn:login')}?next={request.path}"
                messages.info(
                    request,
                    'To change settings here, verify your '
                    f'session with your <a href="{url}" class="alert-link">'
                    f'passkey or hardware token</a>.',
                    extra_tags='safe',
                )
                request.session[settings.USER_PK_KEY] = str(request.user.id)
                request.session.modified = True
        return super().get(request, *args, **kwargs)


# ----------------------------------------------------------------------
class RecentMfaRequiredMsg(RecentMfaWarningMixin):
    def mfa_check_function(self) -> bool:
        return self.mfa_event_required


# ----------------------------------------------------------------------
class RecentActivityOrMfaRequiredMsg(RecentMfaWarningMixin):
    def mfa_check_function(self) -> bool:
        return self.mfa_session_is_stale
