package scim

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"regexp"
	"strings"
	"sync"

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

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

	"github.com/imulab/go-scim/pkg/v2/service"
	"github.com/imulab/go-scim/pkg/v2/spec"
	"github.com/jinzhu/copier"
	"github.com/tidbcloud/account/idl/pbgen/proto/central"
	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

var (
	removeMembersPathReg = regexp.MustCompile("value eq \"(.+)\"")

	filterDisplayNameReg = regexp.MustCompile("displayName eq \"(.+)\"")
)

const (
	membersPath = "members"
)

type ScimGroupSvcIface interface {
	SearchScimResources
	CreateScimResource
	GetScimResource
	ReplaceScimResource
	PatchScimResource
	DeleteScimResource
	ListScimGroups(ctx context.Context, orgID uint64, offset, limit int) (int, []*central.ScimGroup, error)
	UpdateScimGroupRole(ctx context.Context, groupID, orgRole, projectRole string, projectIds []uint64) error
}

var (
	scimGroupSvcInitOnce sync.Once
	scimGroupSvcInstance ScimGroupSvcIface
)

func GetScimGroupSvc() (ScimGroupSvcIface, error) {
	scimGroupSvcInitOnce.Do(func() {
		scimGroupSvcInstance = NewScimGroupSvc(database.DBConn.DB, dao.GetDaoHub())
	})
	return scimGroupSvcInstance, nil
}

type ScimGroupSvc struct {
	db  *gorm.DB
	hub *dao.DaoHub
}

func NewScimGroupSvc(db *gorm.DB, hub *dao.DaoHub) ScimGroupSvcIface {
	return &ScimGroupSvc{
		db:  db,
		hub: hub,
	}
}

func checkSupportGroup(ctx context.Context) error {
	groupResourceType, err := GetResourceTypeByID("Group")
	if err != nil {
		return err
	}
	if groupResourceType == nil {
		tilog.Info(ctx, "not support group")
		return fmt.Errorf("%w: group is not supported", spec.ErrInvalidSyntax)
	}
	return nil
}

func (s *ScimGroupSvc) SearchResources(ctx context.Context, orgID uint64, req *service.QueryRequest) (*ScimQueryResponse, error) {
	tilog.Info(ctx, "ScimGroupSvc ListScimGroups start", zap.Any("orgID", orgID), zap.Any("req", req))
	if err := checkSupportGroup(ctx); err != nil {
		return nil, err
	}
	displayName, err := getDisplayNameFromFilter(req.Filter)
	if err != nil {
		tilog.Error(ctx, "ScimGroupSvc ListScimGroups getDisplayNameFromFilter error", zap.Any("filter", req.Filter), zap.Any("err", err))
		return nil, fmt.Errorf("%w: %s", spec.ErrInvalidFilter, err.Error())
	}

	total, err := s.hub.ScimGroup.CountScimGroupsByOrgIDSelectiveDisplayName(ctx, s.db, orgID, displayName)
	if err != nil {
		tilog.Error(ctx, "ScimGroupSvc ListScimGroups CountScimGroupsByOrgID error", zap.Any("orgID", orgID), zap.Any("err", err))
		return nil, err
	}
	if total == 0 {
		rsp := genQueryResponse(0, req.Pagination.StartIndex, 0, []map[string]interface{}{})
		tilog.Info(ctx, "ScimGroupSvc ListScimGroups total is empty", zap.Any("rsp", rsp))
		return rsp, nil
	}
	offset := req.Pagination.StartIndex - 1
	groups, err := s.hub.ScimGroup.BatchGetScimGroupsByOrgIDSelectiveDisplayName(ctx, s.db, orgID, displayName, offset, req.Pagination.Count)
	if err != nil {
		tilog.Error(ctx, "ScimGroupSvc ListScimGroups BatchGetScimGroupsByOrgID error", zap.Any("err", err))
		return nil, err
	}
	if len(groups) == 0 {
		rsp := genQueryResponse(total, req.Pagination.StartIndex, 0, []map[string]interface{}{})
		tilog.Info(ctx, "ScimGroupSvc ListScimGroups page is empty", zap.Any("rsp", rsp))
		return rsp, nil
	}

	var resources []map[string]interface{}
	for _, group := range groups {
		scimGroup := convDbGroup2ScimGroupData(group)
		resource := map[string]interface{}(scimGroup)
		resources = append(resources, resource)
	}

	rsp := genQueryResponse(total, req.Pagination.StartIndex, len(resources), resources)
	tilog.Info(ctx, "ScimGroupSvc ListScimGroups end", zap.Any("rsp", rsp))
	return rsp, nil
}

func getDisplayNameFromFilter(filter string) (string, error) {
	var displayName = ""
	if filter != "" {
		submatch := filterDisplayNameReg.FindStringSubmatch(filter)
		if len(submatch) != 2 {
			return "", errors.New("parse filter error")
		}
		displayName = submatch[1]
	}
	return displayName, nil
}

