package scim

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

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

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

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

	"github.com/jinzhu/copier"

	"github.com/imulab/go-scim/pkg/v2/service"
	"github.com/imulab/go-scim/pkg/v2/spec"
	"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/jsm"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

var (
	filterEmailReg = regexp.MustCompile("userName eq \"(.+)\"")
)

type ScimUserSvcIface interface {
	SearchScimResources
	CreateScimResource
	GetScimResource
	ReplaceScimResource
	PatchScimResource
	DeleteScimResource
}

var (
	scimUserSvcInitOnce sync.Once
	scimUserSvcInstance ScimUserSvcIface
)

func GetScimUserSvc() (ScimUserSvcIface, error) {
	scimUserSvcInitOnce.Do(func() {
		scimUserSvcInstance = NewScimUserSvc(database.DBConn.DB, dao.GetDaoHub())
	})
	return scimUserSvcInstance, nil
}

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

func NewScimUserSvc(db *gorm.DB, hub *dao.DaoHub) ScimUserSvcIface {
	return &ScimUserSvc{
		db:  db,
		hub: hub,
	}
}

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

	user, err := GetScimCommonSvc().GetUser(ctx, email, 0)
	if err != nil {
		tilog.Error(ctx, "ScimUserSvc ListScimUsers GetUser error", zap.Any("email", email), zap.Any("err", err))
		return nil, err
	}
	if email != "" && user == nil {
		tilog.Info(ctx, "ScimUserSvc ListScimUsers user not exist", zap.Any("email", email))
		return genQueryResponse(0, 1, 0, []map[string]interface{}{}), nil
	}
	var userID uint64 = 0
	if user != nil {
		userID = user.ID.Uint64()
	}
	total, rbUsers, err := GetScimCommonSvc().GetOrgUsers(ctx, orgID, userID, req.Pagination)
	if err != nil {
		tilog.Error(ctx, "ScimUserSvc ListScimUsers GetOrgUsers error", zap.Any("orgID", orgID), zap.Any("user", user), zap.Any("err", err))
		return nil, err
	}
	if len(rbUsers) == 0 {
		tilog.Info(ctx, "ScimUserSvc ListScimUsers org users not exist", zap.Any("orgID", orgID), zap.Any("user", user))
		return genQueryResponse(total, req.Pagination.StartIndex, 0, []map[string]interface{}{}), nil
	}

	var resources []map[string]interface{}
	for _, rbUser := range rbUsers {
		scimUser := convRoleBindingUser2ScimUser(&rbUser)
		resource := map[string]interface{}(scimUser)
		resources = append(resources, resource)
	}
	rsp := genQueryResponse(total, req.Pagination.StartIndex, len(rbUsers), resources)

	tilog.Info(ctx, "ScimUserSvc ListScimUsers end", zap.Any("rsp", rsp))

	return rsp, nil

}

func genQueryResponse(total, startIndex, itemsPerPage int, resources []map[string]interface{}) *ScimQueryResponse {
	return &ScimQueryResponse{
		TotalResults: total,
		StartIndex:   startIndex,
		ItemsPerPage: itemsPerPage,
		Resources:    resources,
	}
}

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

func genRoleBindingUser(u *entity.User, orgRb *entity.RoleBinding) *roleBindingUser {
	return &roleBindingUser{
		UserID:     u.ID.Uint64(),
		Email:      u.Email,
		FirstName:  u.FirstName,
		LastName:   u.LastName,
		UserStatus: string(u.Status),
		OrgBinding: roleBinding{
			ScopeID:           orgRb.ScopeID.Uint64(),
			RoleBindingStatus: orgRb.Status,
			Role:              orgRb.Role,
		},
	}
}

