// Package tenant encapsulates all tenant functionality
package tenant

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

	"github.com/patrickmn/go-cache"
	"github.com/pingcap/dbaas/billing/api/model"
	"github.com/pingcap/dbaas/pkg/auth/auth0"
	"github.com/pingcap/dbaas/pkg/mail"
	"github.com/pingcap/dbaas/pkg/salesforce"
	"github.com/pingcap/dbaas/pkg/zendesk"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"github.com/tidbcloud/central-sdk/config"
	"github.com/tidbcloud/central-sdk/db"
	"github.com/tidbcloud/central-sdk/db/dbutil"
	"github.com/tidbcloud/central-sdk/db/entity"
	"github.com/tidbcloud/central-sdk/db/tenant"
	"github.com/tidbcloud/central-sdk/metrics"
	centralModel "github.com/tidbcloud/central-sdk/model"
	"github.com/tidbcloud/central-sdk/model/billing"
	"github.com/tidbcloud/central-sdk/model/session"
	modelUtil "github.com/tidbcloud/central-sdk/model/util"
	central "github.com/tidbcloud/central-sdk/proto/output"
	"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             *mail.Mail
	billingClient    billing.Service
	management       *management.Management
	billingCache     *cache.Cache
	zendeskClient    *zendesk.Client
	salesforceClient *salesforce.Client
}

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

// NewModel returns a Model
func NewModel(log *zap.Logger, session *session.Session, mail *mail.Mail, management *management.Management, billingClient billing.Service, zendeskClient *zendesk.Client, salesforceClient *salesforce.Client) Model {
	return Model{
		log:              log,
		session:          session,
		mail:             mail,
		billingClient:    billingClient,
		management:       management,
		billingCache:     cache.New(1*time.Hour, 1*time.Minute),
		zendeskClient:    zendeskClient,
		salesforceClient: salesforceClient,
	}
}

// User returns a UserContext
func (m Model) User(user *tenant.User) Context {
	return Context{m, user}
}

// User returns a BillingClient
func (m Model) BillingClient() billing.Service {
	return m.billingClient
}