func (s *ScimGroupSvc) CreateResource(ctx context.Context, orgID uint64, req *service.CreateRequest) (map[string]interface{}, error) {
	tilog.Info(ctx, "ScimGroupSvc CreateScimGroup start", zap.Any("orgID", orgID), zap.Any("req", req))
	if err := checkSupportGroup(ctx); err != nil {
		return nil, err
	}
	if req == nil || req.PayloadSource == nil {
		tilog.Info(ctx, "ScimGroupSvc CreateScimGroup req payload is nil")
		return nil, fmt.Errorf("%w: no payload found", spec.ErrInvalidValue)
	}
	reqScimGroup := ScimGroupData{}
	err := json.NewDecoder(req.PayloadSource).Decode(&reqScimGroup)
	if err != nil {
		tilog.Error(ctx, "ScimGroupSvc CreateScimGroup decode payload to scim group error", zap.Any("err", err))
		return nil, err
	}
	tilog.Info(ctx, "ScimGroupSvc CreateScimGroup get scim group from payload", zap.Any("scimGroup", reqScimGroup))
	if reqScimGroup.GetDisplayName() == "" {
		return nil, fmt.Errorf("%w: displayName is blank", spec.ErrInvalidValue)
	}
	reqMembers := reqScimGroup.GetMembers()
	for _, m := range reqMembers {
		err := checkGroupMemberEmail(ctx, m)
		if err != nil {
			return nil, fmt.Errorf("%w: %s", spec.ErrInvalidValue, err.Error())
		}
	}
	groupMembersStr, err := genDbGroupMembersStr(ctx, reqMembers)
	if err != nil {
		tilog.Error(ctx, "ScimGroupSvc CreateScimGroup genDbGroupMembersStr error", zap.Any("err", err))
		return nil, err
	}
	groupProjectRoleStr, err := genDbGroupProjectRoleStr(ctx, map[uint64]ScimGroupProjectRole{})
	if err != nil {
		tilog.Error(ctx, "ScimGroupSvc CreateScimGroup genDbGroupProjectRoleStr error", zap.Any("err", err))
		return nil, err
	}

	entGroup := &entity.ScimGroup{
		GroupID:     genScimGroupUniqueID(orgID),
		OrgID:       entity.ID(orgID),
		DisplayName: reqScimGroup.GetDisplayName(),
		OrgRole:     "",
		ProjectRole: groupProjectRoleStr,
		Members:     groupMembersStr,
	}
	dbGroup, err := s.hub.ScimGroup.CreateScimGroup(ctx, s.db, entGroup)
	if err != nil {
		tilog.Error(ctx, "ScimGroupSvc CreateScimGroup CreateScimGroup error", zap.Any("scimGroup", entGroup), zap.Any("err", err))
		return nil, err
	}
	tilog.Info(ctx, "ScimGroupSvc CreateScimGroup CreateScimGroup result", zap.Any("dbGroup", dbGroup))

	/*if len(reqMembers) > 0 {
		err = s.persistGroupMembers2RoleBinding(ctx, reqMembers, orgID, entGroup.OrgRole, entGroup.ProjectRole)
		if err != nil {
			tilog.Error(ctx, "ScimGroupSvc CreateScimGroup addGroupMembers2RoleBinding error", zap.Any("scimGroup", entGroup), zap.Any("err", err))
			return nil, err
		}
	}*/

	reqScimGroup.SetID(entGroup.GroupID)
	reqScimGroup.SetMeta(ScimGroupMeta{
		ResourceType: "Group",
	})
	tilog.Info(ctx, "ScimGroupSvc CreateScimGroup end", zap.Any("rsp", reqScimGroup))
	return reqScimGroup, nil
}

func checkGroupMemberEmail(ctx context.Context, m ScimGroupMember) error {
	orgId, userID, err := convScimUserUniqueID(m.Value)
	if err != nil {
		tilog.Error(ctx, "create group check member email convScimUserUniqueID error", zap.Any("groupMember", m), zap.Any("err", err))
		return err
	}
	svc := GetScimCommonSvc()
	user, err := svc.GetUser(ctx, "", userID)
	if err != nil {
		tilog.Error(ctx, "create group check member email convScimUserUniqueID error", zap.Any("groupMember", m), zap.Any("err", err))
		return err
	}
	if user == nil {
		tilog.Info(ctx, "create group check member not found user", zap.Any("groupMember", m))
		return errs.NewNotFound("not found user", nil)
	}
	provisionMsg, err := checkEmailDomainForScimProvision(ctx, orgId, user.Email)
	tilog.Info(ctx, "create group check member email checkEmailDomainForScimProvision result", zap.Any("provisionMsg", provisionMsg), zap.Any("err", err))
	if err != nil {
		return err
	}
	if provisionMsg != "" {
		return errs.NewBadRequest(provisionMsg, nil)
	}
	return nil
}

func genDbGroupProjectRoleStr(ctx context.Context, projectId2RoleMap map[uint64]ScimGroupProjectRole) (string, error) {
	if projectId2RoleMap == nil {
		projectId2RoleMap = map[uint64]ScimGroupProjectRole{}
	}
	scimProjectRoleMarshal := &ScimGroupProjectRoleMarshal{
		ProjectRoleMap: projectId2RoleMap,
	}
	marshal, err := json.Marshal(scimProjectRoleMarshal)
	if err != nil {
		tilog.Error(ctx, "marshal group project role error", zap.Any("scimProjectRoleMarshal", scimProjectRoleMarshal), zap.Any("err", err))
		return "", err
	}
	return string(marshal), nil
}

func genDbGroupMembersStr(ctx context.Context, members []ScimGroupMember) (string, error) {
	id2MemberMap := map[string]ScimGroupMember{}
	for _, m := range members {
		id2MemberMap[m.Value] = m
	}
	scimMemberMarshal := &ScimGroupMemberMarshal{
		MemberMap: id2MemberMap,
	}
	marshal, err := json.Marshal(scimMemberMarshal)
	if err != nil {
		tilog.Error(ctx, "marshal group members error", zap.Any("scimMemberMarshal", scimMemberMarshal), zap.Any("err", err))
		return "", err
	}
	return string(marshal), nil
}

func (s *ScimGroupSvc) persistGroupMembers2RoleBinding(ctx context.Context, members []ScimGroupMember, orgID uint64, orgRole, projectRole string) error {
	var userIDs []uint64
	for _, member := range members {
		_, userID, err := convScimUserUniqueID(member.Value)
		if err != nil {
			tilog.Error(ctx, "persistGroupMembers2RoleBinding convScimUserUniqueID error", zap.Any("groupMember", member), zap.Any("err", err))
			continue
		}
		userIDs = append(userIDs, userID)
	}
	users, err := s.hub.User.ListUsers(ctx, s.db, userIDs, []entity.UserStatus{})
	if err != nil {
		tilog.Error(ctx, "persistGroupMembers2RoleBinding ListUsers error", zap.Any("userIDs", userIDs), zap.Any("err", err))
		return err
	}

	projects, err := s.hub.Project.ListActiveProjectsByOrgID(ctx, s.db, orgID)
	if err != nil {
		tilog.Error(ctx, "persistGroupMembers2RoleBinding ListActiveProjectsByOrgID error", zap.Any("orgID", orgID), zap.Any("err", err))
		return err
	}
	var projectBindings []roleBinding
	for _, p := range projects {
		binding := roleBinding{
			ScopeID:           p.ID.Uint64(),
			Role:              projectRole,
			RoleBindingStatus: entity.RoleBindingStatusActive,
		}
		projectBindings = append(projectBindings, binding)
	}

	orgBinding := roleBinding{
		ScopeID:           orgID,
		Role:              orgRole,
		RoleBindingStatus: entity.RoleBindingStatusActive,
	}

	for _, user := range users {
		// just persist active user to role binding
		if user.Status != entity.UserStatusActive {
			tilog.Error(ctx, "persistGroupMembers2RoleBinding fail, user is not active", zap.Any("user", user))
			continue
		}
		rbUser := roleBindingUser{
			UserID:          user.ID.Uint64(),
			Email:           user.Email,
			OrgID:           orgID,
			OrgBinding:      orgBinding,
			ProjectBindings: projectBindings,
		}
		err := GetScimCommonSvc().PersistRoleBindings(ctx, &rbUser)
		if err != nil {
			tilog.Error(ctx, "persistGroupMembers2RoleBinding PersistRoleBindings error", zap.Any("rbUser", rbUser))
		} else {
			tilog.Info(ctx, "persistGroupMembers2RoleBinding PersistRoleBindings success", zap.Any("rbUser", rbUser))
		}
	}

	return nil
}

