package session

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

	"github.com/pingcap/dbaas/pkg/secret/encrypt"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/central-sdk/db"
	"github.com/tidbcloud/central-sdk/db/dbutil"
	"github.com/tidbcloud/central-sdk/db/entity"
	"gorm.io/gorm"
)

const (
	TokenExpireError  = "token has expired"
	NewEmailSentError = "new email sent"
	ActivatedError    = "user has been activated"

	RegistrationMethod             = "/central.TenantService/Registration"
	RegistrationVerificationMethod = "/registration-verification"
	SocialRegistrationMethod       = "/central.TenantService/SocialRegistration"
	SendVerificationEmailMethod    = "/central.TenantService/SendVerificationEmail"
	InvitationVerificationMethod   = "/invitation-verification"
	CreateAuth0UserMethod          = "/central.TenantService/CreateAuth0User"
	AcceptInvitationMethod         = "/central.TenantService/AcceptInvitation"
)

var (
	CheckMap map[string]bool = map[string]bool{
		CreateAuth0UserMethod:       true,
		RegistrationMethod:          true,
		SendVerificationEmailMethod: true,
	}
	CheckEmailMap map[string]bool = map[string]bool{
		AcceptInvitationMethod:   true,
		SocialRegistrationMethod: true,
	}
)

type Session struct {
	AESCrypt *encrypt.AESCrypt
}

func NewSession(aesCrypt *encrypt.AESCrypt) *Session {
	return &Session{
		AESCrypt: aesCrypt,
	}
}

func (s *Session) EncodeToken(method, email string, tenantID entity.ID, beginTime time.Time) string {
	tokenKey := []byte(fmt.Sprintf("%s&%s&%d&%s", method, email, tenantID, beginTime.Format(time.RFC3339)))
	crypted := s.AESCrypt.Encrypt(tokenKey)
	return base64.URLEncoding.EncodeToString(crypted)
}

func (s *Session) DecodeToken(cryptedStr string) (*entity.UserInviteToken, error) {
	crypted, err := base64.URLEncoding.DecodeString(cryptedStr)
	if err != nil {
		return nil, errors.Annotate(errors.New("token is invalid"), "decode session key token")
	}
	text := s.AESCrypt.Decrypt(crypted)
	tokenKeys := strings.Split(string(text), "&")
	if len(tokenKeys) != 4 {
		return nil, errors.Annotate(errors.New("token is invalid"), "decode session key token token keys lens is not found")
	}
	tenantID, err := strconv.ParseUint(tokenKeys[2], 10, 64)
	if err != nil {
		return nil, errors.Annotate(err, fmt.Sprintf("decode session key token ParseInt tenantID %d", tenantID))
	}
	beginTime, err := time.Parse(time.RFC3339, tokenKeys[3])
	if err != nil {
		return nil, errors.Annotate(err, "decode session key token when Parse beginTime")
	}
	inviteUserToken := &entity.UserInviteToken{
		Method:   tokenKeys[0],
		Email:    tokenKeys[1],
		TenantID: entity.ID(tenantID),
		BeginAt:  beginTime,
	}
	return inviteUserToken, nil
}

func (s *Session) InsertUserInviteToken(tx *gorm.DB, tenantID entity.ID, method string, email string, period time.Duration, isOneTime bool) (string, error) {
	now := time.Now().Truncate(1 * time.Second)
	expire := now.Add(period)

	inviteUserTokenEntity := entity.UserInviteToken{
		Method:    method,
		Email:     email,
		TenantID:  tenantID,
		BeginAt:   now,
		ExpireAt:  expire,
		IsOneTime: isOneTime,
	}

	_, err := dbutil.UpsertUserInviteToken(tx, inviteUserTokenEntity)
	if err != nil {
		return "", errors.Annotate(err,
			fmt.Sprintf("insert invite user token: UpsertInvitUserToken method %s email %s tenantID %d", method, email, tenantID))
	}

	return s.EncodeToken(method, email, tenantID, now), nil
}

func (s *Session) ValidateUserInviteToken(token string, forceKeep bool) (*entity.UserInviteToken, error) {
	inviteUserToken, err := s.DecodeToken(token)
	if err != nil {
		return nil, errors.Annotate(err,
			fmt.Sprintf("validate invite user token: DecodeToken token %s", token))
	}
	now := time.Now()
	email := inviteUserToken.Email
	method := inviteUserToken.Method
	tenantID := inviteUserToken.TenantID

	var userInviteToken entity.UserInviteToken
	var user entity.User
	err = db.Transaction(func(tx *gorm.DB) error {
		err := entity.NewUserQuerySet(tx).EmailEq(email).One(&user)
		if err != nil {
			return errors.Annotatef(err, "validate invite user token: failed to get user by email, method %s email %s tenantID %d", method, email, tenantID)
		}
		if user.Status == entity.UserStatusActive {
			return errors.Annotatef(errors.New(ActivatedError), "validate invite user token: method %s email %s tenantID %d", method, email, tenantID)
		}

		err = entity.NewUserInviteTokenQuerySet(tx).
			TenantIDEq(tenantID).
			MethodEq(method).
			EmailEq(inviteUserToken.Email).
			One(&userInviteToken)
		if err != nil {
			if err == gorm.ErrRecordNotFound {
				return errors.Annotatef(errors.New(TokenExpireError), "token does not exist, method %s email %s tenantID %d", method, email, tenantID)
			}
			return errors.Annotatef(err, "validate invite user token: failed to get token from db, method %s email %s tenantID %d", method, email, tenantID)
		}

		if userInviteToken.ExpireAt.Before(now) {
			return errors.Annotatef(errors.New(TokenExpireError), "validate invite user token: method %s email %s tenantID %d", method, email, tenantID)
		}

		if userInviteToken.IsOneTime && !forceKeep {
			err := userInviteToken.Delete(tx)
			if err != nil {
				return errors.Annotatef(err, "validate invite user token: failed to DeleteUserInviteToken, method %s email %s tenantID %d", method, email, tenantID)
			}
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return inviteUserToken, nil
}

func (s *Session) CheckUserInviteToken(tx *gorm.DB, tenantID entity.ID, method string, email string) error {
	now := time.Now()
	var inviteUserToken entity.UserInviteToken
	err := entity.NewUserInviteTokenQuerySet(tx).
		TenantIDEq(tenantID).
		MethodEq(method).
		EmailEq(email).
		One(&inviteUserToken)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return errors.Annotatef(errors.New(TokenExpireError), "check invite user token: token does not exist, method %s email %s tenantID %d", method, email, tenantID)
		}
		return errors.Annotatef(err, "check invite user token: getUserInviteToken, method %s email %s tenantID %d", method, email, tenantID)
	}

	if inviteUserToken.ExpireAt.Before(now) {
		return errors.Annotatef(errors.New(TokenExpireError), "check invite user token: method %s email %s tenantID %d", method, email, tenantID)
	}

	if inviteUserToken.IsOneTime {
		err := inviteUserToken.Delete(tx)
		if err != nil {
			return errors.Annotate(err,
				fmt.Sprintf("check invite user token: DeleteUserInviteToken, method %s email %s tenantID %d", method, email, tenantID))
		}
	}
	return nil
}

func (s *Session) IsTokenExpiredError(err error) bool {
	return strings.Contains(err.Error(), TokenExpireError)
}

func (s *Session) IsActivatedError(err error) bool {
	return strings.Contains(err.Error(), ActivatedError)
}
