package permission

import (
	"context"
	"errors"
	"fmt"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"

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

	"github.com/tidbcloud/account/central/utils"
	"github.com/tidbcloud/account/commons/database"
	"github.com/tidbcloud/account/commons/metrics"
	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/account/internel/dao/role"
	apikeysvc "github.com/tidbcloud/account/internel/svc/apikey"
	"github.com/tidbcloud/account/internel/svc/rbac"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

const (
	SEPARATOR     = "/"
	REGEXP_NUMBER = "^\\d{1,}$"
	REGEXP_STR    = "^\\{.+\\}$"
)

var (
	regexpNumber = regexp.MustCompile(REGEXP_NUMBER)
	regexpStr    = regexp.MustCompile(REGEXP_STR)
)

var (
	ScopeTypeMapping = map[string]entity.ScopeType{
		"orgs":     entity.RoleBindingScopeOrg,
		"projects": entity.RoleBindingScopeProject,
		"dataapps": entity.RoleBindingScopeDataAPP,
		"tidbs":    entity.RoleBindingScopeTiDB,
	}
)

type RbacPermissionSvcIface interface {
	HasPermissionForAccessToken(ctx context.Context, accessToken, reqUri, resourceUri string) (*PermissionCheckRet, error)
	HasPermissionForUser(ctx context.Context, userId uint64, reqUri, resourceUri string) (*PermissionCheckRet, error)
	HasPermissionForApiKey(ctx context.Context, accessKey, reqUri, resourceUri string) (*PermissionCheckRet, error)
}

type PermissionCheckRet struct {
	HasPermission bool   `json:"has_permission"`
	Message       string `json:"message"`
}

var (
	rbacPermissionSvcInitOnce sync.Once
	rbacPermissionSvc         RbacPermissionSvcIface
)

func GetRbacPermissionSvc() RbacPermissionSvcIface {
	rbacPermissionSvcInitOnce.Do(func() {
		rbacPermissionSvc = &RbacPermissionSvc{}

	})
	return rbacPermissionSvc
}

type RbacPermissionSvc struct {
}

func (r *RbacPermissionSvc) HasPermissionForUser(ctx context.Context, userID uint64, reqUri, resourceUri string) (*PermissionCheckRet, error) {
	logger := tilog.WithContextL(ctx)
	logger.Info("HasPermissionForUser: start permission check", zap.Any("user_id", userID), zap.Any("req_uri", reqUri), zap.Any("resource_uri", resourceUri))
	// record metric
	start := time.Now()
	defer func() {
		metrics.RecPermissionCheckMetrics("HasPermissionForUser", "total", start, true)
		tilog.Info(ctx, "HasPermissionForUser: end permission check", zap.Any("cost", time.Since(start)))
	}()
	return checkIdentityPermission(ctx, entity.RoleBindingTargetUser, userID, reqUri, resourceUri, "HasPermissionForUser")
}

func (r *RbacPermissionSvc) HasPermissionForApiKey(ctx context.Context, accessKey, reqUri, resourceUri string) (*PermissionCheckRet, error) {
	tilog.Info(ctx, "HasPermissionForApiKey: start permission check", zap.Any("access_key", accessKey), zap.Any("req_uri", reqUri), zap.Any("resource_uri", resourceUri))
	// record metric
	start := time.Now()
	defer func() {
		metrics.RecPermissionCheckMetrics("HasPermissionForApiKey", "total", start, true)
		tilog.Info(ctx, "HasPermissionForApiKey: end permission check", zap.Any("cost", time.Since(start)))
	}()

	if accessKey == "" {
		return nil, errs.NewBadRequest("access_key is blank", nil)
	}

	// get api key ent
	ts := time.Now()
	keyEntity, err := apikeysvc.GetApiKeyEntWithCache(ctx, 0, accessKey)
	metrics.RecPermissionCheckMetrics("HasPermissionForApiKey", "get_api_key_ent", ts, false)
	if err != nil {
		tilog.Error(ctx, "HasPermissionForApiKey: get api key ent error", zap.Any("access_key", accessKey), zap.Any("err", err))
		return nil, err
	}
	if keyEntity == nil {
		tilog.Info(ctx, "HasPermissionForApiKey: api key not exist", zap.Any("access_key", accessKey), zap.Any("err", err))
		return nil, errs.NewNotFound("api key not exist", nil)
	}
	tilog.Info(ctx, "HasPermissionForApiKey: get api key result", zap.Any("api_key", keyEntity), zap.Any("err", err))

	return checkIdentityPermission(ctx, entity.RoleBindingTargetAPIKey, keyEntity.ID.Uint64(), reqUri, resourceUri, "HasPermissionForApiKey")
}

func checkIdentityPermission(ctx context.Context, targetType entity.TargetType, targetId uint64, reqUri, resourceUri, logpre string) (*PermissionCheckRet, error) {
	if targetId == 0 || reqUri == "" {
		return nil, errs.NewBadRequest("param invalid", nil)
	}

	// parse and check access scope
	accessScopeMap, matched, err := parseAndCheckAccessScope(ctx, reqUri, logpre)
	if err != nil {
		tilog.Error(ctx, fmt.Sprintf("%s: get and check access scope error", logpre), zap.Any("req_uri", reqUri), zap.Any("err", err))
		return nil, errs.NewBadRequest("parse access scope error", err)
	}
	if !matched {
		ret := &PermissionCheckRet{
			HasPermission: false,
			Message:       "scope resource not match",
		}
		tilog.Info(ctx, fmt.Sprintf("%s: permission check result", logpre), zap.Any("ret", ret), zap.Any("req_uri", reqUri))
		return ret, nil
	}

	if resourceUri == "" {
		return checkPermissionWithoutResourceUri(ctx, targetType, entity.ID(targetId), reqUri, accessScopeMap, logpre)
	}

	assignedRoleIds := getResourceUriAssignedRoleIds(ctx, resourceUri, logpre)
	if len(assignedRoleIds) == 0 {
		ret := &PermissionCheckRet{
			HasPermission: false,
			Message:       fmt.Sprintf("%s is not assigned to any roles", resourceUri),
		}
		tilog.Info(ctx, fmt.Sprintf("%s: permission check result", logpre), zap.Any("ret", ret))
		return ret, nil
	}

	var assignedScopeMap map[entity.ScopeType][]assignedScopeInfo
	var isImpersonate bool
	var impersonateInfo *assignedScopeInfo

	if targetType == entity.RoleBindingTargetUser {
		isImpersonate, impersonateInfo, err = getUserImpersonateScopeInfo(ctx, targetId, logpre)
		if err != nil {
			tilog.Error(ctx, fmt.Sprintf("%s: check user is impersonated error", logpre), zap.Any("user_id", targetId), zap.Any("err", err))
			return nil, err
		}
	}
	if isImpersonate {
		if !slice.Contain(assignedRoleIds, impersonateInfo.RoleId) {
			ret := &PermissionCheckRet{
				HasPermission: false,
				Message:       fmt.Sprintf("user is impersonated but resource_uri is not assigned to (%s-%d) role", impersonateInfo.Role, impersonateInfo.RoleId),
			}
			tilog.Info(ctx, fmt.Sprintf("%s: permission check result", logpre), zap.Any("ret", ret), zap.Any("resource_uri", resourceUri))
			return ret, nil
		}
		assignedScopeMap = map[entity.ScopeType][]assignedScopeInfo{
			impersonateInfo.ScopeType: {*impersonateInfo},
		}
	} else {
		// for api key and not impersonated user get assigned scopes from role_bindings table
		assignedScopeMap, err = getIdentityAssignedScopeInfo(ctx, targetType, targetId, assignedRoleIds, logpre)
		if err != nil {
			tilog.Error(ctx, fmt.Sprintf("%s: get identity assigned scopes error", logpre), zap.Any("target_type", targetType), zap.Any("target_id", targetId), zap.Any("role_ids", assignedRoleIds))
			return nil, err
		}
	}

	checkRet, err := checkAccessScopeMatchAssignedScope(ctx, accessScopeMap, assignedScopeMap, logpre)
	if err != nil {
		tilog.Error(ctx, fmt.Sprintf("%s: check access scope matched assigned scope error", logpre), zap.Any("err", err))
		return nil, err
	}

	tilog.Info(ctx, fmt.Sprintf("%s: permission check result", logpre), zap.Any("ret", checkRet), zap.Any("req_uri", reqUri))
	return checkRet, nil
}

func getResourceUriAssignedRoleIds(ctx context.Context, resourceUri string, logpre string) []uint64 {
	ts := time.Now()
	defer metrics.RecPermissionCheckMetrics(logpre, "get_resource_uri_assigned_role_ids", ts, false)

	assignedRoleIds, _ := rbac.GetRoleIdsByResourceUriFromCache(ctx, resourceUri)
	var uintAssignedRoleIds []uint64
	for rid := range assignedRoleIds {
		uintAssignedRoleIds = append(uintAssignedRoleIds, rid)
	}

	tilog.Info(ctx, fmt.Sprintf("%s: get resource_uri assigned role_ids from cache", logpre), zap.Any("resource_uri", resourceUri), zap.Any("role_ids", uintAssignedRoleIds))

	return uintAssignedRoleIds
}

func parseAndCheckAccessScope(ctx context.Context, reqUri string, logpre string) (map[entity.ScopeType]entity.ID, bool, error) {
	// parse req_uri to get access scope
	ts := time.Now()
	accessScopeMap, err := parseRbacUri(reqUri, ScopeTypeMapping)
	metrics.RecPermissionCheckMetrics(logpre, "parse_rbac_uri", ts, false)
	tilog.Info(ctx, fmt.Sprintf("%s: parse req uri result", logpre), zap.Any("req_uri", reqUri), zap.Any("access_scope_map", accessScopeMap), zap.Any("err", err))
	if err != nil {
		return nil, false, err
	}

	// access scope cascade resource check
	ts = time.Now()
	matched, msg, err := isScopeResourceMatch(ctx, accessScopeMap)
	metrics.RecPermissionCheckMetrics(logpre, "is_scope_resource_match", ts, false)
	tilog.Info(ctx, fmt.Sprintf("%s: is scope resource match check result", logpre), zap.Any("matched", matched), zap.Any("msg", msg), zap.Any("err", err))
	if err != nil {
		return nil, false, err
	}
	return accessScopeMap, matched, nil
}

type assignedScopeInfo struct {
	ScopeType entity.ScopeType
	ScopeId   uint64
	Role      string
	RoleId    uint64
}

func getUserImpersonateScopeInfo(ctx context.Context, userId uint64, logpre string) (bool, *assignedScopeInfo, error) {
	impersonateOrgId, isImpersonated, err := isImpersonatedUser(ctx, userId, logpre)
	if err != nil {
		return false, nil, err
	}
	if !isImpersonated {
		return false, nil, nil
	}

	roleName := entity.RoleImpersonateOrgOwner
	ts := time.Now()
	roleId, _ := rbac.GetRoleIdByRoleNameFromCache(ctx, roleName)
	metrics.RecPermissionCheckMetrics(logpre, "impersonated_get_role_id_by_name", ts, false)

	scopeInfo := assignedScopeInfo{
		ScopeType: entity.RoleBindingScopeOrg,
		ScopeId:   impersonateOrgId,
		Role:      roleName,
		RoleId:    roleId,
	}
	return true, &scopeInfo, nil
}

func getIdentityAssignedScopeInfo(ctx context.Context, targetType entity.TargetType, targetId uint64, roleIds []uint64, prelog string) (map[entity.ScopeType][]assignedScopeInfo, error) {
	ts := time.Now()
	defer metrics.RecPermissionCheckMetrics(prelog, "get_identity_assigned_scope", ts, false)

	bindings, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, role.RoleBindingFilter{
		TargetType: []entity.TargetType{targetType},
		TargetIDs:  []entity.ID{entity.ID(targetId)},
		RoleIDs:    utils.UintToIDList(roleIds),
		Status:     []string{entity.RoleBindingStatusActive},
	})
	if err != nil {
		return nil, err
	}

	assignedScopeMap := map[entity.ScopeType][]assignedScopeInfo{}
	for _, binding := range bindings {
		if binding.Status != entity.RoleBindingStatusActive {
			continue
		}
		scopeInfos, ok := assignedScopeMap[binding.ScopeType]
		if !ok {
			scopeInfos = []assignedScopeInfo{}
		}
		info := assignedScopeInfo{
			ScopeType: binding.ScopeType,
			ScopeId:   binding.ScopeID.Uint64(),
			Role:      binding.Role,
			RoleId:    binding.RoleID.Uint64(),
		}
		scopeInfos = append(scopeInfos, info)
		assignedScopeMap[binding.ScopeType] = scopeInfos
	}

	tilog.Info(ctx, fmt.Sprintf("%s: get identity assigned scope result", prelog), zap.Any("target_id", targetId), zap.Any("role_ids", roleIds), zap.Any("assigned_scope_map", assignedScopeMap))

	return assignedScopeMap, nil
}

