package user

import (
	"context"
	"encoding/base64"
	"encoding/gob"
	"errors"
	"fmt"
	"github.com/cloudreve/Cloudreve/v4/application/dependency"
	"github.com/cloudreve/Cloudreve/v4/ent"
	"github.com/cloudreve/Cloudreve/v4/inventory"
	"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
	"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
	"github.com/cloudreve/Cloudreve/v4/pkg/util"
	"github.com/gin-gonic/gin"
	"github.com/go-webauthn/webauthn/protocol"
	"github.com/go-webauthn/webauthn/webauthn"
	"github.com/gofrs/uuid"
	"github.com/samber/lo"
	"strconv"
	"strings"
)

func init() {
	gob.Register(webauthn.SessionData{})
}

type authnUser struct {
	hasher      hashid.Encoder
	u           *ent.User
	credentials []*ent.Passkey
}

func (a *authnUser) WebAuthnID() []byte {
	return []byte(hashid.EncodeUserID(a.hasher, a.u.ID))
}

func (a *authnUser) WebAuthnName() string {
	return a.u.Email
}

func (a *authnUser) WebAuthnDisplayName() string {
	return a.u.Nick
}

func (a *authnUser) WebAuthnCredentials() []webauthn.Credential {
	if a.credentials == nil {
		return nil
	}

	return lo.Map(a.credentials, func(item *ent.Passkey, index int) webauthn.Credential {
		return *item.Credential
	})
}

const (
	authnSessionKey = "authn_session_"
)

func PreparePasskeyLogin(c *gin.Context) (*PreparePasskeyLoginResponse, error) {
	dep := dependency.FromContext(c)
	webAuthn, err := dep.WebAuthn(c)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to initialize WebAuthn", err)
	}

	options, sessionData, err := webAuthn.BeginDiscoverableLogin()
	if err != nil {
		return nil, serializer.NewError(serializer.CodeInitializeAuthn, "Failed to begin registration", err)
	}

	sessionID := uuid.Must(uuid.NewV4()).String()
	if err := dep.KV().Set(fmt.Sprintf("%s%s", authnSessionKey, sessionID), *sessionData, 300); err != nil {
		return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to store session data", err)
	}

	return &PreparePasskeyLoginResponse{
		Options:   options,
		SessionID: sessionID,
	}, nil
}

type (
	FinishPasskeyLoginParameterCtx struct{}
	FinishPasskeyLoginService      struct {
		Response  string `json:"response" binding:"required"`
		SessionID string `json:"session_id" binding:"required"`
	}
)

func (s *FinishPasskeyLoginService) FinishPasskeyLogin(c *gin.Context) (*ent.User, error) {
	dep := dependency.FromContext(c)
	kv := dep.KV()
	userClient := dep.UserClient()

	sessionDataRaw, ok := kv.Get(fmt.Sprintf("%s%s", authnSessionKey, s.SessionID))
	if !ok {
		return nil, serializer.NewError(serializer.CodeNotFound, "Session not found", nil)
	}

	_ = kv.Delete(authnSessionKey, s.Response)

	webAuthn, err := dep.WebAuthn(c)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to initialize WebAuthn", err)
	}

	sessionData := sessionDataRaw.(webauthn.SessionData)
	pcc, err := protocol.ParseCredentialRequestResponseBody(strings.NewReader(s.Response))
	if err != nil {
		return nil, serializer.NewError(serializer.CodeParamErr, "Failed to parse request", err)
	}

	var loginedUser *ent.User
	discoverUserHandle := func(rawID, userHandle []byte) (user webauthn.User, err error) {
		uid, err := dep.HashIDEncoder().Decode(string(userHandle), hashid.UserID)
		if err != nil {
			return nil, err
		}

		ctx := context.WithValue(c, inventory.LoadUserPasskey{}, true)
		ctx = context.WithValue(ctx, inventory.LoadUserGroup{}, true)
		u, err := userClient.GetLoginUserByID(ctx, uid)
		if err != nil {
			return nil, serializer.NewError(serializer.CodeDBError, "Failed to get user", err)
		}

		if inventory.IsAnonymousUser(u) {
			return nil, errors.New("anonymous user")
		}

		loginedUser = u
		return &authnUser{u: u, hasher: dep.HashIDEncoder(), credentials: u.Edges.Passkey}, nil
	}

	credential, err := webAuthn.ValidateDiscoverableLogin(discoverUserHandle, sessionData, pcc)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeWebAuthnCredentialError, "Failed to validate login", err)
	}

	// Find the credential just used
	usedCredentialId := base64.StdEncoding.EncodeToString(credential.ID)
	usedCredential, found := lo.Find(loginedUser.Edges.Passkey, func(item *ent.Passkey) bool {
		return item.CredentialID == usedCredentialId
	})

	if !found {
		return nil, serializer.NewError(serializer.CodeInternalSetting, "Passkey login passed but credential used is unknown", nil)
	}

	// Update used at
	if err := userClient.MarkPasskeyUsed(c, loginedUser.ID, usedCredential.CredentialID); err != nil {
		return nil, serializer.NewError(serializer.CodeDBError, "Failed to update passkey", err)
	}

	return loginedUser, nil
}

