package tenantctx

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"time"

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

	"github.com/duke-git/lancet/v2/slice"

	projectsvc "github.com/tidbcloud/account/internel/svc/project"

	tokensvc "github.com/tidbcloud/account/internel/svc/token"

	"github.com/tidbcloud/account/internel/svc/dbuser"
	rbac2 "github.com/tidbcloud/account/internel/svc/rbac"
	usersvc "github.com/tidbcloud/account/internel/svc/user"

	"github.com/tidbcloud/account/commons/database"
	"github.com/tidbcloud/account/internel/dao/accesslog"
	projectdao "github.com/tidbcloud/account/internel/dao/project"
	"github.com/tidbcloud/tiinf/errs"

	"github.com/tidbcloud/account/central/utils"

	"github.com/tidbcloud/tiinf/sdk/auditlog"

	"github.com/google/uuid"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/account/commons/config"
	accountpb "github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/account/idl/pbgen/proto/central"
	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/account/internel/dao/role"
	"github.com/tidbcloud/account/thirdparty/dataflow"
	"github.com/tidbcloud/account/thirdparty/infra"
	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/timestamppb"
	"gorm.io/gorm"
)

const (
	ErrDeleteOwnerUser = "Please change the role to member before moving out of the project."

	ErrFailedEmailListTemplate = "<p>Failed to invite user %s, please try again later or request support.</p>"

	ErrInOtherOrgEmailListTemplate = "<p>The email %s is already in another organization. Each user can only be in one organization.</p>"

	ErrRepeatedInviteEmailListTemplate = "<p>The user %s is already associated with the current %s.</p>"
)

const systemFirstName = "system"
const userLastName = "user"

var (
	errFailedInviteEmail = errors.New("failed add email")
)

func ConvertProjectEntityToModel(ctx context.Context, project *entity.Project, downgradeClusterCount bool) (*central.Project, error) {
	daoHub := dao.NewDaoHub()
	userProject := &central.Project{
		Id:   project.ID.Uint64(),
		Name: project.Name,
		Type: project.Type,
	}

	userProject.CreatedAt = timestamppb.New(project.CreatedAt)

	var activeClustersCount int64
	var usersCount int64
	var projectUsers []entity.User
	//var orgOwnerUsers []entity.User
	err := database.Transaction(func(tx *gorm.DB) error {
		err := entity.NewProjectQuerySet(tx.Preload("Users")).IDEq(project.ID).One(project)
		if err != nil {
			return err
		}
		projectUsers, err = daoHub.Cross.ListUsersByProjectID(ctx, tx, project.ID.Uint64(), []string{entity.RoleBindingStatusActive, entity.RoleBindingStatusInactive})
		if err != nil {
			return err
		}

		//orgOwnerUsers, err = daoHub.Cross.ListUsersByOrgIDAndRole(ctx, tx, project.TenantID.Uint64(), entity.RoleOrgOwner, []string{entity.RoleBindingStatusActive, entity.RoleBindingStatusInactive})
		//if err != nil {
		//	return err
		//}
		//query cm
		//activeClustersCount, err = entity.NewClusterQuerySet(tx).StatusNotIn(
		//	entity.DeletedOrClearedClusterStatus...).ProjectIDEq(project.ID).Count()

		activeClustersCount, err = infra.GetHandler().CountClustersByProject(ctx, []uint64{project.ID.Uint64()})
		if err != nil && downgradeClusterCount {
			tilog.Warn(ctx, "failed to query active clusters count and downgrade handled", zap.Any("projectId", project), zap.Error(err))
			err = nil
			activeClustersCount = -1
		}
		return err
	})
	if err != nil {
		tilog.Info(ctx, "failed to query active clusters count and users of project", zap.Any("projectId", project), zap.Error(err))
		return nil, errors.New("failed to query active clusters count and users of project")
	}

	userProject.ActiveClustersCount = int32(activeClustersCount)
	//orgOwnerUserIds := map[uint64]struct{}{}
	//for _, orgOwnerUser := range orgOwnerUsers {
	//	if orgOwnerUser.FirstName == systemFirstName && orgOwnerUser.LastName == userLastName {
	//		continue
	//	}
	//	if orgOwnerUser.Status == entity.UserStatusActive || orgOwnerUser.Status == entity.UserStatusPending {
	//		orgOwnerUserIds[orgOwnerUser.ID.Uint64()] = struct{}{}
	//		usersCount++
	//	}
	//}
	for _, user := range projectUsers {
		// Ignore system user
		if user.FirstName == systemFirstName && user.LastName == userLastName {
			continue
		}
		//if _, ok := orgOwnerUserIds[user.ID.Uint64()]; ok {
		//	continue
		//}
		if user.Status == entity.UserStatusActive || user.Status == entity.UserStatusPending {
			usersCount++
		}
	}
	userProject.UsersCount = uint32(usersCount)
	userProject.AwsCmekEnabled = project.AwsCmekEnabled

	return userProject, nil
}

type Project struct {
	*central.Project

	AwsCmekEnabled bool
}

func ListProjectsByOrg(ctx context.Context, orgID entity.ID, downgradeClusterCount bool) ([]*Project, error) {
	var projects []entity.Project
	var userProjects []*Project
	var err error

	svc, _ := projectsvc.GetProjectSvc()
	projects, err = svc.ListActiveProjectsByOrgID(ctx, orgID.Uint64())
	if err != nil {
		tilog.Error(ctx, "failed to list projects", zap.Any("org_id", orgID), zap.Error(err))
		return nil, err
	}

	for _, project := range projects {
		userProject, err := ConvertProjectEntityToModel(ctx, &project, downgradeClusterCount)
		if err != nil {
			return nil, err
		}

		projectEntity := Project{
			userProject,
			project.AwsCmekEnabled,
		}
		userProjects = append(userProjects, &projectEntity)
	}

	return userProjects, nil
}