func (s *ScimUserSvc) CreateResource(ctx context.Context, orgID uint64, req *service.CreateRequest) (map[string]interface{}, error) {
	tilog.Info(ctx, "ScimUserSvc CreateScimUser start", zap.Any("orgID", orgID), zap.Any("req", req))
	if req == nil || req.PayloadSource == nil {
		tilog.Info(ctx, "ScimUserSvc CreateScimUser req payload is nil")
		return nil, fmt.Errorf("%w: no payload found", spec.ErrInvalidValue)
	}
	reqScimUser := ScimUserData{}
	err := json.NewDecoder(req.PayloadSource).Decode(&reqScimUser)
	if err != nil {
		tilog.Info(ctx, "ScimUserSvc CreateScimUser decode payload to scim user error", zap.Any("err", err))
		return nil, err
	}
	tilog.Info(ctx, "ScimUserSvc CreateScimUser get scim user from payload", zap.Any("scimUser", reqScimUser), zap.Any("entInfo", reqScimUser.GetEntInfo()))
	if reqScimUser.GetUserName() == "" {
		return nil, fmt.Errorf("%w: userName is blank", spec.ErrInvalidValue)
	}
	rbUser := convScimUser2RoleBindingUser(reqScimUser, orgID)
	tilog.Info(ctx, "ScimUserSvc CreateScimUser convert rbUser", zap.Any("rbUser", rbUser))

	provisionMsg, err := checkEmailDomainForScimProvision(ctx, rbUser.OrgID, rbUser.Email)
	tilog.Info(ctx, "ScimUserSvc CreateScimUser checkEmailDomainForScimProvision result", zap.Any("provisionMsg", provisionMsg), zap.Any("err", err))
	if err != nil {
		return nil, fmt.Errorf("%w: check email domain error", spec.ErrInternal)
	}
	if provisionMsg != "" {
		return nil, fmt.Errorf("%w: %s", spec.ErrInvalidValue, provisionMsg)
	}

	dbUser, err := GetScimCommonSvc().PersistUser(ctx, rbUser)
	if err != nil {
		tilog.Error(ctx, "ScimUserSvc CreateScimUser persistUser error", zap.Any("rbUser", rbUser), zap.Any("dbUser", dbUser), zap.Any("err", err))
		return nil, err
	}
	tilog.Info(ctx, "ScimUserSvc CreateScimUser persistUser success", zap.Any("dbUser", dbUser))
	rbUser.UserID = dbUser.ID.Uint64()

	err = GetScimCommonSvc().PersistRoleBindings(ctx, rbUser)
	if err != nil {
		tilog.Error(ctx, "ScimUserSvc CreateScimUser persistRoleBindings error", zap.Any("rbUser", rbUser), zap.Any("err", err))
		return nil, err
	}
	tilog.Info(ctx, "ScimUserSvc CreateScimUser persistRoleBindings success")

	go func() {
		_ = comsvc.SendUserRoleBindingChangeEvent(context.Background(), dbUser.ID.Uint64(), rbUser.OrgID, "ScimProvision CreateUser")
	}()

	uniqueID := genScimUserUniqueID(rbUser.OrgBinding.ScopeID, rbUser.UserID)
	reqScimUser.SetID(uniqueID)
	reqScimUser.SetActive(true)
	reqScimUser.SetMeta(ScimUserMeta{
		ResourceType: "User",
	})

	if rbUser.OrgBinding.Role != "" {
		_ = userevent.PublishAddUserToOrgEvent(ctx, rbUser.OrgBinding.ScopeID, rbUser.UserID, rbUser.OrgBinding.Role)
	}

	go jsm.GetJsmClientSingleton().ProcessSSOUserRegistration(ctx, dbUser, rbUser.OrgBinding.ScopeID)

	tilog.Info(ctx, "ScimUserSvc CreateScimUser end", zap.Any("rsp", reqScimUser))
	return reqScimUser, nil
}

