package token

import (
	"context"
	"encoding/base64"
	"fmt"
	"runtime/debug"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/tidbcloud/tiinf/errs"

	"github.com/tidbcloud/account/commons/config"
	"github.com/tidbcloud/account/commons/database"
	"go.uber.org/zap"

	"github.com/tidbcloud/pkgv2/secret/encrypt"
	"github.com/tidbcloud/tiinf/monitor/tilog"

	"github.com/pingcap/errors"
	"gorm.io/gorm"

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

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

	RegistrationMethod             = "/central.TenantService/Registration"
	RegistrationVerificationMethod = "/registration-verification"
	SendVerificationEmailMethod    = "/central.TenantService/SendVerificationEmail"
	InvitationVerificationMethod   = "/invitation-verification"
	CreateAuth0UserMethod          = "/central.TenantService/CreateAuth0User"
	AcceptInvitationMethod         = "/central.TenantService/AcceptInvitation"
	IP2LocationMethod              = "/central.LocationService/IP2Location"
	LoginMetricsMethod             = "/central.MetricsService/LoginMetrics"
	ValidateThirdPartyTokenMethod  = "/central.TenantService/ValidateThirdPartyToken"
)

//var (
//	SkipAuthMethod = map[string]struct{}{
//		"/central.DbJwtService/GetJwtByToken":             {},
//		"/central.DbJwtService/GetJwtByUser":              {},
//		"/central.DbJwtService/GetJwtByApiKey":            {},
//		"/central.TenantService/ListOrgActiveAuthMethods": {},
//		"/central.OrgAuthMethodService/ShowAuthMethods":   {},
//		"/central.ApiKeyService/CreateApiKeyForMsp":       {},
//	}
//)

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

var (
	sessionInitOnce sync.Once
	sessionInstance *Session
	sessionInitErr  error
)

func GetSession() (*Session, error) {
	sessionInitOnce.Do(func() {
		serverConfig := config.Global().ServerConfig
		aesCrypt, err := encrypt.NewAESCrypt([]byte(serverConfig.SaltSecret))
		if err != nil {
			sessionInitErr = err
			return
		}
		sessionInstance = NewSession(aesCrypt)

	})
	return sessionInstance, sessionInitErr
}

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) (t *entity.UserInviteToken, err error) {
	crypted, err := base64.URLEncoding.DecodeString(cryptedStr)
	if err != nil {
		return nil, errors.Annotate(errors.New("token is invalid"), "decode session key token")
	}
	defer func() {
		if e := recover(); e != nil {
			ctx := context.Background()
			tilog.Info(ctx, "recover ", zap.Any("err", e))
			tilog.Info(ctx, string(debug.Stack()))
			err = 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, operator entity.ID, tenantID entity.ID, method string, email string, period time.Duration, isOneTime bool) (string, error) {
	err := entity.NewUserInviteTokenQuerySet(database.DBConn.DB).
		MethodEq(method).
		EmailEq(email).
		TenantIDEq(tenantID).
		Delete()
	if err != nil {
		return "", errs.NewInternalDB("delete existing user invite token err", err)
	}

	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,
	}
	// wangletian add for dbuser
	if operator != 0 {
		inviteUserTokenEntity.Operator = operator
		tilog.PlainInfo("InviteUser set operator", zap.Any("operator", operator))
	}

	err = inviteUserTokenEntity.Create(tx)
	if err != nil {
		return "", errs.NewInternalDB("create user invite token err", err)
	}

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

func (s *Session) ValidateUserInviteToken(ctx context.Context, 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 = database.Transaction(func(tx *gorm.DB) error {
		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)
		}

		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 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 inviteUserToken, err
	}
	return inviteUserToken, nil
}

func (s *Session) GenUserInviteToken(tx *gorm.DB, tenantID entity.ID, method string, email string) (entity.UserInviteToken, 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 inviteUserToken, errors.Annotatef(errors.New(TokenExpireError), "check invite user token: token does not exist, method %s email %s tenantID %d", method, email, tenantID)
		}
		return inviteUserToken, errors.Annotatef(err, "check invite user token: getUserInviteToken, method %s email %s tenantID %d", method, email, tenantID)
	}

	if inviteUserToken.ExpireAt.Before(now) {
		return inviteUserToken, 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 inviteUserToken, errors.Annotate(err,
				fmt.Sprintf("check invite user token: DeleteUserInviteToken, method %s email %s tenantID %d", method, email, tenantID))
		}
	}
	return inviteUserToken, 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)
}