func checkAccessScopeMatchAssignedScope(ctx context.Context, accessScopeMap map[entity.ScopeType]entity.ID, assignedScopeMap map[entity.ScopeType][]assignedScopeInfo, prelog string) (*PermissionCheckRet, error) {
	ts := time.Now()
	defer metrics.RecPermissionCheckMetrics(prelog, "check_access_scope_match_assigned_scope", ts, false)

	if len(assignedScopeMap) == 0 {
		return &PermissionCheckRet{
			HasPermission: false,
			Message:       "identity is not assigned roles on any scope",
		}, nil
	}
	if len(accessScopeMap) == 0 {
		var msg string
		for scopeType, scopeInfos := range assignedScopeMap {
			scopeInfo := scopeInfos[0]
			msg = fmt.Sprintf("access scope is none and identity assigned role (%s-%d) on (%s-%d) hit privilege", scopeInfo.Role, scopeInfo.RoleId, scopeType, scopeInfo.ScopeId)
			break
		}
		return &PermissionCheckRet{
			HasPermission: true,
			Message:       msg,
		}, nil
	}

	_, eProjectAccessScope := accessScopeMap[entity.RoleBindingScopeProject]

	for accessScopeType, accessScopeId := range accessScopeMap {
		// same scope level check
		if assignedScopeInfos, ok := assignedScopeMap[accessScopeType]; ok {
			for _, scopeInfo := range assignedScopeInfos {
				if scopeInfo.ScopeId == accessScopeId.Uint64() {
					return &PermissionCheckRet{
						HasPermission: true,
						Message:       fmt.Sprintf("identity assigned role (%s-%d) on (%s-%d) hit privilege", scopeInfo.Role, scopeInfo.RoleId, accessScopeType, accessScopeId),
					}, nil
				}
			}
		}
		// if access scope type is tidb，check user has permission on x project (only when resource_uri not contains project scope)
		if accessScopeType == entity.RoleBindingScopeTiDB && !eProjectAccessScope {
			// just check relation for project
			if assignedInfos, ok := assignedScopeMap[entity.RoleBindingScopeProject]; ok {
				resRelations, err := rbac.GetResourceRelationSvc().GetResourceRelations(ctx, entity.RoleBindingScopeTiDB, accessScopeId.Uint64())
				if err != nil {
					return nil, err
				}
				var belongParentId uint64 = 0
				for _, relation := range resRelations {
					if relation.ParentType == entity.RoleBindingScopeProject.String() {
						belongParentId = relation.ParentID
						break
					}
				}
				if belongParentId != 0 {
					for _, assignedInfo := range assignedInfos {
						if assignedInfo.ScopeId == belongParentId {
							return &PermissionCheckRet{
								HasPermission: true,
								Message: fmt.Sprintf("access scope (%s-%d) is belong to (%s-%d) and identity assigned role (%s-%d) on (%s-%d) hit privilege",
									accessScopeType, accessScopeId, assignedInfo.ScopeType, assignedInfo.ScopeId, assignedInfo.Role, assignedInfo.RoleId, assignedInfo.ScopeType, assignedInfo.ScopeId),
							}, nil
						}
					}
				}
			}
		}
	}
	return &PermissionCheckRet{
		HasPermission: false,
		Message:       "access scope is not matched assigned scopes",
	}, nil
}