func (s *ScimUserSvc) GetResource(ctx context.Context, orgID uint64, req *service.GetRequest) (map[string]interface{}, error) {
	tilog.Info(ctx, "ScimUserSvc GetScimUser start", zap.Any("orgID", orgID), zap.Any("req", req))
	orgId, userId, err := convScimUserUniqueID(req.ResourceID)
	if err != nil {
		tilog.Error(ctx, "ScimUserSvc GetScimUser convUniqueID error", zap.Any("id", req.ResourceID), zap.Any("err", err))
		return nil, err
	}
	user, orgBinding, _, err := GetScimCommonSvc().GetUserAndRoleBinding(ctx, orgId, userId)
	if err != nil {
		tilog.Warn(ctx, "ScimUserSvc GetScimUser getUserAndOrgRoleBinding error", zap.Any("orgId", orgId), zap.Any("userId", userId), zap.Any("err", err))
		return nil, err
	}
	scimUser := convRoleBindingUser2ScimUser(genRoleBindingUser(user, orgBinding))
	tilog.Info(ctx, "ScimUserSvc GetScimUser end", zap.Any("scimUser", scimUser))
	return scimUser, nil
}

func (s *ScimUserSvc) ReplaceResource(ctx context.Context, orgID uint64, req *service.ReplaceRequest) (*ScimUpdateResponse, error) {
	tilog.Info(ctx, "ScimUserSvc ReplaceScimUser start", zap.Any("orgID", orgID), zap.Any("resourceID", req.ResourceID))
	reqScimUser := ScimUserData{}
	err := json.NewDecoder(req.PayloadSource).Decode(&reqScimUser)
	if err != nil {
		tilog.Info(ctx, "ScimUserSvc ReplaceScimUser decode payload to scim user error", zap.Any("err", err))
		return nil, err
	}
	tilog.Info(ctx, "ScimUserSvc ReplaceScimUser get reqScimUser", zap.Any("reqScimUser", reqScimUser), zap.Any("entInfo", reqScimUser.GetEntInfo()))

	orgId, userId, err := convScimUserUniqueID(req.ResourceID)
	if err != nil {
		tilog.Info(ctx, "ScimUserSvc ReplaceScimUser convUniqueID error", zap.Any("id", req.ResourceID), zap.Any("err", err))
		return nil, err
	}
	user, orgBinding, projectBindings, err := GetScimCommonSvc().GetUserAndRoleBinding(ctx, orgId, userId)
	if err != nil {
		tilog.Info(ctx, "ScimUserSvc ReplaceScimUser getUserAndOrgRoleBinding error", zap.Any("orgId", orgId), zap.Any("userId", userId), zap.Any("err", err))
		return nil, err
	}
	dbScimUser := convRoleBindingUser2ScimUser(genRoleBindingUser(user, orgBinding))
	tilog.Info(ctx, "ScimUserSvc ReplaceScimUser get dbScimUser", zap.Any("dbScimUser", dbScimUser))

	provisionMsg, err := checkEmailDomainForScimProvision(ctx, orgId, dbScimUser.GetUserName())
	tilog.Info(ctx, "ScimUserSvc ReplaceScimUser checkEmailDomainForScimProvision result", zap.Any("provisionMsg", provisionMsg), zap.Any("err", err))
	if err != nil {
		return nil, fmt.Errorf("%w: check email domain error", spec.ErrInternal)
	}
	if provisionMsg != "" {
		return nil, fmt.Errorf("%w: %s", spec.ErrInvalidValue, provisionMsg)
	}

	replaceScimUser := ScimUserData{}
	_ = copier.Copy(&replaceScimUser, &dbScimUser)
	replaced := false

	dbScimUserName := dbScimUser.GetName()
	reqScimUserName := reqScimUser.GetName()
	if dbScimUserName.GivenName != reqScimUserName.GivenName ||
		dbScimUserName.FamilyName != reqScimUserName.FamilyName {
		tilog.Info(ctx, "ScimUserSvc ReplaceScimUser name changed")
		replaced = true
		replaceScimUser.SetName(ScimUserName{
			GivenName:  reqScimUserName.GivenName,
			FamilyName: reqScimUserName.FamilyName,
		})

		user.FirstName = reqScimUserName.GivenName
		user.LastName = reqScimUserName.FamilyName
		err = user.Update(s.db,
			entity.UserDBSchema.FirstName,
			entity.UserDBSchema.LastName,
		)
		if err != nil {
			tilog.Error(ctx, "ScimUserSvc ReplaceScimUser, name changed UpdateUser error", zap.Any("user", user), zap.Any("err", err))
			return nil, err
		}
		tilog.Info(ctx, "ScimUserSvc ReplaceScimUser name changed UpdateUser success", zap.Any("user", user))
	}

	if dbScimUser.GetActive() != reqScimUser.GetActive() {
		tilog.Info(ctx, "ScimUserSvc ReplaceScimUser active changed", zap.Any("reqActive", reqScimUser.GetActive()))
		replaced = true
		replaceScimUser.SetActive(reqScimUser.GetActive())

		oldOrgBindingStatus := orgBinding.Status
		if reqScimUser.GetActive() {
			err = GetScimCommonSvc().UpdateRoleBindingStatus(ctx, user, orgBinding, projectBindings, entity.RoleBindingStatusActive)
			if err != nil && oldOrgBindingStatus != entity.RoleBindingStatusActive {
				go func() {
					_ = userevent.PublishAddUserToOrgEvent(ctx, orgId, userId, orgBinding.Role)
					jsmClient := jsm.GetJsmClientSingleton()
					if jsmClient.IsEnable() {
						notifyRemoveUserFromOrgErr := jsmClient.NotifyInviteUserToOrgMessage(ctx, orgId, userId, orgBinding.Role)
						if notifyRemoveUserFromOrgErr != nil {
							tilog.Error(ctx, "Sync Jsm failed when removing User from org", zap.Uint64("user", userId), zap.Uint64("org", orgId))
						}
					}
				}()
			}
		} else {
			err = GetScimCommonSvc().UpdateRoleBindingStatus(ctx, user, orgBinding, projectBindings, entity.RoleBindingStatusDelete)
			//err = GetScimCommonSvc(s.db, s.hub).DeleteRoleBindings(ctx, user, orgBinding, projectBindings)
			if err != nil && oldOrgBindingStatus == entity.RoleBindingStatusActive {
				go func() {
					_ = userevent.PublishRemoveUserFromOrgEvent(ctx, orgId, userId)
					jsmClient := jsm.GetJsmClientSingleton()
					if jsmClient.IsEnable() {
						notifyRemoveUserFromOrgErr := jsmClient.NotifyRemoveUserFromOrgMessage(ctx, orgId, userId)
						if notifyRemoveUserFromOrgErr != nil {
							tilog.Error(ctx, "Sync Jsm failed when removing User from org", zap.Uint64("user", userId), zap.Uint64("org", orgId))
						}
					}
				}()
			}
		}
		if err != nil {
			tilog.Error(ctx, "ScimUserSvc ReplaceScimUser active changed UpdateRoleBindingStatus error", zap.Any("user", user), zap.Any("orgBinding", orgBinding), zap.Any("projectBindings", projectBindings), zap.Any("err", err))
			return nil, err
		}
	}

	reqEntInfo := reqScimUser.GetEntInfo()
	dbEntInfo := dbScimUser.GetEntInfo()
	replaceEntInfo := ScimEntInfo{
		OrgRole:      dbEntInfo.OrgRole,
		ProjectRoles: dbEntInfo.ProjectRoles,
	}
	lowerReqOrgRole := strings.ToLower(reqEntInfo.OrgRole)
	lowerDbOrgRole := strings.ToLower(dbEntInfo.OrgRole)
	if lowerReqOrgRole != "" && lowerReqOrgRole != lowerDbOrgRole {
		tilog.Info(ctx, "ScimUserSvc ReplaceScimUser orgRole changed")
		replaced = true
		replaceEntInfo.OrgRole = reqEntInfo.OrgRole

		rbRole := convScimRole2RbRole(reqEntInfo.OrgRole)
		_, err = GetScimCommonSvc().UpdateRoleBindingRole(ctx, *orgBinding, rbRole, user.Email, orgID)
		if err != nil {
			tilog.Error(ctx, "ScimUserSvc ReplaceScimUser updateRoleBindingRole error", zap.Any("rbRole", rbRole), zap.Any("scimRole", reqEntInfo.OrgRole), zap.Any("err", err))
			return nil, err
		}
		tilog.Info(ctx, "ScimUserSvc ReplaceScimUser orgRole changed UpdateRoleBindingRole success", zap.Any("orgBinding", orgBinding))

		if orgBinding.Status == entity.RoleBindingStatusActive {
			commandAction := entity.CommandActionDelete
			if rbRole == entity.RoleOrgOwner {
				commandAction = entity.CommandActionCreate
			}
			err := s.hub.DBUser.CreateCommandUserAction(ctx, s.db, user.ID.Uint64(), user.ID.Uint64(), user.Email, orgId, 0, entity.CommandStatusWaiting, commandAction, entity.RoleBindingTargetUser, entity.RoleBindingTargetUser)
			if err != nil {
				tilog.Error(ctx, "create scim user CreateCommandUserAction error", zap.Any("user", user.Email), zap.Any("resourceID", orgID), zap.Any("err", err))
			} else {
				tilog.Info(ctx, "create scim user CreateCommandUserAction success", zap.Any("user", user.Email), zap.Any("resourceID", orgID))
			}
		}
	}

	if len(reqEntInfo.ProjectRoles) > 0 {
		projects, err := s.hub.Project.ListActiveProjectsByOrgID(ctx, s.db, orgID)
		if err != nil {
			tilog.Error(ctx, "ScimUserSvc ReplaceScimUser ListActiveProjectsByOrgID error", zap.Any("orgID", orgID), zap.Any("err", err))
			return nil, err
		}
		validOrgProjectIds := map[uint64]struct{}{}
		for _, p := range projects {
			validOrgProjectIds[p.ID.Uint64()] = struct{}{}
		}
		var projectIds []entity.ID
		for pid, pRole := range reqEntInfo.ProjectRoles {
			if pRole == "" {
				continue
			}
			parsePid, err := strconv.ParseUint(pid, 10, 64)
			if err != nil {
				tilog.Warn(ctx, "ScimUserSvc ReplaceScimUser project id in scim user fail", zap.Any("projectId", pid), zap.Any("err", err))
				continue
			}
			if _, ok := validOrgProjectIds[parsePid]; !ok {
				tilog.Warn(ctx, "ScimUserSvc ReplaceScimUser project id not belong to org, ignore", zap.Any("projectId", pid), zap.Any("orgId", orgID))
				continue
			}

			projectIds = append(projectIds, entity.ID(parsePid))
		}
		dbRoleBindings, err := s.hub.Role.ListRoleBindings(ctx, s.db, role.RoleBindingFilter{
			ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeProject},
			ScopeIDs:   projectIds,
			TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
			TargetIDs:  []entity.ID{user.ID},
		})
		if err != nil {
			tilog.Error(ctx, "ScimUserSvc ReplaceScimUser ScimUserSvc ReplaceScimUser updateRoleBindingRole error", zap.Any("userID", user.ID), zap.Any("projectIds", projectIds), zap.Any("err", err))
			return nil, err
		}
		for _, binding := range dbRoleBindings {
			scimRole := reqEntInfo.ProjectRoles[fmt.Sprintf("%d", binding.ScopeID)]
			rbRole := convScimRole2RbRole(scimRole)
			if binding.Role != rbRole {
				tilog.Info(ctx, "ScimUserSvc ReplaceScimUser projectRole changed")
				_, err = GetScimCommonSvc().UpdateRoleBindingRole(ctx, binding, rbRole, user.Email, orgID)
				if err != nil {
					tilog.Error(ctx, "ScimUserSvc ReplaceScimUser updateRoleBindingRole error", zap.Any("rbRole", rbRole), zap.Any("scimRole", reqEntInfo.OrgRole), zap.Any("err", err))
					return nil, err
				}
				tilog.Info(ctx, "ScimUserSvc ReplaceScimUser projectRole changed UpdateRoleBindingRole success", zap.Any("projectBinding", binding))

				if binding.Status == entity.RoleBindingStatusActive {
					_ = s.hub.DBUser.CreateCommandUserAction(ctx, s.db, user.ID.Uint64(), user.ID.Uint64(), user.Email, orgId, binding.ScopeID.Uint64(), entity.CommandStatusWaiting, entity.CommandActionDelete, entity.RoleBindingTargetUser, entity.RoleBindingTargetUser)
					_ = s.hub.DBUser.CreateCommandUserAction(ctx, s.db, user.ID.Uint64(), user.ID.Uint64(), user.Email, orgId, binding.ScopeID.Uint64(), entity.CommandStatusWaiting, entity.CommandActionCreate, entity.RoleBindingTargetUser, entity.RoleBindingTargetUser)
				}
			}
		}
	}

	go func() {
		_ = comsvc.SendUserRoleBindingChangeEvent(context.Background(), userId, orgId, "ScimProvision ReplaceUser")
	}()

	replaceScimUser.SetEntInfo(replaceEntInfo)

	rsp := &ScimUpdateResponse{
		Updated:  replaced,
		Ref:      dbScimUser,
		Resource: replaceScimUser,
	}
	tilog.Info(ctx, "ScimUserSvc ReplaceScimUser end", zap.Any("rsp", rsp))
	return rsp, nil

}

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

	orgId, userId, err := convScimUserUniqueID(req.ResourceID)
	if err != nil {
		tilog.Info(ctx, "ScimUserSvc PatchScimUser convUniqueID error", zap.Any("id", req.ResourceID), zap.Any("err", err))
		return nil, err
	}
	user, orgBinding, projectBindings, err := GetScimCommonSvc().GetUserAndRoleBinding(ctx, orgId, userId)
	if err != nil {
		tilog.Error(ctx, "ScimUserSvc PatchScimUser getUserAndOrgRoleBinding error", zap.Any("orgId", orgId), zap.Any("userId", userId), zap.Any("err", err))
		return nil, err
	}
	dbScimUser := convRoleBindingUser2ScimUser(genRoleBindingUser(user, orgBinding))
	tilog.Info(ctx, "ScimUserSvc PatchScimUser get dbScimUser", zap.Any("dbScimUser", dbScimUser))

	provisionMsg, err := checkEmailDomainForScimProvision(ctx, orgId, dbScimUser.GetUserName())
	tilog.Info(ctx, "ScimUserSvc PatchScimUser checkEmailDomainForScimProvision result", zap.Any("provisionMsg", provisionMsg), zap.Any("err", err))
	if err != nil {
		return nil, fmt.Errorf("%w: check email domain error", spec.ErrInternal)
	}
	if provisionMsg != "" {
		return nil, fmt.Errorf("%w: %s", spec.ErrInvalidValue, provisionMsg)
	}

	replaceScimUser, patched, err := s.handlePatchUserAttributes(ctx, reqPayload.Operations, user, orgBinding, projectBindings, dbScimUser)
	if err != nil {
		tilog.Warn(ctx, "ScimUserSvc PatchScimUser getUserAndOrgRoleBinding error", zap.Any("orgId", orgId), zap.Any("userId", userId), zap.Any("err", err))
		return nil, err
	}

	go func() {
		_ = comsvc.SendUserRoleBindingChangeEvent(context.Background(), userId, orgId, "ScimProvision PatchUser")
	}()

	rsp := &ScimUpdateResponse{
		Updated:  patched,
		Ref:      dbScimUser,
		Resource: replaceScimUser,
	}
	tilog.Info(ctx, "ScimUserSvc PatchScimUser end", zap.Any("rsp", rsp))
	return rsp, nil
}