func (s *ScimGroupSvc) GetResource(ctx context.Context, orgID uint64, req *service.GetRequest) (map[string]interface{}, error) {
	tilog.Info(ctx, "ScimGroupSvc GetScimGroup start", zap.Any("orgID", orgID), zap.Any("req", req))
	if err := checkSupportGroup(ctx); err != nil {
		return nil, err
	}

	dbGroup, err := s.hub.ScimGroup.GetScimGroupByGroupID(ctx, s.db, req.ResourceID)
	if err != nil {
		tilog.Error(ctx, "ScimGroupSvc GetScimGroup GetScimGroupByGroupID error", zap.Any("groupID", req.ResourceID), zap.Any("err", err))
		return nil, err
	}
	if dbGroup == nil {
		tilog.Info(ctx, "ScimGroupSvc GetScimGroup group not found", zap.Any("groupID", req.ResourceID))
		return nil, fmt.Errorf("%w: group not found", spec.ErrNotFound)
	}

	scimGroupData := convDbGroup2ScimGroupData(*dbGroup)
	resource := map[string]interface{}(scimGroupData)
	tilog.Info(ctx, "ScimGroupSvc GetScimGroup end", zap.Any("rsp", resource))

	return resource, nil
}

func (s *ScimGroupSvc) ReplaceResource(ctx context.Context, orgID uint64, req *service.ReplaceRequest) (*ScimUpdateResponse, error) {
	tilog.Info(ctx, "ScimGroupSvc ReplaceScimGroup start", zap.Any("orgID", orgID), zap.Any("resourceID", req.ResourceID))
	if err := checkSupportGroup(ctx); err != nil {
		return nil, err
	}
	reqScimGroup := ScimGroupData{}
	err := json.NewDecoder(req.PayloadSource).Decode(&reqScimGroup)
	if err != nil {
		tilog.Info(ctx, "ScimGroupSvc ReplaceScimGroup decode payload to scim group error", zap.Any("err", err))
		return nil, err
	}
	tilog.Info(ctx, "ScimGroupSvc ReplaceScimGroup get reqScimGroup", zap.Any("reqScimGroup", reqScimGroup))

	dbGroup, err := s.hub.ScimGroup.GetScimGroupByGroupID(ctx, s.db, req.ResourceID)
	if err != nil {
		tilog.Error(ctx, "ScimGroupSvc ReplaceScimGroup GetScimGroupByGroupID error", zap.Any("groupID", req.ResourceID), zap.Any("err", err))
		return nil, err
	}
	if dbGroup == nil {
		tilog.Info(ctx, "ScimGroupSvc ReplaceScimGroup group not found", zap.Any("groupID", req.ResourceID))
		return nil, fmt.Errorf("%w: group not found", spec.ErrNotFound)
	}
	dbScimGroup := convDbGroup2ScimGroupData(*dbGroup)
	tilog.Info(ctx, "ScimGroupSvc ReplaceScimGroup get dbScimGroup", zap.Any("dbScimGroup", dbScimGroup))

	replaceScimGroup := ScimGroupData{}
	_ = copier.Copy(&replaceScimGroup, &dbScimGroup)
	replaced := false

	if dbScimGroup.GetDisplayName() != reqScimGroup.GetDisplayName() {
		tilog.Info(ctx, "ScimGroupSvc ReplaceScimGroup displayName changed", zap.Any("reqDisplayName", reqScimGroup.GetDisplayName()), zap.Any("dbDisplayName", dbScimGroup.GetDisplayName()))
		replaced = true
		replaceScimGroup.SetDisplayName(reqScimGroup.GetDisplayName())
		dbGroup.DisplayName = reqScimGroup.GetDisplayName()
	}

	reqMembers := reqScimGroup.GetMembers()
	// display is blank for azure
	reqMembers = setGroupMemberDisplay(ctx, reqMembers)

	dbMembers := dbScimGroup.GetMembers()
	memberChanged := false
	if len(reqMembers) != len(dbMembers) {
		memberChanged = true
	} else {
		dbMemberMap := map[string]ScimGroupMember{}
		for _, m := range dbMembers {
			dbMemberMap[m.Value] = m
		}
		for _, m := range reqMembers {
			if _, ok := dbMemberMap[m.Value]; !ok {
				memberChanged = true
				break
			}
		}
	}
	if memberChanged {
		tilog.Info(ctx, "ScimGroupSvc ReplaceScimGroup groupMembers changed", zap.Any("reqMembers", reqMembers), zap.Any("dbMembers", dbMembers))
		replaced = true
		replaceScimGroup.SetMembers(reqMembers)
		membersStr, err := genDbGroupMembersStr(ctx, reqMembers)
		if err != nil {
			tilog.Error(ctx, "ScimGroupSvc ReplaceScimGroup genDbGroupMembersStr error", zap.Any("reqMembers", reqMembers), zap.Any("err", err))
			return nil, err
		}
		dbGroup.Members = membersStr

		err = s.replaceGroupMembers2RoleBinding(ctx, reqMembers, dbMembers, orgID, dbGroup.OrgRole, dbGroup.ProjectRole)
		if err != nil {
			tilog.Error(ctx, "ScimGroupSvc ReplaceScimGroup replaceGroupMembers2RoleBinding error", zap.Any("reqMembers", reqMembers), zap.Any("dbMembers", dbMembers), zap.Any("err", err))
			return nil, err
		}
	}

	if replaced {
		err = s.hub.ScimGroup.UpdateScimGroup(ctx, s.db, dbGroup)
		if err != nil {
			tilog.Error(ctx, "ScimGroupSvc ReplaceScimGroup UpdateScimGroup error", zap.Any("dbGroup", dbGroup), zap.Any("err", err))
			return nil, err
		}
	}

	rsp := ScimUpdateResponse{
		Updated:  replaced,
		Ref:      dbScimGroup,
		Resource: replaceScimGroup,
	}
	tilog.Info(ctx, "ScimGroupSvc ReplaceScimGroup end", zap.Any("rsp", rsp))

	return &rsp, nil
}