func (c Context) ListProjects(ctx context.Context, orgID entity.ID, types []string) ([]*central.Project, error) {
	svc, _ := projectsvc.GetProjectSvc()

	var projects []entity.Project
	var err error
	if c.tenantAndUser.IsImpersonated {
		projects, err = svc.ListActiveProjectsByOrgID(ctx, orgID.Uint64())
	} else {
		projects, err = svc.ListUserVisibleProjects(ctx, c.tenantAndUser.ID.Uint64(), []uint64{orgID.Uint64()})
	}
	if err != nil {
		tilog.Error(ctx, "failed to list projects", zap.Any("org_id", orgID), zap.Any("is_impersonated", c.tenantAndUser.IsImpersonated), zap.Uint64("user_id", c.tenantAndUser.ID.Uint64()), zap.Error(err))
		return nil, errcode.NewInternalErr(err)
	}

	var userProjects []*central.Project
	for _, project := range projects {
		// if types is not empty, go type filter
		if len(types) > 0 && !slice.Contain(types, project.Type) {
			continue
		}
		userProject, err := ConvertProjectEntityToModel(ctx, &project, true)
		if err != nil {
			tilog.Error(ctx, "failed to convert project to model", zap.Any("project", project), zap.Error(err))
			return nil, errcode.NewInternalErr(err)
		}
		userProjects = append(userProjects, userProject)
	}
	return userProjects, nil
}

type CreateProjectReq struct {
	OrgID                entity.ID
	ProjectName          string
	EnableXAccountDeploy bool
	AwsCmekEnabled       bool
	OperatorID           uint64
	Type                 string
}

const (
	ProjectNameUniqueErrMsg = "The project name already exists."
)

func (c Context) CreateProject(ctx context.Context, req CreateProjectReq) (*central.ProjectId, error) {
	createProjectReq := projectdao.CreateProjectReq{
		OrgId:          req.OrgID.Uint64(),
		Name:           req.ProjectName,
		AwsCmekEnabled: req.AwsCmekEnabled,
		IsCrossAccount: req.EnableXAccountDeploy,
		Type:           req.Type,
		OperatorType:   entity.RoleBindingTargetUser.String(),
		OperatorID:     c.tenantAndUser.ID.Uint64(),
	}
	svc, _ := projectsvc.GetProjectSvc()
	project, err := svc.CreateProject(ctx, createProjectReq)
	if err != nil {
		return nil, err
	}
	return &central.ProjectId{
		Id: project.ID.Uint64(),
	}, nil
}

func CreateProjectForOpenAPI(ctx context.Context, req CreateProjectReq) (uint64, error) {
	_, err := getTenantByID(req.OrgID.Uint64())
	if err != nil {
		return 0, err
	}

	// check whether origin logic not set operator id
	createProjectReq := projectdao.CreateProjectReq{
		Name:           req.ProjectName,
		OrgId:          req.OrgID.Uint64(),
		AwsCmekEnabled: req.AwsCmekEnabled,
		IsCrossAccount: req.EnableXAccountDeploy,
		Type:           entity.ProjectTypeDedicated,
	}
	svc, _ := projectsvc.GetProjectSvc()
	project, err := svc.CreateProject(ctx, createProjectReq)
	if err != nil {
		return 0, err
	}
	return project.ID.Uint64(), nil
}

type UpdateProjectReq struct {
	OrgID       entity.ID
	ProjectID   entity.ID
	ProjectName string
}

func (c Context) UpdateProject(ctx context.Context, req UpdateProjectReq) error {
	svc, _ := projectsvc.GetProjectSvc()
	return svc.UpdateProjectName(ctx, req.ProjectID.Uint64(), req.ProjectName)
}

type ListProjectUsersReq struct {
	OrgID     entity.ID
	ProjectID entity.ID
	Email     *string
}

func (c Context) ListProjectUsers(ctx context.Context, req ListProjectUsersReq) ([]*User, error) {
	projectID := req.ProjectID
	filter := usersvc.ProjectUserFilter{
		ProjectID: projectID,
		Statuses:  []entity.UserStatus{entity.UserStatusActive, entity.UserStatusPending},
		Email:     req.Email,
	}

	var orgOwnerUsers []entity.User
	var users []entity.User
	var project *entity.Project
	err := database.Transaction(func(tx *gorm.DB) error {
		var err error
		users, err = usersvc.ListProjectUsers(ctx, tx, filter, nil)
		if err != nil {
			return err
		}

		email := ""
		if req.Email != nil {
			email = *req.Email
		}
		orgOwnerUsers, err = usersvc.ListOrgOwnerUsers(ctx, tx, req.OrgID, email, []entity.UserStatus{entity.UserStatusActive, entity.UserStatusPending})
		if err != nil {
			return errcode.NewInternalErr(errors.New("failed to list org owner users"))
		}

		project, err = projectdao.GetProject(ctx, tx, projectID)
		return err
	})
	if err != nil {
		c.Log().Error("failed to list users of project", zap.Uint64("projectID", projectID.Uint64()), zap.Error(err))
		return nil, errcode.NewInternalErr(errors.Errorf("failed to query users of project %d", projectID))
	}

	userIDList := make([]uint64, len(users))
	for i, user := range users {
		userIDList[i] = user.ID.Uint64()
	}
	c.Log().Info("list project users", zap.Uint64("projectID", projectID.Uint64()), zap.Uint64s("users", userIDList))

	convProjectUsers, err := convertProjectUsers(ctx, users, project.ID.Uint64())
	if err != nil {
		return nil, err
	}
	convOrgOwnerUsers, err := convertOrgOwnerUsers(ctx, orgOwnerUsers, req.OrgID.Uint64())
	if err != nil {
		return nil, err
	}

	var orgOwnerUserIdMap = map[uint64]struct{}{}
	for _, user := range convOrgOwnerUsers {
		orgOwnerUserIdMap[user.ID] = struct{}{}
	}
	for _, projectUser := range convProjectUsers {
		if _, ok := orgOwnerUserIdMap[projectUser.ID]; !ok {
			convOrgOwnerUsers = append(convOrgOwnerUsers, projectUser)
		}
	}

	sort.SliceStable(convOrgOwnerUsers, func(i, j int) bool {
		return convOrgOwnerUsers[i].ID > convOrgOwnerUsers[j].ID
	})

	return convOrgOwnerUsers, nil
}

