// Package tenantctx encapsulates all tenant functionality
package tenantctx

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

	"github.com/tidbcloud/account/internel/svc/comsvc"

	"github.com/tidbcloud/account/internel/dto"

	"github.com/tidbcloud/account/central/events/publish"

	"github.com/tidbcloud/account/commons/eventbus"

	centralModel "github.com/tidbcloud/account/internel/svc/auditlog"
	"github.com/tidbcloud/account/internel/svc/msp"
	org3 "github.com/tidbcloud/account/internel/svc/org"
	"github.com/tidbcloud/account/internel/svc/org/handler"
	"github.com/tidbcloud/account/internel/svc/rbac"
	tokensvc "github.com/tidbcloud/account/internel/svc/token"
	usersvc "github.com/tidbcloud/account/internel/svc/user"

	"github.com/AlekSi/pointer"
	"github.com/getsentry/sentry-go"
	"github.com/google/uuid"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"github.com/tidbcloud/account/central/events"
	"github.com/tidbcloud/account/central/events/userevent"
	"github.com/tidbcloud/account/central/jobs"
	"github.com/tidbcloud/account/commons/database"
	accountErr "github.com/tidbcloud/account/commons/errors"
	"github.com/tidbcloud/account/internel/dao/project"
	userDao "github.com/tidbcloud/account/internel/dao/user"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"github.com/tidbcloud/pkgv2/zendesk"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"go.uber.org/zap"
	"gopkg.in/auth0.v4/management"
	"gorm.io/gorm"

	"github.com/tidbcloud/account/central/utils"
	"github.com/tidbcloud/account/commons/config"
	accountpb "github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/account/idl/pbgen/proto/acl"
	"github.com/tidbcloud/account/idl/pbgen/proto/central"
	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/account/internel/dao/cross"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/account/internel/dao/org"
	"github.com/tidbcloud/account/internel/dao/role"
	"github.com/tidbcloud/account/thirdparty/account"
	"github.com/tidbcloud/account/thirdparty/billing2"
	"github.com/tidbcloud/account/thirdparty/jsm"
	"github.com/tidbcloud/account/thirdparty/salesforce"
)

type Model struct {
	log               *zap.Logger
	session           *tokensvc.Session
	management        Auth0UserManagementHandler
	zendeskClient     ZendeskHandler
	salesforceClient  SalesforceHandler
	auth0TokenHandler Auth0TokenHandler
	accountAPI        account.AccountAPIHandler
}

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

// NewModel returns a Model
func NewModel(
	log *zap.Logger, session *tokensvc.Session, management Auth0UserManagementHandler, zendeskClient ZendeskHandler,
	salesforceClient SalesforceHandler, auth0TokenHandler Auth0TokenHandler, accountAPI account.AccountAPIHandler,
) Model {
	return Model{
		log:               log,
		session:           session,
		management:        management,
		zendeskClient:     zendeskClient,
		salesforceClient:  salesforceClient,
		auth0TokenHandler: auth0TokenHandler,
		accountAPI:        accountAPI,
	}
}

// User returns a UserContext
func (m Model) User(ctx context.Context, user *usersvc.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(tilog.TraceIDFromContext(ctx)...),
	}
}

func (m Model) Management() Auth0UserManagementHandler {
	return m.management
}

func (m Model) UserWithEntity(ctx context.Context, user *usersvc.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(tilog.TraceIDFromContext(ctx)...),
		tenant:        tenant,
	}
}

func (m Model) GetSubFromAuth0Token(auth0Token string) (string, error) {
	return m.auth0TokenHandler.GetSubFromAuth0Token(auth0Token)
}

func (m Model) GetEmailFromAuth0Token(auth0Token string) (string, error) {
	return m.auth0TokenHandler.GetEmailFromAuth0Token(auth0Token)
}

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

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       uint32 = 1 << 2
	EnableDataStreamToKafkaFlag uint32 = 1 << 1
	EnableDataStreamToMysqlFlag uint32 = 1 << 0
	EnableAuditLogFlag          uint32 = 1 << 3
	EnableXAccountFlag          uint32 = 1 << 4 // cross account deploy cluster
	// EnableOpenAPIFlag           uint32 = 1 << 5
	EnableNewClusterListUIFlag uint32 = 1 << 6

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

var signUpEmailNotVerifyErrorFormat = "Email address is already in use and a verification email has been sent to %s. Click the link in the email to complete sign-up."