func (s *ScimGroupSvc) replaceGroupMembers2RoleBinding(ctx context.Context, reqMembers, dbMembers []ScimGroupMember, orgID uint64, orgRole, projectRole string) error {
	groupProjectRoles, err := convDbGroupProjectRoleStr2RoleSlice(ctx, projectRole)
	if err != nil {
		tilog.Error(ctx, "replaceGroupMembers2RoleBinding convDbGroupProjectRoleStr2RoleSlice error", zap.Any("projectRole", projectRole), zap.Any("err", err))
		return err
	}
	if orgRole == "" && len(groupProjectRoles) == 0 {
		tilog.Info(ctx, "replaceGroupMembers2RoleBinding ignore, not set orgRole projectRole on group", zap.Any("orgID", orgID))
		return nil
	}
	reqMemberMap := map[string]ScimGroupMember{}
	for _, m := range reqMembers {
		reqMemberMap[m.Value] = m
	}
	dbMemberMap := map[string]ScimGroupMember{}
	for _, m := range dbMembers {
		dbMemberMap[m.Value] = m
	}

	var projectBindings []roleBinding
	for _, p := range groupProjectRoles {
		binding := roleBinding{
			ScopeID:           p.ProjectID,
			Role:              p.Role,
			RoleBindingStatus: entity.RoleBindingStatusActive,
		}
		projectBindings = append(projectBindings, binding)
	}
	orgBinding := roleBinding{
		ScopeID:           orgID,
		Role:              orgRole,
		RoleBindingStatus: entity.RoleBindingStatusActive,
	}
	svc := GetScimCommonSvc()
	// foreach reqMembers and add role binding when no exist in dbMembers
	for _, m := range reqMembers {
		if _, ok := dbMemberMap[m.Value]; ok {
			continue
		}
		_, userID, err := convScimUserUniqueID(m.Value)
		if err != nil {
			tilog.Error(ctx, "replaceGroupMembers2RoleBinding convScimUserUniqueID error", zap.Any("groupMember", m), zap.Any("err", err))
			continue
		}
		user, err := svc.GetUser(ctx, "", userID)
		if err != nil {
			tilog.Error(ctx, "replaceGroupMembers2RoleBinding convScimUserUniqueID error", zap.Any("groupMember", m), zap.Any("err", err))
			continue
		}
		if user == nil || user.Status != entity.UserStatusActive {
			tilog.Warn(ctx, "replaceGroupMembers2RoleBinding user not exist or status not active, ignore to persist role binding", zap.Any("groupMember", m), zap.Any("err", err))
			continue
		}
		provisionMsg, err := checkEmailDomainForScimProvision(ctx, orgID, user.Email)
		tilog.Info(ctx, "replaceGroupMembers2RoleBinding checkEmailDomainForScimProvision result", zap.Any("provisionMsg", provisionMsg), zap.Any("err", err))
		if err != nil {
			tilog.Error(ctx, "replaceGroupMembers2RoleBinding check email domain error", zap.Any("groupMember", m), zap.Any("orgID", orgID), zap.Any("email", user.Email), zap.Any("err", err))
			continue
		}
		if provisionMsg != "" {
			tilog.Info(ctx, "replaceGroupMembers2RoleBinding check email domain fail", zap.Any("groupMember", m), zap.Any("orgID", orgID), zap.Any("email", user.Email), zap.Any("provisionMsg", provisionMsg))
			continue
		}
		rbUser := roleBindingUser{
			UserID:          user.ID.Uint64(),
			Email:           user.Email,
			OrgID:           orgID,
			OrgBinding:      orgBinding,
			ProjectBindings: projectBindings,
		}
		err = svc.PersistRoleBindings(ctx, &rbUser)
		if err != nil {
			tilog.Error(ctx, "replaceGroupMembers2RoleBinding PersistRoleBindings error", zap.Any("rbUser", rbUser))
		} else {
			tilog.Info(ctx, "replaceGroupMembers2RoleBinding PersistRoleBindings success", zap.Any("rbUser", rbUser))
		}
		if err == nil {
			go func() {
				_ = comsvc.SendUserRoleBindingChangeEvent(context.Background(), userID, orgID, "ScimProvision ReplaceGroupMember")
			}()
		}
	}

	// foreach dbMembers and delete from role binding when no exist in reqMembers
	for _, m := range dbMembers {
		if _, ok := reqMemberMap[m.Value]; ok {
			continue
		}
		_, userID, err := convScimUserUniqueID(m.Value)
		if err != nil {
			tilog.Error(ctx, "replaceGroupMembers2RoleBinding convScimUserUniqueID error", zap.Any("groupMember", m), zap.Any("err", err))
			continue
		}
		user, oBinding, pBindings, err := svc.GetUserAndRoleBinding(ctx, orgID, userID)
		if err != nil {
			tilog.Info(ctx, "replaceGroupMembers2RoleBinding GetUserAndRoleBinding error", zap.Any("groupMember", m), zap.Any("err", err))
			continue
		}
		if orgRole != "" {
			// reset org role to org:member when set org role before
			_, err = svc.UpdateRoleBindingRole(ctx, *oBinding, entity.RoleOrgMember, user.Email, orgID)
			if err != nil {
				tilog.Error(ctx, "replaceGroupMembers2RoleBinding UpdateRoleBindingRole to org:member error", zap.Any("groupMember", m), zap.Any("err", err))
				continue
			}
		}
		if len(groupProjectRoles) > 0 && len(pBindings) > 0 {
			var toDelProjectBindings []entity.RoleBinding
			groupProjectRoleMap, _ := convDbGroupProjectRoleStr2RoleMap(ctx, projectRole)
			for _, pbinding := range pBindings {
				if _, ok := groupProjectRoleMap[pbinding.ScopeID.Uint64()]; ok {
					// delete project role when set project role before
					toDelProjectBindings = append(toDelProjectBindings, pbinding)
				}
			}
			err = svc.UpdateRoleBindingStatus(ctx, user, nil, toDelProjectBindings, entity.RoleBindingStatusDelete)
			if err != nil {
				tilog.Error(ctx, "replaceGroupMembers2RoleBinding UpdateProjectRoleBindingStatus to inactive error", zap.Any("groupMember", m))
			}
		}
		go func() {
			_ = comsvc.SendUserRoleBindingChangeEvent(context.Background(), userID, orgID, "ScimProvision ReplaceGroupMember")
		}()
	}

	return nil
}
func (s *ScimGroupSvc) PatchResource(ctx context.Context, orgID uint64, req *service.PatchRequest) (*ScimUpdateResponse, error) {
	tilog.Info(ctx, "ScimGroupSvc PatchScimGroup start", zap.Any("orgID", orgID), zap.Any("resourceID", req.ResourceID))
	if err := checkSupportGroup(ctx); err != nil {
		return nil, err
	}
	reqPayload, err := parsePatchRequest(req)
	if err != nil {
		tilog.Error(ctx, "ScimGroupSvc PatchScimGroup parsePatchRequest error", zap.Any("req", req), zap.Any("err", err))
		return nil, err
	}
	if err = reqPayload.Validate(); err != nil {
		tilog.Info(ctx, "ScimGroupSvc PatchScimGroup reqPayload validate not pass", zap.Any("reqPayload", reqPayload), zap.Any("err", err))
		return nil, err
	}
	if len(reqPayload.Operations) == 0 {
		tilog.Info(ctx, "ScimGroupSvc PatchScimGroup no value for operation", zap.Any("reqPayload", reqPayload))
		return nil, fmt.Errorf("%w: no value for operation", spec.ErrInvalidSyntax)
	}

	dbGroup, err := s.hub.ScimGroup.GetScimGroupByGroupID(ctx, s.db, req.ResourceID)
	if err != nil {
		tilog.Error(ctx, "ScimGroupSvc PatchScimGroup GetScimGroupByGroupID error", zap.Any("id", req.ResourceID), zap.Any("err", err))
		return nil, err
	}
	if dbGroup == nil {
		tilog.Info(ctx, "ScimGroupSvc PatchScimGroup group not found", zap.Any("id", req.ResourceID))
		return nil, fmt.Errorf("%w: group not found", spec.ErrNotFound)
	}

	dbScimGroup := convDbGroup2ScimGroupData(*dbGroup)
	tilog.Info(ctx, "ScimGroupSvc PatchScimGroup get dbScimGroup", zap.Any("dbScimGroup", dbScimGroup))

	dbGroup, replaceScimGroup, patched, err := s.handlePatchGroupAttributes(ctx, reqPayload.Operations, orgID, dbGroup, dbScimGroup)
	if err != nil {
		tilog.Warn(ctx, "ScimGroupSvc PatchScimGroup handlePatchGroupAttributes error", zap.Any("dbScimGroup", dbScimGroup), zap.Any("err", err))
		return nil, err

	}

	err = s.hub.ScimGroup.UpdateScimGroup(ctx, s.db, dbGroup)
	if err != nil {
		tilog.Error(ctx, "ScimGroupSvc PatchScimGroup UpdateScimGroup error", zap.Any("dbGroup", dbGroup), zap.Any("err", err))
		return nil, err
	}

	rsp := ScimUpdateResponse{
		Updated:  patched,
		Ref:      dbScimGroup,
		Resource: replaceScimGroup,
	}
	tilog.Info(ctx, "ScimGroupSvc PatchScimGroup end", zap.Any("rsp", rsp))

	return &rsp, nil
}