func convertOrgOwnerUsers(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 k := range users {
			user := users[k]
			// Ignore system user
			if user.FirstName == systemFirstName && user.LastName == userLastName {
				continue
			}

			roleBindings, err := roleDao.ListRolesByTargetAndScope(
				ctx, tx, entity.RoleBindingTargetUser, user.ID.Uint64(), entity.RoleBindingScopeOrg, orgID, nil,
			)
			if err != nil {
				return err
			}
			if len(roleBindings) == 0 {
				continue
			}

			roleBinding := roleBindings[0]
			if roleBinding.Status == entity.RoleBindingStatusDelete {
				continue
			}
			// convert org:owenr -> project:owner to show on front page
			roleBinding.Role = entity.RoleProjectOwner

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

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

	return modelUsers, nil
}

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

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

			roleBindings, err := roleDao.ListRolesByTargetAndScope(
				ctx, tx, entity.RoleBindingTargetUser, user.ID.Uint64(), entity.RoleBindingScopeProject, projectID, nil,
			)
			if err != nil {
				return err
			}
			if len(roleBindings) == 0 {
				continue
			}

			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 project users", zap.Error(err))
		return nil, err
	}

	return modelUsers, nil
}

type InviteUserReq struct {
	OrgID     entity.ID
	ProjectID entity.ID
	Role      entity.UserRole
	EmailList []string
	Operator  entity.ID
	RbacRole  string
}

func (c Context) sendInviteEmail(ctx context.Context, email, token, orgName string, orgID uint64) error {
	logger := tilog.WithContextL(ctx)

	url := fmt.Sprintf("https://%s/invitation-verification?token=%s", utils.GetCentralBackendDns(), token)
	inviter := c.tenantAndUser.Email
	subject := fmt.Sprintf("Invitation to TiDB Cloud: %s", orgName)
	tilog.Info(ctx, "sendInviteEmail,subject:"+subject)
	orgName = utils.EscapeStr(ctx, orgName)
	vars := map[string]string{
		"organizationName": orgName,
		"inviterName":      inviter,
		"url":              url,
	}
	_, err := c.accountAPI.SendEmail(ctx, &accountpb.SendEmailReq{
		OrgId:        orgID,
		Scene:        string(entity.NotificationSceneInvite),
		Identifier:   uuid.New().String(),
		Subject:      subject,
		Emails:       []string{email},
		CcEmails:     nil,
		TemplateId:   config.SendgridTemplateName2id[config.TNInviteUser],
		TemplateVars: vars,
	})
	if err != nil {
		logger.Error("failed to send invitation email", zap.String("feature", "invitation"), zap.String("email", email), zap.Error(err))
		return errors.Wrap(errFailedInviteEmail, err.Error())
	}
	logger.Info("send invitation email", zap.String("feature", "invitation"), zap.String("email", email), zap.Time("time", time.Now().UTC()))

	return nil
}

