// Package tenant encapsulates all tenant functionality
package tenant

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/AlekSi/pointer"
	"github.com/pingcap/dbaas/central/config"
	"github.com/pingcap/dbaas/central/db"
	"github.com/pingcap/dbaas/central/db/dbutil"
	"github.com/pingcap/dbaas/central/db/entity"
	"github.com/pingcap/dbaas/central/db/tenant"
	centralModel "github.com/pingcap/dbaas/central/model"
	"github.com/pingcap/dbaas/central/model/billing"
	"github.com/pingcap/dbaas/central/model/session"
	"github.com/pingcap/dbaas/central/model/tenant/handler"
	modelUtil "github.com/pingcap/dbaas/central/model/util"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/idl/pbgen/proto/central"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/log"
	mailv2 "github.com/tidbcloud/pkgv2/mail/v2"
	"github.com/tidbcloud/pkgv2/zendesk"
	"go.uber.org/zap"
	"gopkg.in/auth0.v4/management"
	"gorm.io/gorm"
)

// Model is created with NewModel
type Model struct {
	log               *zap.Logger
	session           *session.Session
	mail              mailv2.Facade
	management        Auth0UserManagementHandler
	zendeskClient     ZendeskHandler
	salesforceClient  SalesforceHandler
	billingModel      billing.Model
	auth0TokenHandler Auth0TokenHandler
}

// UserContext attaches a User to a Context.
// This helps audit the User
type Context struct {
	Model
	tenantAndUser *tenant.User
	log           *zap.Logger
	tenant        *entity.Tenant
}

// NewModel returns a Model
func NewModel(log *zap.Logger, session *session.Session, mail mailv2.Facade, management Auth0UserManagementHandler, zendeskClient ZendeskHandler, salesforceClient SalesforceHandler, billingModel billing.Model, auth0TokenHandler Auth0TokenHandler) Model {
	return Model{
		log:               log,
		session:           session,
		mail:              mail,
		management:        management,
		zendeskClient:     zendeskClient,
		salesforceClient:  salesforceClient,
		billingModel:      billingModel,
		auth0TokenHandler: auth0TokenHandler,
	}
}

// User returns a UserContext
func (m Model) User(ctx context.Context, user *tenant.User) Context {
	return Context{
		Model:         m,
		tenantAndUser: user,
		log:           m.log.With(zap.Uint64("tenantID", user.TenantID.Uint64()), zap.Uint64("userID", uint64(user.ID)), zap.String("module", "tenant")).With(log.FieldsFromContext(ctx)...),
	}
}

func (m Model) UserWithEntity(ctx context.Context, user *tenant.User, tenant *entity.Tenant) Context {
	return Context{
		Model:         m,
		tenantAndUser: user,
		log:           m.log.With(zap.Uint64("tenantID", user.TenantID.Uint64()), zap.Uint64("userID", uint64(user.ID)), zap.String("module", "tenant")).With(log.FieldsFromContext(ctx)...),
		tenant:        tenant,
	}
}

func (c Context) Log() *zap.Logger {
	return c.log
}

func (c Context) CheckOrgID(orgID entity.ID) error {
	if c.tenantAndUser.IsImpersonated {
		return nil
	}
	userTenantID := c.tenantAndUser.TenantID
	if orgID != userTenantID {
		return errcode.NewForbiddenErr(errors.New("org_id parameter is inconsistent with the user's org ID"))
	}
	return nil
}

func (c Context) CheckOrgIDAndProjectID(orgID, projectID entity.ID) error {
	user := c.tenantAndUser
	if user.IsImpersonated {
		return nil
	}

	if orgID != user.TenantID {
		return errcode.NewForbiddenErr(errors.New("org_id parameter is inconsistent with the user's org ID"))
	}

	return dbutil.CheckProjectID(user.Projects, projectID)
}

func (c Context) CheckIfOwner() error {
	user := c.tenantAndUser
	if user.Role != entity.UserRoleOwner {
		return errcode.NewForbiddenErr(errors.New("only owner can perform this operation"))
	}

	return nil
}

func (c Context) CheckIfOwnerOrBillingAdmin() error {
	user := c.tenantAndUser
	if user.Role == entity.UserRoleOwner || user.Role == entity.UserRoleBillingAdmin {
		return nil
	}
	return errcode.NewForbiddenErr(errors.New("only owner or billing admin can perform this operation"))
}

func (c Context) BillingContext() billing.Context {
	return c.billingModel.User(c.tenantAndUser)
}

const (
	// white list flag means the function white list of customer
	// uint32 means we can use this flag to control 32 functions
	// 1 (1 << 0) means open cdc to mysql. 2 (1 << 1) means open cdc to kafka
	// if this organization open mysql and kafka, this flag should set to 3 ((1 << 0) & (1 << 1))
	EnableReplicationFlag       = 1 << 2
	EnableDataStreamToKafkaFlag = 1 << 1
	EnableDataStreamToMysqlFlag = 1 << 0
	EnableAuditLogFlag          = 1 << 3
	EnableXAccountFlag          = 1 << 4 // cross account deploy cluster

	// error message
	ErrUserInvited = "You have been invited to join TiDB Cloud, please accept the invitation in your email first."
)

