package role

import (
	context "context"
	"fmt"

	"github.com/tidbcloud/tiinf/errs"

	"gorm.io/gorm/clause"

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

type (
	TargetType = entity.TargetType
	ScopeType  = entity.ScopeType
)

type RoleDaoIface interface {
	// simple method
	ListRoleBindings(ctx context.Context, tx *gorm.DB, filter RoleBindingFilter) ([]entity.RoleBinding, error)
	GetUserOrgIds(ctx context.Context, tx *gorm.DB, userId uint64, roleBindingStatus []string) ([]uint64, error)
	GetUserIdsInOrgs(ctx context.Context, tx *gorm.DB, orgIds []uint64) ([]uint64, error)

	UpsertRole(ctx context.Context, tx *gorm.DB, targetType TargetType, targetID uint64, scopeType ScopeType, scopeID uint64, role string, status string) (*entity.RoleBinding, error)
	// create role
	CreateRole(ctx context.Context, tx *gorm.DB, targetType TargetType, targetID uint64, scopeType ScopeType, scopeID uint64, role string, status string) (*entity.RoleBinding, error)
	CreateRoleBinding(ctx context.Context, tx *gorm.DB, ddb *ddbDao.DynamoDBClient, targetType TargetType, targetID uint64, scopeType ScopeType, scopeID uint64, roleID uint64, status string) (*entity.RoleBinding, error)
	BatchCreateRoleBindings(ctx context.Context, tx *gorm.DB, bindings []*entity.RoleBinding) error

	// delete role
	DeleteRole(ctx context.Context, tx *gorm.DB, targetType TargetType, targetID uint64, scopeType ScopeType, scopeID uint64, role string) (bool, error)
	DeleteRoleBinding(ctx context.Context, tx *gorm.DB, ddb *ddbDao.DynamoDBClient, targetType TargetType, targetID uint64, scopeType ScopeType, scopeID uint64, roleID uint64) (bool, error)
	DeleteRoleBindingByTargetInfo(ctx context.Context, tx *gorm.DB, ddb *ddbDao.DynamoDBClient, targetType TargetType, targetID uint64) (bool, error)
	DeleteRoleBindingByScopeInfo(ctx context.Context, tx *gorm.DB, scopeType ScopeType, scopeID uint64, targetType TargetType) (bool, error)
	DeleteRoleByTargetAndScope(ctx context.Context, tx *gorm.DB, targetType TargetType, targetID uint64, scopeType ScopeType, scopeIDs []uint64) (bool, error)
	DeleteRoleByID(ctx context.Context, tx *gorm.DB, id uint64) error

	// basic role check
	GetRole(ctx context.Context, tx *gorm.DB, targetType TargetType, targetID uint64, scopeType ScopeType, scopeID uint64, role string) (*entity.RoleBinding, error)
	HasActiveRole(ctx context.Context, tx *gorm.DB, targetType TargetType, targetID uint64, scopeType ScopeType, scopeID uint64, role string) (bool, error)

	// update role
	ChangeRoleTo(ctx context.Context, tx *gorm.DB, targetType TargetType, targetID uint64, scopeType ScopeType, scopeID uint64, role string, newRole string) (entity.RoleBinding, error)
	UpdateRoleStatusByID(ctx context.Context, tx *gorm.DB, id uint64, status string) error
	UpdateRoleOrStatus(ctx context.Context, tx *gorm.DB, binding entity.RoleBinding) error
	BatchUpdateRoleOrStatus(ctx context.Context, tx *gorm.DB, filter RoleBindingFilter, paramMap map[string]interface{}) (err error)
	UpdateRoleBindingByID(ctx context.Context, tx *gorm.DB, binding entity.RoleBinding) error

	// generic role query
	ListRolesByTarget(ctx context.Context, tx *gorm.DB, targetType TargetType, targetID uint64, status []string) ([]entity.RoleBinding, error)
	ListRolesByTargetOnScopeList(ctx context.Context, tx *gorm.DB, targetType TargetType, targetID uint64, scopeType ScopeType, scopeIDs []uint64, status []string) ([]entity.RoleBinding, error)
	ListRolesByScope(ctx context.Context, tx *gorm.DB, scopeType ScopeType, scopeID uint64, status []string) ([]entity.RoleBinding, error)
	ListRolesByScopeOnTargetList(ctx context.Context, tx *gorm.DB, scopeType ScopeType, scopeID uint64, targetType TargetType, targetIDs []uint64, status []string) ([]entity.RoleBinding, error)
	ListRolesByTargetAndScope(ctx context.Context, tx *gorm.DB, targetType TargetType, targetID uint64, scopeType ScopeType, scopeID uint64, status []string) ([]entity.RoleBinding, error)

	BatchSelectRoleBindings(ctx context.Context, tx *gorm.DB, idStart uint64, limit int) ([]entity.RoleBinding, error)

	// user roles on org
	LegacyGetUserRoleByOrg(ctx context.Context, tx *gorm.DB, userID uint64, orgID uint64, status []string) (*entity.RoleBinding, bool, error)
	LegacySetUserRoleByOrg(ctx context.Context, tx *gorm.DB, userID uint64, orgID uint64, role string) (*entity.RoleBinding, bool, error)
	ListUserRolesByUserIDAndOrg(ctx context.Context, tx *gorm.DB, userID uint64, orgID uint64, status []string) ([]entity.RoleBinding, error)
	ListUserRolesByUserIDAndOrgs(ctx context.Context, tx *gorm.DB, userID uint64, orgIDs []uint64, status []string) ([]entity.RoleBinding, error)
	AddOrgRoleForUser(ctx context.Context, tx *gorm.DB, userID uint64, orgID uint64, role string) (*entity.RoleBinding, error)
	RemoveOrgRoleForUser(ctx context.Context, tx *gorm.DB, userID uint64, orgID uint64, role string) (bool, error)

	// user roles on project
	ListUserRolesByProject(ctx context.Context, tx *gorm.DB, projectID uint64, status []string) ([]entity.RoleBinding, error)
	ListUserRolesByProjects(ctx context.Context, tx *gorm.DB, userID uint64, projectIDs []uint64, status []string) ([]entity.RoleBinding, error)
	ListProjectRolesByUserID(ctx context.Context, tx *gorm.DB, userID uint64, status []string) ([]entity.RoleBinding, error)
	AddProjectRoleForUser(ctx context.Context, tx *gorm.DB, userID uint64, projectID uint64, role string) (*entity.RoleBinding, error)
	RemoveProjectRoleForUser(ctx context.Context, tx *gorm.DB, userID uint64, projectID uint64, role string) (bool, error)

	// api key roles on dataapp
	ListDataAppRoleBindingsByApiKeyID(ctx context.Context, tx *gorm.DB, apiKeyID uint64) ([]entity.RoleBinding, error)
	SaveDataAppRoleBindingsToDynamodbByApiKeyID(ctx context.Context, tx *gorm.DB, ddb *ddbDao.DynamoDBClient, apiKeyID uint64) error
}

type RoleDao struct {
}

func NewDao() RoleDaoIface {
	return &RoleDao{}
}

type RoleBindingFilter struct {
	ScopeTypes []entity.ScopeType
	ScopeIDs   []entity.ID
	TargetType []entity.TargetType
	TargetIDs  []entity.ID
	Status     []string
	Roles      []string
	RoleIDs    []entity.ID
}

func (d *RoleDao) ListRolesByTarget(ctx context.Context, tx *gorm.DB, targetType TargetType, targetID uint64, status []string) ([]entity.RoleBinding, error) {
	var roles []entity.RoleBinding

	qs := tx.Where("target_type = ? AND target_id = ?", targetType, targetID)
	if len(status) > 0 {
		qs = qs.Where("status IN ?", status)
	}

	err := qs.Find(&roles).Error

	if err != nil {
		return nil, err
	}

	return roles, nil
}

func (d *RoleDao) ListRolesByTargetOnScopeList(
	ctx context.Context, tx *gorm.DB,
	targetType TargetType, targetID uint64, scopeType ScopeType, scopeIDs []uint64, status []string,
) ([]entity.RoleBinding, error) {
	var roles []entity.RoleBinding

	qs := tx.Where(
		`target_type = ? AND target_id = ? AND scope_type = ?`,
		targetType, targetID, scopeType,
	)

	if len(scopeIDs) > 0 {
		qs = qs.Where("scope_id IN (?)", scopeIDs)
	}

	if len(status) > 0 {
		qs = qs.Where("status IN ?", status)
	}

	err := qs.Find(&roles).Error

	if err != nil {
		return nil, err
	}

	return roles, nil
}

func (d *RoleDao) ListRolesByScope(ctx context.Context, tx *gorm.DB, scopeType ScopeType, scopeID uint64, status []string) ([]entity.RoleBinding, error) {
	var roles []entity.RoleBinding

	qs := tx.Where("scope_type = ? AND scope_id = ?", scopeType, scopeID)

	if len(status) > 0 {
		qs = qs.Where("status in ?", status)
	}

	err := qs.Find(&roles).Error

	if err != nil {
		return nil, err
	}

	return roles, nil
}

func (d *RoleDao) ListRolesByScopeOnTargetList(
	ctx context.Context, tx *gorm.DB,
	scopeType ScopeType, scopeID uint64, targetType TargetType, targetIDs []uint64, status []string,
) ([]entity.RoleBinding, error) {
	var roles []entity.RoleBinding

	qs := tx.Where(
		`scope_type = ? AND scope_id = ? AND target_type = ? AND target_id IN (?)`,
		scopeType, scopeID, targetType, targetIDs,
	)

	if len(status) > 0 {
		qs = qs.Where("status IN ?", status)
	}

	err := qs.Find(&roles).Error

	if err != nil {
		return nil, err
	}

	return roles, nil
}

func (d *RoleDao) ListRolesByTargetAndScope(
	ctx context.Context, tx *gorm.DB,
	targetType TargetType, targetID uint64, scopeType ScopeType, scopeID uint64, status []string,
) ([]entity.RoleBinding, error) {
	var roles []entity.RoleBinding

	qs := tx.Where(
		`target_type = ? AND target_id = ? AND scope_type = ? AND scope_id = ?`,
		targetType, targetID, scopeType, scopeID,
	)

	if len(status) > 0 {
		qs = qs.Where("status IN ?", status)
	}

	err := qs.Find(&roles).Error

	if err != nil {
		return nil, err
	}

	return roles, nil
}

func (d *RoleDao) GetRole(
	ctx context.Context, tx *gorm.DB,
	targetType TargetType, targetID uint64, scopeType ScopeType, scopeID uint64, role string,
) (*entity.RoleBinding, error) {
	var item entity.RoleBinding

	err := tx.Where(
		`target_type = ? AND target_id = ? AND scope_type = ? AND scope_id = ? AND role = ?`,
		targetType, targetID, scopeType, scopeID, role,
	).First(&item).Error

	if err != nil {
		return nil, err
	}

	return &item, nil
}

func (d *RoleDao) GetRoleWithEntity(ctx context.Context, tx *gorm.DB, role *entity.RoleBinding) (*entity.RoleBinding, error) {
	return d.GetRole(ctx, tx,
		role.TargetType, role.TargetID.Uint64(),
		role.ScopeType, role.ScopeID.Uint64(),
		role.Role,
	)
}

func (d *RoleDao) checkRoleWithCallback(
	ctx context.Context, tx *gorm.DB,
	targetType TargetType, targetID uint64, scopeType ScopeType, scopeID uint64, callback func([]string) bool,
) (bool, error) {
	var roles []entity.RoleBinding
	err := tx.Where(
		`target_type = ? AND target_id = ? AND scope_type = ? AND scope_id = ?`,
		targetType, targetID, scopeType, scopeID,
	).Find(&roles).Error

	if err != nil {
		return false, err
	}

	roleTags := make([]string, len(roles))
	for i, role := range roles {
		roleTags[i] = role.Role
	}

	return callback(roleTags), nil
}

func (d *RoleDao) HasActiveRole(
	ctx context.Context, tx *gorm.DB,
	targetType TargetType, targetID uint64, scopeType ScopeType, scopeID uint64, role string,
) (bool, error) {
	item, err := d.GetRole(ctx, tx, targetType, targetID, scopeType, scopeID, role)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return false, nil
		}

		return false, err
	}

	// item may not be nil
	hasRole := item != nil && item.Status == entity.RoleBindingStatusActive
	return hasRole, nil
}

