package cluster

import (
	"context"

	"github.com/samber/lo"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/featuregate"
)

const cpuErrMsg = "cluster components have heterogeneous CPU configurations"

func (s *serviceImpl) CheckCreateDesiredComponents(
	ctx context.Context,
	cluster *entity.Cluster,
	desiredComponents []*entity.DesiredClusterComponent,
) bizerr.BizError {
	if err := s.componentsSpecialCheck(ctx, cluster, desiredComponents); err != nil {
		return err
	}
	if err := componentReplicaCheck(cluster, desiredComponents); err != nil {
		return err
	}
	if err := storageCheck(cluster, desiredComponents); err != nil {
		return err
	}
	if err := volumeTypeCheck(cluster, desiredComponents); err != nil {
		return err
	}
	if err := cpuArchCheck(cluster, desiredComponents); err != nil {
		return err
	}
	if err := s.cpuGenerationCheck(cluster, desiredComponents); err != nil {
		return err
	}
	if err := s.tenantClusterQuotaCheck(ctx, cluster, desiredComponents); err != nil {
		return err
	}

	return nil
}

const (
	MinimumReplicasOfPD      = 1
	MinimumReplicasOfTiDB    = 1
	MinimumReplicasOfTiKV    = 3
	TiKVNodeQuantityStep     = 3
	MinimumReplicasOfTiFlash = 0
)

var tiproxyAvailableReplicas = []uint32{0, 2, 3, 6, 9, 12, 15, 18, 21, 24}

func componentReplicaCheck(
	cluster *entity.Cluster,
	desiredComponents []*entity.DesiredClusterComponent,
) bizerr.BizError {
	localCluster := copyClusterEntity(cluster)
	mergeClusterComponent(localCluster, desiredComponents)

	for _, comp := range localCluster.GetClusterComponents() {
		switch comp.GetComponentType() {
		case globaldedicatedv1.ComponentType_TIDB:
			if comp.GetCount() < MinimumReplicasOfTiDB {
				return bizerr.New(
					bizerr.ErrCodeInvalidNodeQuantity,
					ErrMsgInvalidMinNodeQuantity,
					comp.GetCount(),
					globaldedicatedv1.ComponentType_TIDB.String(),
					MinimumReplicasOfTiDB,
				)
			}

		case globaldedicatedv1.ComponentType_TIKV:
			if comp.GetCount() < MinimumReplicasOfTiKV {
				return bizerr.New(
					bizerr.ErrCodeInvalidNodeQuantity,
					ErrMsgInvalidMinNodeQuantity,
					comp.GetCount(),
					globaldedicatedv1.ComponentType_TIKV.String(),
					MinimumReplicasOfTiKV,
				)
			}
			if comp.GetCount()%TiKVNodeQuantityStep != 0 {
				return bizerr.New(
					bizerr.ErrCodeInvalidNodeQuantity,
					ErrMsgInvalidNodeQuantityStep,
					comp.GetCount(),
					globaldedicatedv1.ComponentType_TIKV.String(),
					TiKVNodeQuantityStep,
				)
			}

		case globaldedicatedv1.ComponentType_PD:
			if comp.GetCount() < MinimumReplicasOfPD {
				return bizerr.New(
					bizerr.ErrCodeParams,
					"The minimum replicas of component pd is %d",
					MinimumReplicasOfPD,
				)
			}

		case globaldedicatedv1.ComponentType_TIFLASH:
			if comp.GetCount() < MinimumReplicasOfTiFlash {
				return bizerr.New(
					bizerr.ErrCodeInvalidNodeQuantity,
					ErrMsgInvalidMinNodeQuantity,
					comp.GetCount(),
					globaldedicatedv1.ComponentType_TIFLASH.String(),
					MinimumReplicasOfTiFlash,
				)
			}

		case globaldedicatedv1.ComponentType_TIPROXY:
			// TiProxy replica count is checked separately.
			continue

		default:
			return bizerr.New(bizerr.ErrCodeParams, "unsupported component type: %s", comp.GetComponentType().String())
		}
	}

	return nil
}

