package apikey

import (
	"context"
	"errors"
	"sort"
	"strings"
	"sync"

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

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

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

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

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

	"github.com/tidbcloud/account/commons/config"
	"github.com/tidbcloud/account/commons/ddb"

	"github.com/tidbcloud/account/internel/dao/apikey"
	ddbDao "github.com/tidbcloud/account/internel/dao/ddb"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/account/internel/dao/role"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

type ApiKeySvcIface interface {
	CreateApiKey(ctx context.Context, param *CreateApiKeyParam) (*OneConsoleApiKey, error)
	UpdateApiKey(ctx context.Context, param *UpdateApiKeyParam) error
	DeleteApiKey(ctx context.Context, param *DeleteApiKeyParam, opr *comsvc.Operator) error
	BatchSetApiKeyExpireTime(ctx context.Context, param *BatchSetApiKeyExpireTimeParam) error
	RecoverApiKeyPermission(ctx context.Context, param *RecoverApiKeyParam) error
	ListApiKeysByResourceID(ctx context.Context, param *ListKeysByResourceIDParam) (*ApiKeys, error)
	ListResourceInfosByApiKeyID(ctx context.Context, param *GetResourceInfoByApiKeyIDParam) (*ApiKeyResourceInfos, error)
	GetApiKeyByID(ctx context.Context, id uint64) (*ApiKey, error)
	GetApiKeyByAccessKey(ctx context.Context, accessKey string) (*ApiKey, error)
	CheckApiKeyBelongToResource(ctx context.Context, param *CheckApiKeyBelongToResourceParam) (bool, error)

	CreateOneConsoleApiKey(ctx context.Context, param *CreateOneConsoleApiKeyParam, opr *comsvc.Operator) (*OneConsoleApiKey, error)
	UpdateOneConsoleApiKey(ctx context.Context, param *UpdateOneConsoleApiKeyParam, opr *comsvc.Operator) error
	ListOneConsoleApiKeys(ctx context.Context, param *ListApiKeysParam) (int32, []OneConsoleApiKey, error)
}

var (
	IgnoreDbUserApiKeyType = []string{
		entity.ApiKeyTypeBearerToken,
	}
)

var (
	apiKeySvcInitOnce sync.Once
	apiKeySvc         ApiKeySvcIface
	apiKeySvcInitErr  error
)

func GetApiKeySvc() (ApiKeySvcIface, error) {
	apiKeySvcInitOnce.Do(func() {
		dynamoDB, err := ddb.NewDynamoDB(config.Global())
		if err != nil {
			apiKeySvcInitErr = err
			return
		}
		apiKeySvc = NewApiKeySvc(dynamoDB)

	})
	return apiKeySvc, apiKeySvcInitErr
}

type ApiKeySvc struct {
	dynamodb *ddbDao.DynamoDBClient
}

func NewApiKeySvc(dynamodb *ddbDao.DynamoDBClient) ApiKeySvcIface {
	return &ApiKeySvc{
		dynamodb: dynamodb,
	}
}

func (s *ApiKeySvc) createApiKeyWithAkConflictRetry(ctx context.Context, param *ApiKeyAttr, operator string) (*entity.ApiKey, string, error) {
	var count = 0
	hub := dao.GetDaoHub()
	db := database.DBConn.DB

	for {
		if count > 10 {
			return nil, "", errors.New("create api key, ak conflict")
		}
		count++

		ak, sk, err := GetKeyUtil().GenAccessSecretKey(ctx)
		if err != nil {
			tilog.Error(ctx, "svc create api key, gen ak/sk err", zap.Any("err", err))
			return nil, "", err
		}
		keyEntity := genApiKeyEntity(param, ak, sk, operator)

		keyEntity, err = hub.ApiKey.CreateApiKey(ctx, db, s.dynamodb, keyEntity)
		if err == nil {
			return keyEntity, sk, nil
		}
		tilog.Warn(ctx, "svc create api key, create api key err", zap.Any("count", count), zap.Any("err", err))

		exist, cerr := hub.ApiKey.CheckAkExistInDB(ctx, db, ak)
		if cerr != nil {
			tilog.Error(ctx, "svc create api key, check ak exist err", zap.Any("err", cerr))
			return nil, "", err
		}
		if !exist {
			return nil, "", err
		}
	}
}

func (s *ApiKeySvc) CreateApiKey(ctx context.Context, param *CreateApiKeyParam) (*OneConsoleApiKey, error) {
	if len(param.ResourceIds) == 0 {
		return nil, errs.NewBadRequest("resourceIds is empty", nil)
	}
	_, _, err := s.ParseResourceRefScopeTypeAndRole(param.ScopeType, param.Role)
	if err != nil {
		return nil, err
	}

	if param.Type == "" {
		param.Type = entity.ApiKeyTypeNormalKey
	}

	attrs := &ApiKeyAttr{
		Source:       param.Source,
		Type:         param.Type,
		Description:  param.Description,
		RateLimitRpm: param.RateLimitRpm,
		ExpiredAt:    utils.FromTimestamp(param.ExpiredAt),
		BizAttr:      param.BizAttr,
	}

	operator := &comsvc.Operator{
		OpVal: param.Operator,
	}
	if param.OperatorType == entity.OperatorTypeApiKey {
		operator.OpTye = entity.RoleBindingTargetAPIKey
	} else {
		operator.OpTye = entity.RoleBindingTargetUser
	}

	upperScopeType := strings.ToUpper(param.ScopeType)

	assignRoleParam := &comsvc.AssignIdentRolesParam{}
	if upperScopeType == entity.RoleBindingScopeDataAPP.String() {
		var dRoles = map[uint64]string{}
		for _, rid := range param.ResourceIds {
			dRoles[rid] = param.Role
		}
		assignRoleParam.DRoles = dRoles
	} else if upperScopeType == entity.RoleBindingScopeOrg.String() {
		assignRoleParam.OrgID = param.ResourceIds[0]
		assignRoleParam.ORole = &param.Role
	} else if upperScopeType == entity.RoleBindingScopeProject.String() {
		projectId := param.ResourceIds[0]
		project, err := dao.GetDaoHub().Project.GetProjectByID(ctx, database.DBConn.DB, projectId)
		if err != nil {
			tilog.Error(ctx, "CreateApiKey get project info to set project role error", zap.Any("projectId", projectId), zap.Any("err", err))
			return nil, errs.NewInternalUnknown("internal error", err)
		}
		var oRole = entity.RoleOrgMember
		assignRoleParam.OrgID = project.TenantID.Uint64()
		assignRoleParam.ORole = &oRole
		var pRoles = map[uint64]string{}
		for _, pid := range param.ResourceIds {
			pRoles[pid] = param.Role
		}
		assignRoleParam.PRoles = pRoles
	} else if upperScopeType == entity.RoleBindingScopeTiDB.String() {
		clusterId := param.ResourceIds[0]
		relation, err := dao.GetDaoHub().ResourceRelation.GetResourceRelation(ctx, database.DBConn.DB, entity.RoleBindingScopeTiDB.String(), clusterId, entity.RoleBindingScopeOrg.String())
		if err != nil {
			tilog.Error(ctx, "CreateApiKey get cluster org ref info to set cluster role error", zap.Any("clusterId", clusterId), zap.Any("err", err))
			return nil, errs.NewInternalUnknown("internal error", err)
		}
		if relation == nil {
			tilog.Error(ctx, "CreateApiKey get cluster org ref info to set cluster role fail, not found cluster org relation", zap.Any("clusterId", clusterId))
			return nil, errs.NewInternalUnknown("not found cluster org relation", nil)
		}
		var oRole = entity.RoleOrgMember
		assignRoleParam.OrgID = relation.ParentID
		assignRoleParam.ORole = &oRole
		var cRoles = map[uint64]string{}
		for _, pid := range param.ResourceIds {
			cRoles[pid] = param.Role
		}
		assignRoleParam.CRoles = cRoles
	} else {
		return nil, errs.NewBadRequest("inavlid scope type", nil)
	}

	var oneConsoleKey *OneConsoleApiKey
	if upperScopeType == entity.RoleBindingScopeDataAPP.String() {
		// for external system used apikey
		keyParam := &CreateOneConsoleApiKeyParam{
			Attrs: attrs,
		}
		oneConsoleKey, err = s.CreateOneConsoleApiKey(ctx, keyParam, operator)
		if err == nil {
			assignRoleParam.IdentTye = entity.RoleBindingTargetAPIKey
			assignRoleParam.IdentID = oneConsoleKey.Id
			assignRoleParam.IdentVal = oneConsoleKey.AccessKey
			assignRoleParam.Status = entity.RoleBindingStatusActive
			_, err = comsvc.UpdateIdentitySpecRoles(ctx, assignRoleParam)
		}
	} else {
		keyParam := &CreateOneConsoleApiKeyParam{
			Attrs:           attrs,
			AssignRoleParam: assignRoleParam,
		}
		oneConsoleKey, err = s.CreateOneConsoleApiKey(ctx, keyParam, operator)
	}
	if err != nil {
		tilog.Error(ctx, "CreateApiKey call oneconsole svc create apikey error", zap.Any("err", err))
		return nil, errs.NewInternalUnknown("internal error", err)
	}

	return oneConsoleKey, nil
}

func genApiKeyEntity(param *ApiKeyAttr, ak, sk, operator string) *entity.ApiKey {
	apiKeyEntity := entity.ApiKey{
		AccessKey:   ak,
		SecretKey:   GetKeyUtil().EncryptKey(sk),
		Source:      param.Source,
		Description: param.Description,
		Status:      entity.Active,
		ExpiredAt:   param.ExpiredAt,
		BizAttr:     param.BizAttr,
		CreatedBy:   operator,
		UpdatedBy:   operator,
		Type:        param.Type,
	}
	if param.RateLimitRpm != 0 {
		apiKeyEntity.RateLimitRpm = param.RateLimitRpm
	}
	return &apiKeyEntity
}

func (s *ApiKeySvc) UpdateApiKey(ctx context.Context, param *UpdateApiKeyParam) error {
	if len(param.ResourceIds) > 0 {
		_, _, err := s.ParseResourceRefScopeTypeAndRole(param.ScopeType, param.Role)
		if err != nil {
			return err
		}
	}

	// check key ent
	keyEnt, err := dao.GetDaoHub().ApiKey.GetApiKeyByID(ctx, database.DBConn.DB, param.ApiKeyId)
	if err != nil {
		tilog.Error(ctx, "UpdateApiKey GetApiKeyByID error", zap.Any("keyId", param.ApiKeyId), zap.Any("err", err))
		return errs.NewInternalUnknown("get key internal error", err)
	}
	if keyEnt == nil || keyEnt.Status == entity.Delete {
		tilog.Info(ctx, "UpdateApiKey not found key", zap.Any("keyId", param.ApiKeyId))
		return errs.NewNotFound("key not found", nil)
	}

	p := &UpdateOneConsoleApiKeyParam{
		KeyID: param.ApiKeyId,
		Attrs: &ApiKeyAttr{
			Description:  param.Description,
			RateLimitRpm: param.RateLimitRpm,
			ExpiredAt:    utils.FromTimestamp(param.ExpiredAt),
			BizAttr:      param.BizAttr,
		},
	}

	operator := &comsvc.Operator{
		OpVal: param.Operator,
	}
	if param.OperatorType == entity.OperatorTypeApiKey {
		operator.OpTye = entity.RoleBindingTargetAPIKey
	} else {
		operator.OpTye = entity.RoleBindingTargetUser
	}

	err = s.UpdateOneConsoleApiKey(ctx, p, operator)
	if err != nil {
		return err
	}

	if len(param.ResourceIds) > 0 {
		var assignRoleParam = &comsvc.AssignIdentRolesParam{}
		assignRoleParam.IdentTye = entity.RoleBindingTargetAPIKey
		assignRoleParam.IdentID = keyEnt.ID.Uint64()
		assignRoleParam.IdentVal = keyEnt.AccessKey
		assignRoleParam.Status = entity.RoleBindingStatusActive

		upperScopeType := strings.ToUpper(param.ScopeType)
		if upperScopeType == entity.RoleBindingScopeOrg.String() {
			assignRoleParam.OrgID = param.ResourceIds[0]
			assignRoleParam.ORole = &param.Role
		} else if upperScopeType == entity.RoleBindingScopeProject.String() {
			var pRoles = map[uint64]string{}
			for _, pid := range param.ResourceIds {
				pRoles[pid] = param.Role
			}
			assignRoleParam.PRoles = pRoles
		} else if upperScopeType == entity.RoleBindingScopeTiDB.String() {
			var cRoles = map[uint64]string{}
			for _, cid := range param.ResourceIds {
				cRoles[cid] = param.Role
			}
			assignRoleParam.CRoles = cRoles
		} else if upperScopeType == entity.RoleBindingScopeDataAPP.String() {
			var dRoles = map[uint64]string{}
			for _, did := range param.ResourceIds {
				dRoles[did] = param.Role
			}
			assignRoleParam.DRoles = dRoles
		} else {
			return errs.NewBadRequest("invalid scope type", nil)
		}

		_, err = comsvc.UpdateIdentitySpecRoles(ctx, assignRoleParam)
		return err
	}

	return nil
}

func (s *ApiKeySvc) DeleteApiKey(ctx context.Context, param *DeleteApiKeyParam, opr *comsvc.Operator) error {
	tilog.Info(ctx, "svc delete api key start", zap.Any("param", param), zap.Any("opr", opr))
	if param == nil || opr == nil {
		return errs.NewBadRequest("param invalid", nil)
	}

	keyEnt, err := dao.GetDaoHub().ApiKey.GetApiKeyByID(ctx, database.DBConn.DB, param.ApiKeyId)
	if err != nil {
		tilog.Error(ctx, "svc delete api key, get api key err", zap.Any("err", err))
		return err
	}
	if keyEnt == nil {
		tilog.Error(ctx, "svc delete api key, entity not found", zap.Any("param", param))
		return errs.NewNotFound("api key not found", nil)
	}

	oBindings, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, role.RoleBindingFilter{
		TargetType: []entity.TargetType{entity.RoleBindingTargetAPIKey},
		TargetIDs:  []entity.ID{keyEnt.ID},
		ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
	})
	if err != nil {
		tilog.Error(ctx, "svc delete api key, list org role binding error", zap.Any("err", err))
		return err
	}
	// for external api key (dataapp), do not have org role
	var orgEnt *entity.Tenant
	if len(oBindings) > 0 {
		orgId := oBindings[0].ScopeID.Uint64()
		orgEnt, err = dao.GetDaoHub().Org.GetOrgByOrgID(ctx, database.DBConn.DB, orgId)
		if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			tilog.Error(ctx, "svc delete api key, get org by id error", zap.Any("org_id", orgId), zap.Any("err", err))
			return err
		}
		if orgEnt == nil || orgEnt.Status != entity.TenantStatusActive {
			tilog.Warn(ctx, "svc delete api key, org is not active", zap.Any("param", param))
			return errs.NewBadRequest("org is not active", nil)
		}
	}

	err = database.Transaction(func(tx *gorm.DB) error {
		_, err := dao.GetDaoHub().Role.DeleteRoleBindingByTargetInfo(ctx, tx, s.dynamodb, entity.RoleBindingTargetAPIKey, param.ApiKeyId)
		if err != nil {
			tilog.Error(ctx, "svc delete api key, delete role binding err", zap.Any("err", err))
			return err
		}
		err = dao.GetDaoHub().ApiKey.DeleteApiKey(ctx, database.DBConn.DB, s.dynamodb, keyEnt, opr.OpVal)
		if err != nil {
			tilog.Error(ctx, "svc delete api key, delete db err", zap.Any("err", err))
			return err
		}
		return nil
	})
	if err != nil {
		return errs.NewInternalUnknown("delete api key error", err)
	}
	err = DeleteApiKeyEntFromCache(ctx, keyEnt.ID.Uint64(), keyEnt.AccessKey)
	tilog.Info(ctx, "svc delete api key, delete cache result", zap.Any("id", keyEnt.ID), zap.Any("ak", keyEnt.AccessKey), zap.Any("err", err))

	if orgEnt != nil {
		go func() {
			_ = publish.PublishDataChangeEventWithEntData(context.Background(), publish.BizEventApiKeyDeleted, nil, keyEnt, orgEnt, nil)
		}()
	}

	return nil
}