func (d *RoleDao) UpsertRole(ctx context.Context, tx *gorm.DB,
	targetType TargetType, targetID uint64, scopeType ScopeType, scopeID uint64, role string, status string) (
	*entity.RoleBinding, error) {

	rbacRoleID := ConvertOriginRole2RbacRoleID(tx, role)

	item := entity.RoleBinding{
		TargetType: targetType,
		TargetID:   entity.ID(targetID),
		ScopeType:  scopeType,
		ScopeID:    entity.ID(scopeID),
		Role:       role,
		Status:     status,
		RoleID:     entity.ID(rbacRoleID),
	}

	err := tx.Clauses(clause.OnConflict{
		Columns:   []clause.Column{{Name: "target_type"}, {Name: "target_id"}, {Name: "scope_type"}, {Name: "scope_id"}, {Name: "role"}},
		DoUpdates: clause.AssignmentColumns([]string{"status"}),
	}).Create(&item).Error

	if err != nil {
		return nil, err
	}
	return &item, nil
}

// CreateRole creates a relationship between a target and a scope with a role
// On success, returns the created role, and nil error.
// If role is existed, returns double nils.
// On error, returns nils and the error.
func (d *RoleDao) CreateRole(ctx context.Context, tx *gorm.DB, targetType TargetType, targetID uint64, scopeType ScopeType, scopeID uint64, role string, status string) (*entity.RoleBinding, error) {
	rec, err := d.GetRole(ctx, tx, targetType, targetID, scopeType, scopeID, role)
	if err != nil && err != gorm.ErrRecordNotFound {
		return nil, err
	}

	if err == nil {
		if rec.Status != status {
			return d.UpsertRole(ctx, tx, targetType, targetID, scopeType, scopeID, role, status)
		}
		return nil, nil // TODO(yanfei): return a error instead of nil, nil
	}

	rbacRoleID := ConvertOriginRole2RbacRoleID(tx, role)

	item := entity.RoleBinding{
		TargetType: targetType,
		TargetID:   entity.ID(targetID),
		ScopeType:  scopeType,
		ScopeID:    entity.ID(scopeID),
		Role:       role,
		Status:     status,
		RoleID:     entity.ID(rbacRoleID),
	}
	err = tx.Create(&item).Error
	if err != nil {
		tilog.Error(ctx, "CreateRoleBinding err", zap.Error(err))
		return nil, err
	}

	return &item, nil
}