// tiproxyReplicaCountCheck checks TiProxy replica count for each TiDB group separately.
// It only checks the replica count and the presence of corresponding TiDB components.
func tiproxyReplicaCountCheck(
	cluster *entity.Cluster,
) bizerr.BizError {
	localCluster := copyClusterEntity(cluster)
	mergeClusterComponent(localCluster, cluster.DesiredComponents)
	components := localCluster.GetClusterComponents()

	// Group TiProxy components by TidbGroupId and check each group.
	tiproxyByGroup := make(map[uint64]uint32)
	for _, comp := range components {
		if comp.GetComponentType() == globaldedicatedv1.ComponentType_TIPROXY {
			groupId := comp.GetTidbGroupId()
			tiproxyByGroup[groupId] += comp.GetCount()
		}
	}

	// Check each group's TiProxy count.
	for groupId, tiproxyCount := range tiproxyByGroup {
		// Check if the count is in the available replicas list.
		if !lo.Contains(tiproxyAvailableReplicas, tiproxyCount) {
			return bizerr.New(
				bizerr.ErrCodeInvalidNodeQuantity,
				ErrMsgInvalidNodeQuantity,
				tiproxyCount,
				globaldedicatedv1.ComponentType_TIPROXY.String(),
				tiproxyAvailableReplicas,
			)
		}
		// Find the tidb component in the same tidb group.
		tidbComp, ok := lo.Find(
			components,
			func(item *globaldedicatedv1.Cluster_ClusterComponent) bool {
				return item.GetComponentType() == globaldedicatedv1.ComponentType_TIDB &&
					item.GetTidbGroupId() == groupId
			},
		)
		if !ok {
			return bizerr.New(bizerr.ErrCodeParams, "tidb component not found")
		}
		if tidbComp.GetCount() == 1 && tiproxyCount > 0 {
			return bizerr.New(
				bizerr.ErrCodeInvalidNodeQuantity,
				"The Tidb node quantity has 1 node, not support to TiProxy",
			)
		}
	}

	return nil
}

func cpuArchCheck(cluster *entity.Cluster, desiredComponents []*entity.DesiredClusterComponent) bizerr.BizError {
	localCluster := copyClusterEntity(cluster)
	mergeClusterComponent(localCluster, desiredComponents)

	// map[cpu_arch][]component_type
	groups := make(map[string][]string)

	for _, c := range localCluster.GetClusterComponents() {
		// Only check components that must be CPU-homogeneous.
		if _, ok := entity.ComponentsCpuHomogeneous[c.GetComponentType()]; !ok {
			continue
		}
		if _, ok := groups[c.GetInstanceCpuArch().String()]; !ok {
			groups[c.GetInstanceCpuArch().String()] = []string{}
		}
		groups[c.GetInstanceCpuArch().String()] = append(groups[c.GetInstanceCpuArch().String()], c.GetComponentType().String())
	}

	if len(groups) > 1 {
		return bizerr.New(bizerr.ErrCodeParams, cpuErrMsg).Chain("arch: %v", groups)
	}

	return nil
}

func (s *serviceImpl) cpuGenerationCheck(cluster *entity.Cluster, desiredComponents []*entity.DesiredClusterComponent) bizerr.BizError {
	if s.featGateCli.CheckFeatureEnabled(
		featuregate.AllowHeterogeneousCpuGeneration,
		cluster.OrganizationId,
		cluster.ProjectId,
		cluster.GetCloudProviderId().String(),
		cluster.RegionId.String(),
	) {
		return nil
	}

	localCluster := copyClusterEntity(cluster)
	mergeClusterComponent(localCluster, desiredComponents)

	// map[cpu_gen][]component_type
	groups := make(map[string][]string)

	for _, c := range localCluster.GetClusterComponents() {
		// Only check components that must be CPU-homogeneous.
		if _, ok := entity.ComponentsCpuHomogeneous[c.GetComponentType()]; !ok {
			continue
		}
		if _, ok := groups[c.GetInstanceCpuGeneration().String()]; !ok {
			groups[c.GetInstanceCpuGeneration().String()] = []string{}
		}
		groups[c.GetInstanceCpuGeneration().String()] = append(groups[c.GetInstanceCpuGeneration().String()], c.GetComponentType().String())
	}

	if len(groups) > 1 {
		return bizerr.New(bizerr.ErrCodeParams, cpuErrMsg).Chain("gen: %v", groups)
	}

	return nil
}