func (s *ApiKeySvc) BatchSetApiKeyExpireTime(ctx context.Context, param *BatchSetApiKeyExpireTimeParam) error {
	tilog.Info(ctx, "svc set api key expire time start", zap.Any("param", param))
	if len(param.ApiKeyIds) == 0 {
		return nil
	}
	if param.ExpiredAt == nil {
		tilog.Info(ctx, "svc set api key expire time, expired_at is nil, ignore", zap.Any("param", param))
		return nil
	}
	dbKeys, err := dao.GetDaoHub().ApiKey.ListApiKeys(ctx, database.DBConn.DB, &apikey.ListApiKeysParam{
		Filter: apikey.ApiKeyFilter{
			ID: param.ApiKeyIds,
		},
	})
	if err != nil {
		tilog.Error(ctx, "svc set api key expire time, ListApiKeys error", zap.Any("param", param), zap.Any("err", err))
		return err
	}
	if len(dbKeys.List) != len(param.ApiKeyIds) {
		tilog.Warn(ctx, "svc set api key expire time, not found enough keys by ids", zap.Any("param", param), zap.Any("err", err))
		return err
	}
	err = database.DBConn.DB.Transaction(func(tx *gorm.DB) error {
		// expired_at just exist when expire key, so ignore the situation (need resync to ddb): revive the key when key is expired
		err = dao.GetDaoHub().ApiKey.BatchUpdateExpiredAtByIdIn(ctx, tx, param.ApiKeyIds, param.ExpiredAt)
		if err != nil {
			tilog.Error(ctx, "svc set api key expire time, BatchUpdateExpiredAtByIdIn error", zap.Any("param", param), zap.Any("err", err))
			return err
		}
		// update the apikey expired timestamp in ddb
		updateDdbMap := map[string]any{
			"expired_ts": param.ExpiredAt.UnixMilli(),
		}
		for _, dbKey := range dbKeys.List {
			err = ddbDao.UpdateApiKey(ctx, s.dynamodb, dbKey.AccessKey, updateDdbMap)
			if err != nil {
				tilog.Error(ctx, "svc set api key expire time, UpdateApiKey in ddb error", zap.Any("key", dbKey.AccessKey), zap.Any("err", err))
				return err
			}
		}
		return nil
	})

	return err
}