func (s *ScimUserSvc) handlePatchUserAttributes(ctx context.Context, operations []service.PatchOperation, user *entity.User, orgBinding *entity.RoleBinding, projectBindings []entity.RoleBinding, dbScimUser ScimUserData) (ScimUserData, bool, error) {
	replaceScimUser := ScimUserData{}
	_ = copier.Copy(&replaceScimUser, &dbScimUser)
	patched := false

	for _, operation := range operations {
		tilog.Info(ctx, "ScimUserSvc handlePatchUserAttributes parse operation", zap.Any("operation", operation))
		active, exist, err := getUserActiveAttribute(operation)
		if err != nil {
			tilog.Warn(ctx, "ScimUserSvc handlePatchUserAttributes getUserActiveAttribute err", zap.Any("err", err))
			return nil, false, err
		}
		// only support patch active status
		if !exist {
			continue
		}

		if dbScimUser.GetActive() != active {
			tilog.Info(ctx, "ScimUserSvc PatchScimUser active changed", zap.Any("reqActive", active))
			patched = true
			replaceScimUser.SetActive(active)

			oldOrgBindingStatus := orgBinding.Status
			orgId := orgBinding.ScopeID.Uint64()
			userId := user.ID.Uint64()
			if active {
				err = GetScimCommonSvc().UpdateRoleBindingStatus(ctx, user, orgBinding, projectBindings, entity.RoleBindingStatusActive)
				if err != nil && oldOrgBindingStatus != entity.RoleBindingStatusActive {
					go func() {
						_ = userevent.PublishAddUserToOrgEvent(ctx, orgId, userId, orgBinding.Role)
						jsmClient := jsm.GetJsmClientSingleton()
						if jsmClient.IsEnable() {
							notifyRemoveUserFromOrgErr := jsmClient.NotifyInviteUserToOrgMessage(ctx, orgId, userId, orgBinding.Role)
							if notifyRemoveUserFromOrgErr != nil {
								tilog.Error(ctx, "Sync Jsm failed when removing User from org", zap.Uint64("user", userId), zap.Uint64("org", orgId))
							}
						}
					}()
				}
			} else {
				err = GetScimCommonSvc().UpdateRoleBindingStatus(ctx, user, orgBinding, projectBindings, entity.RoleBindingStatusDelete)
				//err = GetScimCommonSvc(s.db, s.hub).DeleteRoleBindings(ctx, user, orgBinding, projectBindings)
				if err != nil && oldOrgBindingStatus == entity.RoleBindingStatusActive {
					go func() {
						_ = userevent.PublishRemoveUserFromOrgEvent(ctx, orgId, userId)
						jsmClient := jsm.GetJsmClientSingleton()
						if jsmClient.IsEnable() {
							notifyRemoveUserFromOrgErr := jsmClient.NotifyRemoveUserFromOrgMessage(ctx, orgId, userId)
							if notifyRemoveUserFromOrgErr != nil {
								tilog.Error(ctx, "Sync Jsm failed when removing User from org", zap.Uint64("user", userId), zap.Uint64("org", orgId))
							}
						}
					}()
				}
			}
			if err != nil {
				tilog.Error(ctx, "ScimUserSvc PatchScimUser active changed updateRoleBindingStatus error", zap.Any("user", user), zap.Any("orgBinding", orgBinding), zap.Any("projectBindings", projectBindings), zap.Any("err", err))
				return nil, false, err
			}
		}
	}

	return replaceScimUser, patched, nil
}

