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,
    base64url_to_bytes,
)
from webauthn.helpers.exceptions import InvalidRegistrationResponse, InvalidAuthenticationResponse
from webauthn.helpers import 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.translation import gettext_lazy as _

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 .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:
        auth_backend = request.session.get(settings.AUTH_BACKEND_KEY)
        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)

        try:
            login(request, user, backend=auth_backend)
        except Exception as e:
            logger.exception(e)
            return HttpResponse(status=403)

        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


# ----------------------------------------------------------------------
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