func (s *ApiKeySvc) getOperatorID(ctx context.Context, operator string, operatorType int32) (uint64, error) {
	var operatorID uint64 = 0
	if operatorType == entity.OperatorTypeApiKey {
		operateKey, err := dao.GetDaoHub().ApiKey.GetApiKeyByAk(ctx, database.DBConn.DB, operator)
		if err != nil {
			return operatorID, err
		}
		operatorID = operateKey.ID.Uint64()
	} else {
		// check operator must be account user
		operateUser, err := dao.GetDaoHub().User.GetUserByEmail(ctx, database.DBConn.DB, operator)
		if err != nil {
			return operatorID, err
		}
		operatorID = operateUser.ID.Uint64()
	}
	return operatorID, nil
}

func (s *ApiKeySvc) RecoverApiKeyPermission(ctx context.Context, param *RecoverApiKeyParam) error {
	tilog.Info(ctx, "svc recover api key permission start", zap.Any("param", param))
	operatorID, err := s.getOperatorID(ctx, param.Operator, param.OperatorType)
	if err != nil {
		tilog.Info(ctx, "svc grant api key permission, get operatorID err", zap.Any("err", err))
		return err
	}
	scopeType, err := s.ParseResourceRefScopeType(param.ScopeType)
	if err != nil {
		return err
	}
	keyEntity, err := GetApiKeyEntWithCache(ctx, param.ApiKeyId, "")
	if err != nil {
		tilog.Error(ctx, "svc recover api key permission, get api key err", zap.Any("err", err))
		return err
	}
	if keyEntity == nil {
		tilog.Info(ctx, "svc recover api key permission, entity not found", zap.Any("req", param))
		return errs.NewBadRequest("api key not found", nil)
	}

	_, err = dao.GetDaoHub().Role.DeleteRoleByTargetAndScope(ctx, database.DBConn.DB, entity.RoleBindingTargetAPIKey, param.ApiKeyId, scopeType, []uint64{param.ResourceId})
	if err != nil {
		tilog.Error(ctx, "svc recover api key permission, DeleteRoleByTargetAndScope error", zap.Any("key_id", param.ApiKeyId), zap.Any("scopeType", scopeType), zap.Any("resource_id", param.ResourceId))
		return errs.NewInternalDB("delete role error", err)
	}
	if scopeType == entity.RoleBindingScopeOrg {
		projects, err := dao.GetDaoHub().Project.ListProjects(ctx, database.DBConn.DB, []uint64{param.ResourceId}, nil, nil)
		if err != nil {
			tilog.Error(ctx, "svc recover api key permission, DeleteRoleByTargetAndScope error", zap.Any("req", param))
			return errs.NewInternalDB("delete role error", err)
		}
		var projectIds []uint64
		for _, p := range projects {
			projectIds = append(projectIds, p.ID.Uint64())
		}
		_, err = dao.GetDaoHub().Role.DeleteRoleByTargetAndScope(ctx, database.DBConn.DB, entity.RoleBindingTargetAPIKey, param.ApiKeyId, entity.RoleBindingScopeProject, projectIds)
		if err != nil {
			tilog.Error(ctx, "svc recover api key permission, DeleteRoleByTargetAndScope error", zap.Any("key_id", param.ApiKeyId), zap.Any("scopeType", entity.RoleBindingScopeProject), zap.Any("resource_id", projectIds))
			return errs.NewInternalDB("delete role error", err)
		}
	}

	err = DeleteDBUser(ctx, dao.GetDaoHub().DBUser, database.DBConn.DB, keyEntity, scopeType, param.ResourceId, operatorID, param.OperatorType)

	if err != nil {
		tilog.Error(ctx, "svc recover api key permission, delete shadow db user err", zap.Any("err", err), zap.Any("param", param))
		return err
	}

	return nil
}