func volumeTypeCheck(cluster *entity.Cluster, desiredComps []*entity.DesiredClusterComponent) bizerr.BizError {
	for _, desiredComp := range desiredComps {
		if _, ok := entity.ComponentsHaveStorage[desiredComp.GetComponentType()]; !ok {
			continue
		}

		checkVolumeType := func(vt globaldedicatedv1.StorageMu_VolumeType) bizerr.BizError {
			// check volume type of cloud provider
			switch cluster.CloudProviderId {
			case globalcommonv1.CloudProvider_AWS:
				if vt != globaldedicatedv1.StorageMu_GP3 && vt != globaldedicatedv1.StorageMu_IO2 {
					return bizerr.New(bizerr.ErrCodeParams, "unsupported data volume type %s for AWS", vt)
				}
			case globalcommonv1.CloudProvider_GCP:
				if vt != globaldedicatedv1.StorageMu_PD_SSD && vt != globaldedicatedv1.StorageMu_HYPERDISK_BALANCED {
					return bizerr.New(bizerr.ErrCodeParams, "unsupported data volume type %s for GCP", vt)
				}
			case globalcommonv1.CloudProvider_AZURE:
				if vt != globaldedicatedv1.StorageMu_PREMIUMV2 && vt != globaldedicatedv1.StorageMu_ULTRASSD {
					return bizerr.New(bizerr.ErrCodeParams, "unsupported data volume type %s for AZURE", vt)
				}
			}

			if vt == globaldedicatedv1.StorageMu_LOCAL_STORAGE {
				return bizerr.New(bizerr.ErrCodeParams, "unsupported data volume type %s", vt)
			}

			return nil
		}

		dataVolumeType := desiredComp.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetVolumeType()
		if err := checkVolumeType(dataVolumeType); err != nil {
			return err
		}

		raftVolumeType := desiredComp.GetStorages()[globaldedicatedv1.Sku_RAFT_LOG.String()].GetVolumeType()
		if raftVolumeType != globaldedicatedv1.StorageMu_VOLUME_TYPE_UNSPECIFIED {
			if err := checkVolumeType(raftVolumeType); err != nil {
				return err
			}
		}
	}

	return nil
}

