package webauthnutil

import (
	"encoding/base64"
	"fmt"
	"time"

	"github.com/pomerium/webauthn"
	"github.com/pomerium/webauthn/cose"

	"github.com/pomerium/pomerium/pkg/cryptutil"
	"github.com/pomerium/pomerium/pkg/grpc/device"
	"github.com/pomerium/pomerium/pkg/grpc/user"
)

const (
	ceremonyTimeout = time.Minute * 15
	rpName          = "Pomerium"
)

// GenerateChallenge generates a new Challenge.
func GenerateChallenge(key []byte, expiry time.Time) cryptutil.SecureToken {
	return cryptutil.GenerateSecureToken(key, expiry, cryptutil.NewRandomToken())
}

// GenerateCreationOptions generates creation options for WebAuthn.
func GenerateCreationOptions(
	key []byte,
	deviceType *device.Type,
	user *user.User,
) *webauthn.PublicKeyCredentialCreationOptions {
	expiry := time.Now().Add(ceremonyTimeout)
	return newCreationOptions(
		GenerateChallenge(key, expiry).Bytes(),
		deviceType,
		user,
	)
}

// GenerateRequestOptions generates request options for WebAuthn.
func GenerateRequestOptions(
	key []byte,
	deviceType *device.Type,
	knownDeviceCredentials []*device.Credential,
) *webauthn.PublicKeyCredentialRequestOptions {
	expiry := time.Now().Add(ceremonyTimeout)
	return newRequestOptions(
		GenerateChallenge(key, expiry).Bytes(),
		deviceType,
		knownDeviceCredentials,
	)
}

// GetCreationOptionsForCredential gets the creation options for the public key creation credential. An error may be
// returned if the challenge used to generate the credential is invalid.
func GetCreationOptionsForCredential(
	key []byte,
	deviceType *device.Type,
	user *user.User,
	credential *webauthn.PublicKeyCreationCredential,
) (*webauthn.PublicKeyCredentialCreationOptions, error) {
	clientData, err := credential.Response.UnmarshalClientData()
	if err != nil {
		return nil, fmt.Errorf("invalid client data: %w", err)
	}

	rawChallenge, err := base64.RawURLEncoding.DecodeString(clientData.Challenge)
	if err != nil {
		return nil, fmt.Errorf("invalid challenge: %w", err)
	}
	var challenge cryptutil.SecureToken
	copy(challenge[:], rawChallenge)

	err = challenge.Verify(key, time.Now())
	if err != nil {
		return nil, err
	}

	return newCreationOptions(challenge.Bytes(), deviceType, user), nil
}

// GetRequestOptionsForCredential gets the request options for the public key request credential. An error may be
// returned if the challenge used to generate the credential is invalid.
func GetRequestOptionsForCredential(
	key []byte,
	deviceType *device.Type,
	knownDeviceCredentials []*device.Credential,
	credential *webauthn.PublicKeyAssertionCredential,
) (*webauthn.PublicKeyCredentialRequestOptions, error) {
	clientData, err := credential.Response.UnmarshalClientData()
	if err != nil {
		return nil, fmt.Errorf("invalid client data: %w", err)
	}

	rawChallenge, err := base64.RawURLEncoding.DecodeString(clientData.Challenge)
	if err != nil {
		return nil, fmt.Errorf("invalid challenge: %w", err)
	}
	var challenge cryptutil.SecureToken
	copy(challenge[:], rawChallenge)

	err = challenge.Verify(key, time.Now())
	if err != nil {
		return nil, err
	}

	return newRequestOptions(challenge.Bytes(), deviceType, knownDeviceCredentials), nil
}

// newCreationOptions gets the creation options for WebAuthn with the provided challenge.
func newCreationOptions(
	challenge []byte,
	deviceType *device.Type,
	user *user.User,
) *webauthn.PublicKeyCredentialCreationOptions {
	options := &webauthn.PublicKeyCredentialCreationOptions{
		RP: webauthn.PublicKeyCredentialRPEntity{
			Name: rpName,
		},
		User:      GetUserEntity(user),
		Challenge: challenge,
		Timeout:   ceremonyTimeout,
	}

	if deviceOptions := deviceType.GetWebauthn().GetOptions(); deviceOptions != nil {
		fillAllPublicKeyCredentialParameters(options, deviceOptions.GetPubKeyCredParams())
		fillAuthenticatorSelection(options, deviceOptions.GetAuthenticatorSelection())
		fillAttestationConveyance(options, deviceOptions.Attestation)
	}

	return options
}

// newRequestOptions gets the request options for WebAuthn with the provided challenge.
func newRequestOptions(
	challenge []byte,
	deviceType *device.Type,
	knownDeviceCredentials []*device.Credential,
) *webauthn.PublicKeyCredentialRequestOptions {
	options := &webauthn.PublicKeyCredentialRequestOptions{
		Challenge: challenge,
		Timeout:   ceremonyTimeout,
	}
	fillRequestUserVerificationRequirement(
		options,
		deviceType.GetWebauthn().GetOptions().GetAuthenticatorSelection().UserVerification,
	)
	for _, knownDeviceCredential := range knownDeviceCredentials {
		if publicKey := knownDeviceCredential.GetWebauthn(); publicKey != nil {
			options.AllowCredentials = append(options.AllowCredentials, webauthn.PublicKeyCredentialDescriptor{
				Type: webauthn.PublicKeyCredentialTypePublicKey,
				ID:   publicKey.GetId(),
			})
		}
	}
	return options
}