func validateRegistration(ctx context.Context, email string) error {
	return db.Transaction(func(tx *gorm.DB) error {
		count, err := entity.NewUserQuerySet(tx).EmailEq(email).DeletedAtIsNull().Count()
		if err != nil {
			log.Error(ctx, "failed to query user by email", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
			return bizerr.New(bizerr.ErrCodeDB, "failed to query user by email")
		}
		if count > 0 {
			return bizerr.New(config.ErrCodeEmailAddressInUse, "Email address is already in use, please sign in.")
		}

		count, err = entity.NewTenantQuerySet(tx).EmailEq(email).Count()
		if err != nil {
			log.Error(ctx, "failed to query tenant by email", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
			return bizerr.New(bizerr.ErrCodeDB, "failed to query tenant by email")
		}
		if count > 0 {
			if count > 0 {
				return bizerr.New(config.ErrCodeEmailAddressInUse, "Email address is already in use, please sign in.")
			}
		}

		if config.Global().ServerConfig.EnableEmailProviderBlocklists {
			emailSlice := strings.Split(email, "@")
			if len(emailSlice) != 2 {
				log.Error(ctx, "illegal email address", zap.String("feature", "signup"), zap.Error(err))
				return bizerr.New(bizerr.ErrCodeUnknown, "illegal email address")
			}
			count, err = entity.NewEmailProviderBlocklistQuerySet(tx).EmailEq(emailSlice[1]).Count()
			if err != nil {
				log.Error(ctx, "failed to check email provider block list", zap.String("feature", "signup"), zap.Error(err))
				return bizerr.New(bizerr.ErrCodeDB, "failed to check email provider block list")
			}
			if count == 1 {
				return bizerr.New(bizerr.ErrCodeParams, "Please use your work email")
			}
		}

		return nil
	})
}

func transEmptyStringToNullable(input string) *string {
	if input == "" {
		return nil
	}
	return &input
}

func transNullableStringToEmpty(input *string) string {
	if input == nil {
		return ""
	}
	return *input
}

type Auth0User struct {
	Email     string
	FirstName string
	LastName  string
}

func getGithubFirstAndLastName(name, nickName, email string) (firstName, lastName string) {
	if name == "" {
		return nickName, ""
	}
	// If GitHub profile name is empty, auth0 will use email as name.
	if name == email {
		return nickName, ""
	}

	names := strings.Split(name, " ")
	if len(names) == 1 {
		return names[0], ""
	}
	return strings.Join(names[:len(names)-1], " "), names[len(names)-1]
}

func (c Context) getUserFromAuth0(connectionType config.Auth0ConnectionType, auth0Token string) (*Auth0User, bizerr.BizError) {
	var firstName, lastName string
	email, err := c.auth0TokenHandler.GetEmailFromAuth0Token(auth0Token)
	if err != nil {
		c.Log().Error("failed to get email from auth0Token field", zap.String("feature", "signup"), zap.Error(err))
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "failed to get email from auth0Token field")
	}

	users, err := c.management.ListByEmail(email)
	if err != nil {
		c.Log().Error("failed to list users by email in auth0", zap.String("feature", "signup"), zap.Error(err))
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "failed to list users by email from auth0")
	}

	var found bool
	for _, u := range users {
		for _, userIdentity := range u.Identities {
			if pointer.GetString(userIdentity.Connection) == connectionType.String() {
				found = true

				if connectionType == config.Auth0SocialConnectionGoogle {
					firstName = pointer.GetString(u.GivenName)
					lastName = pointer.GetString(u.FamilyName)
					if firstName == "" {
						firstName = pointer.GetString(u.Nickname)
					}
				}
				if connectionType == config.Auth0SocialConnectionGithub {
					name := pointer.GetString(u.Name)
					nickName := pointer.GetString(u.Nickname)
					firstName, lastName = getGithubFirstAndLastName(name, nickName, email)
				}

				// fallback to email
				if firstName == "" {
					firstName = email
				}

				break
			}
		}

		if found {
			break
		}
	}
	if !found {
		c.Log().Error("failed to find user by email in auth0", zap.String("feature", "signup"), zap.String("email", email), zap.Stringer("connection", connectionType))
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "failed to find user by email in auth0")
	}
	return &Auth0User{
		Email:     email,
		FirstName: firstName,
		LastName:  lastName,
	}, nil
}

func (c Context) getThirdPartyAccountCode(thirdPartyAccountProvider, thirdPartyAccountToken string) (*string, bizerr.BizError) {
	var accountCode *string = nil
	accountProvider, err := handler.GetAccountHandler().GetAccountProvider(thirdPartyAccountProvider)
	if err != nil {
		c.Log().Error("failed to get third party account provider", zap.String("feature", "signup"), zap.Error(err))
		return accountCode, bizerr.New(bizerr.ErrCodeParams, "failed to get third party account provider")
	}
	accountCode, err = accountProvider.ResolveToken(&thirdPartyAccountToken)
	if err != nil {
		c.Log().Error("failed to get third party account code", zap.String("feature", "signup"), zap.Error(err))
		return accountCode, bizerr.New(bizerr.ErrCodeParams, "failed to get third party account code")
	}
	return accountCode, nil
}