func storageCheck(cluster *entity.Cluster, desiredComponents []*entity.DesiredClusterComponent) bizerr.BizError {
	for _, desiredComp := range desiredComponents {
		if _, ok := entity.ComponentsHaveStorage[desiredComp.GetComponentType()]; !ok {
			continue
		}

		currentCompMayNil, _ := lo.Find(
			cluster.GetClusterComponents(),
			func(i *globaldedicatedv1.Cluster_ClusterComponent) bool {
				return i.GetComponentType() == desiredComp.GetComponentType() &&
					i.GetTidbGroupId() == desiredComp.GetTidbGroupId()
			},
		)

		// Check DATA storage
		dataStorage := desiredComp.GetStorages()[globaldedicatedv1.Sku_DATA.String()]
		if err := componentStorageSizeCheck(
			currentCompMayNil.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetSizeGi(),
			dataStorage.GetSizeGi(),
			desiredComp.Sku,
			globaldedicatedv1.Sku_DATA,
		); err != nil {
			return err
		}
		if err := componentStorageIopsCheck(dataStorage.GetIops(), desiredComp.Sku, globaldedicatedv1.Sku_DATA); err != nil {
			return err
		}
		if err := componentStorageThroughputCheck(dataStorage.GetThroughputMibps(), desiredComp.Sku, globaldedicatedv1.Sku_DATA); err != nil {
			return err
		}

		// Check RAFT_LOG storage
		raftStorage := desiredComp.GetStorages()[globaldedicatedv1.Sku_RAFT_LOG.String()]
		if err := componentStorageSizeCheck(
			currentCompMayNil.GetStorages()[globaldedicatedv1.Sku_RAFT_LOG.String()].GetSizeGi(),
			raftStorage.GetSizeGi(),
			desiredComp.Sku,
			globaldedicatedv1.Sku_RAFT_LOG,
		); err != nil {
			return err
		}
		if err := componentStorageIopsCheck(raftStorage.GetIops(), desiredComp.Sku, globaldedicatedv1.Sku_RAFT_LOG); err != nil {
			return err
		}
		if err := componentStorageThroughputCheck(raftStorage.GetThroughputMibps(), desiredComp.Sku, globaldedicatedv1.Sku_RAFT_LOG); err != nil {
			return err
		}
	}

	return nil
}

func componentStorageSizeCheck(
	currentSizeGi, desiredSizeGi uint32,
	sku *entity.Sku,
	diskType globaldedicatedv1.Sku_DiskType,
) bizerr.BizError {
	var minSizeGi, maxSizeGi int32

	switch diskType {
	case globaldedicatedv1.Sku_DATA:
		minSizeGi = sku.GetDataStorage().GetSizeGiMin()
		maxSizeGi = sku.GetDataStorage().GetSizeGiMax()
	case globaldedicatedv1.Sku_RAFT_LOG:
		minSizeGi = sku.GetRaftlogStorage().GetSizeGiMin()
		maxSizeGi = sku.GetRaftlogStorage().GetSizeGiMax()
	}

	if minSizeGi > 0 && desiredSizeGi < uint32(minSizeGi) {
		return bizerr.New(
			bizerr.ErrCodeInvalidStorageSize,
			ErrMsgInvalidMinStorageSize,
			desiredSizeGi,
			sku.ComponentType.String(),
			minSizeGi,
		)
	}

	// NOTE: storage scale down is not supported in the near future.
	if maxSizeGi > 0 && desiredSizeGi > uint32(maxSizeGi) {
		// when scaling compute down, the current storage size may be greater than the max storage size.
		if desiredSizeGi == currentSizeGi {
			return bizerr.New(bizerr.ErrCodeParams, "%s", ErrMsgInvalidClusterStorageExceededLimit)
		}
		return bizerr.New(
			bizerr.ErrCodeInvalidStorageSize,
			ErrMsgInvalidMaxStorageSize,
			desiredSizeGi,
			sku.ComponentType.String(),
			maxSizeGi,
		)
	}

	return nil
}

func componentStorageIopsCheck(
	desiredIops uint32,
	sku *entity.Sku,
	diskType globaldedicatedv1.Sku_DiskType,
) bizerr.BizError {
	var minIops, maxIops int32
	var storage *globaldedicatedv1.Sku_Storage

	switch diskType {
	case globaldedicatedv1.Sku_DATA:
		storage = sku.GetDataStorage()
	case globaldedicatedv1.Sku_RAFT_LOG:
		storage = sku.GetRaftlogStorage()
	}

	minIops = storage.GetIopsMin()
	maxIops = storage.GetIopsMax()

	if minIops > 0 && desiredIops < uint32(minIops) {
		return bizerr.New(
			bizerr.ErrCodeParams,
			"iops(%d) of the %s storage for node_type(%s) is less than the minimum iops(%d)",
			desiredIops,
			diskType.String(),
			sku.ComponentType.String(),
			minIops,
		)
	}

	if maxIops > 0 && desiredIops > uint32(maxIops) {
		return bizerr.New(
			bizerr.ErrCodeParams,
			"iops(%d) of the %s storage for node_type(%s) is greater than the maximum iops(%d)",
			desiredIops,
			diskType.String(),
			sku.ComponentType.String(),
			maxIops,
		)
	}

	if desiredIops != 0 &&
		(sku.GetCloudProvider() != globalcommonv1.CloudProvider_AWS && sku.GetCloudProvider() != globalcommonv1.CloudProvider_AZURE) &&
		(sku.GetDataStorage().GetVolumeType() != globaldedicatedv1.StorageMu_HYPERDISK_BALANCED) {
		return bizerr.New(
			bizerr.ErrCodeParams,
			"only aws or azure or gcp hyperdisk balanced clusters are able to set iops",
		)
	}

	return nil
}

