package api

import (
	"bytes"
	"encoding/base64"
	"image/png"
	"time"

	"github.com/pquerna/otp/totp"

	"github.com/mayswind/ezbookkeeping/pkg/core"
	"github.com/mayswind/ezbookkeeping/pkg/errs"
	"github.com/mayswind/ezbookkeeping/pkg/log"
	"github.com/mayswind/ezbookkeeping/pkg/models"
	"github.com/mayswind/ezbookkeeping/pkg/services"
)

// TwoFactorAuthorizationsApi represents 2fa api
type TwoFactorAuthorizationsApi struct {
	twoFactorAuthorizations *services.TwoFactorAuthorizationService
	users                   *services.UserService
	tokens                  *services.TokenService
}

// Initialize a 2fa api singleton instance
var (
	TwoFactorAuthorizations = &TwoFactorAuthorizationsApi{
		twoFactorAuthorizations: services.TwoFactorAuthorizations,
		users:                   services.Users,
		tokens:                  services.Tokens,
	}
)

// TwoFactorStatusHandler returns 2fa status of current user
func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.WebContext) (any, *errs.Error) {
	uid := c.GetCurrentUid()
	twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(c, uid)

	if err == errs.ErrTwoFactorIsNotEnabled {
		statusResp := &models.TwoFactorStatusResponse{
			Enable: false,
		}

		return statusResp, nil
	}

	if err != nil {
		log.Errorf(c, "[twofactor_authorizations.TwoFactorStatusHandler] failed to get two-factor setting, because %s", err.Error())
		return nil, errs.Or(err, errs.ErrOperationFailed)
	}

	statusResp := &models.TwoFactorStatusResponse{
		Enable:    true,
		CreatedAt: twoFactorSetting.CreatedUnixTime,
	}

	return statusResp, nil
}

// TwoFactorEnableRequestHandler returns a new 2fa secret and qr code for current user to set 2fa and verify passcode next
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.WebContext) (any, *errs.Error) {
	uid := c.GetCurrentUid()
	enabled, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)

	if err != nil {
		log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to check two-factor setting, because %s", err.Error())
		return nil, errs.Or(err, errs.ErrOperationFailed)
	}

	if enabled {
		return nil, errs.ErrTwoFactorAlreadyEnabled
	}

	user, err := a.users.GetUserById(c, uid)

	if err != nil {
		if !errs.IsCustomError(err) {
			log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to get user, because %s", err.Error())
		}

		return nil, errs.ErrUserNotFound
	}

	if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_ENABLE_2FA) {
		return nil, errs.ErrNotPermittedToPerformThisAction
	}

	key, err := a.twoFactorAuthorizations.GenerateTwoFactorSecret(c, user, c.GetClientLocale())

	if err != nil {
		log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two-factor secret, because %s", err.Error())
		return nil, errs.Or(err, errs.ErrOperationFailed)
	}

	img, err := key.Image(240, 240)

	if err != nil {
		log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two-factor qrcode, because %s", err.Error())
		return nil, errs.ErrOperationFailed
	}

	imgData := &bytes.Buffer{}

	if err = png.Encode(imgData, img); err != nil {
		return nil, errs.ErrOperationFailed
	}

	enableResp := &models.TwoFactorEnableResponse{
		Secret: key.Secret(),
		QRCode: "data:image/png;base64," + base64.StdEncoding.EncodeToString(imgData.Bytes()),
	}

	return enableResp, nil
}

// TwoFactorEnableConfirmHandler enables 2fa for current user
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.WebContext) (any, *errs.Error) {
	var confirmReq models.TwoFactorEnableConfirmRequest
	err := c.ShouldBindJSON(&confirmReq)

	if err != nil {
		log.Warnf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] parse request failed, because %s", err.Error())
		return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
	}

	uid := c.GetCurrentUid()
	exists, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)

	if err != nil {
		log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to check two-factor setting, because %s", err.Error())
		return nil, errs.Or(err, errs.ErrOperationFailed)
	}

	if exists {
		return nil, errs.ErrTwoFactorAlreadyEnabled
	}

	user, err := a.users.GetUserById(c, uid)

	if err != nil {
		if !errs.IsCustomError(err) {
			log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to get user, because %s", err.Error())
		}

		return nil, errs.ErrUserNotFound
	}

	if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_ENABLE_2FA) {
		return nil, errs.ErrNotPermittedToPerformThisAction
	}

	twoFactorSetting := &models.TwoFactor{
		Uid:    uid,
		Secret: confirmReq.Secret,
	}

	if !totp.Validate(confirmReq.Passcode, confirmReq.Secret) {
		log.Warnf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] passcode is invalid")
		return nil, errs.ErrPasscodeInvalid
	}

	recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()

	if err != nil {
		log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to generate two-factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
		return nil, errs.Or(err, errs.ErrOperationFailed)
	}

	err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(c, uid, recoveryCodes, user.Salt)

	if err != nil {
		log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two-factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
		return nil, errs.Or(err, errs.ErrOperationFailed)
	}

	err = a.twoFactorAuthorizations.CreateTwoFactorSetting(c, twoFactorSetting)

	if err != nil {
		log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two-factor setting for user \"uid:%d\", because %s", uid, err.Error())
		return nil, errs.Or(err, errs.ErrOperationFailed)
	}

	log.Infof(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" has enabled two-factor authorization", uid)

	now := time.Now().Unix()
	err = a.tokens.DeleteTokensBeforeTime(c, uid, now)

	if err == nil {
		log.Infof(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
	} else {
		log.Warnf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
	}

	token, claims, err := a.tokens.CreateToken(c, user)

	if err != nil {
		log.Warnf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())

		confirmResp := &models.TwoFactorEnableConfirmResponse{
			RecoveryCodes: recoveryCodes,
		}

		return confirmResp, nil
	}

	c.SetTextualToken(token)
	c.SetTokenClaims(claims)
	c.SetTokenContext("")

	log.Infof(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)

	confirmResp := &models.TwoFactorEnableConfirmResponse{
		Token:         token,
		RecoveryCodes: recoveryCodes,
	}

	return confirmResp, nil
}