func (s *ScimGroupSvc) handlePatchGroupAttributes(ctx context.Context, operations []service.PatchOperation, orgID uint64, dbGroup *entity.ScimGroup, dbScimGroup ScimGroupData) (*entity.ScimGroup, ScimGroupData, bool, error) {
	replaceScimGroup := ScimGroupData{}
	_ = copier.Copy(&replaceScimGroup, &dbScimGroup)
	patched := false

	for _, operation := range operations {
		tilog.Info(ctx, "ScimGroupSvc handlePatchGroupAttributes parse operation", zap.Any("operation", operation))
		switch strings.ToLower(operation.Op) {
		case "add":
			// just support add members in group
			if strings.ToLower(operation.Path) != membersPath {
				tilog.Info(ctx, "ScimGroupSvc handlePatchGroupAttributes not support add path, ignore", zap.Any("value", operation.Value), zap.Any("path", operation.Path))
				continue
			}
			var members []ScimGroupMember
			err := json.Unmarshal(operation.Value, &members)
			if err != nil {
				tilog.Error(ctx, "ScimGroupSvc handlePatchGroupAttributes unmarshal operation value error", zap.Any("value", operation.Value), zap.Any("err", err))
				continue
			}
			// display is blank for azure
			members = setGroupMemberDisplay(ctx, members)
			if len(members) == 0 {
				continue
			}
			dbMembers, err := convDbGroupMemberStr(dbGroup.Members)
			if err != nil {
				tilog.Error(ctx, "ScimGroupSvc handlePatchGroupAttributes convScimGroupMemberStr error", zap.Any("dbMemberStr", dbGroup.Members), zap.Any("err", err))
				continue
			}
			dbMembers = append(dbMembers, members...)
			membersStr, err := genDbGroupMembersStr(ctx, dbMembers)
			if err != nil {
				tilog.Error(ctx, "ScimGroupSvc handlePatchGroupAttributes genDbGroupMembersStr error", zap.Any("dbMembers", dbMembers), zap.Any("err", err))
				continue
			}
			dbGroup.Members = membersStr
			patched = true
			replaceScimGroup.SetMembers(dbMembers)

			err = s.replaceGroupMembers2RoleBinding(ctx, members, []ScimGroupMember{}, orgID, dbGroup.OrgRole, dbGroup.ProjectRole)
			if err != nil {
				tilog.Error(ctx, "ScimGroupSvc handlePatchGroupAttributes addGroupMembers2RoleBinding error", zap.Any("members", members), zap.Any("err", err))
				continue
			}
		case "remove":
			// only support remove members
			// 1. remove part members
			//{
			//	"op": "Remove",
			//	"path": "members",
			//	"value": [{
			//		"$ref": null,
			//		"value": "f648f8d5ea4e4cd38e9c"
			//}
			// 2. remove all members
			//{
			//	"op":"remove","path":"members"
			//}
			// 3. remove members on path
			//{
			//	"op":"remove",
			//	"path": "members[value eq\"2819c223...919d-413861904646\"]"
			//}
			if !strings.HasPrefix(operation.Path, membersPath) {
				tilog.Info(ctx, "ScimGroupSvc handlePatchGroupAttributes not support remove path, ignore", zap.Any("value", operation.Value), zap.Any("path", operation.Path))
				continue
			}
			memberMarshal := ScimGroupMemberMarshal{}
			err := json.Unmarshal([]byte(dbGroup.Members), &memberMarshal)
			if err != nil {
				tilog.Warn(context.Background(), "ScimGroupSvc handlePatchGroupAttributes parse group members fail", zap.Any("memberStr", dbGroup.Members), zap.Any("err", err))
				continue
			}
			memberMap := memberMarshal.MemberMap

			if operation.Path == membersPath {
				if len(operation.Value) == 0 {
					// remove all members
					membersStr, _ := genDbGroupMembersStr(ctx, []ScimGroupMember{})
					dbGroup.Members = membersStr
					patched = true
					replaceScimGroup.SetMembers([]ScimGroupMember{})

					var dbMembers []ScimGroupMember
					for _, m := range memberMap {
						dbMembers = append(dbMembers, m)
					}
					err = s.replaceGroupMembers2RoleBinding(ctx, []ScimGroupMember{}, dbMembers, orgID, dbGroup.OrgRole, dbGroup.ProjectRole)
					if err != nil {
						tilog.Warn(context.Background(), "delete all group members in role binding error", zap.Any("dbMembers", dbMembers), zap.Any("err", err))
						continue
					}
				} else {
					// remove part members
					var toDelMembers []ScimGroupMember
					err := json.Unmarshal(operation.Value, &toDelMembers)
					if err != nil {
						tilog.Error(ctx, "ScimGroupSvc handlePatchGroupAttributes unmarshal operation value error", zap.Any("value", operation.Value), zap.Any("err", err))
						continue
					}
					if len(toDelMembers) == 0 {
						continue
					}
					for _, member := range toDelMembers {
						delete(memberMap, member.Value)
					}
					memberMarshal.MemberMap = memberMap
					marshal, err := json.Marshal(&memberMarshal)
					if err != nil {
						tilog.Error(ctx, "marshal group members error", zap.Any("scimMemberMarshal", memberMarshal), zap.Any("err", err))
						continue
					}
					dbGroup.Members = string(marshal)
					patched = true
					var dbMembers []ScimGroupMember
					for _, m := range memberMap {
						dbMembers = append(dbMembers, m)
					}
					replaceScimGroup.SetMembers(dbMembers)

					err = s.replaceGroupMembers2RoleBinding(ctx, []ScimGroupMember{}, toDelMembers, orgID, dbGroup.OrgRole, dbGroup.ProjectRole)
					if err != nil {
						tilog.Warn(context.Background(), "delete group member in role binding error", zap.Any("toDelMembers", toDelMembers), zap.Any("err", err))
						continue
					}
				}
			} else {
				// remove a single member
				//{ "op":"remove", "path": "members[value eq\"2819c223...919d-413861904646\"]"}
				submatch := removeMembersPathReg.FindStringSubmatch(operation.Path)
				if len(submatch) != 2 {
					tilog.Warn(ctx, "ScimGroupSvc handlePatchGroupAttributes parse remove path fail", zap.Any("path", operation.Path))
					continue
				}
				memberId := submatch[1]
				toDelMember, ok := memberMap[memberId]
				if !ok {
					continue
				}
				delete(memberMap, memberId)
				memberMarshal.MemberMap = memberMap
				marshal, err := json.Marshal(&memberMarshal)
				if err != nil {
					tilog.Error(ctx, "marshal group members error", zap.Any("scimMemberMarshal", memberMarshal), zap.Any("err", err))
					continue
				}
				dbGroup.Members = string(marshal)
				patched = true
				var dbMembers []ScimGroupMember
				for _, m := range memberMap {
					dbMembers = append(dbMembers, m)
				}
				replaceScimGroup.SetMembers(dbMembers)

				err = s.replaceGroupMembers2RoleBinding(ctx, []ScimGroupMember{}, []ScimGroupMember{toDelMember}, orgID, dbGroup.OrgRole, dbGroup.ProjectRole)
				if err != nil {
					tilog.Warn(context.Background(), "delete single group member in role binding error", zap.Any("dbMember", toDelMember), zap.Any("err", err))
					continue
				}
			}

		case "replace":
			if operation.Path == "members" {
				// replace all group member
				var members []ScimGroupMember
				err := json.Unmarshal(operation.Value, &members)
				if err != nil {
					tilog.Error(ctx, "ScimGroupSvc handlePatchGroupAttributes unmarshal operation value error", zap.Any("value", operation.Value), zap.Any("err", err))
					continue
				}
				// display is blank for azure
				members = setGroupMemberDisplay(ctx, members)

				dbMembers, err := convDbGroupMemberStr(dbGroup.Members)
				if err != nil {
					tilog.Error(ctx, "ScimGroupSvc handlePatchGroupAttributes convScimGroupMemberStr error", zap.Any("dbMemberStr", dbGroup.Members), zap.Any("err", err))
					return nil, nil, false, err
				}
				// delete origin db members
				err = s.replaceGroupMembers2RoleBinding(ctx, []ScimGroupMember{}, dbMembers, orgID, dbGroup.OrgRole, dbGroup.ProjectRole)
				if err != nil {
					tilog.Warn(context.Background(), "delete all group members in role binding error", zap.Any("dbMembers", dbMembers), zap.Any("err", err))
					continue
				}
				// add replaced members
				err = s.replaceGroupMembers2RoleBinding(ctx, members, []ScimGroupMember{}, orgID, dbGroup.OrgRole, dbGroup.ProjectRole)
				if err != nil {
					tilog.Warn(context.Background(), "replace all group members in role binding error", zap.Any("members", members), zap.Any("err", err))
					continue
				}
				membersStr, err := genDbGroupMembersStr(ctx, members)
				if err != nil {
					tilog.Warn(context.Background(), "genDbGroupMembersStr error", zap.Any("members", members), zap.Any("err", err))
					continue
				}
				dbGroup.Members = membersStr
				patched = true
				replaceScimGroup.SetMembers(members)
			} else if operation.Path == "" {
				// replace group name
				//{
				//	"op": "replace",
				//	"value": {
				//		"id": "abf4dd94-a4c0-4f67-89c9-76b03340cb9b",
				//		"displayName": "Test SCIMv2"
				//	}
				//}
				var opValue = map[string]interface{}{}
				err := json.Unmarshal(operation.Value, &opValue)
				if err != nil {
					tilog.Error(ctx, "ScimGroupSvc handlePatchGroupAttributes unmarshal operation value error", zap.Any("value", operation.Value), zap.Any("err", err))
					continue
				}
				groupIdI, ok := opValue["id"]
				if !ok {
					tilog.Warn(ctx, "ScimGroupSvc handlePatchGroupAttributes not found group id from operation", zap.Any("value", operation.Value))
					continue
				}
				groupId, ok := groupIdI.(string)
				if !ok {
					tilog.Warn(ctx, "ScimGroupSvc handlePatchGroupAttributes id not string", zap.Any("value", operation.Value))
					continue
				}
				if dbGroup.GroupID != groupId {
					tilog.Warn(ctx, "ScimGroupSvc handlePatchGroupAttributes operation groupId not eq dbGroup groupID", zap.Any("value", operation.Value), zap.Any("dbGroupId", dbGroup.GroupID))
					continue
				}
				displayNameI, ok := opValue["displayName"]
				if !ok {
					tilog.Warn(ctx, "ScimGroupSvc handlePatchGroupAttributes not found displayName from operation", zap.Any("value", operation.Value))
					continue
				}
				displayName, ok := displayNameI.(string)
				if !ok {
					tilog.Warn(ctx, "ScimGroupSvc handlePatchGroupAttributes displayName not string", zap.Any("value", operation.Value))
					continue
				}
				dbGroup.DisplayName = displayName
				patched = true
				replaceScimGroup.SetDisplayName(displayName)

			} else if operation.Path == "displayName" {
				// replace group name
				//{
				//	"op": "Replace",
				//	"path": "displayName",
				//	"value": "1879db59-3bdf-4490-ad68-ab880a269474updatedDisplayName"
				//}
				val := string(operation.Value)
				val = strings.Trim(val, "\"")
				if !strings.HasPrefix(val, dbGroup.GroupID) {
					tilog.Warn(ctx, "ScimGroupSvc handlePatchGroupAttributes replace displayName value invalid", zap.Any("value", operation.Value))
					continue
				}
				updatedDisplayName := strings.TrimPrefix(val, dbGroup.GroupID)

				dbGroup.DisplayName = updatedDisplayName
				patched = true
				replaceScimGroup.SetDisplayName(updatedDisplayName)

			} else {
				tilog.Warn(ctx, "ScimGroupSvc handlePatchGroupAttributes unknown operation path, ignore", zap.Any("op", operation.Path))
				continue
			}
		default:
			tilog.Info(ctx, "ScimGroupSvc handlePatchGroupAttributes unknown operation Op, ignore", zap.Any("op", operation.Op))
			continue
		}
	}

	return dbGroup, replaceScimGroup, patched, nil
}