func (c Context) inviteUserToProjectByEmail(ctx context.Context, operator entity.ID, role entity.UserRole, orgID, projectID entity.ID, orgName, email string) error {
	logger := tilog.WithContextL(ctx)
	daoHub := dao.NewDaoHub()

	var (
		status       = entity.UserStatusPending
		token        string
		userNotInOrg = true
		project      entity.Project
		user         entity.User
		err          error
		inviteAt     = time.Now()
	)

	projectIDsAdd := make([]entity.ID, 0)
	projectIDsRemove := make([]entity.ID, 0)
	userExistsInProject := false
	err = database.Transaction(func(tx *gorm.DB) error {
		// check if the project exists
		err = entity.NewProjectQuerySet(tx).TenantIDEq(orgID).IDEq(projectID).StatusEq(entity.ProjectStatusActive).One(&project)
		if err != nil {
			logger.Error("failed to query project by id", zap.String("feature", "invitation"), zap.String("email", email), zap.Uint64("projectID", projectID.Uint64()), zap.Error(err))
			return errors.New("failed to query project by id")
		}

		// check the user's status by email
		user, err = usersvc.GetUserByEmail(tx, email)
		isNewUser := errors.Cause(err) == gorm.ErrRecordNotFound
		if err != nil && !isNewUser {
			logger.Error("failed to get user from db by email", zap.String("feature", "invitation"), zap.String("email", email), zap.Error(err))
			return errors.New("failed to get user from db by email")
		}

		// if current user has active relationship with the org, we don't send invitation email
		bindings, err := daoHub.Role.ListUserRolesByUserIDAndOrg(ctx, tx, user.ID.Uint64(), orgID.Uint64(), []string{entity.RoleBindingStatusActive})
		if err != nil {
			logger.Error("failed to get user org relationship", zap.String("feature", "invitation"), zap.Uint64("userID", user.ID.Uint64()), zap.Uint64("orgID", orgID.Uint64()), zap.Error(err))
			return errors.New("failed to get user org relationship")
		}
		if len(bindings) > 0 {
			userNotInOrg = false
		}

		firstName := ""
		lastName := ""

		if !isNewUser {
			if user.Status == entity.UserStatusActive {
				firstName = user.FirstName
				lastName = user.LastName
				status = entity.UserStatusActive
			}
		}

		userProjects, err := daoHub.Cross.ListActiveProjectsByUserIDAndOrgID(ctx, tx, user.ID.Uint64(), orgID.Uint64())
		if err != nil {
			logger.Error("failed to get projects by user", zap.Uint64("userID", user.ID.Uint64()), zap.Error(err))
			return errors.Wrap(err, "failed to get projects by user")
		}

		for _, i := range userProjects {
			if i.ID.Uint64() == project.ID.Uint64() {
				userExistsInProject = true
				break
			}
		}
		if !userExistsInProject {
			userProjects = append(userProjects, project)
			projectIDsAdd = append(projectIDsAdd, project.ID)
		}

		// if the invited user is owner, grant all projects to the user.
		if role == entity.UserRoleOwner {
			projects, err := projectdao.GetActiveProjects(ctx, tx, orgID)
			if err != nil {
				logger.Error("failed to query active projects of tenant", zap.Error(err))
				return errors.New("failed to query active projects of tenant")
			}

			for _, project := range userProjects {
				projectIDsRemove = append(projectIDsRemove, project.ID)
			}

			projectIDsAdd = make([]entity.ID, len(projects))
			for i, project := range projects {
				projectIDsAdd[i] = project.ID
			}

			userProjects = projects
		}

		upsertedUser, err := usersvc.UpsertUser(ctx, tx, usersvc.CreateUserRequired{
			Email:     email,
			FirstName: firstName,
			LastName:  lastName,
			InviteAt:  &inviteAt,
			Status:    status,
			TenantID:  orgID,
			Role:      role,
			Projects:  userProjects,
		})
		if err != nil {
			logger.Error("failed to upsert user in db", zap.String("feature", "invitation"), zap.String("email", email), zap.Error(err))
			return errors.New("failed to upsert user in db")
		}
		logger.Info("gonna update user-project relationship", zap.Any("projectIDsRemove", projectIDsRemove), zap.Any("projectIDsAdd", projectIDsAdd))

		for _, projectID := range projectIDsRemove {
			_, err := rbac2.RemoveProjectRoleForUser(ctx, tx, upsertedUser.ID, projectID, entity.RoleProjectOwner)
			if err != nil && err != gorm.ErrRecordNotFound {
				logger.Error("failed to remove project role for user", zap.Uint64("userID", upsertedUser.ID.Uint64()), zap.Uint64("projectID", projectID.Uint64()), zap.Error(err))
				return errors.New("failed to remove project role for user")
			}
		}

		for _, projectID := range projectIDsAdd {
			// if it's the first time user join the project/org, the project status should be inactive, too. It will
			// update to active when user accept the invitation.
			// But, if user already in the org, we add the relationship directly.
			projectRoleStatus := entity.RoleBindingStatusActive
			if userNotInOrg {
				projectRoleStatus = entity.RoleBindingStatusInactive
			}

			_, err := rbac2.AddProjectRoleForUser(ctx, tx, upsertedUser.ID, projectID, entity.RoleProjectOwner, projectRoleStatus)
			if err != nil {
				tilog.Error(ctx, "failed to add project role for user", zap.Uint64("userID", upsertedUser.ID.Uint64()), zap.Uint64("projectID", projectID.Uint64()), zap.Error(err))
				return errors.New("failed to add project role for user")
			}
		}

		if userNotInOrg {
			// NOTE(wangletian) update add operator
			token, err = c.session.InsertUserInviteToken(tx, operator, c.tenantAndUser.TenantID, tokensvc.InvitationVerificationMethod, email, 30*24*time.Hour, true)
			tilog.Info(ctx, "this user need to send invite email ,to create user invite token ", zap.Any("token", token))
			if err != nil {
				logger.Error("failed to create user invite token", zap.String("feature", "invitation"), zap.String("email", email), zap.Error(err))
				return errors.New("failed to create user invite token")
			}
			return accesslog.AddAccessLogNow(tx, orgID, c.tenantAndUser.Email, fmt.Sprintf("invite user: %s", email))
		}
		return nil
	})
	if err != nil {
		return err
	}
	tilog.Info(ctx, "inviteUserToProjectByEmail ", zap.Any("userNotInOrg", userNotInOrg), zap.Any("userExistsInProject", userExistsInProject), zap.Any("role", role))
	if userNotInOrg {
		return c.sendInviteEmail(ctx, email, token, orgName, orgID.Uint64())
	}

	dbUserSvc, _ := dbuser.GetDBUserSvc()
	var action entity.CommandAction
	// if invite user to a new project
	if !userExistsInProject {
		tilog.Info(ctx, "inviteUserToProjectByEmail ，The user already exists in this org , but not in project", zap.Any("project", project.ID))
		action = entity.CommandActionCreate

		// if invite user to  exist project, this operator is change user role
	} else if userExistsInProject && role == entity.UserRoleOwner {
		tilog.Info(ctx, "inviteUserToProjectByEmail ，The user already exists in this org  and this project , This operation may change the role", zap.Any("role", role))
		action = entity.CommandActionCreate
	} else if userExistsInProject && role == entity.UserRoleMember {
		tilog.Info(ctx, "inviteUserToProjectByEmail ，The user already exists in this org  and this project , This operation may change the role", zap.Any("role", role))
		if user.Status == entity.UserStatusActive {

			// 1：delete 2: create
			action = entity.CommandActionDelete
			if err := dbUserSvc.CreateCommandUserAction(ctx, operator.Uint64(), user.ID.Uint64(), user.Email, orgID.Uint64(), 0, entity.CommandStatusWaiting, action, entity.RoleBindingTargetUser, entity.RoleBindingTargetUser); err != nil {
				tilog.Error(ctx, "CreateCommandUserAction fail", zap.Error(err))
			}
			action = entity.CommandActionCreate
		}
	} else if userExistsInProject && (role == entity.UserRoleBillingAdmin || role == entity.UserRoleAuditAdmin) {
		tilog.Info(ctx, "inviteUserToProjectByEmail ，The user already exists in this org  and this project , This operation may change the role", zap.Any("role", role))
		action = entity.CommandActionDelete
	}
	if user.Status == entity.UserStatusActive {
		if err := dbUserSvc.CreateCommandUserAction(ctx, operator.Uint64(), user.ID.Uint64(), user.Email, orgID.Uint64(), 0, entity.CommandStatusWaiting, action, entity.RoleBindingTargetUser, entity.RoleBindingTargetUser); err != nil {
			tilog.Error(ctx, "CreateCommandUserAction fail", zap.Error(err))
		}
	}

	return nil
}