func PreparePasskeyRegister(c *gin.Context) (*protocol.CredentialCreation, error) {
	dep := dependency.FromContext(c)
	userClient := dep.UserClient()
	u := inventory.UserFromContext(c)

	existingKeys, err := userClient.ListPasskeys(c, u.ID)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeDBError, "Failed to list passkeys", err)
	}

	webAuthn, err := dep.WebAuthn(c)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to initialize WebAuthn", err)
	}

	authSelect := protocol.AuthenticatorSelection{
		RequireResidentKey: protocol.ResidentKeyRequired(),
		UserVerification:   protocol.VerificationPreferred,
	}

	options, sessionData, err := webAuthn.BeginRegistration(
		&authnUser{u: u, hasher: dep.HashIDEncoder()},
		webauthn.WithAuthenticatorSelection(authSelect),
		webauthn.WithExclusions(lo.Map(existingKeys, func(item *ent.Passkey, index int) protocol.CredentialDescriptor {
			return protocol.CredentialDescriptor{
				Type:            protocol.PublicKeyCredentialType,
				CredentialID:    item.Credential.ID,
				Transport:       item.Credential.Transport,
				AttestationType: item.Credential.AttestationType,
			}
		})),
	)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeInitializeAuthn, "Failed to begin registration", err)
	}

	if err := dep.KV().Set(fmt.Sprintf("%s%d", authnSessionKey, u.ID), *sessionData, 300); err != nil {
		return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to store session data", err)
	}

	return options, nil
}

type (
	FinishPasskeyRegisterParameterCtx struct{}
	FinishPasskeyRegisterService      struct {
		Response string `json:"response" binding:"required"`
		Name     string `json:"name" binding:"required"`
		UA       string `json:"ua" binding:"required"`
	}
)

func (s *FinishPasskeyRegisterService) FinishPasskeyRegister(c *gin.Context) (*Passkey, error) {
	dep := dependency.FromContext(c)
	kv := dep.KV()
	u := inventory.UserFromContext(c)

	sessionDataRaw, ok := kv.Get(fmt.Sprintf("%s%d", authnSessionKey, u.ID))
	if !ok {
		return nil, serializer.NewError(serializer.CodeNotFound, "Session not found", nil)
	}

	_ = kv.Delete(authnSessionKey, strconv.Itoa(u.ID))

	webAuthn, err := dep.WebAuthn(c)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to initialize WebAuthn", err)
	}

	sessionData := sessionDataRaw.(webauthn.SessionData)
	pcc, err := protocol.ParseCredentialCreationResponseBody(strings.NewReader(s.Response))
	if err != nil {
		return nil, serializer.NewError(serializer.CodeParamErr, "Failed to parse request", err)
	}

	credential, err := webAuthn.CreateCredential(&authnUser{u: u, hasher: dep.HashIDEncoder()}, sessionData, pcc)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeWebAuthnCredentialError, "Failed to finish registration", err)
	}

	client := dep.UAParser().Parse(s.UA)
	name := util.Replace(map[string]string{
		"{os}":      client.Os.Family,
		"{browser}": client.UserAgent.Family,
	}, s.Name)

	passkey, err := dep.UserClient().AddPasskey(c, u.ID, name, credential)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeDBError, "Failed to add passkey", err)
	}

	res := BuildPasskey(passkey)
	return &res, nil
}

type (
	DeletePasskeyService struct {
		ID string `form:"id" binding:"required"`
	}
	DeletePasskeyParameterCtx struct{}
)

func (s *DeletePasskeyService) DeletePasskey(c *gin.Context) error {
	dep := dependency.FromContext(c)
	u := inventory.UserFromContext(c)
	userClient := dep.UserClient()

	existingKeys, err := userClient.ListPasskeys(c, u.ID)
	if err != nil {
		return serializer.NewError(serializer.CodeDBError, "Failed to list passkeys", err)
	}

	var existing *ent.Passkey
	for _, key := range existingKeys {
		if key.CredentialID == s.ID {
			existing = key
			break
		}
	}

	if existing == nil {
		return serializer.NewError(serializer.CodeNotFound, "Passkey not found", nil)
	}

	if err := userClient.RemovePasskey(c, u.ID, s.ID); err != nil {
		return serializer.NewError(serializer.CodeDBError, "Failed to delete passkey", err)
	}

	return nil
}