func setGroupMemberDisplay(ctx context.Context, members []ScimGroupMember) []ScimGroupMember {
	var retMembers []ScimGroupMember
	for _, m := range members {
		display := m.Display
		if display == "" {
			display = getDisplayByValue(ctx, m.Value)
			tilog.Info(ctx, "setGroupMemberDisplay result", zap.Any("value", m.Value), zap.Any("display", display))
		}
		retM := ScimGroupMember{
			Value:   m.Value,
			Display: display,
		}

		retMembers = append(retMembers, retM)
	}
	return retMembers
}

func getDisplayByValue(ctx context.Context, value string) string {
	_, userID, err := convScimUserUniqueID(value)
	if err != nil {
		tilog.Error(ctx, "getDisplayByValue convScimUserUniqueID error", zap.Any("value", value), zap.Any("err", err))
		return ""
	}
	user, err := GetScimCommonSvc().GetUser(ctx, "", userID)
	if err != nil {
		tilog.Error(ctx, "getDisplayByValue GetUser error", zap.Any("userID", userID), zap.Any("err", err))
		return ""
	}
	return user.Email
}

func (s *ScimGroupSvc) DeleteResource(ctx context.Context, orgID uint64, req *service.DeleteRequest) (map[string]interface{}, error) {
	tilog.Info(ctx, "ScimGroupSvc DeleteScimGroup start", zap.Any("orgID", orgID), zap.Any("req", req))
	if err := checkSupportGroup(ctx); err != nil {
		return nil, err
	}

	dbGroup, err := s.hub.ScimGroup.GetScimGroupByGroupID(ctx, s.db, req.ResourceID)
	if err != nil {
		tilog.Error(ctx, "ScimGroupSvc DeleteScimGroup GetScimGroupByGroupID error", zap.Any("id", req.ResourceID), zap.Any("err", err))
		return nil, err
	}
	if dbGroup == nil {
		tilog.Info(ctx, "ScimGroupSvc DeleteScimGroup group not found", zap.Any("id", req.ResourceID))
		return nil, fmt.Errorf("%w: group not found", spec.ErrNotFound)
	}
	err = s.hub.ScimGroup.DeleteScimGroupByID(ctx, s.db, dbGroup.ID.Uint64())
	if err != nil {
		tilog.Error(ctx, "ScimGroupSvc DeleteScimGroup DeleteScimGroupByID error", zap.Any("id", req.ResourceID), zap.Any("err", err))
		return nil, err
	}
	scimGroupData := convDbGroup2ScimGroupData(*dbGroup)

	// delete group not to unbind role bindings
	/*groupMembers := scimGroupData.GetMembers()
	for _, gm := range groupMembers {
		_, userID, err := convScimUserUniqueID(gm.Value)
		if err != nil {
			tilog.Error(ctx, "ScimGroupSvc DeleteResource convScimUserUniqueID error", zap.Any("groupMember", gm), zap.Any("err", err))
			continue
		}
		user, orgBinding, projectBindings, err := GetScimCommonSvc(s.db, s.hub).GetUserAndRoleBinding(ctx, orgID, userID)
		if err != nil {
			tilog.Error(ctx, "ScimGroupSvc DeleteResource GetUserAndRoleBinding error", zap.Any("groupMember", gm), zap.Any("err", err))
			continue
		}
		err = GetScimCommonSvc(s.db, s.hub).UpdateRoleBindingStatus(ctx, user, orgBinding, projectBindings, entity.RoleBindingStatusInactive)
		//err = GetScimCommonSvc(s.db, s.hub).DeleteRoleBindings(ctx, user, orgBinding, projectBindings)
		if err != nil {
			tilog.Error(ctx, "ScimGroupSvc DeleteResource DeleteRoleBindings error", zap.Any("groupMember", gm), zap.Any("err", err))
			continue
		}
		tilog.Info(ctx, "ScimGroupSvc DeleteResource UpdateRoleBindingStatus success", zap.Any("groupMember", gm))
	}*/

	tilog.Info(ctx, "ScimGroupSvc DeleteScimGroup end", zap.Any("rsp", scimGroupData))
	return scimGroupData, nil
}