func (c Context) InviteUser(ctx context.Context, req InviteUserReq) error {
	tilog.Info(ctx, "InviteUser", zap.Any("InviteUserReq", req))
	if len(req.EmailList) == 0 {
		return errcode.NewInvalidInputErr(errors.New("email is required"))
	}

	oneConsoleUserSvc := oneconsole.GetOneConsoleUserSvc()

	operator := &comsvc.Operator{
		OpTye: entity.RoleBindingTargetUser,
		OpID:  c.tenantAndUser.ID.Uint64(),
		OpVal: c.tenantAndUser.Email,
	}

	var pRoles map[uint64]string
	if req.ProjectID != 0 {
		pRoles = map[uint64]string{
			req.ProjectID.Uint64(): req.RbacRole,
		}
	}

	var (
		failedEmailList   []string
		repeatedEmailList []string
	)

	for _, email := range req.EmailList {
		user, oBinding, pBinding, err := comsvc.GetUserAndScopeRoleBinding(ctx, email, req.OrgID.Uint64(), req.ProjectID.Uint64(), []string{entity.RoleBindingStatusActive})
		if err != nil {
			tilog.Error(ctx, "InviteUser GetUserAndScopeRoleBinding error", zap.Any("email", email), zap.Any("err", err))
			failedEmailList = append(failedEmailList, email)
			continue
		}
		if req.ProjectID != 0 {
			// invite to project
			if pBinding != nil {
				repeatedEmailList = append(repeatedEmailList, email)
				continue
			}
			if oBinding != nil {
				_, err := comsvc.UpdateIdentitySpecRoles(ctx, &comsvc.AssignIdentRolesParam{
					IdentTye: entity.RoleBindingTargetUser,
					IdentID:  user.ID.Uint64(),
					IdentVal: user.Email,
					OrgID:    req.OrgID.Uint64(),
					PRoles:   pRoles,
					Status:   oBinding.Status,
				})
				if err != nil {
					failedEmailList = append(failedEmailList, email)
				}
			} else {
				ret, err := oneConsoleUserSvc.InviteUsersToOrg(ctx, &oneconsole.InviteUsersParam{
					EmailList: []string{email},
					OrgID:     req.OrgID.Uint64(),
					ORole:     entity.RoleOrgMember,
					PRoles:    pRoles,
				}, operator)
				if err != nil {
					failedEmailList = append(failedEmailList, email)
					continue
				}
				retCode := ret[0].Code
				if retCode != entity.InviteUserSuccess {
					if retCode == entity.InviteUserRepeated {
						repeatedEmailList = append(repeatedEmailList, email)
					} else {
						failedEmailList = append(failedEmailList, email)
					}
				}
			}

		} else {
			// invite to org
			if oBinding != nil {
				repeatedEmailList = append(repeatedEmailList, email)
				continue
			}
			ret, err := oneConsoleUserSvc.InviteUsersToOrg(ctx, &oneconsole.InviteUsersParam{
				EmailList: []string{email},
				OrgID:     req.OrgID.Uint64(),
				ORole:     req.RbacRole,
			}, operator)
			if err != nil {
				failedEmailList = append(failedEmailList, email)
				continue
			}
			retCode := ret[0].Code
			if retCode != entity.InviteUserSuccess {
				if retCode == entity.InviteUserRepeated {
					repeatedEmailList = append(repeatedEmailList, email)
				} else {
					failedEmailList = append(failedEmailList, email)
				}
			}
		}
	}

	var ErrMessage string
	if len(repeatedEmailList) > 0 {
		resource := "organization"
		if req.ProjectID != 0 {
			resource = "project"
		}
		ErrMessage += fmt.Sprintf(ErrRepeatedInviteEmailListTemplate, strings.Join(repeatedEmailList, ", "), resource)
	}
	if len(failedEmailList) != 0 {
		ErrMessage = fmt.Sprintf(ErrFailedEmailListTemplate, strings.Join(failedEmailList, ", "))
	}
	if ErrMessage != "" {
		if len(failedEmailList) != 0 {
			return errs.NewInternalUnknown(ErrMessage, nil)
		}
		return errs.NewBadRequest(ErrMessage, nil)
	}

	return nil
}

func (c Context) logErr(err error) error {
	if err == nil {
		return nil
	}

	c.Log().Error("error occurred", zap.Error(err))
	return err
}