func componentStorageThroughputCheck(
	desiredThroughput uint32,
	sku *entity.Sku,
	diskType globaldedicatedv1.Sku_DiskType,
) bizerr.BizError {
	var minThroughput, maxThroughput int32
	var volumeType globaldedicatedv1.StorageMu_VolumeType
	var storage *globaldedicatedv1.Sku_Storage

	switch diskType {
	case globaldedicatedv1.Sku_DATA:
		storage = sku.GetDataStorage()
	case globaldedicatedv1.Sku_RAFT_LOG:
		storage = sku.GetRaftlogStorage()
	}

	minThroughput = storage.GetThroughputMibpsMin()
	maxThroughput = storage.GetThroughputMibpsMax()
	volumeType = storage.GetVolumeType()

	if minThroughput != 0 && desiredThroughput < uint32(minThroughput) {
		return bizerr.New(
			bizerr.ErrCodeParams,
			"throughput(%d) of the %s storage for node_type(%s) is less than the minimum throughput(%d)",
			desiredThroughput,
			diskType.String(),
			sku.ComponentType.String(),
			minThroughput,
		)
	}

	if maxThroughput != 0 && desiredThroughput > uint32(maxThroughput) {
		return bizerr.New(
			bizerr.ErrCodeParams,
			"throughput(%d) of the %s storage for node_type(%s) is greater than the maximum throughput(%d)",
			desiredThroughput,
			diskType.String(),
			sku.ComponentType.String(),
			maxThroughput,
		)
	}

	if desiredThroughput != 0 &&
		(sku.GetCloudProvider() != globalcommonv1.CloudProvider_AWS && sku.GetCloudProvider() != globalcommonv1.CloudProvider_AZURE) &&
		(volumeType != globaldedicatedv1.StorageMu_HYPERDISK_BALANCED) {
		return bizerr.New(
			bizerr.ErrCodeParams,
			"only aws or azure or gcp hyperdisk balanced clusters are able to set throughput",
		)
	}

	// aws io2 or azure ultrassd not support throughput.
	if desiredThroughput != 0 &&
		(volumeType == globaldedicatedv1.StorageMu_IO2 || volumeType == globaldedicatedv1.StorageMu_ULTRASSD) {
		return bizerr.New(bizerr.ErrCodeParams, "io2 or ultrassd volume type not support throughput")
	}

	return nil
}