func (d *RoleDao) BatchCreateRoleBindings(ctx context.Context, tx *gorm.DB, bindings []*entity.RoleBinding) error {
	err := tx.Create(bindings).Error
	if err != nil {
		return errs.WrapGORMError(err, "BatchCreateRoleBindings error")
	}
	return nil
}

func (d *RoleDao) ListDataAppRoleBindingsByApiKeyID(ctx context.Context, tx *gorm.DB, apiKeyID uint64) ([]entity.RoleBinding, error) {
	return d.ListRoleBindings(ctx, tx, RoleBindingFilter{
		TargetType: []TargetType{entity.RoleBindingTargetAPIKey},
		TargetIDs:  []entity.ID{entity.ID(apiKeyID)},
		ScopeTypes: []ScopeType{entity.RoleBindingScopeDataAPP},
		Status:     []string{entity.RoleBindingStatusActive},
	})
}

func (d *RoleDao) SaveDataAppRoleBindingsToDynamodbByApiKeyID(ctx context.Context, tx *gorm.DB, ddb *ddbDao.DynamoDBClient, apiKeyID uint64) error {
	apiKey, err := GetApiKeyEntity(ctx, tx, apiKeyID)
	if err != nil {
		return err
	}
	// list roleBindings with scopeType of RoleBindingScopeDataAPP
	roleBindings, err := d.ListDataAppRoleBindingsByApiKeyID(ctx, tx, apiKeyID)
	if err != nil {
		return err
	}
	return ddbDao.UpdateApiKeyRoleMap(ctx, ddb, apiKey.AccessKey, roleBindings)
}