func (c Context) UpdateProjectUserRole(ctx context.Context, orgID, projectID, userID entity.ID, role string) error {
	logger := tilog.WithContextL(ctx)
	logger.Info("update project user role", zap.Uint64("projectID", projectID.Uint64()), zap.Uint64("userID", userID.Uint64()), zap.String("role", role))
	daoHub := dao.NewDaoHub()
	var user *entity.User
	var err error
	var existedRoleBinding *entity.RoleBinding
	var oldRole string
	err = database.Transaction(func(tx *gorm.DB) error {
		user, err = daoHub.User.GetUserByID(ctx, tx, userID.Uint64())
		if err != nil {
			logger.Error("failed to query user", zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to query user"))
		}
		isOrgOwner, err := checkUserHasRole(ctx, userID, entity.RoleBindingScopeOrg, orgID, entity.RoleOrgOwner, []string{})
		if err != nil {
			logger.Error("failed to query user roles", zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to query user roles"))
		}
		if isOrgOwner {
			oldRole = entity.RoleProjectOwner
			logger.Info("user is org:owner, not support to update project role", zap.Uint64("projectID", projectID.Uint64()), zap.Uint64("userID", userID.Uint64()), zap.String("role", role), zap.Any("error", err))
			return errcode.NewForbiddenErr(errors.New("no need to update project role"))
		}
		alreadyHasRole, err := checkUserHasRole(ctx, userID, entity.RoleBindingScopeProject, projectID, role, []string{})
		if err != nil {
			logger.Error("failed to query user roles", zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to query user roles"))
		}
		if alreadyHasRole {
			oldRole = role
			return nil
		}
		roleBindings, err := daoHub.Role.ListRolesByTargetAndScope(ctx, tx, entity.RoleBindingTargetUser, userID.Uint64(), entity.RoleBindingScopeProject, projectID.Uint64(), nil)
		if err != nil {
			logger.Error("update project user role fail, list role binding err", zap.Uint64("projectID", projectID.Uint64()), zap.Uint64("userID", userID.Uint64()), zap.String("role", role), zap.Any("error", err))
			return err
		}
		if len(roleBindings) == 0 {
			logger.Info("update project user role fail, not found relate role binding", zap.Uint64("projectID", projectID.Uint64()), zap.Uint64("userID", userID.Uint64()), zap.String("role", role))
			return errcode.NewInvalidInputErr(errors.Errorf("not found relate role binding"))
		}
		existedRoleBinding = &roleBindings[0]
		oldRole = existedRoleBinding.Role
		if existedRoleBinding.Role == entity.RoleProjectOwner {
			// check can not delete project last project:owner
			lastOwner, err := checkUserIsLastProjectOwner(ctx, orgID, projectID, userID)
			if err != nil {
				logger.Error("check user is last project owner fail", zap.Uint64("projectID", projectID.Uint64()), zap.Uint64("userID", userID.Uint64()), zap.String("role", role))
				return errcode.NewInvalidInputErr(errors.Errorf("check user is last project owner fail"))
			}
			if lastOwner {
				logger.Error("can not delete project last project_owner", zap.Uint64("projectID", projectID.Uint64()), zap.Uint64("userID", userID.Uint64()), zap.String("role", role))
				return errcode.NewInvalidInputErr(errors.Errorf("can not delete project %d last project_owner", projectID))
			}
		}
		_, err = daoHub.Role.ChangeRoleTo(ctx, tx, entity.RoleBindingTargetUser, userID.Uint64(), entity.RoleBindingScopeProject, projectID.Uint64(), existedRoleBinding.Role, role)
		if err != nil {
			logger.Error("update project user role fail, change db role err", zap.Uint64("projectID", projectID.Uint64()), zap.Uint64("userID", userID.Uint64()), zap.String("userID", role), zap.Any("err", err))
			return errcode.NewInternalErr(errors.Errorf("change db role err"))
		}
		return nil
	})
	if err != nil {
		return err
	}

	details := auditlog.Details{
		auditlog.KeyUserEmail: user.Email,
		auditlog.KeyOldRole:   entity.RoleBindingDecode(oldRole),
		auditlog.KeyNewRole:   entity.RoleBindingDecode(role),
		auditlog.KeyError:     err,
	}
	auditlog.NewEvent(ctx, auditlog.EventUpdateUserRole).SetOrgID(orgID.Uint64()).SetProjectID(projectID.Uint64()).SetDetails(details.Build()).SetResult(err == nil).Submit() //nolint:errcheck

	// db account
	if existedRoleBinding != nil && existedRoleBinding.Status == entity.RoleBindingStatusActive {
		svc, _ := dbuser.GetDBUserSvc()
		ignore, err := svc.IsIgnoreProjectDbUser(ctx, userID.Uint64(), projectID.Uint64(), orgID.Uint64())
		// if err, just to create
		if err != nil || !ignore {
			err = svc.CreateCommandUserAction(ctx, c.tenantAndUser.ID.Uint64(), userID.Uint64(), user.Email, orgID.Uint64(), projectID.Uint64(), entity.CommandStatusWaiting, entity.CommandActionDelete, entity.RoleBindingTargetUser, entity.RoleBindingTargetUser)
			if err != nil {
				tilog.Error(ctx, "UpdateProjectUserRole CreateCommandUserAction error", zap.Any("user", user.Email), zap.Any("projectID", projectID.Uint64()), zap.Any("err", err))
			}
			err = svc.CreateCommandUserAction(ctx, c.tenantAndUser.ID.Uint64(), userID.Uint64(), user.Email, orgID.Uint64(), projectID.Uint64(), entity.CommandStatusWaiting, entity.CommandActionCreate, entity.RoleBindingTargetUser, entity.RoleBindingTargetUser)
			if err != nil {
				tilog.Error(ctx, "UpdateProjectUserRole CreateCommandUserAction error", zap.Any("user", user.Email), zap.Any("projectID", projectID.Uint64()), zap.Any("err", err))
			}
		}
	}

	return err
}

func checkUserHasRole(ctx context.Context, userID entity.ID, scopeType entity.ScopeType, scopeID entity.ID, roleBindingRole string, roleBindingStatus []string) (bool, error) {
	logger := tilog.WithContextL(ctx)
	daoHub := dao.NewDaoHub()

	var roleBindings []entity.RoleBinding
	var err error

	err = database.Transaction(func(tx *gorm.DB) error {
		filter := role.RoleBindingFilter{
			ScopeTypes: []entity.ScopeType{scopeType},
			ScopeIDs:   []entity.ID{scopeID},
			TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
			TargetIDs:  []entity.ID{userID},
			Status:     roleBindingStatus,
			Roles:      []string{roleBindingRole},
		}
		roleBindings, err = daoHub.Role.ListRoleBindings(ctx, tx, filter)
		if err != nil {
			logger.Error("failed to query org role_bindings", zap.Any("filter", filter), zap.Any("err", err))
			return errcode.NewInternalErr(errors.Errorf("failed to query org role_bindings"))
		}
		return nil
	})
	if err != nil {
		return false, err
	}
	if len(roleBindings) > 0 {
		return true, nil
	}
	return false, nil
}

func checkUserIsLastProjectOwner(ctx context.Context, orgID, projectID, userID entity.ID) (bool, error) {
	logger := tilog.WithContextL(ctx)
	daoHub := dao.NewDaoHub()

	var projectRoleBindings []entity.RoleBinding
	var orgRoleBindings []entity.RoleBinding
	var err error
	err = database.Transaction(func(tx *gorm.DB) error {
		projectRoleBindings, err = daoHub.Role.ListRolesByScope(ctx, tx, entity.RoleBindingScopeProject, projectID.Uint64(), []string{entity.RoleBindingStatusActive})
		if err != nil {
			logger.Error("failed to query project role_bindings", zap.Uint64("projectID", projectID.Uint64()), zap.Any("err", err))
			return errcode.NewInternalErr(errors.Errorf("failed to query project role_bindings"))
		}
		orgRoleBindings, err = daoHub.Role.ListRolesByScope(ctx, tx, entity.RoleBindingScopeOrg, orgID.Uint64(), []string{entity.RoleBindingStatusActive})
		if err != nil {
			logger.Error("failed to query org role_bindings", zap.Uint64("orgID", orgID.Uint64()), zap.Any("err", err))
			return errcode.NewInternalErr(errors.Errorf("failed to query org role_bindings"))
		}
		return nil
	})
	if err != nil {
		return false, err
	}
	isOrgOwner := false
	isProjectOwner := false
	for _, projectRoleBinding := range projectRoleBindings {
		if projectRoleBinding.Role == entity.RoleProjectOwner && projectRoleBinding.TargetID != userID {
			return false, nil
		}
		if projectRoleBinding.Role == entity.RoleProjectOwner && projectRoleBinding.TargetID == userID {
			isProjectOwner = true
		}
	}
	for _, orgRoleBinding := range orgRoleBindings {
		if orgRoleBinding.Role == entity.RoleOrgOwner && orgRoleBinding.TargetID != userID {
			return false, nil
		}
		if orgRoleBinding.Role == entity.RoleOrgOwner && orgRoleBinding.TargetID == userID {
			isOrgOwner = true
		}
	}

	if isOrgOwner || isProjectOwner {
		return true, nil
	}

	return false, nil
}

func (c Context) DeleteProjectUser(ctx context.Context, orgID, projectID, userID entity.ID) error {
	var project entity.Project
	daoHub := dao.NewDaoHub()

	if c.tenantAndUser.ID == userID {
		return errcode.NewInvalidInputErr(errors.New("can not remove yourself"))
	}

	err := database.Transaction(func(tx *gorm.DB) error {
		var err error
		err = entity.NewProjectQuerySet(tx.Preload("Users")).IDEq(projectID).StatusEq(entity.ProjectStatusActive).One(&project)
		if err != nil {
			return c.logErr(errcode.NewInternalErr(errors.Errorf("failed to query users of project %d", projectID)))
		}

		isOrgOwner, err := checkUserHasRole(ctx, userID, entity.RoleBindingScopeOrg, orgID, entity.RoleOrgOwner, nil)
		if err != nil {
			tilog.Error(ctx, "check user is org owner fail", zap.Uint64("orgID", orgID.Uint64()), zap.Uint64("projectID", projectID.Uint64()), zap.Uint64("userID", userID.Uint64()))
			return errcode.NewInvalidInputErr(errors.Errorf("check user is org owner fail"))
		}
		if isOrgOwner {
			tilog.Info(ctx, "user is org:owner, not support to delete project role", zap.Uint64("projectID", projectID.Uint64()), zap.Uint64("userID", userID.Uint64()), zap.Any("error", err))
			return errcode.NewForbiddenErr(errors.New("unable to delete the user with the role of organization owner from the project"))
		}

		// check can not delete project last project:owner
		lastOwner, err := checkUserIsLastProjectOwner(ctx, orgID, projectID, userID)
		if err != nil {
			tilog.Error(ctx, "check user is last project owner fail", zap.Uint64("orgID", orgID.Uint64()), zap.Uint64("projectID", projectID.Uint64()), zap.Uint64("userID", userID.Uint64()))
			return errcode.NewInvalidInputErr(errors.Errorf("check user is last project owner fail"))
		}
		if lastOwner {
			tilog.Error(ctx, "can not delete project last project_owner", zap.Uint64("orgID", orgID.Uint64()), zap.Uint64("projectID", projectID.Uint64()), zap.Uint64("userID", userID.Uint64()))
			//return errcode.NewInvalidInputErr(errors.Errorf("can not delete project %d last project_owner", projectID))
			return errcode.NewInvalidInputErr(errors.Errorf("unable to delete this user as this is the only project owner of %s", project.Name))
		}

		bindings, err := daoHub.Role.ListRolesByTargetAndScope(ctx, tx, entity.RoleBindingTargetUser, userID.Uint64(), entity.RoleBindingScopeProject, projectID.Uint64(), nil)
		if err != nil {
			return c.logErr(errcode.NewInternalErr(errors.Errorf("failed to query org roles of user %d", userID.Uint64())))
		}
		if len(bindings) == 0 {
			return c.logErr(errcode.NewInternalErr(errors.Errorf("failed to query project roles of user %d", userID.Uint64())))
		}

		for _, binding := range bindings {
			//DeleteRole(ctx context.Context, tx *gorm.DB, targetType TargetType, targetID uint64, scopeType ScopeType, scopeID uint64, role string) (bool, error)
			_, err := daoHub.Role.DeleteRole(ctx, tx, binding.TargetType, binding.TargetID.Uint64(), binding.ScopeType, binding.ScopeID.Uint64(), binding.Role)
			if err != nil {
				return c.logErr(errcode.NewInternalErr(errors.Errorf("failed to delete user from project %d", projectID.Uint64())))
			}
		}

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

	return nil
}

func DeleteProject(ctx context.Context, orgID, projectID entity.ID) error {
	var (
		clusters []*core.Cluster
		err      error
	)
	err = database.Transaction(func(tx *gorm.DB) error {
		clusters, err = infra.GetHandler().GetProjectClustersNotInStatus(
			ctx, projectID.Uint64(),
			core.ClusterStatus_CLUSTER_STATUS_DELETED, core.ClusterStatus_CLUSTER_STATUS_CLEARED, core.ClusterStatus_CLUSTER_STATUS_DELETECOMPLETE,
		)
		tilog.Info(ctx, "query normal clusters in under project", zap.Uint64("project_id", projectID.Uint64()), zap.Int("clusters_count", len(clusters)))
		if err != nil {
			tilog.Error(ctx, "query active clusters under the project", zap.Uint64("project_id", projectID.Uint64()), zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to query active clusters under the project"))
		}
		if len(clusters) > 0 {
			return errcode.NewInvalidInputErr(errors.New(config.ErrDeleteProject))
		}

		clusters, err = infra.GetHandler().GetProjectClustersInStatus(
			ctx, projectID.Uint64(),
			core.ClusterStatus_CLUSTER_STATUS_DELETED, core.ClusterStatus_CLUSTER_STATUS_DELETECOMPLETE,
		)

		tilog.Info(ctx, "query deleted clusters under the project", zap.Uint64("project_id", projectID.Uint64()), zap.Int("clusters_count", len(clusters)))
		if err != nil {
			tilog.Error(ctx, "query deleted clusters under the project", zap.Uint64("projectID", projectID.Uint64()), zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to query deleted clusters under the project"))
		}

		for _, cluster := range clusters {
			// delete backup of deleted cluster
			if !cluster.IsDevTier {
				tilog.Info(ctx, "delete deleted dedicate clusters backup", zap.Uint64("project_id", projectID.Uint64()), zap.Uint64("cluster", cluster.Id))
				err = dataflow.GetHandler().DeleteDeletedClusterBackups(ctx, orgID.Uint64(), projectID.Uint64(), cluster.Id)
				if err != nil {
					tilog.Error(ctx, "delete backup of deleted cluster ", zap.Uint64("projectID", projectID.Uint64()), zap.Uint64("clusterID", cluster.Id), zap.Error(err))
					return errcode.NewInternalErr(errors.New("failed to delete backup of deleted cluster"))
				}
			}
		}

		svc, _ := projectsvc.GetProjectSvc()
		err = svc.DeleteProject(ctx, projectID.Uint64())
		if err != nil {
			tilog.Error(ctx, "failed to delete project", zap.Uint64("projectID", projectID.Uint64()), zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to delete project"))
		}
		return nil
	})
	if err != nil {
		return err
	}
	return nil
}

func GetProjectName(ctx context.Context, orgID, projectID entity.ID) (string, error) {
	logger := tilog.WithContextL(ctx)
	var project entity.Project
	err := database.Transaction(func(tx *gorm.DB) error {
		err := entity.NewProjectQuerySet(tx).TenantIDEq(orgID).IDEq(projectID).StatusEq(entity.ProjectStatusActive).One(&project)
		if err != nil {
			return errcode.NewInternalErr(errors.New("failed to query project by id"))
		}
		return nil
	})
	if err != nil {
		logger.Error("failed to get project name", zap.Error(err))
		return "", err
	}
	return project.Name, nil
}

func GetUserEmailAndRole(ctx context.Context, orgID, userID entity.ID) (string, entity.UserRole, *entity.RoleBinding, error) {
	logger := tilog.WithContextL(ctx)
	var (
		user               *entity.User
		userRole           entity.UserRole
		dao                = dao.NewDaoHub()
		userOrgRoleBinding *entity.RoleBinding
	)
	err := database.Transaction(func(tx *gorm.DB) error {
		var err error
		user, err = usersvc.GetUserByID(tx, userID)
		if err != nil {
			logger.Error("failed to get user by id", zap.Uint64("userID", userID.Uint64()), zap.Error(err))
			return errcode.NewInternalErr(errors.New("failed to query user by id"))
		}

		var found bool
		userOrgRoleBinding, found, err = dao.Role.LegacyGetUserRoleByOrg(ctx, tx, user.ID.Uint64(), orgID.Uint64(), nil)
		if err != nil {
			return err
		}
		if !found {
			return errors.New("User roles should not be empty")
		}
		userRole = entity.RoleBindingDecode(userOrgRoleBinding.Role)
		return nil
	})
	if err != nil {
		logger.Warn("failed to get project name", zap.Error(err))
		return "", "", nil, err
	}
	return user.Email, userRole, userOrgRoleBinding, nil
}