func (c Context) Log() *zap.Logger {
	return c.log.With(zap.Uint64("tenantID", c.tenantAndUser.TenantID.Uint64()), zap.Uint64("userID", uint64(c.tenantAndUser.ID)), zap.String("module", "tenant"))
}

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 already in use, please <a href="https://%s" target="_blank">sign in</a>`, config.Global().ServerConfig.FrontendDNS)
}

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

func validateRegistration(email string) error {
	return db.Transaction(func(tx *gorm.DB) error {
		freeTrialOrgsCount, err := entity.NewTenantQuerySet(tx).PlanIn(entity.TenantPlanNewUser, entity.TenantPlanFreeTrial).Count()
		if err != nil {
			log.Error("failed to get free trial orgs count", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to get free trial orgs count"))
		}
		if freeTrialOrgsCount > *config.Global().ServerConfig.MaxFreeTrialOrgs {
			log.Error("the maximum number of free trial users allowed by the service has been reached", zap.String("feature", "signup"), zap.String("email", email))
			return errcode.NewInvalidInputErr(errors.New("the maximum number of free trial users allowed by the service has been reached"))
		}
		count, err := entity.NewUserQuerySet(tx).EmailEq(email).DeletedAtIsNull().Count()
		if err != nil {
			log.Error("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("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())
		}

		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("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 (c Context) Registration(req central.RegistrationReq, clientIP string) error {
	email := req.Email
	company := req.Company
	err := validateRegistration(email)
	if err != nil {
		return err
	}

	u := management.User{
		Connection:    pointer.StringPtr(config.Global().Auth0Config.Auth0DatabaseConnectionName),
		Email:         pointer.StringPtr(email),
		Name:          pointer.StringPtr(email),
		Password:      pointer.StringPtr(req.Password),
		EmailVerified: pointer.BoolPtr(true),
		VerifyEmail:   pointer.BoolPtr(false),
	}

	err = c.management.User.Create(&u)
	if err != nil && !auth0.IsAuth0UserExistError(err) {
		log.Error("failed to create user in auth0", zap.String("feature", "signup"), 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"))
	}

	tenantEntity := &entity.Tenant{
		Name:          company,
		Country:       req.Country,
		Email:         email,
		TimeZone:      config.DefaultUtcOffset,
		Status:        entity.TenantStatusPending,
		Plan:          entity.TenantPlanNewUser,
		AcceptService: req.AcceptService,
	}

	userEnity := &entity.User{
		FirstName:   req.FirstName,
		LastName:    req.LastName,
		Email:       email,
		Status:      entity.UserStatusPending,
		Job:         req.Job,
		Role:        entity.UserRoleOwner,
		PhoneNumber: req.PhoneNumber,
	}

	registrationLogEntity := &entity.RegistrationLog{
		Name:      company,
		Email:     email,
		FirstName: req.FirstName,
		LastName:  req.LastName,
		Source:    config.RegistrationSourceEmail,
		IPAddress: clientIP,
		InvitedAt: time.Now(),
	}

	var token string
	err = db.Transaction(func(tx *gorm.DB) error {
		tenant, err := dbutil.RegisterTenant(tx, tenantEntity, userEnity, registrationLogEntity)
		if err != nil {
			log.Error("failed to register tenant in db", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to register user in db"))
		}

		token, err = c.session.InsertUserInviteToken(tx, tenant.ID, session.RegistrationVerificationMethod, email, 24*time.Hour, true)
		if err != nil {
			log.Error("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
	}
	url := fmt.Sprintf("https://%s/registration-verification?token=%s", modelUtil.GetCentralBackendDns(), token)
	err = c.mail.SendVerificationEmail(email, url, req.FirstName)
	if err != nil {
		log.Error("failed to send verification email", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
		return errcode.NewInternalErr(errors.New("failed to send verification email"))
	}
	log.Info("send verification email", zap.String("feature", "signup"), zap.String("email", email), zap.Time("time", time.Now().UTC()))
	return nil
}

func (c Context) SendVerificationEmail(email string) error {
	var token string
	var user entity.User
	err := db.Transaction(func(tx *gorm.DB) error {
		var err error
		user, err = dbutil.GetUserByEmail(tx, email)
		if err != nil {
			log.Error("failed to get user from db", zap.String("feature", "signup"), zap.String("email", email), zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to get user from db"))
		}
		if user.Status == entity.UserStatusActive {
			return errcode.NewInvalidInputErr(errors.Errorf(`Your account has been successfully activated. You can now <a href="https://%s" target="_blank">sign in</a>.`, config.Global().ServerConfig.FrontendDNS))
		}
		if user.Status == entity.UserStatusInactive {
			return errcode.NewInvalidInputErr(errors.New("your account is currently inactive"))
		}
		if user.InvitedAt != nil {
			return errcode.NewInvalidInputErr(errors.New(ErrUserInvited))
		}
		token, err = c.session.InsertUserInviteToken(tx, user.TenantID, session.RegistrationVerificationMethod, email, 24*time.Hour, true)
		if err != nil {
			log.Error("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
	}
	url := fmt.Sprintf("https://%s/registration-verification?token=%s", modelUtil.GetCentralBackendDns(), token)
	err = c.mail.SendVerificationEmail(email, url, user.FirstName)
	if err != nil {
		log.Error("failed to send verification email", zap.String("feature", "signup"), zap.Error(err))
		return errcode.NewInternalErr(errors.New("failed to send verification email"))
	}
	log.Info("send verification email", zap.String("feature", "signup"), zap.String("email", email), zap.Time("time", time.Now().UTC()))
	return nil
}

type Organization struct {
	ID                  entity.ID
	Name                string
	CreateAt            time.Time
	Email               string
	Country             string
	Timezone            int32
	Status              string
	Plan                string
	TotalCredit         int64
	RemainingCredit     int64
	ActiveClustersCount uint32
	HasDiscount         bool
	DiscountDescription string
	DiscountStartTime   int64
	DiscountEndTime     int64
	Discount            float64
	NodeUsageFee        int64
}

func (c Context) GetTenant() (*entity.Tenant, error) {
	var t entity.Tenant
	err := db.Transaction(func(tx *gorm.DB) error {
		return entity.NewTenantQuerySet(tx.Preload("Quotas")).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
	}
	return &t, 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,
		CreateAt: t.CreatedAt,
		Email:    t.Email,
		Country:  t.Country,
		Status:   string(t.Status),
		Plan:     string(t.Plan),
	}

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

	var count int64
	hasDiscount := false
	var discount = 1.0
	discountDescription := ""
	var startTime, endTime int64
	err = db.Transaction(func(tx *gorm.DB) error {
		var tenantDiscount []entity.Discount
		if err := entity.NewDiscountQuerySet(tx).TenantIDEq(t.ID).All(&tenantDiscount); err != nil {
			return err
		}
		var discounts []entity.GlobalDiscount
		err = entity.NewGlobalDiscountQuerySet(tx).All(&discounts)
		if err != nil {
			return errors.Annotate(err, "fetch global discount failed")
		}
		hasDiscount, discount, discountDescription, startTime, endTime = centralModel.MergeDiscount(time.Now(), discounts, tenantDiscount)
		count, err = entity.NewClusterQuerySet(tx).StatusNotIn(
			entity.DeletedOrClearedClusterStatus...).TenantIDEq(t.ID).Count()
		return err
	})
	if err != nil {
		c.Log().Error("failed to GetActiveClustersCount", zap.Error(err))
		return organization, errors.New("failed to GetActiveClustersCount")
	}
	organization.ActiveClustersCount = uint32(count)

	if t.Plan != entity.TenantPlanOnDemand {
		var totalAmount int64
		var trialUsage int64
		err := db.Transaction(func(tx *gorm.DB) error {
			var err error
			totalAmount, err = dbutil.GetTotalFreeTrialPoint(tx, t.ID)
			if err != nil {
				return err
			}
			trialUsage, err = centralModel.GetNodeUsageFromDB(tx, t.ID)
			if err != nil {
				c.Log().Error("failed to get node usage", zap.Error(err))
				return err
			}
			return err
		})
		if err != nil {
			c.Log().Error("failed to GetTrialCredit", zap.Error(err))
			return organization, errors.New("failed to GetTrialCredit")
		}
		organization.TotalCredit = totalAmount
		credit := totalAmount
		usage := trialUsage
		c.Log().Info("GetTrialCredit and GetNodeUsageFee", zap.Int64("credit", credit), zap.Int64("usage", usage))
		remainingCredit := credit - usage
		if remainingCredit < 0 {
			remainingCredit = 0
		}
		organization.NodeUsageFee = trialUsage
		organization.RemainingCredit = remainingCredit
	}
	organization.Discount = discount
	organization.DiscountDescription = discountDescription
	organization.HasDiscount = hasDiscount
	organization.DiscountStartTime = startTime
	organization.DiscountEndTime = endTime
	return organization, nil
}

func (c Context) UpdateOrganization(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("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("failed to update organization ZendeskID", zap.String("email", t.Email), zap.Error(err))
					return nil
				}
			}
		}
		return nil
	})

	return err
}

func (c Context) UpdateOrgDefaultPayment(defaultSource string) error {
	t, err := c.GetTenant()
	if err != nil {
		return err
	}

	cards, err := c.billingClient.ListPaymentMethods(fmt.Sprintf("%d", t.ID), billing.Pagination{Limit: 10})
	if err != nil {
		c.Log().Error("failed to list payment methods", zap.Error(err))
		return errcode.NewInternalErr(errors.New("failed to list payment methods"))
	}
	if len(cards.Items) == 0 {
		return errcode.NewInvalidInputErr(errors.New("please bind a credit card"))
	}

	if t.Plan != entity.TenantPlanOnDemand {
		t.Plan = entity.TenantPlanOnDemand
		now := time.Now()
		t.BindCreditCardAt = &now
		err := db.Transaction(func(tx *gorm.DB) error {
			err := t.Update(tx, entity.TenantDBSchema.Plan, entity.TenantDBSchema.BindCreditCardAt)
			if err != nil {
				c.Log().Error("failed to update tenant Plan to on_demand and BindCreditCardAt field", zap.Error(err))
				return errcode.NewInternalErr(errors.New("failed to update tenant Plan to on_demand and BindCreditCardAt field"))
			}

			var clusters []entity.Cluster
			err = entity.NewClusterQuerySet(tx).StatusNotIn(
				entity.ClusterStatusDeleted, entity.ClusterStatusCleared).TenantIDEq(t.ID).All(&clusters)
			if err != nil {
				c.Log().Error("failed to get clusters of organization", zap.Error(err))
				return errcode.NewInternalErr(errors.New("failed to get clusters of organization"))
			}

			for _, cluster := range clusters {
				_, err = dbutil.CreateBackupSchedule(tx, cluster.CreatedByID, cluster.ID)
				if err != nil {
					c.Log().Error("failed to create backup schedule for cluster", zap.Uint64("clusterID", uint64(cluster.ID)), zap.Error(err))
					return errcode.NewInternalErr(errors.New("failed to create backup schedule for cluster"))
				}
			}
			return nil
		})
		if err != nil {
			return err
		}

		salesforceConfig := config.Global().SalesforceConfig
		if salesforceConfig.EnableSalesforce {
			account := salesforce.Account{
				Plan:             string(entity.TenantPlanOnDemand),
				PortalID:         strconv.FormatUint(t.ID.Uint64(), 10),
				BindCreditCardAt: now.UTC().Format(salesforce.TimeFortmat),
				BillingEmail:     t.BillingEmail,
			}
			err = c.salesforceClient.UpdateAccount(account)
			if err != nil {
				metrics.SalesforceUpdateAccountFailed.WithLabelValues(fmt.Sprintf("%d", t.ID.Uint64())).Inc()
				c.Log().Error("failed to call salesforce UpdateAccount API", zap.String("email", t.Email), zap.Error(err))
			} else {
				c.Log().Info("successfully call salesforce UpdateAccount API", zap.String("email", t.Email), zap.Reflect("payload", account))
				metrics.SalesforceUpdateAccountFailed.WithLabelValues(fmt.Sprintf("%d", t.ID.Uint64())).Set(0)
			}
		}
	}

	payload := model.Customer{DefaultSource: defaultSource}
	_, err = c.billingClient.UpdateCustomer(fmt.Sprintf("%d", t.ID), &payload)
	if err != nil {
		c.Log().Error("failed to update default payment", zap.Error(err))
		return errcode.NewInternalErr(errors.New("failed to update default payment"))
	}
	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),
	}
}

func (c Context) ListUsers(email string, tenantID entity.ID) ([]*User, error) {
	err := c.CheckOrgID(tenantID)
	if err != nil {
		return nil, err
	}
	var users []entity.User
	if email != "" {
		err = db.Transaction(func(tx *gorm.DB) error {
			return entity.NewUserQuerySet(tx).DeletedAtIsNull().EmailEq(email).TenantIDEq(tenantID).All(&users)
		})
	} else {
		err = db.Transaction(func(tx *gorm.DB) error {
			return entity.NewUserQuerySet(tx).DeletedAtIsNull().TenantIDEq(tenantID).All(&users)
		})
	}
	if err != nil {
		return nil, errcode.NewInternalErr(errors.New("failed to list 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
}

func (c Context) UpdateUser(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 get user by email"))
		}

		if user.Role == req.Role {
			return nil
		}

		userProjects := user.Projects
		user.Role = req.Role
		if user.Role == entity.UserRoleOwner {
			var projects []entity.Project
			err := entity.NewProjectQuerySet(tx).TenantIDEq(req.OrgID).All(&projects)
			if err != nil {
				c.Log().Error("failed to query users of all project", zap.Error(err))
				return errors.New("failed to query users of all project")
			}
			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 errors.New("failed to upsert user in db")
		}
		return nil
	})

	if err != nil {
		return errcode.NewInternalErr(errors.New("failed to update user"))
	}
	return nil
}

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

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

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

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

func (c Context) CreateAuth0User(orgID entity.ID, token, password string) error {
	userInviteToken, err := c.session.ValidateUserInviteToken(token, true)
	if err != nil {
		log.Error("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.User.Create(&u)
	if err != nil && !auth0.IsAuth0UserExistError(err) {
		log.Error("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("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(tenantID entity.ID) (*central.UserProfile, error) {
	err := c.CheckOrgID(tenantID)
	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 {
			return entity.NewProjectQuerySet(tx).TenantIDEq(tenantID).All(&projects)
		})
		if err != nil {
			c.Log().Error("failed to query all projects of tenant", zap.Error(err))
			return nil, errcode.NewInternalErr(errors.Annotatef(err, "failed to query all projects of tenant %d", tenantID))
		}
	} 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)
}