func (s *ScimGroupSvc) UpdateScimGroupRole(ctx context.Context, groupID, orgRole, projectRole string, projectIds []uint64) error {
	dbGroup, err := s.hub.ScimGroup.GetScimGroupByGroupID(ctx, s.db, groupID)
	if err != nil {
		tilog.Error(ctx, "ScimGroupSvc FrontUpdateScimGroupRole GetScimGroupByGroupID error", zap.Any("groupID", groupID), zap.Any("err", err))
		return err
	}
	if dbGroup == nil {
		tilog.Info(ctx, "ScimGroupSvc FrontUpdateScimGroupRole group not found", zap.Any("groupID", groupID))
		return errs.NewNotFound("scim group not found", nil)
	}
	// org role already set org:owner, no need to update project role
	if projectRole != "" && dbGroup.OrgRole == entity.RoleOrgOwner {
		return errs.NewForbidden("The group role is already set as org owner, so there is no need to change the role for the project.", nil)
	}
	if orgRole != "" {
		dbGroup.OrgRole = orgRole
	}
	if projectRole != "" && len(projectIds) > 0 {
		projectRoleMap, err := convDbGroupProjectRoleStr2RoleMap(ctx, dbGroup.ProjectRole)
		if err != nil {
			tilog.Error(ctx, "ScimGroupSvc FrontUpdateScimGroupRole convDbGroupProjectRoleStr2RoleSlice error", zap.Any("projectRole", dbGroup.ProjectRole), zap.Any("err", err))
			return err
		}
		for _, pid := range projectIds {
			role := ScimGroupProjectRole{
				ProjectID: pid,
				Role:      projectRole,
			}
			projectRoleMap[pid] = role
		}
		projectRoleStr, err := genDbGroupProjectRoleStr(ctx, projectRoleMap)
		if err != nil {
			tilog.Error(ctx, "ScimGroupSvc FrontUpdateScimGroupRole genDbGroupProjectRoleStr error", zap.Any("projectRoleMap", projectRoleMap), zap.Any("err", err))
			return err
		}
		dbGroup.ProjectRole = projectRoleStr
	}
	err = s.hub.ScimGroup.UpdateScimGroup(ctx, s.db, dbGroup)
	if err != nil {
		tilog.Error(ctx, "ScimGroupSvc FrontUpdateScimGroupRole UpdateScimGroup error", zap.Any("dbGroup", dbGroup), zap.Any("err", err))
		return err
	}

	members, err := convDbGroupMemberStr(dbGroup.Members)
	if err != nil {
		tilog.Error(ctx, "ScimGroupSvc FrontUpdateScimGroupRole convScimGroupMemberStr error", zap.Any("dbGroup", dbGroup), zap.Any("err", err))
		return err
	}
	svc := GetScimCommonSvc()
	for _, member := range members {
		_, userID, err := convScimUserUniqueID(member.Value)
		if err != nil {
			tilog.Error(ctx, "ScimGroupSvc FrontUpdateScimGroupRole convScimUserUniqueID error", zap.Any("groupMember", member), zap.Any("err", err))
			continue
		}
		user, orgBinding, _, err := svc.GetUserAndRoleBinding(ctx, dbGroup.OrgID.Uint64(), userID)
		if err != nil {
			tilog.Info(ctx, "ScimGroupSvc FrontUpdateScimGroupRole GetUserAndRoleBinding error", zap.Any("groupMember", member), zap.Any("err", err))
			continue
		}
		if orgRole != "" {
			_, err = svc.UpdateRoleBindingRole(ctx, *orgBinding, orgRole, user.Email, dbGroup.OrgID.Uint64())
			if err != nil {
				tilog.Error(ctx, "ScimGroupSvc FrontUpdateScimGroupRole UpdateOrgRoleBindingRole error", zap.Any("groupMember", member), zap.Any("binding", orgBinding), zap.Any("err", err))
				continue
			}
		}
		if projectRole != "" && len(projectIds) > 0 {
			var pBindings []roleBinding
			for _, pid := range projectIds {
				pBinding := roleBinding{
					ScopeID: pid,
					Role:    projectRole,
				}
				pBindings = append(pBindings, pBinding)
			}
			rbUser := &roleBindingUser{
				UserID:          user.ID.Uint64(),
				Email:           user.Email,
				OrgID:           dbGroup.OrgID.Uint64(),
				ProjectBindings: pBindings,
			}
			err = svc.PersistRoleBindings(ctx, rbUser)
			if err != nil {
				tilog.Error(ctx, "ScimGroupSvc FrontUpdateScimGroupRole UpdateProjectRoleBindingRole error", zap.Any("groupMember", member), zap.Any("rbUser", rbUser), zap.Any("err", err))
				continue
			}
		}
		go func() {
			_ = comsvc.SendUserRoleBindingChangeEvent(context.Background(), userID, dbGroup.OrgID.Uint64(), "UpdateScimGroupRole")
		}()

	}

	return nil

}