func (s *ApiKeySvc) ListResourceInfosByApiKeyID(ctx context.Context, param *GetResourceInfoByApiKeyIDParam) (*ApiKeyResourceInfos, error) {
	keyEntity, err := GetApiKeyEntWithCache(ctx, param.ApiKeyId, "")
	if err != nil {
		return nil, err
	}
	if keyEntity == nil {
		return nil, errs.NewBadRequest("api key not found", nil)
	}
	dbRoleBindings, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, role.RoleBindingFilter{
		TargetType: []entity.TargetType{entity.RoleBindingTargetAPIKey},
		TargetIDs:  []entity.ID{entity.ID(param.ApiKeyId)},
		Status:     []string{entity.RoleBindingStatusActive},
	})
	if err != nil {
		return nil, err
	}
	// sort by id desc
	sort.SliceStable(dbRoleBindings, func(i, j int) bool {
		return dbRoleBindings[i].ID.Uint64() > dbRoleBindings[j].ID.Uint64()
	})
	retRoleBindings := dbRoleBindings
	if param.PageNo > 0 && param.PageSize > 0 {
		start := int((param.PageNo - 1) * param.PageSize)
		end := start + int(param.PageSize)
		if end > len(dbRoleBindings) {
			end = len(dbRoleBindings)
		}
		retRoleBindings = dbRoleBindings[start:end]
	}
	resoruceInfos := ConvertRoleBinding2ResourceInfo(int32(len(dbRoleBindings)), retRoleBindings)

	return resoruceInfos, nil
}