func checkPermissionWithoutResourceUri(ctx context.Context, targetType entity.TargetType, targetId entity.ID, reqUri string, accessScopeMap map[entity.ScopeType]entity.ID, logpre string) (*PermissionCheckRet, error) {
	tilog.Info(ctx, fmt.Sprintf("%s: resource_uri is blank", logpre), zap.Any("req_uri", reqUri))

	var isImpersonate bool
	var impersonateInfo *assignedScopeInfo
	var err error
	var roleIds []uint64

	if targetType == entity.RoleBindingTargetUser {
		isImpersonate, impersonateInfo, err = getUserImpersonateScopeInfo(ctx, targetId.Uint64(), logpre)
		if err != nil {
			tilog.Error(ctx, fmt.Sprintf("%s: check user is impersonated error", logpre), zap.Any("user_id", targetId), zap.Any("err", err))
			return nil, err
		}
	}
	if isImpersonate {
		roleIds = append(roleIds, impersonateInfo.RoleId)
	} else {
		roleIds, err = getIdentityInScopeRoles(ctx, targetType, targetId, accessScopeMap, logpre)
		if err != nil {
			tilog.Error(ctx, fmt.Sprintf("%s: get identity in scope roles error", logpre), zap.Any("user_id", targetId), zap.Any("access_scope_map", accessScopeMap), zap.Any("err", err))
			return nil, err
		}
	}
	if len(roleIds) == 0 {
		tilog.Info(ctx, fmt.Sprintf("%s: find role empty", logpre), zap.Any("target_type", targetType), zap.Any("target_id", targetId), zap.Any("reqUri", reqUri))
		ret := &PermissionCheckRet{
			HasPermission: false,
			Message:       "identity's role is null",
		}
		return ret, nil
	}
	return checkRoleIdAssignedPrivsMatchReqUri(ctx, targetId.Uint64(), roleIds, reqUri, logpre)
}

