package user

import (
	context "context"
	"encoding/base64"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/tiinf/errs"

	"github.com/tidbcloud/account/commons/config"
	"github.com/tidbcloud/pkgv2/secret/encrypt"

	"github.com/juju/errors"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	"github.com/tidbcloud/account/internel/dao/entity"
	"gorm.io/gorm"
)

const (
	TokenExpireError  = "token has expired"
	TokenInvalidError = "token is invalid"
	TokenFieldSize    = 5
)

type CreateInviteTokenResp struct {
	TokenStr    string
	InviteToken entity.UserInviteToken
}

func getAesCrypt(ctx context.Context) (*encrypt.AESCrypt, error) {
	serverConfig := config.Global().ServerConfig

	aesCrypt, err := encrypt.NewAESCrypt([]byte(serverConfig.SaltSecret))
	if err != nil {
		tilog.Error(ctx, "create AESCrypt failed", zap.Error(err))
		return nil, err
	}
	tilog.Info(ctx, "get SaltSecret", zap.Any("serverConfig.SaltSecret", serverConfig.SaltSecret))
	return aesCrypt, err

}

func (d *UserDao) GenUserInviteToken(ctx context.Context, tx *gorm.DB, inviteUserToken entity.UserInviteToken, period time.Duration) (*CreateInviteTokenResp, error) {
	now := time.Now().Truncate(1 * time.Second)
	expire := now.Add(period)
	inviteUserToken.ExpireAt = expire

	dbInviteToken, err := saveOrUpdateInviteToken(ctx, tx, inviteUserToken)
	if err != nil {
		return nil, errors.Annotate(err,
			fmt.Sprintf("insert invite user token: UpsertInvitUserToken method %s email %s tenantID %d", inviteUserToken.Method, inviteUserToken.Email, inviteUserToken.TenantID))
	}
	inviteTokenStr, err := encodeToken(ctx, inviteUserToken)
	if err != nil {
		return nil, err
	}
	resp := &CreateInviteTokenResp{InviteToken: dbInviteToken, TokenStr: inviteTokenStr}
	tilog.Info(ctx, "GenUserInviteToken success")
	return resp, nil
}
func (d *UserDao) NewUserInviteToken(ctx context.Context, tx *gorm.DB, inviteUserToken entity.UserInviteToken, period time.Duration) (*CreateInviteTokenResp, error) {
	now := time.Now().Truncate(1 * time.Second)
	expire := now.Add(period)
	inviteUserToken.ExpireAt = expire
	err := inviteUserToken.Create(tx)
	if err != nil {
		tilog.Error(ctx, fmt.Sprintf("insert invite user token: UpsertInvitUserToken method %s email %s tenantID %d", inviteUserToken.Method, inviteUserToken.Email, inviteUserToken.TenantID))
		return nil, err
	}
	inviteTokenStr, err := encodeToken(ctx, inviteUserToken)
	if err != nil {

		return nil, err
	}
	resp := &CreateInviteTokenResp{InviteToken: inviteUserToken, TokenStr: inviteTokenStr}
	tilog.Info(ctx, "NewUserInviteToken success")
	return resp, nil
}

func saveOrUpdateInviteToken(ctx context.Context, tx *gorm.DB, userInviteToken entity.UserInviteToken) (entity.UserInviteToken, error) {
	var existingUserInviteToken entity.UserInviteToken
	err := entity.NewUserInviteTokenQuerySet(tx).MethodEq(
		userInviteToken.Method).EmailEq(userInviteToken.Email).One(&existingUserInviteToken)
	if err == nil {
		userInviteToken.ID = existingUserInviteToken.ID
		err = userInviteToken.Update(
			tx,
			entity.UserInviteTokenDBSchema.TenantID,
			entity.UserInviteTokenDBSchema.BeginAt,
			entity.UserInviteTokenDBSchema.ExpireAt,
			entity.UserInviteTokenDBSchema.IsOneTime,
		)
	} else if err == gorm.ErrRecordNotFound {
		err = userInviteToken.Create(tx)
	}
	return userInviteToken, errors.Annotate(err, "Upsert User Invite Token")
}