func (s *ScimGroupSvc) ListScimGroups(ctx context.Context, orgID uint64, offset, limit int) (int, []*central.ScimGroup, error) {
	total, err := s.hub.ScimGroup.CountScimGroupsByOrgIDSelectiveDisplayName(ctx, s.db, orgID, "")
	if err != nil {
		tilog.Error(ctx, "ScimGroupSvc FrontListScimGroups CountScimGroupsByOrgID error", zap.Any("orgID", orgID), zap.Any("offset", offset), zap.Any("limit", limit), zap.Any("err", err))
		return 0, nil, err
	}
	var groups []entity.ScimGroup
	if offset == 0 && limit == 0 {
		groups, err = s.hub.ScimGroup.ListScimGroupsByOrgID(ctx, s.db, orgID)
	} else {
		groups, err = s.hub.ScimGroup.BatchGetScimGroupsByOrgIDSelectiveDisplayName(ctx, s.db, orgID, "", offset, limit)
	}
	if err != nil {
		tilog.Error(ctx, "ScimGroupSvc FrontListScimGroups ListScimGroupsByOrgID error", zap.Any("orgID", orgID), zap.Any("offset", offset), zap.Any("limit", limit), zap.Any("err", err))
		return 0, nil, err
	}
	var retGroups []*central.ScimGroup
	for _, group := range groups {
		members, err := convDbGroupMemberStr(group.Members)
		if err != nil {
			tilog.Error(ctx, "ScimGroupSvc FrontListScimGroups convScimGroupMemberStr error", zap.Any("group", group), zap.Any("err", err))
			continue
		}
		var cms []*central.ScimGroupMember
		for _, m := range members {
			cm := central.ScimGroupMember{
				Value:   m.Value,
				Display: m.Display,
			}
			cms = append(cms, &cm)
		}
		projectRoles, err := convDbGroupProjectRoleStr2RoleSlice(ctx, group.ProjectRole)
		if err != nil {
			tilog.Error(ctx, "ScimGroupSvc FrontListScimGroups convDbGroupProjectRoleStr error", zap.Any("group", group), zap.Any("err", err))
			continue
		}
		var prs []*central.ScimGroupProjectRole
		for _, r := range projectRoles {
			pRole := r.Role
			if group.OrgRole == entity.RoleOrgOwner {
				pRole = entity.RoleProjectOwner
			}
			pr := central.ScimGroupProjectRole{
				ProjectId:   r.ProjectID,
				ProjectRole: pRole,
			}
			prs = append(prs, &pr)
		}
		cg := &central.ScimGroup{
			Id:           group.ID.Uint64(),
			OrgId:        group.OrgID.Uint64(),
			GroupId:      group.GroupID,
			GroupName:    group.DisplayName,
			OrgRole:      group.OrgRole,
			ProjectRoles: prs,
			Members:      cms,
		}
		retGroups = append(retGroups, cg)
	}
	return total, retGroups, nil
}