// get scopeType and scope_id in reqUri
func parseRbacUri(reqUri string, scopeMapping map[string]entity.ScopeType) (map[entity.ScopeType]entity.ID, error) {
	//  org/1/project/2/delete
	//  Split by  "/"
	reqSlice := strings.Split(reqUri, SEPARATOR)
	length := len(reqSlice)
	var scopeMap = map[entity.ScopeType]entity.ID{}
	for i := 0; i < length; i++ {
		scopeType := getScope(reqSlice[i], scopeMapping)
		if scopeType != "" && i+1 < length {
			// get scopeTypeID
			scopeID := reqSlice[i+1]
			// check string scopeID
			matchNumber := regexpNumber.MatchString(scopeID)
			if matchNumber {
				id, err := strconv.ParseUint(scopeID, 10, 64)
				if err != nil {
					return nil, err
				}
				if scopeMap[scopeType] != 0 {
					return nil, errors.New("reqUri is error,contains more same scope")
				}
				scopeMap[scopeType] = entity.ID(id)
				i++
			}
		}
	}
	return scopeMap, nil
}

func getScope(str string, scopeMapping map[string]entity.ScopeType) entity.ScopeType {
	scopeType, ok := scopeMapping[str]
	if ok {
		return scopeType
	}
	return ""
}