func (d *RoleDao) CreateRoleBinding(ctx context.Context, tx *gorm.DB, ddb *ddbDao.DynamoDBClient, targetType TargetType, targetID uint64, scopeType ScopeType, scopeID uint64, roleID uint64, status string) (*entity.RoleBinding, error) {
	originRole := ConvertRbacRoleID2OriginRole(tx, roleID)
	item := entity.RoleBinding{
		TargetType: targetType,
		TargetID:   entity.ID(targetID),
		ScopeType:  scopeType,
		ScopeID:    entity.ID(scopeID),
		RoleID:     entity.ID(roleID),
		Status:     status,
		Role:       originRole,
	}
	// Only sync the roleBinding of apiKey and dataApp to dynamodb
	if targetType == entity.RoleBindingTargetAPIKey && scopeType == entity.RoleBindingScopeDataAPP {
		err := tx.Transaction(func(tx *gorm.DB) error {
			err := tx.Create(&item).Error
			if err != nil {
				return err
			}
			return d.SaveDataAppRoleBindingsToDynamodbByApiKeyID(ctx, tx, ddb, targetID)
		})
		if err != nil {
			return nil, err
		}
	} else {
		err := tx.Create(&item).Error
		if err != nil {
			return nil, err
		}
	}
	return &item, nil
}

// DeleteRole delete a role in role-bindings.
// On success, returns true and nil error.
// If role is not existed, returns false and gorm.RecordNotFound error.
// On error, returns false and error.
func (d *RoleDao) DeleteRole(
	ctx context.Context, tx *gorm.DB,
	targetType TargetType, targetID uint64, scopeType ScopeType, scopeID uint64, role string,
) (bool, error) {
	item, err := d.GetRole(ctx, tx, targetType, targetID, scopeType, scopeID, role)
	if err != nil {
		return false, err
	}

	err = tx.Delete(&item).Error
	if err != nil {
		return false, err
	}

	return true, nil
}

func (d *RoleDao) DeleteRoleByTargetAndScope(ctx context.Context, tx *gorm.DB, targetType TargetType, targetID uint64, scopeType ScopeType, scopeIDs []uint64) (bool, error) {
	qs := tx.Where("target_type = ? and target_id = ? and scope_type = ?", targetType, targetID, scopeType)
	if len(scopeIDs) > 0 {
		qs = qs.Where("scope_id IN ?", scopeIDs)
	}
	err := qs.Delete(&entity.RoleBinding{}).Error
	if err != nil {
		return false, err
	}
	return true, nil
}