func (s *ApiKeySvc) ListApiKeysByResourceID(ctx context.Context, param *ListKeysByResourceIDParam) (*ApiKeys, error) {
	scopeType, err := s.ParseResourceRefScopeType(param.ScopeType)
	if err != nil {
		return nil, err
	}
	if len(param.ApiKeyTypes) == 0 {
		// default to list normal key
		param.ApiKeyTypes = []string{entity.ApiKeyTypeNormalKey}
	}
	dbRoleBindings, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, role.RoleBindingFilter{
		TargetType: []entity.TargetType{entity.RoleBindingTargetAPIKey},
		ScopeTypes: []entity.ScopeType{scopeType},
		ScopeIDs:   []entity.ID{entity.ID(param.ResourceId)},
		Status:     []string{entity.RoleBindingStatusActive},
		Roles:      param.Roles,
	})
	if err != nil {
		return nil, err
	}
	if len(dbRoleBindings) == 0 {
		return &ApiKeys{
			Total: 0,
			Items: []*ApiKey{},
		}, nil
	}

	keyID2RoleBindingsMap := map[uint64][]entity.RoleBinding{}
	var keyIDs []uint64
	for _, dbRoleBinding := range dbRoleBindings {
		keyID := dbRoleBinding.TargetID.Uint64()
		bindings, ok := keyID2RoleBindingsMap[keyID]
		if !ok {
			keyIDs = append(keyIDs, keyID)
			bindings = []entity.RoleBinding{}
		}
		bindings = append(bindings, dbRoleBinding)
		keyID2RoleBindingsMap[keyID] = bindings
	}

	listRet, err := dao.GetDaoHub().ApiKey.ListApiKeys(ctx, database.DBConn.DB, &apikey.ListApiKeysParam{
		Filter: apikey.ApiKeyFilter{
			ID:       keyIDs,
			AKLike:   param.AccessKey,
			DescLike: param.Desc,
		},
		SortBy: "id",
		Order:  "desc",
	})
	if err != nil {
		return nil, err
	}

	needFilterApiKeyType := len(param.ApiKeyTypes) > 0
	var exposedKeys []entity.ApiKey
	for _, rk := range listRet.List {
		if needFilterApiKeyType && !slice.Contain(param.ApiKeyTypes, rk.Type) {
			continue
		}
		exposedKeys = append(exposedKeys, rk)
	}
	sort.Slice(exposedKeys, func(i, j int) bool {
		return exposedKeys[i].ID > exposedKeys[j].ID
	})
	total := len(exposedKeys)
	if param.PageNo > 0 && param.PageSize > 0 {
		start := int((param.PageNo - 1) * param.PageSize)
		if start < 0 {
			start = 0
		}
		end := start + int(param.PageSize)
		if end > len(exposedKeys) {
			end = len(exposedKeys)
		}
		if start > end {
			start = end
		}
		exposedKeys = exposedKeys[start:end]
	}
	retApiKeys := &ApiKeys{
		Total: int32(total),
		Items: []*ApiKey{},
	}
	for _, keyEntity := range exposedKeys {
		bindings := keyID2RoleBindingsMap[keyEntity.ID.Uint64()]
		centralApiKey := ConvertKeyEntity2ApiKey(&keyEntity, keyEntity.SecretKey, bindings)
		retApiKeys.Items = append(retApiKeys.Items, centralApiKey)
	}
	return retApiKeys, nil
}