// okta example
//
//	{
//		"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
//		"Operations": [{
//			"op": "replace",
//			"value": {
//				"active": false
//			}
//		}]
//	}
//
// azure example
//
//	{
//		"Operations": [
//			{
//				"op": "Replace",
//				"path": "active",
//				"value": false
//			}
//		],
//		"schemas": [
//			"urn:ietf:params:scim:api:messages:2.0:PatchOp"
//		]
//	}
//
// only support patch user active status by replace op
func getUserActiveAttribute(operation service.PatchOperation) (active, exist bool, err error) {
	if strings.ToLower(operation.Op) != "replace" {
		return false, false, nil
	}
	if operation.Path == "active" {
		val := string(operation.Value)
		val = strings.Trim(val, "\"")
		active, err := strconv.ParseBool(val)
		if err != nil {
			return false, false, err
		}
		return active, true, nil
	}
	if operation.Path == "" {
		var opValue = map[string]interface{}{}
		err := json.Unmarshal(operation.Value, &opValue)
		if err != nil {
			return false, false, err
		}
		v, ok := opValue["active"]
		if !ok {
			return false, false, nil
		}
		active, ok := v.(bool)
		if !ok {
			return false, false, fmt.Errorf("%w: active type not bool", spec.ErrInvalidValue)
		}
		return active, true, nil
	}

	return false, false, nil
}