func (d *RoleDao) DeleteRoleBinding(ctx context.Context, tx *gorm.DB, ddb *ddbDao.DynamoDBClient, targetType TargetType, targetID uint64, scopeType ScopeType, scopeID uint64, roleID uint64) (bool, error) {
	var err error

	if targetType == entity.RoleBindingTargetAPIKey && scopeType == entity.RoleBindingScopeDataAPP {
		err = tx.Transaction(func(tx *gorm.DB) error {
			err := entity.NewRoleBindingQuerySet(tx).TargetTypeEq(targetType).TargetIDEq(entity.ID(targetID)).ScopeTypeEq(scopeType).ScopeIDEq(entity.ID(scopeID)).RoleIDEq(entity.ID(roleID)).Delete()
			if err != nil {
				return err
			}
			return d.SaveDataAppRoleBindingsToDynamodbByApiKeyID(ctx, tx, ddb, targetID)
		})
	} else {
		err = entity.NewRoleBindingQuerySet(tx).TargetTypeEq(targetType).TargetIDEq(entity.ID(targetID)).ScopeTypeEq(scopeType).ScopeIDEq(entity.ID(scopeID)).RoleIDEq(entity.ID(roleID)).Delete()
	}

	if err != nil {
		return false, err
	}
	return true, nil
}

func (d *RoleDao) DeleteRoleByID(ctx context.Context, tx *gorm.DB, id uint64) error {
	err := tx.Where("id = ?", id).Delete(&entity.RoleBinding{}).Error
	return err
}

func (d *RoleDao) DeleteRoleBindingByTargetInfo(ctx context.Context, tx *gorm.DB, ddb *ddbDao.DynamoDBClient, targetType TargetType, targetID uint64) (bool, error) {
	var err error

	if targetType == entity.RoleBindingTargetAPIKey {
		err = tx.Transaction(func(tx *gorm.DB) error {
			err := entity.NewRoleBindingQuerySet(tx).TargetTypeEq(targetType).TargetIDEq(entity.ID(targetID)).Delete()
			if err != nil {
				return err
			}
			return d.SaveDataAppRoleBindingsToDynamodbByApiKeyID(ctx, tx, ddb, targetID)
		})
	} else {
		err = entity.NewRoleBindingQuerySet(tx).TargetTypeEq(targetType).TargetIDEq(entity.ID(targetID)).Delete()
	}

	if err != nil {
		return false, err
	}
	return true, nil
}

func (d *RoleDao) DeleteRoleBindingByScopeInfo(ctx context.Context, tx *gorm.DB, scopeType ScopeType, scopeID uint64, targetType TargetType) (bool, error) {
	var err error

	qs := entity.NewRoleBindingQuerySet(tx).ScopeTypeEq(scopeType).ScopeIDEq(entity.ID(scopeID))
	if targetType != "" {
		qs = qs.TargetTypeEq(targetType)
	}
	err = qs.Delete()

	if err != nil {
		return false, err
	}
	return true, nil
}

// ChangeRoleTo performs a role change on specified target and scope.
// It calls DeleteRole on old role and CreateRole on new role.
// On success, returns new role and nil error.
// If old role is not existed, returns nil and gorm.RecordNotFound error, the new role WILL NOT be created.
// On error, returns nil and error.
func (d *RoleDao) ChangeRoleTo(ctx context.Context, tx *gorm.DB, targetType TargetType, targetID uint64, scopeType ScopeType, scopeID uint64, role string, newRole string) (entity.RoleBinding, error) {
	var item entity.RoleBinding

	err := tx.Where(
		`target_type = ? AND target_id = ? AND scope_type = ? AND scope_id = ? AND role = ?`,
		targetType, targetID, scopeType, scopeID, role,
	).First(&item).Error

	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return item, nil
		}

		return item, err
	}

	newRbacRoleID := ConvertOriginRole2RbacRoleID(tx, newRole)

	item.Role = newRole
	item.RoleID = entity.ID(newRbacRoleID)
	err = item.Update(tx, entity.RoleBindingDBSchema.Role, entity.RoleBindingDBSchema.RoleID)
	if err != nil {
		return item, err
	}
	tilog.Info(ctx, "update roleBinding  new role ", zap.Any("RoleBinding", item))

	return item, nil
}

// Get first role in role_bindings table, simulating the legacy behavior
func (d *RoleDao) LegacyGetUserRoleByOrg(ctx context.Context, tx *gorm.DB, userID uint64, orgID uint64, status []string) (*entity.RoleBinding, bool, error) {
	roles, err := d.ListUserRolesByUserIDAndOrg(ctx, tx, userID, orgID, status)
	if err != nil {
		return nil, false, err
	}
	if len(roles) == 0 {
		return nil, false, nil
	}
	if len(roles) > 1 {
		tilog.Warn(ctx, "User has more than one roles in org", zap.Uint64("userID", userID), zap.Uint64("orgID", orgID))
	}
	return &roles[0], true, nil
}