// api/v1   /org/1/project/2/delete    org/{org_id}/project/{project_id}/delete
func checkRequestUri(reqUri string, resourceUri string) bool {

	reqSlice := strings.Split(reqUri, SEPARATOR)
	resourceSlice := strings.Split(resourceUri, SEPARATOR)
	if len(reqSlice) != len(resourceSlice) {
		return false
	}
	for i := 0; i < len(reqSlice); i++ {
		// check scope_id must be number
		scopeType := getScope(reqSlice[i], ScopeTypeMapping)
		if scopeType != "" && i+1 < len(reqSlice) {
			matchPattern := regexpStr.MatchString(resourceSlice[i+1])
			matchNumber := regexpNumber.MatchString(reqSlice[i+1])
			if matchPattern && !matchNumber {
				return false
			}
		}
		if reqSlice[i] != resourceSlice[i] {
			// is {org_id}  pattern ?
			matchPattern := regexpStr.MatchString(resourceSlice[i])
			//  {org_id}  is number ?
			//matchNumber := regexp.MustCompile(REGEXP_NUMBER).MatchString(reqSlice[i])
			notEmpty := len(reqSlice[i]) > 0
			if !matchPattern || !notEmpty {
				return false
			}
		}
	}
	return true
}

func getIdentityInScopeRoles(ctx context.Context, targetType entity.TargetType, targetID entity.ID, scopeMap map[entity.ScopeType]entity.ID, logPre string) ([]uint64, error) {
	ts := time.Now()
	defer metrics.RecPermissionCheckMetrics(logPre, "get_identity_in_scope_roles", ts, false)

	logger := tilog.WithContextL(ctx)

	filter := role.RoleBindingFilter{
		TargetType: []entity.TargetType{targetType},
		TargetIDs:  []entity.ID{targetID},
		Status:     []string{string(entity.UserStatusActive)},
	}
	var roleIDs []uint64
	// select role_bindings
	if len(scopeMap) == 0 {
		roleIDsByFilter, err := getUserRoleIdsByFilter(ctx, filter)
		if err != nil {
			return nil, err
		}
		roleIDs = append(roleIDs, roleIDsByFilter...)
		logger.Info(fmt.Sprintf("%s: no scope_map, find final identity roles", logPre), zap.Any("target_type", targetType), zap.Any("target_id", targetID), zap.Any("role_ids", roleIDs))
	} else {
		for k, v := range scopeMap {
			filter.ScopeTypes = []entity.ScopeType{k}
			filter.ScopeIDs = []entity.ID{v}
			roleIDsByFilter, err := getUserRoleIdsByFilter(ctx, filter)
			if err != nil {
				return nil, err
			}
			logger.Info(fmt.Sprintf("%s: find identity role", logPre), zap.Any("target_type", targetType), zap.Any("target_id", targetID), zap.Any("scope_type", k), zap.Any("scope_id", v), zap.Any("role_id", roleIDsByFilter))
			roleIDs = append(roleIDs, roleIDsByFilter...)
		}
		logger.Info(fmt.Sprintf("%s: find final identity roles", logPre), zap.Any("target_type", targetType), zap.Any("target_id", targetID), zap.Any("role_ids", roleIDs))
	}

	// delete repeated role_id
	roleIdMap := make(map[uint64]struct{})
	for _, roleID := range roleIDs {
		roleIdMap[roleID] = struct{}{}
	}
	var finalRoleIds []uint64
	for k := range roleIdMap {
		finalRoleIds = append(finalRoleIds, k)
	}

	return finalRoleIds, nil
}