func parsePatchRequest(req *service.PatchRequest) (*service.PatchPayload, error) {
	if req == nil || req.PayloadSource == nil {
		return nil, fmt.Errorf("%w: no payload for patch service", spec.ErrInternal)
	}

	patch := new(service.PatchPayload)
	err := json.NewDecoder(req.PayloadSource).Decode(patch)
	if err != nil {
		return nil, fmt.Errorf("%w: failed to read request body", spec.ErrInternal)
	}
	return patch, nil
}

func (s *ScimUserSvc) DeleteResource(ctx context.Context, orgID uint64, req *service.DeleteRequest) (map[string]interface{}, error) {
	tilog.Info(ctx, "ScimUserSvc DeleteScimUser start", zap.Any("orgID", orgID), zap.Any("resourceID", req.ResourceID))
	orgId, userId, err := convScimUserUniqueID(req.ResourceID)
	if err != nil {
		tilog.Info(ctx, "ScimUserSvc DeleteScimUser convUniqueID error", zap.Any("id", req.ResourceID), zap.Any("err", err))
		return nil, err
	}
	user, orgBinding, projectBindings, err := GetScimCommonSvc().GetUserAndRoleBinding(ctx, orgId, userId)
	if err != nil {
		tilog.Warn(ctx, "ScimUserSvc DeleteScimUser getUserAndOrgRoleBinding error", zap.Any("id", req.ResourceID), zap.Any("err", err))
		return nil, err
	}
	dbScimUser := convRoleBindingUser2ScimUser(genRoleBindingUser(user, orgBinding))
	tilog.Info(ctx, "ScimUserSvc DeleteScimUser get dbScimUser", zap.Any("dbScimUser", dbScimUser))

	if orgBinding.Status != entity.RoleBindingStatusActive {
		tilog.Info(ctx, "ScimUserSvc DeleteScimUser dbScimUser not active, ignore")
		return dbScimUser, nil
	}

	err = GetScimCommonSvc().UpdateRoleBindingStatus(ctx, user, orgBinding, projectBindings, entity.RoleBindingStatusDelete)
	//err = GetScimCommonSvc(s.db, s.hub).DeleteRoleBindings(ctx, user, orgBinding, projectBindings)
	if err != nil {
		tilog.Error(ctx, "ScimUserSvc DeleteScimUser DeleteRoleBindings error", zap.Any("user", user), zap.Any("orgBinding", orgBinding), zap.Any("projectBindings", projectBindings), zap.Any("err", err))
		return nil, err
	}
	go func() {
		_ = userevent.PublishRemoveUserFromOrgEvent(ctx, orgId, userId)
		jsmClient := jsm.GetJsmClientSingleton()
		if jsmClient.IsEnable() {
			notifyRemoveUserFromOrgErr := jsmClient.NotifyRemoveUserFromOrgMessage(ctx, orgID, userId)
			if notifyRemoveUserFromOrgErr != nil {
				tilog.Error(ctx, "Sync Jsm failed when removing User from org", zap.Uint64("user", userId), zap.Uint64("org", orgID))
			}
		}
	}()
	tilog.Info(ctx, "ScimUserSvc DeleteScimUser DeleteRoleBindings success", zap.Any("user", user), zap.Any("orgBinding", orgBinding), zap.Any("projectBindings", projectBindings))

	deletedScimUser := convRoleBindingUser2ScimUser(genRoleBindingUser(user, orgBinding))
	tilog.Info(ctx, "ScimUserSvc DeleteScimUser end", zap.Any("rsp", deletedScimUser))
	return deletedScimUser, nil
}