// Set first role in role_bindings table, simulating the legacy behavior
// The second return value means if an existing role is updated
func (d *RoleDao) LegacySetUserRoleByOrg(ctx context.Context, tx *gorm.DB, userID uint64, orgID uint64, role string) (*entity.RoleBinding, bool, error) {
	// In the legacy code, user role is member by default
	if role == "" {
		role = entity.RoleOrgMember
	}
	// Check if role binding exists
	roleBinding, found, err := d.LegacyGetUserRoleByOrg(ctx, tx, userID, orgID, nil)
	if err != nil {
		return nil, false, err
	}
	if found {
		// If same role binding exists, return directly
		if roleBinding.Role == role {
			return roleBinding, false, nil
		}

		// If role not match, update it
		roleBinding.Role = role
		roleBinding.RoleID = entity.ID(ConvertOriginRole2RbacRoleID(tx, role))
		err = roleBinding.Update(tx, entity.RoleBindingDBSchema.Role, entity.RoleBindingDBSchema.RoleID)
		if err != nil {
			return nil, false, err
		}
		return roleBinding, true, err
	}

	// Create new role binding
	roleBinding, err = d.AddOrgRoleForUser(ctx, tx, userID, orgID, role)
	if err != nil {
		return nil, false, err
	}

	return roleBinding, false, nil
}

func (d *RoleDao) ListUserRolesByUserIDAndOrg(ctx context.Context, tx *gorm.DB, userID uint64, orgID uint64, status []string) ([]entity.RoleBinding, error) {
	return d.ListRolesByTargetAndScope(
		ctx, tx, entity.RoleBindingTargetUser, userID, entity.RoleBindingScopeOrg, orgID, status,
	)
}

func (d *RoleDao) ListUserRolesByUserIDAndOrgs(ctx context.Context, tx *gorm.DB, userID uint64, orgIDs []uint64, status []string) ([]entity.RoleBinding, error) {
	return d.ListRolesByTargetOnScopeList(
		ctx, tx, entity.RoleBindingTargetUser, userID, entity.RoleBindingScopeOrg, orgIDs, status,
	)
}

func (d *RoleDao) AddOrgRoleForUser(ctx context.Context, tx *gorm.DB, userID uint64, orgID uint64, role string) (*entity.RoleBinding, error) {
	return d.CreateRole(
		ctx, tx, entity.RoleBindingTargetUser, userID, entity.RoleBindingScopeOrg, orgID, role, entity.RoleBindingStatusActive,
	)
}

func (d *RoleDao) RemoveOrgRoleForUser(ctx context.Context, tx *gorm.DB, userID uint64, orgID uint64, role string) (bool, error) {
	return d.DeleteRole(
		ctx, tx, entity.RoleBindingTargetUser, userID, entity.RoleBindingScopeOrg, orgID, role,
	)
}

func (d *RoleDao) listTargetByScopeID(ctx context.Context, tx *gorm.DB, scopeType ScopeType, scopeID uint64, targetType TargetType, status []string) ([]entity.RoleBinding, error) {
	var roles []entity.RoleBinding

	qs := tx.Where("scope_type = ? AND scope_id = ? AND target_type = ?", scopeType, scopeID, targetType)
	if len(status) > 0 {
		qs = qs.Where("status IN ?", status)
	}

	err := qs.Find(&roles).Error

	if err != nil {
		return nil, err
	}

	return roles, nil
}

func (d *RoleDao) ListUserRolesByProject(ctx context.Context, tx *gorm.DB, projectID uint64, status []string) ([]entity.RoleBinding, error) {
	return d.listTargetByScopeID(ctx, tx, entity.RoleBindingScopeProject, projectID, entity.RoleBindingTargetUser, status)
}

func (d *RoleDao) ListUserRolesByProjects(ctx context.Context, tx *gorm.DB, userID uint64, projectIDs []uint64, status []string) ([]entity.RoleBinding, error) {
	return d.ListRolesByTargetOnScopeList(
		ctx, tx, entity.RoleBindingTargetUser, userID, entity.RoleBindingScopeProject, projectIDs, status,
	)
}

func (d *RoleDao) UpdateRoleStatusByID(ctx context.Context, tx *gorm.DB, id uint64, status string) error {
	var roleBinding entity.RoleBinding
	err := entity.NewRoleBindingQuerySet(tx).IDEq(entity.ID(id)).One(&roleBinding)
	if err != nil {
		return err
	}

	roleBinding.Status = status

	return roleBinding.Update(tx, entity.RoleBindingDBSchema.Status)
}