func validateRegistration(ctx context.Context, email string) error {
	if email == "" {
		return accountErr.NewEmailAddressInUseError("Email is blank.", nil)
	}
	return database.Transaction(func(tx *gorm.DB) error {
		dbUser := &entity.User{}
		err := entity.NewUserQuerySet(tx).EmailEq(email).DeletedAtIsNull().One(dbUser)
		if err != nil {
			if goerrors.Is(err, gorm.ErrRecordNotFound) {
				dbUser = nil
			} else {
				tilog.Error(ctx, "failed to query user by email", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
				return errs.NewInternalDB("failed to query user by email", err)
			}
		}
		if dbUser != nil {
			if dbUser.Status == entity.UserStatusActive {
				tilog.Info(ctx, "query user by email is active, not allow to signup", zap.String("feature", "signup"), zap.String("email", email))
				return accountErr.NewEmailAddressInUseError("Email address is already in use, please sign in.", nil)
			}
			if dbUser.Status == entity.UserStatusPending {
				dbUserInviteToken := &entity.UserInviteToken{}
				err = entity.NewUserInviteTokenQuerySet(tx).MethodEq(tokensvc.RegistrationVerificationMethod).EmailEq(email).One(dbUserInviteToken)
				if err != nil {
					if goerrors.Is(err, gorm.ErrRecordNotFound) {
						dbUserInviteToken = nil
					} else {
						tilog.Error(ctx, "failed to query user invite token", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
						return errs.NewInternalDB("failed to query user invite token", err)
					}
				}
				if dbUserInviteToken != nil && dbUserInviteToken.ExpireAt.After(time.Now()) {
					tilog.Info(ctx, "query user by email is pending, need to verify email", zap.String("feature", "signup"), zap.String("email", email))
					errorTip := fmt.Sprintf(signUpEmailNotVerifyErrorFormat, email)
					return accountErr.NewEmailAddressNotVerifyError(errorTip, nil)
				}
			}

			now := time.Now()
			dbUser.Email = fmt.Sprintf("deleted-%d", dbUser.ID) //nolint
			dbUser.DeletedAt = &now                             //nolint
			dbUser.Status = entity.UserStatusInactive
			err = dbUser.Update(tx, entity.UserDBSchema.Email, entity.UserDBSchema.DeletedAt, entity.UserDBSchema.Status)
			if err != nil {
				tilog.Error(ctx, "failed to delete origin user", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
				return errs.NewInternalDB("failed to delete origin user", err)
			}
		}

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

		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 parseFirstNameAndLastName(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) GetUserBasicInfoFromAuth0(ctx context.Context, auth0Token string) (*Auth0User, error) {
	if auth0Token == "" {
		return nil, errs.NewBadRequest("auth0_token is blank", nil)
	}
	email, err := c.auth0TokenHandler.GetEmailFromAuth0Token(auth0Token)
	if err != nil {
		tilog.Error(ctx, "GetUserBasicInfoFromAuth0 GetEmailFromAuth0Token error", zap.Any("auth0Token", auth0Token), zap.Any("err", err))
		return nil, errs.NewInternalUnknown("failed to get email from auth0Token", err)
	}
	tilog.Info(ctx, "GetUserBasicInfoFromAuth0 get email from auth0 token", zap.Any("auth0Token", auth0Token), zap.Any("email", email))
	users, err := c.management.ListByEmail(email)
	if err != nil {
		tilog.Error(ctx, "GetUserBasicInfoFromAuth0 ListByEmail error", zap.String("email", "email"), zap.Error(err))
		return nil, errs.NewInternalUnknown("failed to list users by email from auth0", err)
	}
	sub, err := c.auth0TokenHandler.GetSubFromAuth0Token(auth0Token)
	if err != nil {
		tilog.Error(ctx, "GetUserBasicInfoFromAuth0 GetSubFromAuth0Token error", zap.Any("auth0Token", auth0Token), zap.Any("err", err))
		return nil, errs.NewInternalUnknown("failed to get email from auth0Token", err)
	}
	tilog.Info(ctx, "GetUserBasicInfoFromAuth0 get sub from auth0 token", zap.Any("auth0Token", auth0Token), zap.Any("sub", sub))
	for _, u := range users {
		userID := pointer.GetString(u.ID)
		if userID != sub {
			continue
		}
		tilog.Info(ctx, "GetUserBasicInfoFromAuth0 found matched auth0 user", zap.Any("email", email), zap.Any("sub", sub), zap.Any("user", u))

		firstName := pointer.GetString(u.GivenName)
		lastName := pointer.GetString(u.FamilyName)

		// if any one of first name and last name is not blank
		if firstName != "" || lastName != "" {
			// fallback to email
			if firstName == "" {
				firstName = email
			}
			return &Auth0User{
				Email:     email,
				FirstName: firstName,
				LastName:  lastName,
			}, nil
		}

		// use name and nickname replace first name and last name
		nickName := pointer.GetString(u.Nickname)
		name := pointer.GetString(u.Name)
		firstName, lastName = parseFirstNameAndLastName(name, nickName, email)
		// fallback to email
		if firstName == "" {
			firstName = email
		}
		return &Auth0User{
			Email:     email,
			FirstName: firstName,
			LastName:  lastName,
		}, nil

	}
	return nil, nil
}

func (c Context) GetUserFromAuth0(ctx context.Context, connectionType config.Auth0ConnectionType, auth0Token string) (*Auth0User, error) {
	logger := tilog.WithContextL(ctx)

	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, errs.NewInternalUnknown("failed to get email from auth0Token field", err)
	}

	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, errs.NewInternalUnknown("failed to list users by email from auth0", err)
	}

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

				logger.Info("mapping connection type", zap.String("connectionType", connectionType.String()), zap.Any("user", u))
				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 = parseFirstNameAndLastName(name, nickName, email)
				}
				if connectionType == config.Auth0SocialConnectionMicrosoft {
					name := pointer.GetString(u.Name)
					nickName := pointer.GetString(u.Nickname)
					firstName, lastName = parseFirstNameAndLastName(name, nickName, email)
				}

				if connectionType.IsOKTAConnection() {
					name := pointer.GetString(u.Name)
					nickName := pointer.GetString(u.Nickname)
					// login from okta is nearly the same with GitHub, including retrieve first name and last name
					firstName, lastName = parseFirstNameAndLastName(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, errs.NewInternalUnknown("failed to find user by email in auth0", nil)
	}
	return &Auth0User{
		Email:     email,
		FirstName: firstName,
		LastName:  lastName,
	}, nil
}

func getThirdPartyAccountCode(thirdPartyAccountProvider, thirdPartyAccountToken string) (*string, error) {
	var accountCode *string = nil
	accountProvider, err := handler.GetAccountHandler().GetAccountProvider(thirdPartyAccountProvider)
	if err != nil {
		log.Warn("failed to get third party account provider", zap.String("feature", "signup"), zap.Error(err))
		return accountCode, errs.NewBadRequest("failed to get third party account provider", err)
	}
	accountCode, err = accountProvider.ResolveToken(&thirdPartyAccountToken)
	if err != nil {
		log.Error("failed to get third party account code", zap.String("feature", "signup"), zap.Error(err))
		return accountCode, err
	}
	return accountCode, nil
}

func getMspAccountCode(ctx context.Context, thirdPartyAccountToken string) (*string, error) {
	userSvc, _ := usersvc.GetUserSvc()
	//inviterToken, err := c.session.ValidateUserInviteToken(ctx, thirdPartyAccountToken, false)
	inviterToken, err := userSvc.ValidateUserInviteTokenIgnoreEmail(ctx, thirdPartyAccountToken, false)
	if err != nil {
		tilog.Info(ctx, "third_party_account_token invalid", zap.String("thirdPartyAccountToken", "thirdPartyAccountToken"), zap.Error(err))
		//
		//return nil, errs.NewBadRequest("third_party_account_token invalid", nil)
		tokenErrMsg := "Token in URL is invalid. Please ask your MSP to update the signup URL with a valid token."
		return nil, accountErr.NewErrMessageForFrontEnd(tokenErrMsg, nil)
	}
	orgIdStr := strconv.FormatInt(int64(inviterToken.TenantID), 10)
	return &orgIdStr, nil

}
func deleteMspThirdPartyAccountToken(ctx context.Context, thirdPartyAccountToken string) error {
	userSvc, _ := usersvc.GetUserSvc()
	err := userSvc.DeleteUserInviteToken(ctx, thirdPartyAccountToken)
	if err != nil {
		tilog.Error(ctx, "DeleteUserInviteToken err", zap.String("thirdPartyAccountToken", "thirdPartyAccountToken"), zap.Error(err))
		return err
	}

	return nil
}

func (c Context) createDatabaseConnectionAuth0User(ctx context.Context, email, password string) error {
	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) {
			tilog.Error(ctx, "password strength", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
			return accountErr.NewUserPasswordPolicyError("password strength", err)
		}
		tilog.Error(ctx, "failed to create user in auth0", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
		return errs.NewInternalUnknown("failed to create user in auth0", err)
	}
	return nil
}

// sendVerificationEmail
//
//	@Description: sendVerificationEmail  when  email register
//	@receiver c
//	@param ctx
//	@param email
//	@param firstName
//	@param token
//	@param orgID
//	@return error
func (c Context) sendVerificationEmail(ctx context.Context, email, firstName, token string, returnUrl string, orgID uint64) error {
	//returnUrl := "https://feat-oauth-authorize--dbaas-dev.netlify.app/oauth/signin"
	url := fmt.Sprintf("https://%s/registration-verification?token=%s", utils.GetCentralBackendDns(), token)
	//  register form oauth,redirect to return_url
	if returnUrl != "" {
		encodeUrl := base64.URLEncoding.EncodeToString([]byte(returnUrl))
		url = url + fmt.Sprintf("%sreturn_url=%s", "&", encodeUrl)
	}
	tilog.Info(ctx, "sendVerificationEmail not encode returnUrl", zap.Any("url", url))
	tilog.Info(ctx, "sendVerificationEmail encode url", zap.Any("url", url))

	firstName = utils.EscapeStr(ctx, firstName)
	vars := map[string]string{
		"userName": firstName,
		"url":      url,
		"token":    token,
	}

	_, err := c.accountAPI.SendEmail(ctx, &accountpb.SendEmailReq{
		OrgId:        orgID,
		Scene:        string(entity.NotificationSceneVerification),
		Identifier:   uuid.New().String(),
		Subject:      config.EmailSubjectVerification,
		Emails:       []string{email},
		CcEmails:     nil,
		TemplateId:   config.SendgridTemplateName2id[config.TNRegistrationVerify],
		TemplateVars: vars,
	})

	if err != nil {
		tilog.Error(ctx, "failed to send verification email", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
		return err
	}
	tilog.Info(ctx, "send verification email", zap.String("feature", "signup"), zap.String("email", email), zap.Time("time", time.Now().UTC()))
	return nil
}

type RegistrationSource struct {
	WebsiteReferrerURL    string
	UtmCampaign           string
	UtmContent            string
	UtmMedium             string
	UtmSource             string
	UtmTerm               string
	MostRecentReferralUrl string
	SignupSource          string
	ProviderSource        string
}

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
	ThirdPartyFreeTrial       bool
	Source                    *RegistrationSource
}

type ValidateThirdPartyTokenReq struct {
	ThirdPartyAccountProvider string
	ThirdPartyAccountToken    string
}

type ValidateThirdPartyTokenResponse struct {
	ThirdPartyAccountProvider          string
	ThirdPartyAccountCode              string
	ThirdPartyAccountCodeAlreadyBinded bool
	ThirdPartyAccountResolveTokenRet   string
}

type OrgBindThirdPartyReq struct {
	OrgID                     entity.ID
	ThirdPartyAccountProvider string
	ThirdPartyAccountToken    string
	ThirdPartyFreeTrial       bool
}

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
	ThirdPartyAccountResolveTokenRet *string
	ThirdPartyAccountCode            *string
	ThirdPartyFreeTrial              bool
	Source                           *RegistrationSource
	OrgTag                           entity.OrgTag
}

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,
		ThirdPartyFreeTrial:       params.ThirdPartyFreeTrial,
		Source:                    params.Source,
	}
}

func (c Context) saveTenant(ctx context.Context, en *OrgEntity, returnUrl string) (*entity.Tenant, *entity.User, error) {
	// Set default org name
	// https://jira.tidbcloud.com/browse/DM-7058
	orgName := en.Name
	if orgName == "" {
		orgName = fmt.Sprintf("%s's Org", en.FirstName)
	}

	tenantEntity := &entity.Tenant{
		Name:                      orgName,
		Alias:                     orgName,
		Country:                   en.Country,
		Email:                     en.Email,
		TimeZone:                  "",
		Status:                    en.TenantStatus,
		AcceptService:             true,
		ThirdPartyAccountProvider: transEmptyStringToNullable(en.ThirdPartyAccountProvider),
		ThirdPartyAccountCode:     en.ThirdPartyAccountCode,
		ThirdPartyFreeTrial:       en.ThirdPartyFreeTrial,
		OrgTag:                    string(en.OrgTag),
		OrgType:                   entity.OrgTypePersonal,
	}

	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:      orgName,
		Email:     en.Email,
		FirstName: en.FirstName,
		LastName:  en.LastName,
		Source:    en.ConnectionType.String(),
		IPAddress: en.ClientIP,
		InvitedAt: time.Now(),
	}
	if en.Source != nil {
		source := en.Source
		registrationLogEntity.WebsiteReferrerURL = source.WebsiteReferrerURL
		registrationLogEntity.UtmCampaign = source.UtmCampaign
		registrationLogEntity.UtmContent = source.UtmContent
		registrationLogEntity.UtmMedium = source.UtmMedium
		registrationLogEntity.UtmSource = source.UtmSource
		registrationLogEntity.UtmTerm = source.UtmTerm
		registrationLogEntity.MostRecentReferralUrl = source.MostRecentReferralUrl
		registrationLogEntity.SignupSource = source.SignupSource
		registrationLogEntity.ProviderSource = source.ProviderSource
	}

	projectEntity := &entity.Project{
		Name:   config.DefaultVirtualProjectName,
		Status: entity.ProjectStatusActive,
		Type:   entity.ProjectTypeTiDBXVirtual,
	}

	tenantGroup := &org3.TenantGroup{
		Tenant:          tenantEntity,
		User:            userEntity,
		Project:         projectEntity,
		RegistrationLog: registrationLogEntity,
	}

	email := en.Email
	var token string
	var tenantStatus entity.TenantStatus
	err := database.Transaction(func(tx *gorm.DB) error {
		if en.ThirdPartyAccountProvider != "" && (en.ThirdPartyAccountProvider != entity.MspThirdPartyAccountProvider) {
			res, err := entity.NewTenantQuerySet(tx).ThirdPartyAccountProviderEq(*tenantEntity.ThirdPartyAccountProvider).ThirdPartyAccountCodeEq(*tenantEntity.ThirdPartyAccountCode).Count()
			if err != nil {
				tilog.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 errs.NewInternalDB("failed to query tenant in third party account if exists when register", err)
			}
			if res > 0 {
				tilog.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 accountErr.NewErrDuplicatedThirdPartyAccountError("this marketplace account has already been registered", nil)
			}
		}
		if en.ThirdPartyAccountProvider == handler.AliCloudAccountProvider && en.ThirdPartyAccountCode != nil {
			subscriptionRecord, err := billing2.GetHandler().GetActiveSubscriptionRecordByAliUid(ctx, *en.ThirdPartyAccountCode)
			if err != nil {
				tilog.Error(ctx, "failed to get aliyun subscription record when register", zap.String("aliUid", *en.ThirdPartyAccountCode), zap.Error(err))
				return errs.NewInternalDB("failed to get aliyun subscription record", err)
			}
			if !subscriptionRecord.IsThirdParty {
				tenantGroup.Tenant.OrgTag = string(entity.OrgTagAlicloudCustomer)
			}
		}
		tenantEntity, err := org3.SaveTenant(ctx, tx, tenantGroup)
		tilog.Info(ctx, "saveTenant result", zap.Any("tenantEntity", tenantEntity), zap.Any("err", err))
		if err != nil {
			if strings.Contains(err.Error(), "Duplicate entry") {
				tilog.Info(ctx, "failed to register tenant in db", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
				return errs.NewBadRequest("failed to register tenant in db", err)
			}
			tilog.Error(ctx, "failed to register tenant in db", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
			return errs.NewInternalDB("failed to register tenant in db", err)
		}
		c.tenantAndUser = &usersvc.User{
			TenantID:       tenantEntity.ID,
			IsImpersonated: false,
		}
		tenantStatus = tenantEntity.Status

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

		if en.ThirdPartyAccountProvider != "" && (en.ThirdPartyAccountProvider != entity.MspThirdPartyAccountProvider) {
			accountProvider, err := handler.GetAccountHandler().GetAccountProvider(en.ThirdPartyAccountProvider)
			if err != nil {
				log.Error("failed to get third party account provider", zap.String("feature", "signup"), zap.Error(err))
				return errs.NewBadRequest(err.Error(), err)
			}
			err = accountProvider.ApproveCode(en.ThirdPartyAccountCode)
			if err != nil {
				log.Error("failed to approve third party account code", zap.String("feature", "signup"), zap.Error(err))
				return err
			}
		}

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

	var orgEventType entity.EntityEventType
	if tenantStatus == entity.TenantStatusPending {
		orgEventType = entity.EntityEventTypeCreate
	} else if tenantStatus == entity.TenantStatusActive {
		orgEventType = entity.EntityEventTypeActivate
	}
	if orgEventType != "" {
		if err := publish.PublishOrgEvent(ctx, dto.OrgEventData{OrgId: tenantEntity.ID.Uint64(), OrgEventType: orgEventType}); err != nil {
			tilog.Error(ctx, "PublishOrgEvent err", zap.Any("TenantID", tenantEntity.ID), zap.Error(err))
		}
	}

	c.NotifyJsmUserRegistion(ctx, tenantGroup)

	if en.Source != nil {
		notifyReq := &billing2.NotifyRegistrationSourceReq{
			TenantID:                  tenantEntity.ID.Uint64(),
			WebsiteReferrerUrl:        en.Source.WebsiteReferrerURL,
			UtmCampaign:               en.Source.UtmCampaign,
			UtmContent:                en.Source.UtmContent,
			UtmMedium:                 en.Source.UtmMedium,
			UtmSource:                 en.Source.UtmSource,
			UtmTerm:                   en.Source.UtmTerm,
			MostRecentReferralUrl:     en.Source.MostRecentReferralUrl,
			SignupSource:              en.Source.SignupSource,
			ProviderSource:            en.Source.ProviderSource,
			ThirdPartyAccountProvider: en.ThirdPartyAccountProvider,
			ThirdPartyAccountCode:     transNullableStringToEmpty(en.ThirdPartyAccountCode),
		}
		err := billing2.GetHandler().NotifyRegistrationSource(ctx, notifyReq)
		tilog.Info(ctx, "NotifyRegistrationSource result", zap.Any("notifyReq", notifyReq), zap.Error(err))
	}

	go func() {
		if en.Source != nil && en.Source.ProviderSource != "" {
			_ = publish.PublishOrgRegistrationProviderSourceEvent(ctx, c.tenantAndUser.TenantID.Uint64(), en.Source.ProviderSource)
		}
		_ = userevent.PublishAddUserToOrgEvent(ctx, tenantGroup.Tenant.ID.Uint64(), tenantGroup.User.ID.Uint64(), entity.RoleOrgOwner)
	}()

	if en.ConnectionType == config.Auth0DatabaseConnection {
		// send verification email
		err = c.sendVerificationEmail(ctx, email, en.FirstName, token, returnUrl, tenantEntity.ID.Uint64())
		if err != nil {
			return nil, nil, errs.NewInternalUnknown("failed to send verification email", err)
		}
	}

	if en.ConnectionType.IsAuth0SocialConnection() {
		err = c.TasksAfterRegistration(ctx, email, tenantEntity.ID.Uint64())
		if err != nil {
			tilog.Error(ctx, "failed to run tasks after registration", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
		}
	}
	return tenantEntity, userEntity, nil
}
func (c Context) NotifyJsmUserRegistion(ctx context.Context, tenantGroup *org3.TenantGroup) {
	user := tenantGroup.User
	tenant := tenantGroup.Tenant
	project := tenantGroup.Project
	jsmClient := jsm.GetJsmClientSingleton()
	if jsmClient.IsEnable() {
		_, createJsmAccountErr := jsmClient.CreateJsmAccount(ctx, user.Email)
		notifyCreateUserErr := jsmClient.NotifyCreateUserMessage(ctx, user)
		notifyCreateOrgErr := jsmClient.NotifyCreateOrgMessage(ctx, tenant)
		notifyInviteUserToOrgErr := jsmClient.NotifyInviteUserToOrgMessage(ctx, tenant.ID.Uint64(), user.ID.Uint64(), entity.RoleOrgOwner)
		if createJsmAccountErr != nil || notifyCreateUserErr != nil || notifyCreateOrgErr != nil || notifyInviteUserToOrgErr != nil {
			tilog.Warn(ctx, "Sync Jsm failed in new user registration", zap.Uint64("user", user.ID.Uint64()), zap.Uint64("org", tenant.ID.Uint64()))
		}
		notifyCreateProjectErr := jsmClient.NotifyCreateProjectMessage(ctx, uint64(project.ID), tenant.ID.Uint64())
		notifyInviteUserToProjectErr := jsmClient.NotifyInviteUserToProjectMessage(ctx, uint64(project.ID), user.ID.Uint64())
		if notifyCreateProjectErr != nil || notifyInviteUserToProjectErr != nil {
			tilog.Warn(ctx, "Sync Jsm failed in new user registration - create new project", zap.Uint64("user", user.ID.Uint64()), zap.Uint64("org", tenant.ID.Uint64()),
				zap.Uint64("project", project.ID.Uint64()))
		}
	}
}

func (c Context) CreateOrg(ctx context.Context, params *CreateOrgParams, returnUrl string) error {
	en := buildOrgEntity(params)
	tilog.Info(ctx, "CreateOrg", zap.Any("params", params))
	if params.ConnectionType.IsAuth0SocialConnection() {
		auth0User, err := c.GetUserFromAuth0(ctx, 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 errs.WrapGORMError(validateErr, "failed to validate registration")
	}

	var accountCode *string
	var err error
	// is msp customer org
	// ThirdPartyAccountProvider = "msp_account_provider"
	// ThirdPartyAccountCode = MSP OrgId

	en.ThirdPartyFreeTrial = params.ThirdPartyFreeTrial

	if params.ThirdPartyAccountProvider == entity.MspThirdPartyAccountProvider {
		// if register form msp customer , validate register token and set ThirdPartyAccountCode = msp org id
		en.ThirdPartyAccountCode, err = getMspAccountCode(ctx, params.ThirdPartyAccountToken)
		if err != nil {
			return err
		}
		en.OrgTag = entity.OrgTagMspCustomer
		tilog.Info(ctx, "this org is msp customer,set MspThirdPartyAccountProvider", zap.Any("ThirdPartyAccountProvider", entity.MspThirdPartyAccountProvider), zap.Any("ThirdPartyAccountCode", en.ThirdPartyAccountCode))
	} else if params.ThirdPartyAccountProvider == entity.ResaleThirdPartyAccountProvider {
		en.ThirdPartyAccountProvider = entity.MspThirdPartyAccountProvider // only msp can skip cluster creating check, actually resale do not care that param but the registration process need it
		en.ThirdPartyAccountCode, err = getMspAccountCode(ctx, params.ThirdPartyAccountToken)
		if err != nil {
			return err
		}
		en.OrgTag = entity.OrgTagResaleCustomer
		tilog.Info(ctx, "this org is resale customer,set MspThirdPartyAccountProvider", zap.Any("ThirdPartyAccountProvider", entity.MspThirdPartyAccountProvider), zap.Any("ThirdPartyAccountCode", en.ThirdPartyAccountCode))
	} else if params.ThirdPartyAccountProvider != "" {
		accountCode, err = getThirdPartyAccountCode(params.ThirdPartyAccountProvider, params.ThirdPartyAccountToken)
		if err != nil {
			return err
		}
		en.ThirdPartyAccountCode = accountCode
		en.ThirdPartyAccountResolveTokenRet = accountCode
		// CreateOrg: get the real `free-trial` status for aws subscription
		if params.ThirdPartyAccountProvider == handler.AWSAccountProvider {
			isInTrial, err := billing2.GetHandler().GetAWSSubscriptionFreeTrialStatus(context.Background(), *accountCode)
			if err != nil {
				return err
			}
			en.ThirdPartyFreeTrial = isInTrial
		}
	}

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

	tenant, user, err := c.saveTenant(ctx, en, returnUrl)
	if err != nil {
		if errs.IsError(err, errs.TiErr) {
			return err
		}

		return errs.NewInternalUnknown("failed to save tenant", err)
	}
	//  delete token when customer register success
	if en.ThirdPartyAccountProvider == entity.MspThirdPartyAccountProvider && params.ThirdPartyAccountToken != "" {
		err := deleteMspThirdPartyAccountToken(ctx, params.ThirdPartyAccountToken)
		if err != nil {
			tilog.Error(ctx, "deleteMspThirdPartyAccountToken err", zap.Any("token", params.ThirdPartyAccountToken))
			return err
		}

	}
	details := auditlog.Details{
		auditlog.KeySignupMethod:    en.ConnectionType.String(),
		auditlog.KeyFirstName:       en.FirstName,
		auditlog.KeyLastName:        en.LastName,
		auditlog.KeyCountryOrRegion: en.Country,
		auditlog.KeyCompanyName:     en.Name,
		auditlog.KeyPhoneNumber:     en.PhoneNumber,
		auditlog.KeyError:           err,
	}
	auditlog.NewEvent(ctx, auditlog.EventRegistration).SetOperatorType(auditlog.OperatorTypeUser).
		SetOperatorEmail(user.Email).SetOperatorID(user.ID.Uint64()).SetOperatorLoginMethod(en.ConnectionType.String()).
		SetOrgID(tenant.ID.Uint64()).SetResult(err == nil).SetDetails(details.Build()).Submit() //nolint:errcheck
	auditlog.NewEvent(ctx, auditlog.EventCreateOrg).SetOperatorType(auditlog.OperatorTypeUser).
		SetOperatorEmail(user.Email).SetOperatorID(user.ID.Uint64()).SetOperatorLoginMethod(en.ConnectionType.String()).
		SetOrgID(tenant.ID.Uint64()).SetResult(err == nil).SetDetails(auditlog.Details{auditlog.KeyError: err}.Build()).
		Submit() //nolint:errcheck

	return nil
}

func ValidateThirdPartyToken(ctx context.Context, params *ValidateThirdPartyTokenReq) (*ValidateThirdPartyTokenResponse, error) {
	accountCode, err := getThirdPartyAccountCode(params.ThirdPartyAccountProvider, params.ThirdPartyAccountToken)
	if err != nil {
		return nil, err
	}
	db := database.NewProvider().WriteDB(ctx)

	cnt, err := entity.NewTenantQuerySet(db).ThirdPartyAccountCodeEq(*accountCode).ThirdPartyAccountProviderEq(params.ThirdPartyAccountProvider).Count()
	if err != nil {
		return nil, errs.NewInternalUnknown("failed validate token", err)
	}

	return &ValidateThirdPartyTokenResponse{
		ThirdPartyAccountProvider:          params.ThirdPartyAccountProvider,
		ThirdPartyAccountCode:              *accountCode,
		ThirdPartyAccountCodeAlreadyBinded: cnt > 0,
		ThirdPartyAccountResolveTokenRet:   *accountCode,
	}, nil
}

const TenantBindThirdPartyEventType = "tenant-bind-thirdparty"

func (c Context) OrgBindThirdParty(ctx context.Context, params *OrgBindThirdPartyReq) error {
	if params.ThirdPartyFreeTrial {
		return errs.NewBadRequest("Your account already has a free trial — no need to link a free trial subscription, you can create free clusters right away. To pay through your AWS bill, you can switch to TiDB Cloud subscription, which supports linking to this organization.", nil)
	}

	res, err := ValidateThirdPartyToken(ctx, &ValidateThirdPartyTokenReq{
		ThirdPartyAccountProvider: params.ThirdPartyAccountProvider,
		ThirdPartyAccountToken:    params.ThirdPartyAccountToken,
	})

	if err != nil {
		return err
	}

	accountCode := res.ThirdPartyAccountCode

	if res.ThirdPartyAccountCodeAlreadyBinded {
		tilog.Info(ctx, "third party account already exists", zap.String("feature", "signup"), zap.Uint("orgID", uint(params.OrgID)),
			zap.String("third_party_account_provider", params.ThirdPartyAccountProvider), zap.String("third_party_account_code", accountCode))
		return accountErr.NewErrDuplicatedThirdPartyAccountError("this marketplace account has already been registered", nil)
	}

	dbOrg, err := dao.GetDaoHub().Org.GetOrgByOrgID(ctx, database.DBConn.DB, params.OrgID.Uint64())
	if err != nil {
		log.Error("org bind third party, failed to get org", zap.String("org_id", "params.OrgID"), zap.Error(err))
		return err
	}
	if dbOrg.ThirdPartyAccountCode != nil && *dbOrg.ThirdPartyAccountCode != "" {
		tilog.Info(ctx, "org already exists third party account", zap.Uint("orgID", uint(params.OrgID)), zap.Any("third_party_account_provider", dbOrg.ThirdPartyAccountProvider), zap.Any("third_party_account_code", dbOrg.ThirdPartyAccountCode))
		return accountErr.NewErrDuplicatedThirdPartyAccountError("this org has already bind marketplace account", nil)
	}

	// OrgBindThirdParty: get the real `free-trial` status for aws subscription
	if params.ThirdPartyAccountProvider == handler.AWSAccountProvider {
		isInTrial, err := billing2.GetHandler().GetAWSSubscriptionFreeTrialStatus(context.Background(), accountCode)
		if err != nil {
			return err
		}
		params.ThirdPartyFreeTrial = isInTrial
	}

	err = database.Transaction(func(tx *gorm.DB) error {
		// push to billing
		err := eventbus.Get().PublishEventWithTransaction(TenantBindThirdPartyEventType, &eventbus.Event{
			EventID:   uuid.NewString(),
			EventType: TenantBindThirdPartyEventType,
			EventData: map[string]interface{}{
				"tenant_id": params.OrgID.Uint64(),
			},
		}, tx)

		if err != nil {
			return err
		}

		err = entity.NewTenantQuerySet(tx).IDEq(params.OrgID).GetUpdater().SetThirdPartyAccountProvider(&params.ThirdPartyAccountProvider).SetThirdPartyAccountCode(&accountCode).SetThirdPartyFreeTrial(params.ThirdPartyFreeTrial).Update()

		if err != nil {
			return errs.NewInternalUnknown("failed update org entity", err)
		}
		accountProvider, err := handler.GetAccountHandler().GetAccountProvider(params.ThirdPartyAccountProvider)
		if err != nil {
			log.Error("failed to get third party account provider", zap.String("feature", "signup"), zap.Error(err))
			return err
		}
		err = accountProvider.ApproveCode(&accountCode)
		if err != nil {
			log.Error("failed to approve third party account code", zap.String("feature", "signup"), zap.Error(err))
			return err
		}
		return nil
	})

	if err != nil {
		return err
	}

	err = c.salesforceClient.UpdateAccount(ctx, salesforce.Account{
		Source:   params.ThirdPartyAccountProvider,
		PortalID: strconv.FormatUint(params.OrgID.Uint64(), 10),
	})
	if err != nil {
		tilog.Error(ctx, "failed to sync salesforce", zap.Uint64("orgID", params.OrgID.Uint64()), zap.Error(err))
	}

	return nil
}

func (c Context) SendVerificationEmail(ctx context.Context, email string, returnUrl string) error {
	var token string
	var user entity.User
	var org entity.Tenant
	err := database.Transaction(func(tx *gorm.DB) error {
		var err error
		user, err = usersvc.GetUserByEmail(tx, email)
		if err != nil {
			tilog.Warn(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 {
			tilog.Info(ctx, "reject to send verification email as user status is active", zap.String("email", email))
			return errcode.NewInvalidInputErr(errors.New("your account has been successfully activated"))
			// 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 {
			tilog.Info(ctx, "reject to send verification email as user status is inactive", zap.String("email", email))
			return errcode.NewInvalidInputErr(errors.New("your account is currently inactive"))
			// return errcode.NewInvalidInputErr(errors.New("your account is currently inactive"))
		}
		if user.InvitedAt != nil {
			tilog.Warn(ctx, "reject to send verification email as user is invited to TiDB cloud", zap.String("email", email))
			return errcode.NewInvalidInputErr(errors.New(ErrUserInvited))
		}

		orgs, err := org3.ListOrgsByUserIDStatus(ctx, tx, user.ID, []string{entity.RoleBindingStatusActive, entity.RoleBindingStatusInactive})
		if err != nil || len(orgs) == 0 {
			tilog.Error(ctx, "failed to get user's orgs from db", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to get user's orgs from db"))
		}
		org = orgs[0]
		token, err = c.session.InsertUserInviteToken(tx, 0, org.ID, tokensvc.RegistrationVerificationMethod, email, 24*time.Hour, true)
		if err != nil {
			tilog.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, returnUrl, org.ID.Uint64())
	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  // if user not set time zone Timezone=0
	UserSetTimezone           string // if user not set time zone theUserSetTimezone=""
	Status                    string
	Plan                      string
	TotalCredit               int64
	RemainingCredit           int64
	HasDiscount               bool
	Discount                  int64
	DiscountDescription       string
	NodeUsageFee              int64
	ThirdPartyAccountProvider string
	ThirdPartyAccountCode     string
	ThirdPartyFreeTrial       bool
	WhitelistFlag             uint32
	Kind                      string
	AccountManager            string
	OrgType                   string
	OrgTag                    string
	CompanyName               string
}

func (c Context) GetTenant() (*entity.Tenant, error) {
	if c.tenant != nil {
		return c.tenant, nil
	}
	var t entity.Tenant
	err := database.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 getTenantByID(orgID uint64) (*entity.Tenant, error) {
	t := entity.Tenant{}
	err := database.Transaction(func(tx *gorm.DB) error {
		return entity.NewTenantQuerySet(tx).IDEq(entity.ID(orgID)).One(&t)
	})
	if err != nil {
		return nil, err
	}
	return &t, 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),
		Kind:                      string(t.Kind),
		AccountManager:            utils.GetOrgAccountManager(context.Background(), t.ID.Uint64()),
		ThirdPartyAccountProvider: transNullableStringToEmpty(t.ThirdPartyAccountProvider),
		ThirdPartyAccountCode:     transNullableStringToEmpty(t.ThirdPartyAccountCode),
		ThirdPartyFreeTrial:       t.ThirdPartyFreeTrial,
		OrgTag:                    t.OrgTag,
	}
	return organization, nil
}

func (m Model) getOrganizationByTenant(ctx context.Context, tenantEntity *entity.Tenant) (*Organization, error) {
	if tenantEntity == nil {
		return nil, nil
	}

	c := m.User(ctx, &usersvc.User{TenantID: tenantEntity.ID})

	organization := &Organization{
		ID:                        tenantEntity.ID,
		Name:                      tenantEntity.Name,
		Alias:                     tenantEntity.Alias,
		CreateAt:                  tenantEntity.CreatedAt,
		Email:                     tenantEntity.Email,
		Country:                   tenantEntity.Country,
		Status:                    string(tenantEntity.Status),
		Kind:                      string(tenantEntity.Kind),
		AccountManager:            "",
		ThirdPartyAccountProvider: transNullableStringToEmpty(tenantEntity.ThirdPartyAccountProvider),
		ThirdPartyAccountCode:     transNullableStringToEmpty(tenantEntity.ThirdPartyAccountCode),
		UserSetTimezone:           utils.ConvTimezone2NewTimezone(tenantEntity.TimeZone),
		ThirdPartyFreeTrial:       tenantEntity.ThirdPartyFreeTrial,
		OrgTag:                    tenantEntity.OrgTag,
	}

	if tenantEntity.TimeZone == "" {
		// if user not set time zone Timezone=0
		organization.Timezone = 0
	} else {
		oldTimezone := utils.ConvTimezone2OldTimezone(tenantEntity.TimeZone)
		timezone, err := strconv.Atoi(oldTimezone)
		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
	)

	// context reference
	billingHandler := billing2.GetHandler()
	planInfo, err := billingHandler.GetTenantPlanInfo(ctx, tenantEntity.ID.Uint64())
	if err != nil {
		c.Log().Error("failed get tenant plan info in get tenants info")
		return nil, err
	}

	discount = utils.DecimalToInt(planInfo.EffectiveDiscount)
	hasDiscount = discount < 100

	organization.Plan = planInfo.EffectivePlan
	// hasTriedDevTier = planInfo.DevTierPlanInfo.Status == "acitve"
	//
	//	err = db.Transaction(func(tx *gorm.DB) error {
	//		activeClustersCount, err = entity.NewClusterQuerySet(tx).StatusNotIn(
	//			entity.DeletedOrClearedClusterStatus...).TenantIDEq(tenantEntity.ID).Count()
	//		if err != nil {
	//			return errors.Annotate(err, "failed to query active cluster count")
	//		}
	//		freeClustersCount, err = entity.NewClusterQuerySet(tx).StatusNotIn(
	//			entity.DeletedOrClearedClusterStatus...).TenantIDEq(tenantEntity.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))
	//
	//		if tenantEntity.Country == "CN" {
	//			var acknowledgeCnPolicyEntity entity.AcknowledgeCnPolicy
	//			err = entity.NewAcknowledgeCnPolicyQuerySet(tx).TenantIDEq(tenantEntity.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")

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

	organization.RemainingCredit = utils.DecimalToInt(planInfo.POCCredits)
	organization.OrgType = string(tenantEntity.OrgType)
	organization.CompanyName = tenantEntity.CompanyName

	return organization, nil
}

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

	return c.getOrganizationByTenant(ctx, t)
}

func (c Context) GetOrganizationByCustomName(ctx context.Context, companyName string) (*Organization, error) {
	svc, _ := org3.GetOrgSvc()
	t, err := svc.GetOrgByCompanyName(ctx, companyName)
	if err != nil {
		return nil, err
	}
	if t == nil {
		return nil, errs.NewNotFound("company_name not found org", nil)
	}

	return c.getOrganizationByTenant(ctx, t)
}

func (c Context) ListOrganizations(ctx context.Context) ([]*Organization, error) {
	userID := c.tenantAndUser.ID

	orgDao := org.NewDao()
	crossDao := cross.NewDao()
	var rawOrgs []entity.Tenant = nil
	err := database.Transaction(func(tx *gorm.DB) error {
		var e error
		if c.tenantAndUser.IsImpersonated {
			impersonatedOrg, e := orgDao.GetOrgByOrgID(ctx, tx, c.tenantAndUser.TenantID.Uint64())
			if e != nil {
				return e
			}
			rawOrgs = append(rawOrgs, *impersonatedOrg)
		} else {
			rawOrgs, e = crossDao.ListOrgsByUserID(ctx, tx, userID.Uint64(), []string{string(entity.TenantStatusActive)})
			if e != nil {
				return e
			}
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	orgs := make([]*Organization, len(rawOrgs))
	for i, org := range rawOrgs {
		o, e := c.getOrganizationByTenant(ctx, &org)
		if e != nil {
			return nil, err
		}

		orgs[i] = o
	}

	return orgs, 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
		}
	}
	for _, id := range config.Global().ServerConfig.EnableNewClusterListUI {
		if oId == id {
			whitelistFlag = whitelistFlag | EnableNewClusterListUIFlag
		}
	}

	return whitelistFlag
}

func (c Context) UpdateOrganization(ctx context.Context, req *central.UpdateOrgReq) error {
	if c.tenantAndUser.Role != acl.OrgRole_ORG_ROLE_OWNER {
		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 = database.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 = req.UserSetTimezone
		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 {
				if zerr, ok := err.(zendesk.Error); ok && zerr.Status() == 422 {
					tilog.Info(ctx, "failed to update organization on Zendesk, no need to sync, ignore", zap.String("email", t.Email), zap.Error(err))
					return nil
				}
				tilog.Warn(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 {
					tilog.Error(ctx, "failed to update organization ZendeskID", zap.String("email", t.Email), zap.Error(err))
					return nil
				}
			}
		}
		return nil
	})

	if err == nil {
		_ = publish.PublishDataChangeEventWithEntData(context.Background(), publish.BizEventOrgChanged, nil, nil, t, []entity.RoleBinding{})
	}

	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 := database.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
	RbacRole  string
}

// FromDB creates a User model from the Database
func fromDB(u entity.User, roleBinding entity.RoleBinding) *User {
	var name string
	if u.FirstName == "" && u.LastName == "" {
		name = ""
	} else {
		name = fmt.Sprintf("%s %s", u.FirstName, u.LastName)
	}
	var status = entity.UserStatusPending
	if roleBinding.Status == entity.RoleBindingStatusActive {
		status = entity.UserStatusActive
	}

	return &User{
		ID:        uint64(u.ID),
		Name:      name,
		Email:     u.Email,
		Status:    strings.ToUpper(string(status)),
		InvitedAt: &roleBinding.CreatedAt,
		Role:      string(entity.RoleBindingDecode(roleBinding.Role)),
		RbacRole:  roleBinding.Role,
	}
}

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

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

	err := database.Transaction(func(tx *gorm.DB) error {
		var err error
		//	SELECT * FROM `role_bindings` WHERE (scope_type = 'ORG' AND scope_id = '1' AND target_type = 'USER') AND status IN ('active','inactive')
		users, err = usersvc.ListUsersByOrg(tx, req.TenantID, []string{entity.RoleBindingStatusActive, entity.RoleBindingStatusInactive}, filter, 0, 0)
		return err
	})
	if err != nil {
		return nil, errcode.NewInternalErr(errors.New("failed to list organization users"))
	}

	return convertUsers(ctx, users, req.TenantID.Uint64())
}

func convertUsers(ctx context.Context, users []entity.User, orgID uint64) ([]*User, error) {
	logger := tilog.WithContextL(ctx)
	roleDao := role.NewDao()

	var modelUsers []*User
	err := database.Transaction(func(tx *gorm.DB) error {
		for _, user := range users {
			// Ignore system user
			if user.FirstName == "system" && user.LastName == "user" {
				continue
			}

			// FIXME it's not a good behavior to read database in a for-loop, fix it if it cause performance problem.
			roleBindings, err := roleDao.ListRolesByTargetAndScope(
				ctx, tx, entity.RoleBindingTargetUser, user.ID.Uint64(), entity.RoleBindingScopeOrg, orgID, nil,
			)
			if err != nil {
				return err
			}
			if len(roleBindings) != 1 {
				// FIXME(letian) you may need to change this when introduce RBAC
				userIDList := make([]uint64, len(users))
				for i, user := range users {
					userIDList[i] = user.ID.Uint64()
				}
				logger.Error(
					"currently one user should not have zero or multiple role in same org",
					zap.Any("roleBindings", roleBindings), zap.Uint64("userID", user.ID.Uint64()),
					zap.Uint64("orgID", orgID), zap.Uint64s("userIDs", userIDList),
				)
				sentry.CaptureMessage(fmt.Sprintf("currently one user should not have zero or multiple role in same org: %d, %d, %+v", user.ID.Uint64(), orgID, roleBindings))
				return errors.New("bad relationship in user and org")
			}

			roleBinding := roleBindings[0]

			if roleBinding.Status == entity.RoleBindingStatusDelete {
				continue
			}

			modelUsers = append(modelUsers, fromDB(user, roleBinding))
		}

		return nil
	})
	if err != nil {
		logger.Error("failed to convert users", zap.Error(err))
		return nil, err
	}

	return modelUsers, nil
}

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

// UpdateUser updates the role of a user in an organization
func (c Context) UpdateUser(ctx context.Context, req UpdateUserReq) error {
	daoHub := dao.NewDaoHub()

	err := database.Transaction(func(tx *gorm.DB) error {
		user, err := usersvc.GetUserByID(tx, 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"))
		}

		// Skip if user role already exists
		roleExists, err := rbac.HasActiveRole(ctx, tx, entity.RoleBindingTargetUser, user.ID.Uint64(), entity.RoleBindingScopeOrg, req.OrgID.Uint64(), req.RbacRole)
		if err != nil {
			c.Log().Error("failed to query user roles", zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to query user roles"))
		}
		if roleExists {
			return nil
		}

		isOwner, err := rbac.HasActiveRole(ctx, tx, entity.RoleBindingTargetUser, user.ID.Uint64(), entity.RoleBindingScopeOrg, req.OrgID.Uint64(), entity.RoleOrgOwner)
		if err != nil {
			c.Log().Error("failed to query user roles", zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to query user roles"))
		}
		if isOwner {
			roleBindings, err := daoHub.Role.ListRolesByScope(ctx, tx, entity.RoleBindingScopeOrg, req.OrgID.Uint64(), []string{entity.RoleBindingStatusActive})
			if err != nil {
				c.Log().Error("failed to query owner count", zap.Error(err))
				return errcode.NewInternalErr(errors.New("failed to query owner count"))
			}

			var activeOwnerCount uint64
			for _, roleBinding := range roleBindings {
				if roleBinding.TargetType == entity.RoleBindingTargetUser && roleBinding.Role == entity.RoleOrgOwner {
					activeOwnerCount += 1
				}
			}

			if activeOwnerCount == 1 {
				userFilter := usersvc.UserFilter{
					Roles:    []entity.UserRole{entity.UserRoleOwner},
					Statuses: []entity.UserStatus{entity.UserStatusActive},
				}
				activeOwners, err := usersvc.ListUsersByOrg(tx, req.OrgID, []string{entity.RoleBindingStatusActive}, userFilter, 0, 0)
				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))
				}
			}
		}

		//if req.Role == entity.UserRoleOwner {
		//	if err = addAllActiveProjectsInOrgIDToUser(ctx, tx, req.OrgId, user.ID); err != nil {
		//		c.Log().Error("failed to update user projects", zap.String("email", user.Email), zap.Error(err))
		//		return errcode.NewInternalErr(errors.New("failed to update user projects"))
		//	}
		//}

		userProjects, err := daoHub.Cross.ListActiveProjectsByUserIDAndOrgID(ctx, tx, user.ID.Uint64(), req.OrgID.Uint64())
		if err != nil {
			c.Log().Error("failed to query user projects", zap.String("email", user.Email), zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to query user projects"))
		}

		_, err = usersvc.UpsertUser(ctx, tx, usersvc.CreateUserRequired{
			Email:     user.Email,
			FirstName: user.FirstName,
			LastName:  user.LastName,
			InviteAt:  user.InvitedAt,
			Status:    user.Status,
			TenantID:  req.OrgID,
			Role:      entity.RoleBindingDecode(req.RbacRole),
			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) RemoveUserFromOrg(ctx context.Context, orgID, userID entity.ID) error {

	if c.tenantAndUser.ID == userID {
		return errcode.NewInvalidInputErr(errors.New("can not remove yourself"))
	}
	operator, err := usersvc.GetUserFromContext(ctx)
	if err != nil {
		tilog.Info(ctx, "failed to get user from context", zap.Error(err))
		return err
	}

	if operator.ID.Uint64() == userID.Uint64() {
		return errcode.NewInvalidInputErr(errors.New("user cannot delete himself"))
	}
	//shouldDeleteUser := false
	//userEmail := "user.Email"
	deleteEmail, shouldDeleteUser, err := usersvc.DeleteUserFromOrg(ctx, orgID.Uint64(), userID.Uint64(), operator.ID.Uint64())
	if err != nil {
		return err
	}
	if shouldDeleteUser && deleteEmail != "" {
		users, err := c.management.ListByEmail(deleteEmail)
		if err != nil {
			tilog.Error(ctx, "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", deleteEmail), 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(ctx, token, true)
	if err != nil {
		tilog.Info(ctx, "failed to validate invitation token", zap.String("feature", "invitation"))
		return errcode.NewInvalidInputErr(errors.New("failed to validate invitation token, " + err.Error()))
	}

	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) {
		tilog.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"))
	}
	tilog.Info(ctx, "create user in auth0", zap.String("feature", "invitation"), zap.String("email", email))

	return nil
}

func updateUserOrgRelationToActive(ctx context.Context, tx *gorm.DB, userID uint64, orgID uint64) (string, error) {

	var orgRole = ""
	// update relationship with user+org to active
	daoHub := dao.NewDaoHub()
	roleBindings, err := daoHub.Role.ListRolesByTargetAndScope(ctx, tx, entity.RoleBindingTargetUser, userID, entity.RoleBindingScopeOrg, orgID, []string{entity.RoleBindingStatusInactive})
	if err != nil {
		return "", err
	}
	tilog.Info(ctx, "trying to update user org relation to active", zap.Uint64("ortID", orgID), zap.Uint64("userID", userID), zap.Any("roleBindings", roleBindings))
	if len(roleBindings) > 0 {
		if len(roleBindings) > 1 {
			tilog.Error(ctx, "Too many inactive role bindings on org and user", zap.Uint64("ortID", orgID), zap.Uint64("userID", userID), zap.Any("roleBindings", roleBindings))
		}

		// we will only update the first one...
		roleBinding := roleBindings[0]
		err = daoHub.Role.UpdateRoleStatusByID(ctx, tx, roleBinding.ID.Uint64(), entity.RoleBindingStatusActive)
		if err != nil {
			return "", err
		}
		orgRole = roleBinding.Role
	}

	return orgRole, nil
}

func updateUserProjectRelationToActive(ctx context.Context, tx *gorm.DB, userID uint64, orgID uint64) ([]uint64, error) {

	daoHub := dao.NewDaoHub()

	// get all projects in current org
	projects, err := daoHub.Project.ListActiveProjectsByOrgID(ctx, tx, orgID)
	if err != nil {
		return nil, err
	}

	projectIDs := make([]uint64, len(projects))
	for i, project := range projects {
		projectIDs[i] = project.ID.Uint64()
	}

	roles, err := daoHub.Role.ListUserRolesByProjects(ctx, tx, userID, projectIDs, []string{entity.RoleBindingStatusInactive})
	if err != nil {
		return nil, err
	}

	if len(roles) == 0 {
		tilog.Info(ctx, "no inactive roles need to update", zap.Uint64("userID", userID), zap.Uint64("orgID", orgID))
		return nil, nil
	}

	for _, role := range roles {
		err = daoHub.Role.UpdateRoleStatusByID(ctx, tx, role.ID.Uint64(), entity.RoleBindingStatusActive)
		if err != nil {
			return nil, err
		}
	}

	return projectIDs, nil
}

func (c Context) AcceptInvitation(ctx context.Context, tenantID entity.ID, email, firstName, lastName string) (entity.UserInviteToken, entity.User, error) {

	logger := tilog.WithContextL(ctx)
	var err error
	var entityUser entity.User

	var inviteToken entity.UserInviteToken
	var projectIds []uint64
	var orgRole string
	isUpdateUser := false

	entityUser, err = usersvc.GetUserByEmail(database.DBConn.DB, email)
	if err != nil {
		return inviteToken, entityUser, errcode.NewInternalErr(errors.New("failed to get user by email"))
	}
	cBindings, err := comsvc.ListIdentRoleBindings(ctx, entity.RoleBindingTargetUser, entityUser.ID.Uint64(), tenantID.Uint64(), []entity.ScopeType{entity.RoleBindingScopeTiDB}, []string{entity.RoleBindingStatusInactive})
	if err != nil {
		return inviteToken, entityUser, errcode.NewInternalErr(errors.New("failed to get cluster role bindings"))
	}

	err = database.Transaction(func(tx *gorm.DB) error {

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

		if entityUser.Status != entity.UserStatusActive {
			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 {
				logger.Error("failed to update user", zap.Error(err))
				return errcode.NewInternalErr(errors.New("failed to update user"))
			}
			isUpdateUser = true
		}

		if orgRole, err = updateUserOrgRelationToActive(ctx, tx, entityUser.ID.Uint64(), tenantID.Uint64()); err != nil {
			logger.Error("failed to update user org relation to active", zap.Error(err))
			return errcode.NewInternalErr(err)
		}

		if projectIds, err = updateUserProjectRelationToActive(ctx, tx, entityUser.ID.Uint64(), tenantID.Uint64()); err != nil {
			logger.Error("failed to update user project relation to active", zap.Error(err))
			return errcode.NewInternalErr(err)
		}

		// Temporary verification, later changed to batch modification
		for _, rb := range cBindings {
			err = dao.GetDaoHub().Role.UpdateRoleStatusByID(ctx, tx, rb.ID.Uint64(), entity.RoleBindingStatusActive)
			if err != nil {
				logger.Error("failed to update cluster role binding to active", zap.Error(err))
				return errcode.NewInternalErr(err)
			}
		}

		return nil
	})
	if err != nil {
		return inviteToken, entityUser, err
	}

	// send account data change event
	go func() {
		_ = comsvc.SendUserRoleBindingChangeEvent(context.Background(), entityUser.ID.Uint64(), tenantID.Uint64(), "AcceptInvitation")
	}()

	jsmClient := jsm.GetJsmClientSingleton()
	if jsmClient.IsEnable() {
		var createJsmAccountErr error
		if isUpdateUser {
			_, createJsmAccountErr = jsmClient.CreateJsmAccount(ctx, email)
		}
		notifyCreateUserErr := jsmClient.NotifyCreateUserMessage(ctx, &entityUser)
		notifyInviteUserToOrgErr := jsmClient.NotifyInviteUserToOrgMessage(ctx, tenantID.Uint64(), entityUser.ID.Uint64(), entity.RoleOrgOwner)
		if createJsmAccountErr != nil || notifyCreateUserErr != nil || notifyInviteUserToOrgErr != nil {
			tilog.Error(ctx, "Sync Jsm failed in inviting user to org", zap.Uint64("user", entityUser.ID.Uint64()), zap.Uint64("org", tenantID.Uint64()))
		}
		for _, projectId := range projectIds {
			notifyInviteUserToProjectErr := jsmClient.NotifyInviteUserToProjectMessage(ctx, projectId, entityUser.ID.Uint64())
			if notifyInviteUserToProjectErr != nil {
				tilog.Error(ctx, "Sync Jsm failed in inviting user to project", zap.Uint64("user", entityUser.ID.Uint64()), zap.Uint64("projectId", projectId))
			}
		}
	}

	if orgRole != "" {
		go func() {
			_ = userevent.PublishAddUserToOrgEvent(ctx, tenantID.Uint64(), entityUser.ID.Uint64(), orgRole)
		}()
	}

	return inviteToken, entityUser, err
}

type UpdateUserProfileReq struct {
	Email       string
	FirstName   string
	LastName    string
	Company     string
	Country     string
	PhoneNumber string
}

func (c Context) GetUpdateUserProfile(ctx context.Context, orgID entity.ID) (*central.UserProfile, error) {
	user := c.tenantAndUser

	daoHub := dao.NewDaoHub()

	tenant, err := c.GetTenant()

	if err != nil {
		c.Log().Error("failed to query tenant", zap.Error(err))
		return nil, err
	}

	var projects []entity.Project
	err = database.Transaction(func(tx *gorm.DB) error {
		if c.tenantAndUser.IsImpersonated {
			projects, err = project.GetActiveProjects(ctx, tx, orgID)
		} else {
			projects, err = daoHub.Cross.ListActiveProjectsByUserIDAndOrgID(ctx, tx, user.ID.Uint64(), orgID.Uint64())
		}
		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))
	}

	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,
		PhoneNumber: user.PhoneNumber, // add by wangletian 2022-26-09
		Company:     tenant.Name,
		Country:     tenant.Country,
	}
	return userProfile, nil
}

func (c Context) UpdateUserProfile(tenantID entity.ID, req UpdateUserProfileReq) error {

	var syncUser *entity.User
	err := database.Transaction(func(tx *gorm.DB) error {
		user, err := usersvc.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
		user.PhoneNumber = req.PhoneNumber
		err = (&user).Update(tx, entity.UserDBSchema.FirstName, entity.UserDBSchema.LastName,
			entity.UserDBSchema.PhoneNumber)

		if err != nil {
			c.Log().Error("failed to update user profile", zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to update user profile"))
		}
		syncUser = &user

		return nil
	})

	if err == nil {
		_ = publish.PublishDataChangeEventWithEntData(context.Background(), publish.BizEventUserChanged, syncUser, nil, nil, []entity.RoleBinding{})
	}

	return err
}

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

// 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, tenantID uint64) error {
	var (
		user         entity.User
		tenantEntity entity.Tenant
		err          error
	)
	err = database.Transaction(func(tx *gorm.DB) error {
		user, err = usersvc.GetUserByEmail(tx, email)
		if err != nil {
			tilog.Error(ctx, "failed to get user by email", zap.String("email", email), zap.Error(err))
			return err
		}

		tenantEntity, err = org3.GetTenantByID(tx, entity.ID(tenantID))
		if err != nil {
			tilog.Error(ctx, "failed to get tenant by ID", zap.Uint64("id", tenantID), zap.Error(err))
			return err
		}
		return nil
	})
	if err != nil {
		return err
	}

	// delete
	if (tenantEntity.OrgTag == string(entity.OrgTagMspCustomer) || tenantEntity.OrgTag == string(entity.OrgTagResaleCustomer) || tenantEntity.OrgTag == string(entity.OrgTagAlicloudCustomer)) && tenantEntity.Status == entity.TenantStatusActive {
		// if msp or resale customer org save msp and customer relationship
		mspSvc, _ := msp.GetMspSvc()
		mspId, _ := strconv.ParseUint(*tenantEntity.ThirdPartyAccountCode, 10, 64)

		nowTime := time.Now()
		var partnerType entity.PartnerType
		switch tenantEntity.OrgTag {
		case string(entity.OrgTagMspCustomer):
			partnerType = entity.PartnerTypeMsp
		case string(entity.OrgTagResaleCustomer):
			partnerType = entity.PartnerTypeResale
		case string(entity.OrgTagAlicloudCustomer):
			partnerType = entity.PartnerTypeAlicloud
			mspId = config.Global().BillingConfig.AliCloudManagerOrgID
		}
		mspCustomer := &entity.PartnerCustomer{
			PartnerType:   string(partnerType),
			PartnerOrgID:  entity.ID(mspId),
			CustomerOrgID: tenantEntity.ID,
			Status:        entity.Active,
			EffectedAt:    &nowTime}

		err = mspSvc.CreateCustomer(ctx, mspCustomer, false)
		if err != nil {
			tilog.Error(ctx, "CreateCustomer err when partner customer  register", zap.Any("mspOrgId", mspId), zap.Any("customer org email", tenantEntity.Email), zap.Error(err))
			return errs.NewInternalDB("failed to register tenant in db", err)
		}
		tilog.Info(ctx, "msp customer org save partner and customer relationship", zap.Any("mspCustomer", mspCustomer))
	}

	err = eventbus.Get().PublishEvent("tenant_registered", &eventbus.Event{
		EventID:   fmt.Sprint(tenantID),
		EventType: "tenant_registered",
		EventData: map[string]interface{}{
			"tenant_id": tenantID,
		},
	})
	if err != nil {
		return err
	}
	if err := publish.PublishOrgEvent(ctx, dto.OrgEventData{OrgId: tenantEntity.ID.Uint64(), OrgEventType: entity.EntityEventTypeActivate}); err != nil {
		tilog.Error(ctx, "PublishOrgEvent err", zap.Any("TenantID", tenantEntity.ID), zap.Error(err))
	}

	c = c.Model.User(ctx, &usersvc.User{User: user, TenantID: entity.ID(tenantID), IsImpersonated: false}) // nolint
	jobs.SaveSalesforceJobData(ctx, tenantEntity.ID.Uint64())
	//jobs.SaveZendeskJobData(ctx, tenantEntity.ID.Uint64())
	return nil

	//
	//salesforceData := config.SyncSalesforceSchema{TenantID: tenantEntity.ID.Uint64()}
	//data, err := json.Marshal(salesforceData)
	//if err != nil {
	//	tilog.Error(ctx, "failed to marshal salesforce data", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
	//	return err
	//}
	//// NOTE: we call this function directly temporarily, because self-hosted PoC apply does not ready yet.
	//if err = jobs.SyncSalesforce(ctx, data); err != nil {
	//	tilog.Error(ctx, "failed to sync salesforce", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
	//	return err
	//}
	// err = jobqueue.Enqueue(config.JobSyncSalesforce, salesforceData)
	// if err != nil {
	// c.Log().Error("failed to enqueue sync salesforce job", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
	// }
	// why not set org_id ???
	//
	/*
			delete welcome email ,the welcome email has been replaced by hubspot
			welcomeEmailData := config.SendWelcomeEmailSchema{
					Email:    email,
					UserName: user.FirstName,
					URL:      fmt.Sprintf("https://%s", config.Global().ServerConfig.FrontendDNS),
				}
				err = jobqueue.Enqueue(config.JobSendWelcomeEmail, welcomeEmailData)
				if err != nil {
					tilog.Error(ctx, "failed to send welcome email", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
				}

		zendeskData := config.SyncZendeskDataSchema{
			TenantID: tenantEntity.ID.Uint64(),
		}
		if tenantEntity.Name != "" {
			err = jobqueue.Enqueue(config.JobSyncZendesk, zendeskData)
			if err != nil {
				c.Log().Warn("failed to sync zendesk", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
			}
		}
	*/

}

func (c Context) RegistrationVerification(ctx context.Context, token, clientIP string) (uint64, *string, *bool, error) {
	tilog.Info(ctx, "RegistrationVerification in email，validate email url token start")
	channel := false
	userInviteToken, err := c.session.ValidateUserInviteToken(ctx, token, true)
	if err != nil {
		if c.session.IsActivatedError(err) {
			tilog.Info(ctx, "user was activated before, ignore the request")
		}
		tilog.Info(ctx, "failed to validate registration token", zap.String("feature", "signup"), zap.Error(err))
		return 0, nil, nil, err
	}
	email := userInviteToken.Email
	orgID := userInviteToken.TenantID.Uint64()

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

	var userWasActivated = false
	var user entity.User
	err = database.Transaction(func(tx *gorm.DB) error {
		user, err = usersvc.GetUserByEmail(tx, email)
		if err != nil {
			return err
		}
		if user.Status == entity.UserStatusActive {
			tilog.Info(ctx, "user.Status is active  ", zap.Any("email", email))
			userWasActivated = true
			return nil
		}

		err = usersvc.UpdateRegistration(ctx, tx, email, userInviteToken.TenantID)
		if err != nil {
			tilog.Error(ctx, "failed to update registration", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
			return err
		}
		return nil
	})
	if err != nil {
		tilog.Info(ctx, "failed to update registration", zap.String("email", err.Error()), zap.String("feature", "signup"), zap.String("email", email))
		return orgID, nil, nil, err
	}
	if userWasActivated {
		tilog.Info(ctx, "userWasActivated, return  ", zap.Any("email", email))
		return orgID, &email, &channel, nil
	}

	auditlog.NewEvent(ctx, auditlog.EventCreateOrg).SetOperatorType(auditlog.OperatorTypeUser).
		SetOperatorEmail(user.Email).SetOperatorID(user.ID.Uint64()).SetOperatorIP(clientIP).
		SetOrgID(userInviteToken.TenantID.Uint64()).SetResult(err == nil).SetDetails(auditlog.Details{auditlog.KeyError: err}.Build()).
		Submit() //nolint:errcheck

	_ = userevent.PublishAddUserToOrgEvent(ctx, userInviteToken.TenantID.Uint64(), user.ID.Uint64(), entity.RoleOrgOwner)

	err = c.TasksAfterRegistration(ctx, email, userInviteToken.TenantID.Uint64())
	if err != nil {
		tilog.Error(ctx, "failed to run tasks after registration", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
	}
	//// if the org is msp_customer ,save SaveMspUserServicesAgreement
	//org, err := getTenantByID(userInviteToken.TenantID.Uint64())
	//
	//if org.OrgTag == string(entity.OrgTagMspCustomer) {
	//	svc, _ := deps.GetMspSvc()
	//	err = svc.SaveMspUserServicesAgreement(ctx, user.ID.Uint64(), userInviteToken.TenantID.Uint64())
	//	tilog.Info(ctx, "SaveMspUserServicesAgreement after complete email register", zap.Any("err", err))
	//}

	if user.ID != 0 && user.Email != config.TestEmail {
		channel = IsChannel(ctx, user.ID.Uint64())
		pushEvent(ctx, user, channel, clientIP)
	}
	tilog.Info(ctx, "Registration in email success", zap.Any("user", user), zap.Any("register from channel", channel))
	return orgID, &email, &channel, nil
}

// wangletian01 update
func pushEvent(ctx context.Context, user entity.User, channel bool, clientIP string) {
	//testEmail := "test@pingcap.com"
	if user.Email != "" && user.Email != config.TestEmail && user.ID != 0 {
		userId := user.ID.Uint64()
		q := events.UserQuery{UserId: userId, Email: user.Email, ClientIP: clientIP}
		if err := events.PublishRegisterEvent(ctx, q, &channel); err != nil {
			tilog.Error(ctx, "PublishRegisterEvent in email register err", zap.Any("user", user.Email), zap.Error(err))
		}

	}
}

func IsChannel(ctx context.Context, userId uint64) bool {
	filter := userDao.LandingActionFilter{
		UserIDs:       []entity.ID{entity.ID(userId)},
		TargetActions: []entity.UserTargetAction{entity.RegisterChannelAction},
	}
	userSvc, err := usersvc.GetUserSvc()
	if err != nil {
		tilog.Error(ctx, "LoadUserSvc err ", zap.Error(err))
	}
	isExist, err := userSvc.IsExist(ctx, filter)
	if err != nil {
		tilog.Error(ctx, "IsExist fail ", zap.Any("filter", filter), zap.Error(err))
	}

	tilog.Info(ctx, "user is Register-Form-Channel result ", zap.Any("channel", isExist))
	channel := isExist

	/*	list, err := userSvc.ListUserLandingActions(ctx, filter)
		tilog.Info(ctx, "query UserLandingActions", zap.Any("result", list))
		if err != nil {
			tilog.Error(ctx, "ListUserLandingActions ", zap.Any("filter", filter))
		}
		channel := false
		if len(list) == 1 {
			channel = true
		}
	*/
	return *channel
}