func (c Context) createDatabaseConnectionAuth0User(ctx context.Context, email, password string) bizerr.BizError {
	u := management.User{
		Connection:    pointer.ToString(config.Global().Auth0Config.Auth0DatabaseConnectionName),
		Email:         pointer.ToString(email),
		Name:          pointer.ToString(email),
		Password:      pointer.ToString(password),
		EmailVerified: pointer.ToBool(false),
		VerifyEmail:   pointer.ToBool(false),
	}

	err := c.management.Create(&u)
	if err != nil && !auth0.IsAuth0UserExistError(err) {
		if auth0.IsAuth0PasswordStrengthError(err) {
			log.Error(ctx, "password strength", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
			return bizerr.New(config.ErrCodeUserPasswordPolicy, "password strength")
		}
		log.Error(ctx, "failed to create user in auth0", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
		return bizerr.New(bizerr.ErrCodeUnknown, "failed to create user in auth0")
	}
	return nil
}

func (c Context) sendVerificationEmail(ctx context.Context, email, firstName, token string) error {
	url := fmt.Sprintf("https://%s/registration-verification?token=%s", modelUtil.GetCentralBackendDns(), token)
	vars := map[string]string{
		"userName": firstName,
		"url":      url,
		"token":    token,
	}
	err := c.mail.SendMailWithTemplate(ctx, []string{email}, config.EmailSubjectVerification, config.TNRegistrationVerify, vars, nil)
	if err != nil {
		log.Error(ctx, "failed to send verification email", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
		return err
	}
	log.Info(ctx, "send verification email", zap.String("feature", "signup"), zap.String("email", email), zap.Time("time", time.Now().UTC()))
	return nil
}

type CreateOrgParams struct {
	Name                      string
	Email                     string
	Country                   string
	Password                  string
	FirstName                 string
	LastName                  string
	Job                       string
	PhoneNumber               string
	ConnectionType            config.Auth0ConnectionType
	ClientIP                  string
	Auth0Token                string
	ThirdPartyAccountProvider string
	ThirdPartyAccountToken    string
}

type OrgEntity struct {
	Name                      string
	Email                     string
	Country                   string
	FirstName                 string
	LastName                  string
	Job                       string
	PhoneNumber               string
	ConnectionType            config.Auth0ConnectionType
	ClientIP                  string
	TenantStatus              entity.TenantStatus
	UserStatus                entity.UserStatus
	ThirdPartyAccountProvider string
	ThirdPartyAccountCode     *string
}

func buildOrgEntity(params *CreateOrgParams) *OrgEntity {
	return &OrgEntity{
		Name:                      params.Name,
		Email:                     params.Email,
		Country:                   params.Country,
		FirstName:                 params.FirstName,
		LastName:                  params.LastName,
		Job:                       params.Job,
		PhoneNumber:               params.PhoneNumber,
		ConnectionType:            params.ConnectionType,
		ClientIP:                  params.ClientIP,
		TenantStatus:              entity.TenantStatusPending,
		UserStatus:                entity.UserStatusPending,
		ThirdPartyAccountProvider: params.ThirdPartyAccountProvider,
		ThirdPartyAccountCode:     nil,
	}
}

func (c Context) saveTenant(ctx context.Context, en *OrgEntity) bizerr.BizError {
	tenantEntity := &entity.Tenant{
		Name:                      en.Name,
		Alias:                     en.Name,
		Country:                   en.Country,
		Email:                     en.Email,
		TimeZone:                  config.DefaultUtcOffset,
		Status:                    en.TenantStatus,
		Plan:                      entity.TenantPlanNewUser,
		AcceptService:             true,
		ThirdPartyAccountProvider: transEmptyStringToNullable(en.ThirdPartyAccountProvider),
		ThirdPartyAccountCode:     en.ThirdPartyAccountCode,
	}

	userEntity := &entity.User{
		FirstName:   en.FirstName,
		LastName:    en.LastName,
		Email:       en.Email,
		Status:      en.UserStatus,
		Job:         en.Job,
		Role:        entity.UserRoleOwner,
		PhoneNumber: en.PhoneNumber,
	}

	registrationLogEntity := &entity.RegistrationLog{
		Name:      en.Name,
		Email:     en.Email,
		FirstName: en.FirstName,
		LastName:  en.LastName,
		Source:    en.ConnectionType.String(),
		IPAddress: en.ClientIP,
		InvitedAt: time.Now(),
	}
	tenantGroup := &dbutil.TenantGroup{
		Tenant:          tenantEntity,
		User:            userEntity,
		RegistrationLog: registrationLogEntity,
	}

	email := en.Email
	var token string
	err := db.Transaction(func(tx *gorm.DB) error {
		if en.ThirdPartyAccountProvider != "" {
			res, err := entity.NewTenantQuerySet(tx).ThirdPartyAccountProviderEq(*tenantEntity.ThirdPartyAccountProvider).ThirdPartyAccountCodeEq(*tenantEntity.ThirdPartyAccountCode).Count()
			if err != nil {
				log.Error(ctx, "failed to query tenant in third party account if exists when register", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
				return bizerr.New(bizerr.ErrCodeDB, "failed to query tenant in third party account if exists when register")
			}
			if res > 0 {
				log.Info(ctx, "third party account already exists", zap.String("feature", "signup"), zap.String("email", email),
					zap.String("third_party_account_provider", *tenantEntity.ThirdPartyAccountProvider), zap.String("third_party_account_code", *tenantEntity.ThirdPartyAccountCode))
				return bizerr.New(config.ErrCodeDuplicatedThirdPartyAccount, "this marketplace account has already been registered")
			}
		}

		tenantEntity, err := dbutil.SaveTenant(tx, tenantGroup)
		c.tenantAndUser = &tenant.User{
			TenantID:       tenantEntity.ID,
			IsImpersonated: false,
		}
		if err != nil {
			log.Error(ctx, "failed to register tenant in db", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
			return bizerr.New(bizerr.ErrCodeDB, "failed to register tenant in db")
		}

		if en.ConnectionType == config.Auth0DatabaseConnection {
			token, err = c.session.InsertUserInviteToken(tx, tenantEntity.ID, session.RegistrationVerificationMethod, email, 24*time.Hour, true)
			if err != nil {
				log.Error(ctx, "failed to create user invite token", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
				return bizerr.New(bizerr.ErrCodeDB, "failed to create user invite token")
			}
		}

		return nil
	})
	if err != nil {
		return bizerr.WrapError(err, "failed to save tenant")
	}

	if en.ConnectionType == config.Auth0DatabaseConnection {
		err = c.sendVerificationEmail(ctx, email, en.FirstName, token)
		if err != nil {
			return bizerr.New(bizerr.ErrCodeUnknown, "failed to send verification email")
		}
	}

	if en.ConnectionType.IsAuth0SocialConnection() {
		err = c.TasksAfterRegistration(ctx, email)
		if err != nil {
			log.Error(ctx, "failed to run tasks after registration", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
		}
	}
	return nil
}

func (c Context) CreateOrg(ctx context.Context, params *CreateOrgParams) bizerr.BizError {
	en := buildOrgEntity(params)
	if params.ConnectionType.IsAuth0SocialConnection() {
		auth0User, err := c.getUserFromAuth0(params.ConnectionType, params.Auth0Token)
		if err != nil {
			return err
		}
		en.Email = auth0User.Email
		en.FirstName = auth0User.FirstName
		en.LastName = auth0User.LastName
		en.UserStatus = entity.UserStatusActive
		en.TenantStatus = entity.TenantStatusActive
	}

	validateErr := validateRegistration(ctx, en.Email)
	if validateErr != nil {
		return bizerr.WrapGormError(validateErr, "failed to validate registration")
	}

	var accountCode *string = nil
	var err error
	if params.ThirdPartyAccountProvider != "" {
		accountCode, err = c.getThirdPartyAccountCode(params.ThirdPartyAccountProvider, params.ThirdPartyAccountToken)
		if err != nil {
			return bizerr.New(bizerr.ErrCodeUnknown, "failed to get third party account code")
		}
	}
	en.ThirdPartyAccountCode = accountCode

	if params.ConnectionType == config.Auth0DatabaseConnection {
		err := c.createDatabaseConnectionAuth0User(ctx, en.Email, params.Password)
		if err != nil {
			return err
		}
	}

	err = c.saveTenant(ctx, en)
	if err != nil {
		return bizerr.WrapError(err, "failed to save tenant")
	}

	return nil
}

func (c Context) SendVerificationEmail(ctx context.Context, email string) error {
	var token string
	var user entity.User
	err := db.Transaction(func(tx *gorm.DB) error {
		var err error
		user, err = dbutil.GetUserByEmail(tx, email)
		if err != nil {
			log.Error(ctx, "failed to get user from db", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
			return errcode.NewInvalidInputErr(errors.New("invalid request"))
		}
		if user.Status == entity.UserStatusActive {
			return errcode.NewInvalidInputErr(errors.New("invalid request"))
			// TODO(Louis): Uncomment when error code is supported
			// return errcode.NewInvalidInputErr(errors.Errorf(`Your account has been successfully activated. You can now <a href="https://%s" target="_blank">sign in</a>.`, config.Global().ServerConfig.FrontendDNS))
		}
		if user.Status == entity.UserStatusInactive {
			return errcode.NewInvalidInputErr(errors.New("invalid request"))
			//return errcode.NewInvalidInputErr(errors.New("your account is currently inactive"))
		}
		if user.InvitedAt != nil {
			return errcode.NewInvalidInputErr(errors.New(ErrUserInvited))
		}
		token, err = c.session.InsertUserInviteToken(tx, user.TenantID, session.RegistrationVerificationMethod, email, 24*time.Hour, true)
		if err != nil {
			log.Error(ctx, "failed to create user invite token", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to create user invite token"))
		}
		return nil
	})
	if err != nil {
		return err
	}
	err = c.sendVerificationEmail(ctx, email, user.FirstName, token)
	if err != nil {
		return err
	}

	return nil
}

type Organization struct {
	ID                        entity.ID
	Name                      string
	Alias                     string
	CreateAt                  time.Time
	Email                     string
	Country                   string
	Timezone                  int32
	Status                    string
	Plan                      string
	TotalCredit               int64
	RemainingCredit           int64
	ActiveClustersCount       uint32
	FreeClustersCount         uint32
	HasDiscount               bool
	Discount                  int64
	DiscountDescription       string
	NodeUsageFee              int64
	AcknowledgeCnPolicy       bool
	HasTriedDevTier           bool
	ThirdPartyAccountProvider string
	ThirdPartyAccountCode     string
	WhitelistFlag             uint32
	Kind                      string
	AccountManager            string
}

func (c Context) GetTenant() (*entity.Tenant, error) {
	if c.tenant != nil {
		return c.tenant, nil
	}
	var t entity.Tenant
	err := db.Transaction(func(tx *gorm.DB) error {
		return entity.NewTenantQuerySet(tx).IDEq(c.tenantAndUser.TenantID).One(&t)
	})
	if err != nil {
		c.log.Error("failed to get tenant by ID", zap.Int64("ID", int64(c.tenantAndUser.TenantID)), zap.Error(err))
		return nil, err
	}
	c.tenant = &t
	return c.tenant, nil
}

func (c Context) GetOrganizationBrief() (*Organization, error) {
	t, err := c.GetTenant()
	if err != nil {
		return nil, err
	}

	organization := &Organization{
		ID:                        t.ID,
		Name:                      t.Name,
		Alias:                     t.Alias,
		CreateAt:                  t.CreatedAt,
		Email:                     t.Email,
		Country:                   t.Country,
		Status:                    string(t.Status),
		Plan:                      string(t.Plan),
		Kind:                      string(t.Kind),
		AccountManager:            t.AccountManager,
		ThirdPartyAccountProvider: transNullableStringToEmpty(t.ThirdPartyAccountProvider),
		ThirdPartyAccountCode:     transNullableStringToEmpty(t.ThirdPartyAccountCode),
	}
	return organization, nil
}

func (c Context) GetOrganization() (*Organization, error) {
	t, err := c.GetTenant()
	if err != nil {
		return nil, err
	}

	organization := &Organization{
		ID:                        t.ID,
		Name:                      t.Name,
		Alias:                     t.Alias,
		CreateAt:                  t.CreatedAt,
		Email:                     t.Email,
		Country:                   t.Country,
		Status:                    string(t.Status),
		Plan:                      string(t.Plan),
		Kind:                      string(t.Kind),
		AccountManager:            t.AccountManager,
		ThirdPartyAccountProvider: transNullableStringToEmpty(t.ThirdPartyAccountProvider),
		ThirdPartyAccountCode:     transNullableStringToEmpty(t.ThirdPartyAccountCode),
	}

	timezone, err := strconv.Atoi(t.TimeZone)
	if err != nil {
		return organization, errors.Annotate(err, "invalid timezone value")
	}
	organization.Timezone = int32(timezone)

	var (
		activeClustersCount int64
		freeClustersCount   int64
		discount            int64
		hasDiscount         bool
		discountDescription string = config.Global().BillingConfig.DiscountDescription
		acknowledgeCnPolicy bool   = false
		hasTriedDevTier     bool   = false
	)

	billingContext := c.BillingContext()

	discount, err = billingContext.GetOnDemandEffectiveDiscount()
	if err != nil {
		return organization, err
	}

	hasDiscount = discount < 100

	err = db.Transaction(func(tx *gorm.DB) error {
		activeClustersCount, err = entity.NewClusterQuerySet(tx).StatusNotIn(
			entity.DeletedOrClearedClusterStatus...).TenantIDEq(t.ID).Count()
		if err != nil {
			return errors.Annotate(err, "failed to query active cluster count")
		}
		freeClustersCount, err = entity.NewClusterQuerySet(tx).StatusNotIn(
			entity.DeletedOrClearedClusterStatus...).TenantIDEq(t.ID).FreeEq(true).Count()
		if err != nil {
			return errors.Annotate(err, "failed to query active free cluster count")
		}
		c.Log().Info("tenant info", zap.Int64("freeClustersCount", freeClustersCount), zap.Int64("activeClustersCount", activeClustersCount))

		devTierTrialCount, err := entity.NewTrialQuerySet(tx).TenantIDEq(t.ID).TrialTypeEq(entity.TrialTypeDevTier).Count()
		if err != nil {
			return errors.Annotate(err, "failed to query trial record")
		}
		if devTierTrialCount > 0 {
			hasTriedDevTier = true
		}

		if t.Country == "CN" {
			var acknowledgeCnPolicyEntity entity.AcknowledgeCnPolicy
			err = entity.NewAcknowledgeCnPolicyQuerySet(tx).TenantIDEq(t.ID).One(&acknowledgeCnPolicyEntity)
			if err != nil && errors.Cause(err) != gorm.ErrRecordNotFound {
				return errors.Annotate(err, "failed to query acknowledge cn policy")
			}
			if err == nil {
				acknowledgeCnPolicy = true
			}
		}
		return nil
	})
	if err != nil {
		c.Log().Error("failed to query organization info from db", zap.Error(err))
		return organization, err
	}
	organization.ActiveClustersCount = uint32(activeClustersCount)
	organization.FreeClustersCount = uint32(freeClustersCount)
	organization.AcknowledgeCnPolicy = acknowledgeCnPolicy
	organization.HasTriedDevTier = hasTriedDevTier
	c.Log().Info("tenant info have loaded")
	pocEnable, err := billingContext.IsPOCEnabled()
	if err != nil {
		return organization, err
	}

	if pocEnable {
		credits, err := billingContext.GetPOCCredits()
		if err != nil {
			return organization, err
		}
		organization.RemainingCredit = credits
	}

	organization.WhitelistFlag = TransferWhiteFlag(t.ID.Uint64())
	organization.Discount = discount
	organization.HasDiscount = hasDiscount
	organization.DiscountDescription = discountDescription

	return organization, nil
}

func TransferWhiteFlag(oId uint64) uint32 {
	whitelistFlag := uint32(0)
	for _, id := range config.Global().ServerConfig.EnableDataStreamToKafka {
		if oId == id {
			whitelistFlag = whitelistFlag | EnableDataStreamToKafkaFlag
		}
	}
	for _, id := range config.Global().ServerConfig.EnableDataStreamToMysql {
		if oId == id {
			whitelistFlag = whitelistFlag | EnableDataStreamToMysqlFlag
		}
	}
	for _, id := range config.Global().ServerConfig.EnableReplication {
		if oId == id {
			whitelistFlag = whitelistFlag | EnableReplicationFlag
		}
	}
	for _, id := range config.Global().ServerConfig.EnableAuditLog {
		if oId == id {
			whitelistFlag = whitelistFlag | EnableAuditLogFlag
		}
	}
	for _, id := range config.Global().ServerConfig.EnableXAccount {
		if oId == id {
			whitelistFlag = whitelistFlag | EnableXAccountFlag
		}
	}
	return whitelistFlag
}

func (c Context) UpdateOrganization(ctx context.Context, req *central.UpdateOrgReq) error {
	if c.tenantAndUser.Role != entity.UserRoleOwner {
		return errcode.NewInvalidInputErr(errors.New("you do not have permission to modify organization settings"))
	}
	t, err := c.GetTenant()
	if err != nil {
		return errcode.NewInternalErr(errors.New("failed to get tenant by ID"))
	}

	err = db.Transaction(func(tx *gorm.DB) error {
		updateZendeskOrgName := false
		zendeskConfig := config.Global().ZendeskConfig
		if zendeskConfig.EnableZendesk && t.Name != req.Name {
			updateZendeskOrgName = true
		}

		t.Name = req.Name
		t.TimeZone = strconv.Itoa(int(req.Timezone))
		err = t.Update(tx, entity.TenantDBSchema.Name, entity.TenantDBSchema.TimeZone)
		if err != nil {
			c.Log().Error("failed to update organization", zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to update organization"))
		}

		if updateZendeskOrgName {
			orgReq := zendesk.Organization{
				Name:       req.Name,
				ID:         uint64(t.ZendeskID),
				ExternalID: strconv.FormatUint(uint64(t.ID), 10),
			}

			org, err := c.zendeskClient.CreateOrUpdateOrganization(orgReq)
			if err != nil {
				log.Error(ctx, "failed to update organization name on Zendesk", zap.String("email", t.Email), zap.Error(err))
				return nil
			}
			if t.ZendeskID != entity.ID(org.ID) {
				t.ZendeskID = entity.ID(org.ID)
				err = t.Update(tx, entity.TenantDBSchema.ZendeskID)
				if err != nil {
					log.Error(ctx, "failed to update organization ZendeskID", zap.String("email", t.Email), zap.Error(err))
					return nil
				}
			}
		}
		return nil
	})

	return err
}

func (c Context) AcknowledgeCNPolicy(ipAddress string) error {
	acknowledgeCNPolicy := entity.AcknowledgeCnPolicy{
		TenantID:    c.tenantAndUser.TenantID,
		CreatedByID: c.tenantAndUser.ID,
		ConfirmTime: time.Now(),
		IPAddress:   ipAddress,
	}
	err := db.Transaction(func(tx *gorm.DB) error {
		return acknowledgeCNPolicy.Create(tx)
	})
	if err != nil {
		c.Log().Error("failed to acknowledge CN policy", zap.Error(err))
		return err
	}
	return nil
}

// User is how we model a user of the application
type User struct {
	ID        uint64
	Name      string
	Email     string
	Status    string
	Role      string
	InvitedAt *time.Time
}

// FromDB creates a User model from the Database
func fromDB(u entity.User) *User {
	var name string
	if u.FirstName == "" && u.LastName == "" {
		name = ""
	} else {
		name = fmt.Sprintf("%s %s", u.FirstName, u.LastName)
	}
	return &User{
		ID:        uint64(u.ID),
		Name:      name,
		Email:     u.Email,
		Status:    string(u.Status),
		InvitedAt: u.InvitedAt,
		Role:      string(u.Role),
	}
}

type ListUsersReq struct {
	TenantID entity.ID
	Email    *string
}

func (c Context) ListUsers(req ListUsersReq) ([]*User, error) {
	var users []entity.User
	filter := dbutil.UserFilter{
		TenantID: &req.TenantID,
		Statuses: []entity.UserStatus{entity.UserStatusActive, entity.UserStatusPending},
		Email:    req.Email,
	}

	err := db.Transaction(func(tx *gorm.DB) error {
		var err error
		users, err = dbutil.ListUsers(tx, filter)
		return err
	})
	if err != nil {
		return nil, errcode.NewInternalErr(errors.New("failed to list organization users"))
	}

	return convertUsers(users), nil
}

func convertUsers(users []entity.User) []*User {
	var modelUsers []*User
	for _, user := range users {
		// Ignore system user
		if user.FirstName == "system" && user.LastName == "user" {
			continue
		}
		modelUsers = append(modelUsers, fromDB(user))
	}
	return modelUsers
}

type UpdateUserReq struct {
	OrgID  entity.ID
	UserID entity.ID
	Role   entity.UserRole
}

// UpdateUser updates the role of a user in an organization
func (c Context) UpdateUser(ctx context.Context, req UpdateUserReq) error {
	err := db.Transaction(func(tx *gorm.DB) error {
		user, err := dbutil.GetUserByID(tx, req.OrgID, req.UserID)
		if err != nil {
			c.Log().Error("failed to get user by email", zap.Uint64("userID", req.UserID.Uint64()), zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to query user by email"))
		}

		// no change
		if user.Role == req.Role {
			return nil
		}

		activeOwnerCount, err := dbutil.GetActiveOwnerCount(tx, req.OrgID)
		if err != nil {
			c.Log().Error("failed to query owner count", zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to update user role"))
		}
		if activeOwnerCount == 1 && user.Role == entity.UserRoleOwner {
			activeOwners, err := dbutil.ListUsers(tx, dbutil.UserFilter{
				TenantID: &req.OrgID,
				Roles:    []entity.UserRole{entity.UserRoleOwner},
				Statuses: []entity.UserStatus{entity.UserStatusActive},
			})
			if err != nil {
				c.Log().Error("failed to query active owners", zap.Error(err))
				return errcode.NewInternalErr(errors.New("failed to update user role"))
			}
			if len(activeOwners) == 1 && activeOwners[0].ID == user.ID {
				return errcode.NewInvalidInputErr(errors.New(config.ErrAtLeastOneOwner))
			}
		}

		userProjects := user.Projects
		user.Role = req.Role
		if user.Role == entity.UserRoleOwner {
			projects, err := dbutil.GetActiveProjects(ctx, tx, req.OrgID)
			if err != nil {
				c.Log().Error("failed to query active projects", zap.Error(err))
				return errcode.NewInternalErr(errors.New("failed to update user role"))
			}
			userProjects = projects
		}

		err = dbutil.UpsertUser(tx, dbutil.CreateUserRequired{
			Email:     user.Email,
			FirstName: user.FirstName,
			LastName:  user.LastName,
			InviteAt:  user.InvitedAt,
			Status:    user.Status,
			TenantID:  req.OrgID,
			Role:      user.Role,
			Projects:  userProjects,
		})
		if err != nil {
			c.Log().Error("failed to upsert user in db", zap.String("email", user.Email), zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to update user role"))
		}
		return nil
	})

	if err != nil {
		return err
	}
	return nil
}

func (c Context) DeleteUser(tenantID, userID entity.ID) error {
	var user entity.User
	var userEmail string
	err := db.Transaction(func(tx *gorm.DB) error {
		err := entity.NewUserQuerySet(tx).TenantIDEq(tenantID).IDEq(userID).One(&user)
		if err != nil {
			return errcode.NewInternalErr(errors.New("failed to get user from db"))
		}

		if c.tenantAndUser.Email == user.Email {
			return errcode.NewInvalidInputErr(errors.New("user cannot delete himself"))
		}

		userEmail = user.Email
		now := time.Now()
		user.Email = fmt.Sprintf("deleted-%d", user.ID)
		user.DeletedAt = &now
		user.Status = entity.UserStatusInactive
		err = user.Update(tx, entity.UserDBSchema.Email, entity.UserDBSchema.DeletedAt, entity.UserDBSchema.Status)
		if err != nil {
			c.Log().Error("failed to delete user in db", zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to delete user from db"))
		}
		return dbutil.AddAccessLogNow(tx, c.tenantAndUser.User.TenantID, c.tenantAndUser.Email, fmt.Sprintf("delete user:  %s", user.Email))
	})
	if err != nil {
		return err
	}

	users, err := c.management.ListByEmail(userEmail)
	if err != nil {
		c.Log().Error("failed to list users by email in auth0", zap.Error(err))
		return errcode.NewInternalErr(errors.New("failed to list users by email from auth0"))
	}
	for _, u := range users {
		err := c.management.Delete(*u.ID)
		if err != nil {
			c.Log().Error("failed to delete user in auth0", zap.String("email", userEmail), zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to delete user in auth0"))
		}
	}
	return nil
}

func (c Context) CreateAuth0User(ctx context.Context, orgID entity.ID, token, password string) error {
	userInviteToken, err := c.session.ValidateUserInviteToken(token, true)
	if err != nil {
		log.Error(ctx, "failed to validate invitation token", zap.String("feature", "invitation"), zap.Error(err))
		return errcode.NewInvalidInputErr(errors.New("failed to validate invitation token"))
	}

	userTenantID := userInviteToken.TenantID
	if orgID != userTenantID {
		return errcode.NewInvalidInputErr(errors.New("org_id parameter is inconsistent with the user's org ID"))
	}

	email := userInviteToken.Email
	u := management.User{
		Connection:    pointer.ToString(config.Global().Auth0Config.Auth0DatabaseConnectionName),
		Email:         pointer.ToString(email),
		Name:          pointer.ToString(email),
		Password:      pointer.ToString(password),
		EmailVerified: pointer.ToBool(true),
		VerifyEmail:   pointer.ToBool(false),
	}
	err = c.management.Create(&u)
	if err != nil && !auth0.IsAuth0UserExistError(err) {
		log.Error(ctx, "failed to create user in auth0", zap.String("feature", "invitation"), zap.String("email", email), zap.Error(err))
		if auth0.IsAuth0PasswordStrengthError(err) {
			return errcode.NewInvalidInputErr(errors.New(auth0.UserPasswordErrMsg))
		}
		return errcode.NewInternalErr(errors.New("failed to create user in auth0"))
	}
	log.Info(ctx, "create user in auth0", zap.String("feature", "invitation"), zap.String("email", email))

	return nil
}

func (c Context) AcceptInvitation(tenantID entity.ID, email, firstName, lastName string) error {
	err := db.Transaction(func(tx *gorm.DB) error {
		// Check user is not deleted
		entityUser, err := dbutil.GetUserByEmail(tx, email)
		if err != nil {
			return errcode.NewInternalErr(errors.New("failed to get user by email"))
		}
		if entityUser.Status == entity.UserStatusActive {
			return errcode.NewInvalidInputErr(errors.New("user has been activated"))
		}

		if err := c.session.CheckUserInviteToken(tx, tenantID, session.InvitationVerificationMethod, email); err != nil {
			return errcode.NewInvalidInputErr(errors.New("token is invalid"))
		}

		entityUser.Status = entity.UserStatusActive
		entityUser.FirstName = firstName
		entityUser.LastName = lastName
		err = entityUser.Update(tx, entity.UserDBSchema.Status, entity.UserDBSchema.FirstName, entity.UserDBSchema.LastName)
		if err != nil {
			return errcode.NewInternalErr(errors.New("failed to update user"))
		}

		return nil
	})
	return err
}

type UpdateUserProfileReq struct {
	Email     string
	FirstName string
	LastName  string
}

func (c Context) GetUpdateUserProfile(ctx context.Context, orgID entity.ID) (*central.UserProfile, error) {
	err := c.CheckOrgID(orgID)
	if err != nil {
		return nil, err
	}
	user := c.tenantAndUser

	var projects []entity.Project
	if c.tenantAndUser.IsImpersonated {
		err = db.Transaction(func(tx *gorm.DB) error {
			projects, err = dbutil.GetActiveProjects(ctx, tx, orgID)
			return err
		})
		if err != nil {
			c.Log().Error("failed to query active projects of tenant", zap.Error(err))
			return nil, errcode.NewInternalErr(errors.Annotatef(err, "failed to query active projects of tenant %d", orgID))
		}
	} else {
		projects = user.Projects
	}

	userProjects := []*central.UserProject{}
	for _, project := range projects {
		userProjects = append(userProjects, &central.UserProject{
			Id:             project.ID.Uint64(),
			Name:           project.Name,
			IsCrossAccount: project.IsCrossAccount,
		})
	}

	userProfile := &central.UserProfile{
		Id:        user.ID.Uint64(),
		Email:     user.Email,
		FirstName: user.FirstName,
		LastName:  user.LastName,
		Role:      string(user.Role),
		Projects:  userProjects,
	}
	return userProfile, nil
}

func (c Context) UpdateUserProfile(tenantID entity.ID, req UpdateUserProfileReq) error {
	err := c.CheckOrgID(tenantID)
	if err != nil {
		return err
	}

	err = db.Transaction(func(tx *gorm.DB) error {
		user, err := dbutil.GetUserByEmail(tx, req.Email)
		if err != nil {
			c.Log().Error("failed to get user by email", zap.String("email", req.Email), zap.Error(err))
			return err
		}
		user.FirstName = req.FirstName
		user.LastName = req.LastName
		err = (&user).Update(tx, entity.UserDBSchema.FirstName, entity.UserDBSchema.LastName)
		if err != nil {
			c.Log().Error("failed to update user profile", zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to update user profile"))
		}

		return nil
	})

	return err
}

func (c Context) ListAccessLog(query *centralModel.ListAccessLogQuery) ([]centralModel.AccessLog, int64, error) {
	return centralModel.ListAuditLogs(c.tenantAndUser.TenantID, query)
}

func (c Context) syncZendesk(tenant *entity.Tenant) error {
	email := tenant.Email
	zendeskConfig := config.Global().ZendeskConfig
	if zendeskConfig.EnableZendesk {
		orgReq := zendesk.Organization{
			Name:       tenant.Name,
			ExternalID: strconv.FormatUint(uint64(tenant.ID), 10),
		}

		org, err := c.zendeskClient.CreateOrganization(orgReq)
		if err != nil {
			c.Log().Warn("failed to create organization on Zendesk", zap.String("name", tenant.Name), zap.String("feature", "signup"), zap.Error(err))
			return err
		}
		tenant.ZendeskID = entity.ID(org.ID)
		err = db.Transaction(func(tx *gorm.DB) error {
			return tenant.Update(tx, entity.TenantDBSchema.ZendeskID)
		})
		if err != nil {
			c.Log().Warn("failed to update organization ZendeskID", zap.String("email", email), zap.String("feature", "signup"), zap.Error(err))
			return err
		}
	}

	return nil
}

// SetUserEmailVerified set EmailVerified field of user to true in auth0
func (c Context) SetUserEmailVerified(email string) error {
	users, err := c.management.ListByEmail(email)
	if err != nil {
		return errors.Annotate(err, "failed to list users by email in auth0")
	}
	var auth0User *management.User
	for _, u := range users {
		c.Log().Info("query user in auth0", zap.String("email", email), zap.Any("user", u))
		for _, identity := range u.Identities {
			if *identity.Connection == config.Global().Auth0Config.Auth0DatabaseConnectionName {
				auth0User = u
			}
		}
	}
	if auth0User == nil {
		return errors.New("failed to query the user of custom-email connection by email in auth0")
	}
	updateUser := &management.User{
		EmailVerified: pointer.ToBool(true),
	}
	err = c.management.Update(*auth0User.ID, updateUser)
	if err != nil {
		return errors.Annotate(err, "failed to update EmailVerified field of user in auth0")
	}
	return nil
}

func (c Context) TasksAfterRegistration(ctx context.Context, email string) error {
	var user entity.User
	var err error
	err = db.Transaction(func(tx *gorm.DB) error {
		user, err = dbutil.GetUserByEmail(tx, email)
		if err != nil {
			c.Log().Error("failed to get user by email", zap.String("email", email), zap.Error(err))
			return err
		}
		return nil
	})
	if err != nil {
		return err
	}
	tenantEntity := user.Tenant

	c = c.Model.User(ctx, &tenant.User{User: user, TenantID: user.TenantID, IsImpersonated: false})
	if tenantEntity.ThirdPartyAccountProvider != nil {
		err := c.BillingContext().CreatePayment()
		if err != nil {
			c.Log().Error("failed to create tenant payment", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
			return err
		}

		// do not sync salesforce in enable org demand plan, call salesforce sync twice in short time will get an server error. so merge the sync in one time here.
		err = c.BillingContext().EnableOrgOnDemandPlanWithoutSalesforceSync()
		if err != nil {
			c.Log().Error("failed to update org plan", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
			return err
		}
	}

	err = c.BillingContext().SyncSalesforce()
	if err != nil {
		c.Log().Error("failed to syncSalesforce", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
	}

	err = c.BillingContext().PrepareDevTier()
	if err != nil {
		c.Log().Error("failed to prepare dev tier", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
		return err
	}

	url := fmt.Sprintf("https://%s", config.Global().ServerConfig.FrontendDNS)
	emailVars := map[string]string{
		"userName": user.FirstName,
		"url":      url,
	}
	if err = c.mail.SendMailWithTemplate(ctx, []string{email}, config.EmailSubjectWelcome, config.TNNewUserWelcome, emailVars, nil); err != nil {
		c.Log().Error("failed to send welcome email", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
	}
	c.Log().Info("send welcome email", zap.String("feature", "signup"), zap.String("email", email), zap.Time("time", time.Now().UTC()))

	err = c.syncZendesk(tenantEntity)
	if err != nil {
		c.Log().Warn("failed to syncZendesk", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
	}

	return nil
}

func (c Context) RegistrationVerification(ctx context.Context, token string) (*string, error) {
	userInviteToken, err := c.session.ValidateUserInviteToken(token, true)
	if err != nil {
		if c.session.IsActivatedError(err) {
			return nil, nil
		}
		c.Log().Error("failed to validate registration token", zap.String("feature", "signup"), zap.Error(err))
		return nil, err
	}
	email := userInviteToken.Email

	err = c.SetUserEmailVerified(email)
	if err != nil {
		c.Log().Error("failed to set EmailVerified field of user to true in auth0", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
		return nil, errors.New("failed to update user in auth0")
	}

	err = db.Transaction(func(tx *gorm.DB) error {
		err2 := dbutil.UpdateRegistration(tx, email)
		if err2 != nil {
			c.Log().Error("failed to update registration", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err2))
			return err2
		}
		return nil
	})
	if err != nil {
		return nil, err
	}

	err = c.TasksAfterRegistration(ctx, email)
	if err != nil {
		c.Log().Error("failed to run tasks after registration", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
	}
	return &email, nil
}
