import json
from django.db import IntegrityError, transaction
from django.utils import timezone
from rest_framework.exceptions import ValidationError, AuthenticationFailed
from webauthn import generate_registration_options, generate_authentication_options, verify_registration_response, verify_authentication_response
from webauthn.helpers import options_to_json, bytes_to_base64url, base64url_to_bytes
from webauthn.helpers.structs import AuthenticatorSelectionCriteria, ResidentKeyRequirement, UserVerificationRequirement, PublicKeyCredentialDescriptor
from webauthn.helpers.exceptions import WebAuthnException
from .security_models import PasskeyCredential
from .security_config import rp_for
from .security_mfa import verify_factor
from .security_sessions import issue
from . import security_challenges as challenges


def begin(origin,user=None):
    rp=rp_for(origin)
    if user:
        options=generate_registration_options(rp_id=rp,rp_name='FTG',user_name=user.email,user_id=user.pk.bytes,
            user_display_name=user.display_name or user.username,
            authenticator_selection=AuthenticatorSelectionCriteria(resident_key=ResidentKeyRequirement.REQUIRED,
                user_verification=UserVerificationRequirement.REQUIRED),
            exclude_credentials=[PublicKeyCredentialDescriptor(id=base64url_to_bytes(c.credential_id)) for c in user.passkeys.filter(rp_id=rp)])
    else:
        options=generate_authentication_options(rp_id=rp,user_verification=UserVerificationRequirement.REQUIRED)
    payload=json.loads(options_to_json(options))
    row=challenges.create('PASSKEY_LINK' if user else 'PASSKEY_LOGIN',{'challenge':payload['challenge'],'origin':origin,'rp_id':rp},user)
    return {'challenge_id':str(row.pk),'options':payload}


def finish(challenge_id,credential,origin,request,user=None,name='Passkey',otp=''):
    def operation(row):
        if row.payload['origin']!=origin:
            raise ValidationError('Passkey origin mismatch.')
        args={'credential':credential,'expected_challenge':base64url_to_bytes(row.payload['challenge']),
            'expected_rp_id':row.payload['rp_id'],'expected_origin':origin,'require_user_verification':True}
        try:
            if user:
                verified=verify_registration_response(**args)
                try:
                    with transaction.atomic():
                        key=PasskeyCredential.objects.create(user=user,credential_id=bytes_to_base64url(verified.credential_id),
                            public_key=verified.credential_public_key,sign_count=verified.sign_count,name=name[:80],rp_id=row.payload['rp_id'])
                except IntegrityError as exc:
                    raise ValidationError('This passkey is already registered.') from exc
                return {'linked':True,'passkey_id':str(key.pk)}
            candidate=PasskeyCredential.objects.filter(credential_id=credential.get('id'),rp_id=row.payload['rp_id']).values_list('user_id',flat=True).first()
            from .models import User
            account_user=User.objects.select_for_update().filter(pk=candidate).first()
            key=PasskeyCredential.objects.select_for_update(of=('self',)).select_related('user').filter(
                credential_id=credential.get('id'),rp_id=row.payload['rp_id']).first()
            if key is None or account_user is None or not account_user.is_active or not account_user.is_email_verified:
                raise AuthenticationFailed('Unknown or inactive passkey account.')
            verified=verify_authentication_response(**args,credential_public_key=bytes(key.public_key),credential_current_sign_count=key.sign_count)
            handle=credential.get('response',{}).get('userHandle')
            if handle and base64url_to_bytes(handle)!=key.user.pk.bytes:
                raise ValidationError('Passkey user handle mismatch.')
            verify_factor(account_user,otp)
            key.sign_count=verified.new_sign_count;key.last_used_at=timezone.now();key.save(update_fields=['sign_count','last_used_at'])
            return issue(account_user,request,'PASSKEY')
        except (WebAuthnException,ValueError,TypeError,KeyError) as exc:
            raise ValidationError('Passkey verification failed.') from exc
    return challenges.consume(challenge_id,'PASSKEY_LINK' if user else 'PASSKEY_LOGIN',operation,user)