func (s *ApiKeySvc) GetApiKeyByAccessKey(ctx context.Context, accessKey string) (*ApiKey, error) {
	keyEntity, err := GetApiKeyEntWithCache(ctx, 0, accessKey)
	if err != nil {
		return nil, err
	}
	if keyEntity == nil {
		return nil, nil
	}
	dbRoleBindings, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, role.RoleBindingFilter{
		TargetType: []entity.TargetType{entity.RoleBindingTargetAPIKey},
		TargetIDs:  []entity.ID{keyEntity.ID},
		ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg, entity.RoleBindingScopeProject, entity.RoleBindingScopeDataAPP},
		Status:     []string{entity.RoleBindingStatusActive},
	})
	if err != nil {
		return nil, err
	}
	apiKey := ConvertKeyEntity2ApiKey(keyEntity, keyEntity.SecretKey, dbRoleBindings)
	return apiKey, nil
}

func (s *ApiKeySvc) GetApiKeyByID(ctx context.Context, id uint64) (*ApiKey, error) {
	keyEntity, err := GetApiKeyEntWithCache(ctx, id, "")
	if err != nil {
		return nil, err
	}
	if keyEntity == nil {
		return nil, nil
	}
	dbRoleBindings, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, role.RoleBindingFilter{
		TargetType: []entity.TargetType{entity.RoleBindingTargetAPIKey},
		TargetIDs:  []entity.ID{keyEntity.ID},
		Status:     []string{entity.RoleBindingStatusActive},
	})
	if err != nil {
		return nil, err
	}
	apiKey := ConvertKeyEntity2ApiKey(keyEntity, keyEntity.SecretKey, dbRoleBindings)
	return apiKey, nil
}

func (s *ApiKeySvc) CheckApiKeyBelongToResource(ctx context.Context, param *CheckApiKeyBelongToResourceParam) (bool, error) {
	scopeType, err := s.ParseResourceRefScopeType(param.ScopeType)
	if err != nil {
		tilog.Warn(ctx, "svc check api key belong to resource error", zap.Any("err", err), zap.Any("param", param))
		return false, err
	}
	dbRoleBindings, err := dao.GetDaoHub().Role.ListRolesByTargetAndScope(ctx, database.DBConn.DB, entity.RoleBindingTargetAPIKey, param.ApiKeyId, scopeType, param.ResourceId, []string{entity.RoleBindingStatusActive})
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			tilog.Warn(ctx, "svc check api key belong to resource, not pass", zap.Any("param", param))
			return false, nil
		}
		tilog.Warn(ctx, "svc check api key belong to resource error", zap.Any("err", err), zap.Any("param", param))
		return false, err
	}
	if len(dbRoleBindings) > 0 {
		tilog.Info(ctx, "svc check api key belong to resource, pass", zap.Any("param", param))
		return true, nil
	}
	tilog.Warn(ctx, "svc check api key belong to resource, not pass", zap.Any("param", param))
	return false, nil

}