// 2C/4C Component Rules:
//
//	2 vCPU components can only work with other 2 vCPU components
//	4 vCPU components can only work with other 4 vCPU components
//
// When TiDB has 2 or 4 vCPUs:
//
//	Only one TiDB node group is allowed
//	TiDB node quantity must be 1 or 2
//	TiFlash is not supported
//
// When TiDB has 2 vCPUs:
//
//	TiProxy is not supported
//
// When TiKV has 2 vCPUs:
//
//	Must have exactly 3 nodes
//
// When TiKV has 4 vCPUs:
//
//	Feature gate Multi4cTikv required to have > 3 nodes
//	Node count must be less than tenantClusterQuotaRepo.Max4CTikvCount()
func (s *serviceImpl) componentsSpecialCheck(
	ctx context.Context,
	cluster *entity.Cluster,
	desiredComponents []*entity.DesiredClusterComponent,
) bizerr.BizError {
	var (
		// Assume components of the same type have the same cpu num.
		tidbNodeCpuNum, tikvNodeCpuNum                   uint32
		tidbComponents                                   []*globaldedicatedv1.Cluster_ClusterComponent
		tiproxyCount, tikvCount, tiflashCount, tidbCount uint32
	)

	localCluster := copyClusterEntity(cluster)
	mergeClusterComponent(localCluster, desiredComponents)

	for _, comp := range localCluster.GetClusterComponents() {
		switch comp.GetComponentType() {
		case globaldedicatedv1.ComponentType_TIDB:
			tidbNodeCpuNum = comp.GetCpuNum()
			tidbComponents = append(tidbComponents, comp)
			tidbCount += comp.GetCount()
		case globaldedicatedv1.ComponentType_TIKV:
			tikvNodeCpuNum = comp.GetCpuNum()
			tikvCount = comp.GetCount()
		case globaldedicatedv1.ComponentType_TIPROXY:
			tiproxyCount += comp.GetCount()
		case globaldedicatedv1.ComponentType_TIFLASH:
			tiflashCount = comp.GetCount()
		}
	}

	// Early return if tidb and tidb node cpu num are not equal to 2 nor 4.
	if (tidbNodeCpuNum != 2 && tikvNodeCpuNum != 2) && (tidbNodeCpuNum != 4 && tikvNodeCpuNum != 4) {
		return nil
	}

	// If cpu num is 2 or 4, only (2C tidb + 2C tikv) or (4C tidb + 4C tikv) are allowed.
	if (tidbNodeCpuNum != 2 || tikvNodeCpuNum != 2) && (tidbNodeCpuNum != 4 || tikvNodeCpuNum != 4) {
		return bizerr.New(bizerr.ErrCodeInvalidClusterCombination, "%s", ErrMsgInvalidClusterCombination)
	}

	if len(tidbComponents) > 1 {
		return bizerr.New(
			bizerr.ErrCodeParams,
			"When TiDB component has 2 or 4 vCPUs, only one TiDB node group is allowed",
		)
	}

	if tidbCount != 1 && tidbCount != 2 {
		return bizerr.New(
			bizerr.ErrCodeParams,
			"The node_quantity must be 1 or 2 when the vCPUs of TiDB is %d",
			tidbNodeCpuNum,
		)
	}

	// TiProxy only works with TiDB when the vCPUs of TiDB vCPUs >= 4.
	if tiproxyCount > 0 && tidbNodeCpuNum == 2 {
		return bizerr.New(
			bizerr.ErrCodeParams,
			"When TiDB component has 2 vCPUs, the TiProxy component is not supported",
		)
	}

	if tikvCount > MinimumReplicasOfTiKV && tikvNodeCpuNum == 2 {
		return bizerr.New(bizerr.ErrCodeParams, "The node_quantity must be 3 when the vCPUs of TiKV is 2")
	}

	// If a tenant wants to create a cluster with more than 3 4C TiKV nodes, the tenant must enable the feature gate.
	if tikvCount > MinimumReplicasOfTiKV &&
		!s.featGateCli.CheckFeatureEnabled(
			featuregate.Multi4cTikv,
			cluster.OrganizationId,
			cluster.ProjectId,
			cluster.GetCloudProviderId().String(),
			cluster.RegionId.String(),
		) {
		return bizerr.New(bizerr.ErrCodeParams, "The node_quantity must be 3 when the vCPUs of TiKV is 4")
	}

	// 4C TiKV node count has an upper limit.
	max4CTikvCount := s.tenantClusterQuotaRepo.Max4CTikvCount(ctx)
	if tikvCount > uint32(max4CTikvCount) {
		return bizerr.New(
			bizerr.ErrCodeParams,
			"The node_quantity must be less than %d when the vCPUs of TiKV is 4",
			max4CTikvCount,
		)
	}

	// TiFlash is not supported when the vCPUs of TiDB is 2 or 4.
	if tiflashCount > 0 {
		return bizerr.New(bizerr.ErrCodeInvalidClusterCombination, "%s", ErrMsgInvalidClusterCombinationTiFlash)
	}

	return nil
}

