package handles

import (
	"encoding/base64"
	"encoding/binary"
	"encoding/json"
	"fmt"

	"github.com/OpenListTeam/OpenList/v4/internal/authn"
	"github.com/OpenListTeam/OpenList/v4/internal/conf"
	"github.com/OpenListTeam/OpenList/v4/internal/db"
	"github.com/OpenListTeam/OpenList/v4/internal/model"
	"github.com/OpenListTeam/OpenList/v4/internal/op"
	"github.com/OpenListTeam/OpenList/v4/internal/setting"
	"github.com/OpenListTeam/OpenList/v4/server/common"
	"github.com/gin-gonic/gin"
	"github.com/go-webauthn/webauthn/protocol"
	"github.com/go-webauthn/webauthn/webauthn"
)

func BeginAuthnLogin(c *gin.Context) {
	enabled := setting.GetBool(conf.WebauthnLoginEnabled)
	if !enabled {
		common.ErrorStrResp(c, "WebAuthn is not enabled", 403)
		return
	}
	authnInstance, err := authn.NewAuthnInstance(c)
	if err != nil {
		common.ErrorResp(c, err, 400)
		return
	}

	var (
		options     *protocol.CredentialAssertion
		sessionData *webauthn.SessionData
	)
	if username := c.Query("username"); username != "" {
		var user *model.User
		user, err = db.GetUserByName(username)
		if err == nil {
			options, sessionData, err = authnInstance.BeginLogin(user)
		}
	} else { // client-side discoverable login
		options, sessionData, err = authnInstance.BeginDiscoverableLogin()
	}
	if err != nil {
		common.ErrorResp(c, err, 400)
		return
	}

	val, err := json.Marshal(sessionData)
	if err != nil {
		common.ErrorResp(c, err, 400)
		return
	}
	common.SuccessResp(c, gin.H{
		"options": options,
		"session": val,
	})
}

func FinishAuthnLogin(c *gin.Context) {
	enabled := setting.GetBool(conf.WebauthnLoginEnabled)
	if !enabled {
		common.ErrorStrResp(c, "WebAuthn is not enabled", 403)
		return
	}
	authnInstance, err := authn.NewAuthnInstance(c)
	if err != nil {
		common.ErrorResp(c, err, 400)
		return
	}

	sessionDataString := c.GetHeader("session")
	sessionDataBytes, err := base64.StdEncoding.DecodeString(sessionDataString)
	if err != nil {
		common.ErrorResp(c, err, 400)
		return
	}

	var sessionData webauthn.SessionData
	if err := json.Unmarshal(sessionDataBytes, &sessionData); err != nil {
		common.ErrorResp(c, err, 400)
		return
	}

	var user *model.User
	if username := c.Query("username"); username != "" {
		user, err = db.GetUserByName(username)
		if err != nil {
			common.ErrorResp(c, err, 400)
			return
		}
		_, err = authnInstance.FinishLogin(user, sessionData, c.Request)
	} else { // client-side discoverable login
		_, err = authnInstance.FinishDiscoverableLogin(func(_, userHandle []byte) (webauthn.User, error) {
			// first param `rawID` in this callback function is equal to ID in webauthn.Credential,
			// but it's unnnecessary to check it.
			// userHandle param is equal to (User).WebAuthnID().
			userID := uint(binary.LittleEndian.Uint64(userHandle))
			user, err = db.GetUserById(userID)
			if err != nil {
				return nil, err
			}

			return user, nil
		}, sessionData, c.Request)
	}
	if err != nil {
		common.ErrorResp(c, err, 400)
		return
	}

	token, err := common.GenerateToken(user)
	if err != nil {
		common.ErrorResp(c, err, 400, true)
		return
	}
	common.SuccessResp(c, gin.H{"token": token})
}

func BeginAuthnRegistration(c *gin.Context) {
	enabled := setting.GetBool(conf.WebauthnLoginEnabled)
	if !enabled {
		common.ErrorStrResp(c, "WebAuthn is not enabled", 403)
		return
	}
	user := c.Request.Context().Value(conf.UserKey).(*model.User)

	authnInstance, err := authn.NewAuthnInstance(c)
	if err != nil {
		common.ErrorResp(c, err, 400)
	}

	options, sessionData, err := authnInstance.BeginRegistration(user)

	if err != nil {
		common.ErrorResp(c, err, 400)
	}

	val, err := json.Marshal(sessionData)
	if err != nil {
		common.ErrorResp(c, err, 400)
	}

	common.SuccessResp(c, gin.H{
		"options": options,
		"session": val,
	})
}

func FinishAuthnRegistration(c *gin.Context) {
	enabled := setting.GetBool(conf.WebauthnLoginEnabled)
	if !enabled {
		common.ErrorStrResp(c, "WebAuthn is not enabled", 403)
		return
	}
	user := c.Request.Context().Value(conf.UserKey).(*model.User)
	sessionDataString := c.GetHeader("Session")

	authnInstance, err := authn.NewAuthnInstance(c)
	if err != nil {
		common.ErrorResp(c, err, 400)
		return
	}

	sessionDataBytes, err := base64.StdEncoding.DecodeString(sessionDataString)
	if err != nil {
		common.ErrorResp(c, err, 400)
		return
	}

	var sessionData webauthn.SessionData
	if err := json.Unmarshal(sessionDataBytes, &sessionData); err != nil {
		common.ErrorResp(c, err, 400)
		return
	}

	credential, err := authnInstance.FinishRegistration(user, sessionData, c.Request)

	if err != nil {
		common.ErrorResp(c, err, 400)
		return
	}
	err = db.RegisterAuthn(user, credential)
	if err != nil {
		common.ErrorResp(c, err, 400)
		return
	}
	err = op.DelUserCache(user.Username)
	if err != nil {
		common.ErrorResp(c, err, 400)
		return
	}
	common.SuccessResp(c, "Registered Successfully")
}

func DeleteAuthnLogin(c *gin.Context) {
	user := c.Request.Context().Value(conf.UserKey).(*model.User)
	type DeleteAuthnReq struct {
		ID string `json:"id"`
	}
	var req DeleteAuthnReq
	err := c.ShouldBind(&req)
	if err != nil {
		common.ErrorResp(c, err, 400)
		return
	}
	err = db.RemoveAuthn(user, req.ID)
	if err != nil {
		common.ErrorResp(c, err, 400)
		return
	}
	err = op.DelUserCache(user.Username)
	if err != nil {
		common.ErrorResp(c, err, 400)
		return
	}
	common.SuccessResp(c, "Deleted Successfully")
}

func GetAuthnCredentials(c *gin.Context) {
	type WebAuthnCredentials struct {
		ID          []byte `json:"id"`
		FingerPrint string `json:"fingerprint"`
	}
	user := c.Request.Context().Value(conf.UserKey).(*model.User)
	credentials := user.WebAuthnCredentials()
	res := make([]WebAuthnCredentials, 0, len(credentials))
	for _, v := range credentials {
		credential := WebAuthnCredentials{
			ID:          v.ID,
			FingerPrint: fmt.Sprintf("% X", v.Authenticator.AAGUID),
		}
		res = append(res, credential)
	}
	common.SuccessResp(c, res)
}