func checkRoleIdAssignedPrivsMatchReqUri(ctx context.Context, targetID uint64, roleIds []uint64, reqUri string, logPre string) (*PermissionCheckRet, error) {
	ts := time.Now()
	var hitResUri string
	exist := rbac.ForEachResourceUriByRoleIds(ctx, roleIds, func(resUri string) bool {
		if checkRequestUri(reqUri, resUri) {
			hitResUri = resUri
			return false
		}
		return true
	})
	metrics.RecPermissionCheckMetrics(logPre, "get_resource_uris_by_role_ids", ts, false)
	if !exist {
		tilog.Info(ctx, fmt.Sprintf("%s: the identity privileges is null ", logPre), zap.Any("target_id", targetID), zap.Any("req_uri", reqUri), zap.Any("roleIDs", roleIds))
		ret := &PermissionCheckRet{
			HasPermission: false,
			Message:       fmt.Sprintf("identity's role (%s) relate privileges is null", genUserRoleStr(ctx, roleIds)),
		}
		return ret, nil
	}

	if hitResUri != "" {
		tilog.Info(ctx, fmt.Sprintf("%s: has  privileges ", logPre), zap.Any("targetID ", targetID), zap.Any("reqUri", reqUri), zap.Any("resourceUri", hitResUri))
		ret := &PermissionCheckRet{
			HasPermission: true,
			Message:       fmt.Sprintf("user's role (%s) relate privileges hited req_uri (%s)", genUserRoleStr(ctx, roleIds), reqUri),
		}
		return ret, nil
	}
	metrics.RecPermissionCheckMetrics(logPre, "check_all_priv_match_req_uri", ts, false)

	tilog.Info(ctx, fmt.Sprintf("%s: has no privileges ", logPre), zap.Any("targetID", targetID), zap.Any("reqUri", reqUri))
	ret := &PermissionCheckRet{
		HasPermission: false,
		Message:       fmt.Sprintf("user's role (%s) relate privileges not hited req_uri (%s)", genUserRoleStr(ctx, roleIds), reqUri),
	}
	return ret, nil
}

func genUserRoleStr(ctx context.Context, roleIDs []uint64) string {
	retInfoStrs := []string{}
	for i := 0; i < len(roleIDs); i++ {
		roleName := "NA"
		ra, exist := rbac.GetRoleNameByRoleIdFromCache(ctx, roleIDs[i])
		if exist {
			roleName = ra
		}
		roleItem := fmt.Sprintf("%s/%d", roleName, roleIDs[i])
		retInfoStrs = append(retInfoStrs, roleItem)
	}
	return strings.Join(retInfoStrs, ",")
}

func getUserRoleIdsByFilter(ctx context.Context, filter role.RoleBindingFilter) ([]uint64, error) {
	roleBindings, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, filter)
	if err != nil {
		return nil, err
	}
	var roleIds []uint64
	for _, rb := range roleBindings {
		roleIds = append(roleIds, rb.RoleID.Uint64())
	}
	return roleIds, nil
}