// TwoFactorDisableHandler disables 2fa for current user
func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.WebContext) (any, *errs.Error) {
	var disableReq models.TwoFactorDisableRequest
	err := c.ShouldBindJSON(&disableReq)

	if err != nil {
		log.Warnf(c, "[twofactor_authorizations.TwoFactorDisableHandler] parse request failed, because %s", err.Error())
		return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
	}

	uid := c.GetCurrentUid()
	user, err := a.users.GetUserById(c, uid)

	if err != nil {
		if !errs.IsCustomError(err) {
			log.Warnf(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
		}

		return nil, errs.ErrUserNotFound
	}

	if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_DISABLE_2FA) {
		return nil, errs.ErrNotPermittedToPerformThisAction
	}

	if !a.users.IsPasswordEqualsUserPassword(disableReq.Password, user) {
		return nil, errs.ErrUserPasswordWrong
	}

	enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)

	if err != nil {
		log.Errorf(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to check two-factor setting, because %s", err.Error())
		return nil, errs.Or(err, errs.ErrOperationFailed)
	}

	if !enableTwoFactor {
		return nil, errs.ErrTwoFactorIsNotEnabled
	}

	err = a.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(c, uid)

	if err != nil {
		log.Errorf(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two-factor recovery codes for user \"uid:%d\"", uid)
		return nil, errs.Or(err, errs.ErrOperationFailed)
	}

	err = a.twoFactorAuthorizations.DeleteTwoFactorSetting(c, uid)

	if err != nil {
		log.Errorf(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two-factor setting for user \"uid:%d\"", uid)
		return nil, errs.Or(err, errs.ErrOperationFailed)
	}

	log.Infof(c, "[twofactor_authorizations.TwoFactorDisableHandler] user \"uid:%d\" has disabled two-factor authorization", uid)

	return true, nil
}

// TwoFactorRecoveryCodeRegenerateHandler returns new 2fa recovery codes and revokes old recovery codes for current user
func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *core.WebContext) (any, *errs.Error) {
	var regenerateReq models.TwoFactorRegenerateRecoveryCodeRequest
	err := c.ShouldBindJSON(&regenerateReq)

	if err != nil {
		log.Warnf(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] parse request failed, because %s", err.Error())
		return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
	}

	uid := c.GetCurrentUid()
	user, err := a.users.GetUserById(c, uid)

	if err != nil {
		if !errs.IsCustomError(err) {
			log.Warnf(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
		}

		return nil, errs.ErrUserNotFound
	}

	if !a.users.IsPasswordEqualsUserPassword(regenerateReq.Password, user) {
		return nil, errs.ErrUserPasswordWrong
	}

	enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)

	if err != nil {
		log.Errorf(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to check two-factor setting, because %s", err.Error())
		return nil, errs.Or(err, errs.ErrOperationFailed)
	}

	if !enableTwoFactor {
		return nil, errs.ErrTwoFactorIsNotEnabled
	}

	recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()

	if err != nil {
		log.Errorf(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to generate two-factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
		return nil, errs.Or(err, errs.ErrOperationFailed)
	}

	err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(c, uid, recoveryCodes, user.Salt)

	if err != nil {
		log.Errorf(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to create two-factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
		return nil, errs.Or(err, errs.ErrOperationFailed)
	}

	recoveryCodesResp := &models.TwoFactorEnableConfirmResponse{
		RecoveryCodes: recoveryCodes,
	}

	log.Infof(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] user \"uid:%d\" has regenerated two-factor recovery codes", uid)

	return recoveryCodesResp, nil
}
