// Package tenant encapsulates all tenant functionality
package tenant

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

	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/metrics-server/central/config"
	"github.com/tidbcloud/metrics-server/central/db"
	"github.com/tidbcloud/metrics-server/central/db/dbutil"
	"github.com/tidbcloud/metrics-server/central/db/entity"
	"github.com/tidbcloud/metrics-server/central/db/tenant"
	"github.com/tidbcloud/metrics-server/central/metrics"
	centralModel "github.com/tidbcloud/metrics-server/central/model"
	"github.com/tidbcloud/metrics-server/central/model/session"
	"github.com/tidbcloud/metrics-server/idl/pbgen/proto/central"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"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"
	"k8s.io/utils/pointer"
)

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

// 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) Model {
	return Model{
		log:              log,
		session:          session,
		mail:             mail,
		management:       management,
		zendeskClient:    zendeskClient,
		salesforceClient: salesforceClient,
	}
}

// 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 emailAddressInUseErr() error {
	return errors.Errorf(`Email address is already in use, please <a href="https://%s" target="_blank">sign in</a>.`, config.Global().ServerConfig.FrontendDNS)
}

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

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

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 errcode.NewInternalErr(errors.New("failed to query user by email"))
		}
		if count > 0 {
			return errcode.NewInvalidInputErr(emailAddressInUseErr())
		}

		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 errcode.NewInternalErr(errors.New("failed to query tenant by email"))
		}
		if count > 0 {
			return errcode.NewInvalidInputErr(emailAddressInUseErr())
		}

		if config.Global().ServerConfig.EnableEmailProviderBlocklists {
			emailSlice := strings.Split(email, "@")
			if len(emailSlice) != 2 {
				return errcode.NewInvalidInputErr(errors.New("illegal email address"))
			}
			count, err = entity.NewEmailProviderBlocklistQuerySet(tx).EmailEq(emailSlice[1]).Count()
			if err != nil {
				log.Error(ctx, "failed to check email provider blocklist", zap.String("feature", "signup"), zap.Error(err))
				return errcode.NewInternalErr(errors.New("failed to check email provider blocklist"))
			}
			if count == 1 {
				return errcode.NewInvalidInputErr(errors.New("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 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 = ""
		acknowledgeCnPolicy bool   = false
		hasTriedDevTier     bool   = false
	)

	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")

	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
		}
	}
	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 {
				metrics.ZendeskCreateOrUpdateOrganizationFailed.WithLabelValues(fmt.Sprintf("%d", t.ID.Uint64())).Inc()
				log.Error(ctx, "failed to update organization name on Zendesk", zap.String("email", t.Email), zap.Error(err))
				return nil
			}
			metrics.ZendeskCreateOrUpdateOrganizationFailed.WithLabelValues(fmt.Sprintf("%d", t.ID.Uint64())).Set(0)
			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) 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.StringPtr(config.Global().Auth0Config.Auth0DatabaseConnectionName),
		Email:         pointer.StringPtr(email),
		Name:          pointer.StringPtr(email),
		Password:      pointer.StringPtr(password),
		EmailVerified: pointer.BoolPtr(true),
		VerifyEmail:   pointer.BoolPtr(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,
		})
	}

	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, user *entity.User) 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 {
			metrics.ZendeskCreateOrganizationFailed.WithLabelValues(fmt.Sprintf("%d", tenant.ID.Uint64())).Inc()
			c.Log().Error("failed to create organization on Zendesk", zap.String("name", tenant.Name), zap.String("feature", "signup"), zap.Error(err))
			return err
		}
		metrics.ZendeskCreateOrganizationFailed.WithLabelValues(fmt.Sprintf("%d", tenant.ID.Uint64())).Set(0)
		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().Error("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.BoolPtr(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) RegistrationVerification(ctx context.Context, token string) (*entity.User, error) {
	session := c.session
	userInviteToken, err := session.ValidateUserInviteToken(token, true)
	if err != nil {
		if 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")
	}

	var tenantEntity *entity.Tenant
	var user *entity.User
	err = db.Transaction(func(tx *gorm.DB) error {
		var err2 error
		tenantEntity, user, 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
	}

	c = c.Model.User(ctx, &tenant.User{User: *user, TenantID: user.TenantID, IsImpersonated: false})

	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, user)
	if err != nil {
		c.Log().Error("failed to syncZendesk", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
	}
	return user, nil
}