func (s *serviceImpl) tenantClusterQuotaCheck(
	ctx context.Context,
	cluster *entity.Cluster,
	desiredComponents []*entity.DesiredClusterComponent,
) bizerr.BizError {
	tenantClusterQuota, err := s.tenantClusterQuotaRepo.Load(ctx, cluster.GetOrganizationId())
	if err != nil {
		return err
	}

	localCluster := copyClusterEntity(cluster)
	mergeClusterComponent(localCluster, desiredComponents)

	var pdCnt, tidbCnt, tikvCnt, tiflashCnt, tiproxyCnt uint32
	for _, comp := range localCluster.GetClusterComponents() {
		switch comp.GetComponentType() {
		case globaldedicatedv1.ComponentType_PD:
			pdCnt += comp.GetCount()
		case globaldedicatedv1.ComponentType_TIDB:
			tidbCnt += comp.GetCount()
		case globaldedicatedv1.ComponentType_TIKV:
			tikvCnt += comp.GetCount()
		case globaldedicatedv1.ComponentType_TIFLASH:
			tiflashCnt += comp.GetCount()
		case globaldedicatedv1.ComponentType_TIPROXY:
			tiproxyCnt += comp.GetCount()
		}
	}

	// 0 means no limit.
	if tenantClusterQuota.PDQuota > 0 && pdCnt > uint32(tenantClusterQuota.PDQuota) {
		return bizerr.New(
			bizerr.ErrCodeClusterQuotaExceed,
			ErrMsgClusterQuotaExceed,
			globaldedicatedv1.ComponentType_PD.String(),
			pdCnt,
			tenantClusterQuota.PDQuota,
		)
	}
	if tenantClusterQuota.TiDBQuota > 0 && tidbCnt > uint32(tenantClusterQuota.TiDBQuota) {
		return bizerr.New(
			bizerr.ErrCodeClusterQuotaExceed,
			ErrMsgClusterQuotaExceed,
			globaldedicatedv1.ComponentType_TIDB.String(),
			tidbCnt,
			tenantClusterQuota.TiDBQuota,
		)
	}
	if tenantClusterQuota.TiKVQuota > 0 && tikvCnt > uint32(tenantClusterQuota.TiKVQuota) {
		return bizerr.New(
			bizerr.ErrCodeClusterQuotaExceed,
			ErrMsgClusterQuotaExceed,
			globaldedicatedv1.ComponentType_TIKV.String(),
			tikvCnt,
			tenantClusterQuota.TiKVQuota,
		)
	}
	if tenantClusterQuota.TiFlashQuota > 0 && tiflashCnt > uint32(tenantClusterQuota.TiFlashQuota) {
		return bizerr.New(
			bizerr.ErrCodeClusterQuotaExceed,
			ErrMsgClusterQuotaExceed,
			globaldedicatedv1.ComponentType_TIFLASH.String(),
			tiflashCnt,
			tenantClusterQuota.TiFlashQuota,
		)
	}
	if tenantClusterQuota.TiProxyQuota > 0 && tiproxyCnt > uint32(tenantClusterQuota.TiProxyQuota) {
		return bizerr.New(
			bizerr.ErrCodeClusterQuotaExceed,
			ErrMsgClusterQuotaExceed,
			globaldedicatedv1.ComponentType_TIPROXY.String(),
			tiproxyCnt,
			tenantClusterQuota.TiProxyQuota,
		)
	}

	return nil
}