func (d *RoleDao) UpdateRoleOrStatus(ctx context.Context, tx *gorm.DB, binding entity.RoleBinding) error {
	if binding.TargetType == "" || binding.TargetID == 0 || binding.ScopeType == "" || binding.ScopeID == 0 {
		return errs.NewBadRequest("invalid param ", nil)
	}

	paramMap := map[string]interface{}{}
	if binding.Status != "" {
		paramMap["status"] = binding.Status
	}
	if binding.Role != "" {
		paramMap["role"] = binding.Role
	}
	if binding.RoleID != 0 {
		paramMap["role_id"] = binding.RoleID
	}

	tx.Model(&binding).Where("scope_type = ? AND scope_id = ? AND target_type = ? AND target_id = ? ",
		binding.ScopeType, binding.ScopeID.Uint64(), binding.TargetType, binding.TargetID.Uint64()).Updates(paramMap)
	return nil
}

func (d *RoleDao) UpdateRoleBindingByID(ctx context.Context, tx *gorm.DB, binding entity.RoleBinding) error {
	paramMap := map[string]interface{}{}
	if binding.Status != "" {
		paramMap["status"] = binding.Status
	}
	if binding.Role != "" {
		rbacRoleID := ConvertOriginRole2RbacRoleID(tx, binding.Role)
		paramMap["role"] = binding.Role
		paramMap["role_id"] = rbacRoleID
	}

	tx.Model(&binding).Where("id = ? ", binding.ID.Uint64()).Updates(paramMap)
	return nil
}

func (d *RoleDao) BatchUpdateRoleOrStatus(ctx context.Context, tx *gorm.DB, filter RoleBindingFilter, paramMap map[string]interface{}) (err error) {

	tx = tx.Model(&entity.RoleBinding{})
	tx, err = buildRoleBindingFilter(tx, filter)
	if err != nil {
		return err
	}
	tx.Updates(paramMap)
	return nil
}

func (d *RoleDao) AddProjectRoleForUser(ctx context.Context, tx *gorm.DB, userID uint64, projectID uint64, role string) (*entity.RoleBinding, error) {
	return d.CreateRole(
		ctx, tx, entity.RoleBindingTargetUser, userID, entity.RoleBindingScopeProject, projectID, role, entity.RoleBindingStatusActive,
	)
}

func (d *RoleDao) RemoveProjectRoleForUser(ctx context.Context, tx *gorm.DB, userID uint64, projectID uint64, role string) (bool, error) {
	return d.DeleteRole(
		ctx, tx, entity.RoleBindingTargetUser, userID, entity.RoleBindingScopeProject, projectID, role,
	)
}

func (d *RoleDao) ListProjectRolesByUserID(ctx context.Context, tx *gorm.DB, userID uint64, status []string) ([]entity.RoleBinding, error) {
	var roleBindings []entity.RoleBinding

	qs := tx.Where("target_type = ? AND target_id = ? AND scope_type = ?", entity.RoleBindingTargetUser, userID, entity.RoleBindingScopeProject)
	if len(status) > 0 {
		qs = qs.Where("status IN ?", status)
	}

	err := qs.Find(&roleBindings).Error

	if err != nil {
		return nil, err
	}

	return roleBindings, nil
}