func fillAllPublicKeyCredentialParameters(
	options *webauthn.PublicKeyCredentialCreationOptions,
	allDeviceParams []*device.WebAuthnOptions_PublicKeyCredentialParameters,
) {
	options.PubKeyCredParams = nil
	for _, deviceParams := range allDeviceParams {
		p := webauthn.PublicKeyCredentialParameters{}
		fillPublicKeyCredentialParameters(&p, deviceParams)
		options.PubKeyCredParams = append(options.PubKeyCredParams, p)
	}
}

func fillAttestationConveyance(
	options *webauthn.PublicKeyCredentialCreationOptions,
	attestationConveyance *device.WebAuthnOptions_AttestationConveyancePreference,
) {
	options.Attestation = ""
	if attestationConveyance == nil {
		return
	}

	switch *attestationConveyance {
	case device.WebAuthnOptions_NONE:
		options.Attestation = webauthn.AttestationConveyanceNone
	case device.WebAuthnOptions_INDIRECT:
		options.Attestation = webauthn.AttestationConveyanceIndirect
	case device.WebAuthnOptions_DIRECT:
		options.Attestation = webauthn.AttestationConveyanceDirect
	case device.WebAuthnOptions_ENTERPRISE:
		options.Attestation = webauthn.AttestationConveyanceEnterprise
	}
}

func fillAuthenticatorAttachment(
	criteria *webauthn.AuthenticatorSelectionCriteria,
	authenticatorAttachment *device.WebAuthnOptions_AuthenticatorAttachment,
) {
	criteria.AuthenticatorAttachment = ""
	if authenticatorAttachment == nil {
		return
	}

	switch *authenticatorAttachment {
	case device.WebAuthnOptions_CROSS_PLATFORM:
		criteria.AuthenticatorAttachment = webauthn.AuthenticatorAttachmentCrossPlatform
	case device.WebAuthnOptions_PLATFORM:
		criteria.AuthenticatorAttachment = webauthn.AuthenticatorAttachmentPlatform
	}
}

func fillAuthenticatorSelection(
	options *webauthn.PublicKeyCredentialCreationOptions,
	deviceCriteria *device.WebAuthnOptions_AuthenticatorSelectionCriteria,
) {
	options.AuthenticatorSelection = new(webauthn.AuthenticatorSelectionCriteria)
	fillAuthenticatorAttachment(options.AuthenticatorSelection, deviceCriteria.AuthenticatorAttachment)
	fillResidentKeyRequirement(options.AuthenticatorSelection, deviceCriteria.ResidentKeyRequirement)
	options.AuthenticatorSelection.RequireResidentKey = deviceCriteria.GetRequireResidentKey()
	fillUserVerificationRequirement(options.AuthenticatorSelection, deviceCriteria.UserVerification)
}

func fillPublicKeyCredentialParameters(
	params *webauthn.PublicKeyCredentialParameters,
	deviceParams *device.WebAuthnOptions_PublicKeyCredentialParameters,
) {
	params.Type = ""
	params.COSEAlgorithmIdentifier = 0
	if deviceParams == nil {
		return
	}

	switch deviceParams.Type {
	case device.WebAuthnOptions_PUBLIC_KEY:
		params.Type = webauthn.PublicKeyCredentialTypePublicKey
	}
	params.COSEAlgorithmIdentifier = cose.Algorithm(deviceParams.GetAlg())
}

func fillRequestUserVerificationRequirement(
	options *webauthn.PublicKeyCredentialRequestOptions,
	userVerificationRequirement *device.WebAuthnOptions_UserVerificationRequirement,
) {
	options.UserVerification = ""
	if userVerificationRequirement == nil {
		return
	}

	switch *userVerificationRequirement {
	case device.WebAuthnOptions_USER_VERIFICATION_DISCOURAGED:
		options.UserVerification = webauthn.UserVerificationDiscouraged
	case device.WebAuthnOptions_USER_VERIFICATION_PREFERRED:
		options.UserVerification = webauthn.UserVerificationPreferred
	case device.WebAuthnOptions_USER_VERIFICATION_REQUIRED:
		options.UserVerification = webauthn.UserVerificationRequired
	}
}

func fillResidentKeyRequirement(
	criteria *webauthn.AuthenticatorSelectionCriteria,
	residentKeyRequirement *device.WebAuthnOptions_ResidentKeyRequirement,
) {
	criteria.ResidentKey = ""
	if residentKeyRequirement == nil {
		return
	}

	switch *residentKeyRequirement {
	case device.WebAuthnOptions_RESIDENT_KEY_DISCOURAGED:
		criteria.ResidentKey = webauthn.ResidentKeyDiscouraged
	case device.WebAuthnOptions_RESIDENT_KEY_PREFERRED:
		criteria.ResidentKey = webauthn.ResidentKeyPreferred
	case device.WebAuthnOptions_RESIDENT_KEY_REQUIRED:
		criteria.ResidentKey = webauthn.ResidentKeyRequired
	}
}

func fillUserVerificationRequirement(
	criteria *webauthn.AuthenticatorSelectionCriteria,
	userVerificationRequirement *device.WebAuthnOptions_UserVerificationRequirement,
) {
	criteria.UserVerification = ""
	if userVerificationRequirement == nil {
		return
	}

	switch *userVerificationRequirement {
	case device.WebAuthnOptions_USER_VERIFICATION_DISCOURAGED:
		criteria.UserVerification = webauthn.UserVerificationDiscouraged
	case device.WebAuthnOptions_USER_VERIFICATION_PREFERRED:
		criteria.UserVerification = webauthn.UserVerificationPreferred
	case device.WebAuthnOptions_USER_VERIFICATION_REQUIRED:
		criteria.UserVerification = webauthn.UserVerificationRequired
	}
}