func (d *UserDao) ValidateUserInviteToken(ctx context.Context, tx *gorm.DB, token string, isDelete bool) (*entity.UserInviteToken, error) {
	return validate(ctx, tx, token, false, isDelete)
}
func (d *UserDao) ValidateUserInviteTokenIgnoreEmail(ctx context.Context, tx *gorm.DB, token string, isDelete bool) (*entity.UserInviteToken, error) {
	return validate(ctx, tx, token, true, isDelete)
}
func validate(ctx context.Context, tx *gorm.DB, token string, ignoreValidateEmail bool, isDelete bool) (*entity.UserInviteToken, error) {
	tilog.Info(ctx, "validate token req", zap.Any("token", token), zap.Any("ignoreValidateEmail", ignoreValidateEmail), zap.Any("isDelete", isDelete))
	inviteUserToken, err := decodeToken(ctx, token)
	tilog.Info(ctx, "decodeToken err", zap.Any("inviteUserToken", inviteUserToken))
	if err != nil {
		tilog.Info(ctx, "decodeToken err info", zap.Error(err))
		return nil, errs.NewBadRequest(TokenInvalidError, nil)
	}
	now := time.Now()
	id := inviteUserToken.ID
	email := inviteUserToken.Email
	method := inviteUserToken.Method
	tenantID := inviteUserToken.TenantID

	var userInviteToken entity.UserInviteToken
	var user entity.User

	err = entity.NewUserInviteTokenQuerySet(tx).IDEq(id).TenantIDEq(tenantID).MethodEq(method).EmailEq(inviteUserToken.Email).
		One(&userInviteToken)
	if err != nil {
		tilog.Info(ctx, "query token err", zap.Error(err))
		if err == gorm.ErrRecordNotFound {
			tilog.Info(ctx, "query token not found", zap.Any("token", inviteUserToken))
			return nil, errs.NewBadRequest(TokenInvalidError, nil)
			//return nil, errors.Annotatef(errors.New(TokenExpireError), "")

		}
		tilog.Info(ctx, fmt.Sprintf("validate invite user token: failed to get token from db, method %s email %s tenantID %d", method, email, tenantID))
		return nil, err
	}
	// validate  user's email in user table
	if !ignoreValidateEmail {
		err = entity.NewUserQuerySet(tx).EmailEq(email).One(&user)
		if err != nil {

			tilog.Error(ctx, "query token user email  fail", zap.Any("token", inviteUserToken), zap.Error(err))
			return nil, err
		}
	}

	if userInviteToken.ExpireAt.Before(now) {
		tilog.Info(ctx, TokenExpireError, zap.Any("token", inviteUserToken))
		return nil, errs.NewBadRequest(TokenExpireError, nil)
	}

	if userInviteToken.IsOneTime && isDelete {
		err := userInviteToken.Delete(tx)
		tilog.Info(ctx, "the token is one time , delete the token in this validate", zap.Any("token", inviteUserToken))
		if err != nil {

			tilog.Error(ctx, "delete token err", zap.Any("token", inviteUserToken))
			return nil, err
		}
	}

	if err != nil {
		tilog.Error(ctx, "validate err", zap.Any("err", err))
		return nil, err
	}
	return inviteUserToken, nil
}

func (d *UserDao) DeleteUserInviteToken(ctx context.Context, tx *gorm.DB, token string) error {
	inviteUserToken, err := decodeToken(ctx, token)
	tilog.Info(ctx, "decodeToken", zap.Any("token", token), zap.Any("parse token", inviteUserToken))
	if err != nil {
		return errs.NewBadRequest(TokenInvalidError, nil)
	}

	err = entity.NewUserInviteTokenQuerySet(tx).IDEq(inviteUserToken.ID).Delete()
	if err != nil {
		tilog.Error(ctx, "", zap.Any("inviteUserToken", inviteUserToken), zap.Error(err))
		return err
	}
	tilog.Info(ctx, "Delete Token", zap.Any("token", token), zap.Any("parse token", inviteUserToken))

	return nil
}

func encodeToken(ctx context.Context, userInviteToken entity.UserInviteToken) (string, error) {
	//  5 filed
	tokenKey := []byte(fmt.Sprintf("%s&%s&%d&%s&%d", userInviteToken.Method, userInviteToken.Email, userInviteToken.TenantID, userInviteToken.BeginAt.Format(time.RFC3339), userInviteToken.ID.Uint64()))
	en, err := getAesCrypt(ctx)
	if err != nil {
		return "", err
	}
	crypted := en.Encrypt(tokenKey)
	return base64.URLEncoding.EncodeToString(crypted), nil
}

func decodeToken(ctx context.Context, cryptedStr string) (t *entity.UserInviteToken, err error) {
	defer func() {
		if r := recover(); r != nil {
			tilog.Info(ctx, "decodeToken Decrypt panic")
			err = errs.NewBadRequest(TokenInvalidError, nil)
		}
	}()
	crypted, err := base64.URLEncoding.DecodeString(cryptedStr)
	if err != nil {
		tilog.Info(ctx, "DecodeString  err", zap.Any("cryptedStr", cryptedStr), zap.Error(err))

		return nil, err
	}

	en, err := getAesCrypt(ctx)
	if err != nil {
		tilog.Error(ctx, "getAesCrypt  err", zap.Error(err))
		return nil, err
	}

	text := en.Decrypt(crypted)
	tokenKeys := strings.Split(string(text), "&")
	if len(tokenKeys) != TokenFieldSize {
		tilog.Info(ctx, "decodeToken err", zap.Error(err))
		tilog.Info(ctx, "encodeToken  err ,keys lens is err ", zap.Any("tokenKeys", tokenKeys), zap.Any("TokenFieldSize", TokenFieldSize))
		return nil, errs.NewBadRequest(TokenInvalidError, nil)

	}
	tenantID, err := strconv.ParseUint(tokenKeys[2], 10, 64)
	if err != nil {
		tilog.Info(ctx, "get tenantID from token  err", zap.Any("tenantID", tenantID), zap.Error(err))
		return nil, err
	}
	id, err := strconv.ParseUint(tokenKeys[4], 10, 64)
	if err != nil {
		tilog.Info(ctx, fmt.Sprintf("token ParseInt tenantID %d err", tenantID), zap.Error(err))
		return nil, err
	}
	beginTime, err := time.Parse(time.RFC3339, tokenKeys[3])
	if err != nil {
		tilog.Info(ctx, "get beginTime from token  err", zap.Error(err))
		return nil, err
	}
	inviteUserToken := &entity.UserInviteToken{

		Method:   tokenKeys[0],
		Email:    tokenKeys[1],
		TenantID: entity.ID(tenantID),
		BeginAt:  beginTime,
		Base:     entity.Base{ID: entity.ID(id)},
	}
	return inviteUserToken, err
}