func (d *RoleDao) ListRoleBindings(ctx context.Context, tx *gorm.DB, filter RoleBindingFilter) ([]entity.RoleBinding, error) {
	var roleBindings = make([]entity.RoleBinding, 0)
	qs := entity.NewRoleBindingQuerySet(tx)
	if len(filter.TargetType) > 0 {
		qs = qs.TargetTypeIn(filter.TargetType...)
	}
	if len(filter.TargetIDs) > 0 {
		qs = qs.TargetIDIn(filter.TargetIDs...)
	}
	if len(filter.ScopeTypes) > 0 {
		qs = qs.ScopeTypeIn(filter.ScopeTypes...)
	}
	if len(filter.ScopeIDs) > 0 {
		qs = qs.ScopeIDIn(filter.ScopeIDs...)
	}
	if len(filter.RoleIDs) > 0 {
		qs = qs.RoleIDIn(filter.RoleIDs...)
	}
	if len(filter.Status) > 0 {
		qs = qs.StatusIn(filter.Status...)
	}
	if len(filter.Roles) > 0 {
		qs = qs.RoleIn(filter.Roles...)
	}

	err := qs.All(&roleBindings)

	if err != nil {
		tilog.Error(ctx, "ListRoleBindings error", zap.Error(err))
		return nil, err
	}
	tilog.Info(ctx, "ListRoleBindings size", zap.Any("size", len(roleBindings)), zap.Any("RoleBindingFilter", filter))
	return roleBindings, nil
}
func (d *RoleDao) GetUserIdsInOrgs(ctx context.Context, tx *gorm.DB, orgIds []uint64) ([]uint64, error) {

	var orgIDs []entity.ID
	for _, orgId := range orgIds {
		orgIDs = append(orgIDs, entity.ID(orgId))
	}
	filter := RoleBindingFilter{
		TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
		ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
		ScopeIDs:   orgIDs,
		Status:     []string{entity.RoleBindingStatusActive},
	}

	roleBindings, err := d.ListRoleBindings(ctx, tx, filter)

	if err != nil {
		if err == gorm.ErrRecordNotFound {
			tilog.Info(ctx, "null user in the org", zap.Any("orgIds", orgIds))
		} else {
			tilog.Error(ctx, "RoleDao GetUserOrgIds err: "+fmt.Sprintf("orgIds=%d", orgIds), zap.Error(err))
		}
		return nil, err
	}

	userIds := make([]uint64, 0, len(roleBindings))

	for _, r := range roleBindings {
		userIds = append(userIds, r.TargetID.Uint64())
	}

	tilog.Info(ctx, "query org users", zap.Any("orgIds", orgIds), zap.Any("userIds", userIds))
	return userIds, nil
}
func (d *RoleDao) GetUserOrgIds(ctx context.Context, tx *gorm.DB, userId uint64, roleBindingStatus []string) ([]uint64, error) {
	filter := RoleBindingFilter{
		TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
		TargetIDs:  []entity.ID{entity.ID(userId)},
		ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
		Status:     roleBindingStatus,
	}

	roleBindings, err := d.ListRoleBindings(ctx, tx, filter)

	if err != nil {
		if err == gorm.ErrRecordNotFound {
			tilog.Info(ctx, "user is not belong to any  active org", zap.Any("userID", userId))
		} else {
			tilog.Error(ctx, "RoleDao GetUserOrgIds err: "+fmt.Sprintf("userId=%d", userId), zap.Error(err))
		}
		return nil, err
	}

	var orgIds []uint64

	for _, r := range roleBindings {
		orgIds = append(orgIds, r.ScopeID.Uint64())
	}

	tilog.Info(ctx, "GetUserOrgIds", zap.Any("orgIds", orgIds), zap.Any("userID", userId))
	return orgIds, nil

}

func (d *RoleDao) BatchSelectRoleBindings(ctx context.Context, tx *gorm.DB, idStart uint64, limit int) ([]entity.RoleBinding, error) {
	var roleBindings []entity.RoleBinding
	err := tx.Where("id > ?", idStart).Order("id asc").Limit(limit).Find(&roleBindings).Error

	return roleBindings, err
}

/*
	func buildRoleBindingFilter(tx *gorm.DB, filter RoleBindingFilter) (*gorm.DB, error) {
		if len(filter.ScopeTypes) != 0 {
			scopeTypes := make([]string, 0, len(filter.ScopeTypes))
			for _, each := range filter.ScopeTypes {
				scopeTypes = append(scopeTypes, each.String())
			}
			tx = tx.Where("scope_type in (?)", scopeTypes)
		}
		if len(filter.ScopeIDs) != 0 {
			tx = tx.Where("scope_id in (?)", filter.ScopeIDs)
		}
		if len(filter.TargetType) != 0 {
			targetTypes := make([]string, 0, len(filter.TargetType))
			for _, each := range filter.TargetType {
				targetTypes = append(targetTypes, each.String())
			}
			tx = tx.Where("target_type in (?)", targetTypes)
		}
		if len(filter.TargetIDs) != 0 {
			tx = tx.Where("target_id in (?)", filter.TargetIDs)
		}
		if len(filter.Roles) != 0 {
			tx = tx.Where("role in (?)", filter.Roles)
		}
		if len(filter.RoleIDs) != 0 {
			tx = tx.Where("role_id in (?)", filter.RoleIDs)
		}

		return tx, nil
	}
*/
func buildRoleBindingFilter(tx *gorm.DB, filter RoleBindingFilter) (*gorm.DB, error) {
	if len(filter.ScopeTypes) > 0 {

		tx = tx.Where("scope_type in (?)", filter.ScopeTypes)
	}
	if len(filter.ScopeIDs) > 0 {
		tx = tx.Where("scope_id in (?)", filter.ScopeIDs)
	}
	if len(filter.TargetType) > 0 {

		tx = tx.Where("target_type in (?)", filter.TargetType)
	}
	if len(filter.TargetIDs) > 0 {
		tx = tx.Where("target_id in (?)", filter.TargetIDs)
	}
	if len(filter.Roles) > 0 {
		tx = tx.Where("role in (?)", filter.Roles)
	}
	if len(filter.RoleIDs) > 0 {
		tx = tx.Where("role_id in (?)", filter.RoleIDs)
	}
	if len(filter.ScopeTypes) == 0 && len(filter.ScopeIDs) == 0 && len(filter.TargetType) == 0 &&
		len(filter.TargetIDs) == 0 && len(filter.Roles) == 0 && len(filter.RoleIDs) == 0 {
		return nil, errs.NewBadRequest("RoleBindingFilter is invalid", nil)
	}
	return tx, nil
}
