package v1

import (
	"context"
	"encoding/json"
	"fmt"
	"slices"
	"sort"
	"strconv"
	"strings"

	"github.com/samber/lo"
	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/intstr"
	"sigs.k8s.io/controller-runtime/pkg/log"
)

const (
	DefaultPublicSQLEndpointPort  = int32(4000)
	DefaultPrivateSQLEndpointPort = int32(4000)

	// DefaultLoadBalancerIDAnnotationKey is copied from pkg/consts/annotations.go
	DefaultLoadBalancerIDAnnotationKey = "tidbcloud.com/default-tidbgroup-access-id"
)

type ComponentType string

const (
	ComponentTiDB           ComponentType = "tidb"
	ComponentTiKV           ComponentType = "tikv"
	ComponentPD             ComponentType = "pd"
	ComponentTiProxy        ComponentType = "tiproxy"
	ComponentTiFlash        ComponentType = "tiflash"
	ComponentTiFlashLearner ComponentType = "tiflash-learner" // TiflashLearner is used to define TiFlash Proxy config
	ComponentRestore        ComponentType = "restore"
	ComponentBackup         ComponentType = "backup"
	ComponentCompactBackup  ComponentType = "compactbackup"
	ComponentTiCDC          ComponentType = "ticdc"
	ComponentAdminTiDB      ComponentType = "extra-tidb"
	ComponentLightning      ComponentType = "lightning"
	ComponentTiflow         ComponentType = "tiflow"
	ComponentTiflowMaster   ComponentType = "tiflow-master"
	ComponentTiflowExecutor ComponentType = "tiflow-executor"
)

var (
	// TODO(ideascf): remove it after the mgmt call ScaleCluster API with `tidb_group_id` for tikv/pd/tiflash.
	// For backward compatibility only.
	// Currently, the mgmt service call ScaleCluster API without the `tidb_group_id` if the target component is tikv/pd/tiflash.
	// To keep compatibility, we ignore `tidb_group_id` if the component type is one of the following when trying to get component spec/status.
	UngroupedClusterComponentTypes = map[ComponentType]bool{
		ComponentPD:             true,
		ComponentTiKV:           true,
		ComponentTiFlash:        true,
		ComponentTiFlashLearner: true,
	}
)

// PrefixedId return a identifier of the cluster prefixed by "tidb", which is used as a lot of reference key
// in our legacy system
func (c *Cluster) PrefixedId() string {
	return ClusterPrefixId(c.Name)
}

// ClusterPrefixId return an identifier of the cluster prefixed by "tidb", which is used as a lot of reference key
// in our legacy system
func ClusterPrefixId(clusterName string) string {
	return fmt.Sprintf("tidb%s", clusterName)
}

// GetProviderType gets provider type from cluster labels
func (c *Cluster) GetProviderType() string {
	return c.Labels[ProviderTypeLabel]
}

// GetCloudProvider gets cloud provider from the cluster's labels.
func (c *Cluster) GetCloudProvider() string {
	switch c.Labels[ProviderTypeLabel] {
	case ProviderTypeEKS, ProviderTypeAWS:
		return CloudProviderAWS
	case ProviderTypeGKE, ProviderTypeGCP:
		return CloudProviderGCP
	case ProviderTypeAKS:
		return CloudProviderAzure
	default:
		return ""
	}
}

// GetRegion gets region from cluster labels
func (c *Cluster) GetRegion() string {
	return c.Labels[RegionLabel]
}

const (
	// unknown is copied from o11y code
	// This field may have been used for some tidb configs' generation
	// DON'T change it unless you have checked all prod clusters
	defaultTenant  = "unknown"
	defaultProject = "default"
)

// fallback to "unknown"
func (c *Cluster) GetTenantID() string {
	v, ok := c.Labels[TenantTagKey]
	if ok && v != "" {
		return v
	}
	return defaultTenant
}

// fallback to network name
// if network name is empty, fallback to "default"
func (c *Cluster) GetProjectID() string {
	v, ok := c.Labels[ProjectTagKey]
	if ok && v != "" {
		return v
	}
	networkName := c.GetNetworkRef().Name
	if networkName != "" {
		return networkName
	}

	return defaultProject
}

// fallback to cluster name
func (c *Cluster) GetClusterID() string {
	v, ok := c.Labels[ClusterTagKey]
	if ok && v != "" {
		return v
	}
	return c.Name
}

func (c *Cluster) HasBug(name string) bool {
	name = amendBugName(name)
	bugList := c.BugList()
	for _, bug := range bugList {
		if bug == name {
			return true
		}
	}

	return false
}

func (c *Cluster) BugList() []string {
	var out []string
	bugList := c.Annotations[AnnoBugList]
	for _, bug := range strings.Split(bugList, ",") {
		if bug == "" {
			continue
		}

		out = append(out, bug)
	}

	return out
}

func (c *Cluster) AddBug(name string) {
	name = amendBugName(name)
	if c.HasBug(name) {
		return
	}

	list := c.BugList()
	c.Annotations[AnnoBugList] = strings.Join(append(list, name), ",")
}

func (c *Cluster) FormatFeature(featureName string) string {
	return AnnoKeyFeatureNamePrefix + featureName
}

func (c *Cluster) EnableFeature(featureName string) {
	if c.Annotations == nil {
		c.Annotations = make(map[string]string)
	}
	c.Annotations[c.FormatFeature(featureName)] = "true"
}

func (c *Cluster) DisableFeature(featureName string) {
	delete(c.Annotations, c.FormatFeature(featureName))
}

func (c *Cluster) FeatureEnabled(featureName string) bool {
	return c.Annotations[c.FormatFeature(featureName)] == "true"
}

// TODO(ideascf): replace all usage of AnnoClusterUpdateAnnotationKeyInstanceType with this method
// TODO(ideascf): use groupId to instead of tcName
func (c *Cluster) InstanceTypeChangedComponent(tcName string) []ComponentType {
	annoKey := c.GetInstanceTypeChangedComponentAnnoKey(tcName)
	annoValue := c.Annotations[annoKey]
	var components []ComponentType
	// this annotation will be added before pods restarted and deleted after the nodegroup deletion called
	if annoValue != "" {
		for _, comp := range strings.Split(annoValue, AnnoInstanceTypeSeparator) {
			components = append(components, ComponentType(comp))
		}
	}
	return components
}

func (c *Cluster) ClearInstanceTypeChangedComponents(tcName string) {
	annoKey := c.GetInstanceTypeChangedComponentAnnoKey(tcName)
	delete(c.Annotations, annoKey)
}

// AppendInstanceTypeChangedComponent append this component to cluster annotations `AnnoClusterUpdateAnnotationKeyInstanceType`.
// This operation will cause tidbgorup.go / tidbcluster.go to apply instance type change to TC.
func (c *Cluster) AppendInstanceTypeChangedComponent(comp ClusterComponentSpecWrapped, tcName string) {
	c.mutateInstanceTypeChangedComponent(comp, tcName, func(compTypes []string) []string {
		tp := string(comp.GetComponentType())
		if lo.Contains(compTypes, tp) {
			return compTypes
		}
		compTypes = append(compTypes, tp)
		return compTypes
	})
}

// RemoveInstanceTypeChangedComponent remove this component from cluster annotations `AnnoClusterUpdateAnnotationKeyInstanceType`.
// When the component is already scaled up/down, call this method to remove.
func (c *Cluster) RemoveInstanceTypeChangedComponent(comp ClusterComponentSpecWrapped, tcName string) {
	c.mutateInstanceTypeChangedComponent(comp, tcName, func(compTypes []string) []string {
		tp := string(comp.GetComponentType())
		if !lo.Contains(compTypes, tp) {
			return compTypes
		}
		compTypes = lo.Reject(compTypes, func(item string, _ int) bool { return item == tp })
		return compTypes
	})
}

func (c *Cluster) mutateInstanceTypeChangedComponent(comp ClusterComponentSpecWrapped, tcName string, m func(compTypes []string) []string) {
	if c.Annotations == nil {
		c.Annotations = make(map[string]string)
	}

	annoKey := c.GetInstanceTypeChangedComponentAnnoKey(tcName)
	annoValue := c.Annotations[annoKey]
	var components []string
	if annoValue != "" {
		components = strings.Split(annoValue, AnnoInstanceTypeSeparator)
	}

	components = m(components)
	// sort the component types to avoid unnecessary update due to different order
	// e.g. `pd,tidb` vs `tidb,pd`
	// TidbCluster will be updated later
	sort.Strings(components)
	c.Annotations[annoKey] = strings.Join(components, AnnoInstanceTypeSeparator)
}

func (c *Cluster) GetInstanceTypeChangedComponentAnnoKey(tcName string) string {
	const tcNameOfDefaultGroup = "db"
	var key string
	if tcNameOfDefaultGroup == tcName {
		key = AnnoClusterUpdateAnnotationKeyInstanceType
	} else {
		key = AnnoClusterUpdateAnnotationKeyInstanceType + "-" + tcName
	}
	return key
}

func (c *Cluster) GetMinReadySeconds(ctx context.Context, groupId string, compType ComponentType) string {
	const (
		defaultMinReadySeconds = 180
	)

	var key string
	switch compType {
	case ComponentTiDB:
		key = AnnoKeyOverrideTiDBMinReadySeconds
	case ComponentTiProxy:
		key = AnnoKeyOverrideTiProxyMinReadySeconds
	default:
		panic(fmt.Sprintf("unsupported component type %s for min-ready-seconds override", compType))
	}

	val := c.getOverriddenIntValueFromAnno(ctx, groupId, key, defaultMinReadySeconds)
	return fmt.Sprintf("%d", val)
}

func (c *Cluster) GetTiDBPreStopHookSleepSeconds(ctx context.Context, groupId string, defValue int64) int64 {
	val := c.getOverriddenIntValueFromAnno(ctx, groupId, AnnoKeyOverrideTiDBPreStopHookSleepSeconds, defValue)
	return val
}

func (c *Cluster) GetTiFlashGracefulShutdownSleepSeconds(ctx context.Context, defValue int64) int64 {
	return c.getOverriddenIntValueFromAnno(ctx, "", AnnoKeyOverrideTiFlashGracefulShutdownSleepSeconds, defValue)
}

func (c *Cluster) getOverriddenIntValueFromAnno(ctx context.Context, groupId, baseKey string, defValue int64) int64 {
	logger := log.FromContext(ctx).WithValues("groupId", groupId, "baseKey", baseKey)
	val := c.getOverriddenValueFromAnno(ctx, groupId, baseKey, fmt.Sprintf("%d", defValue))
	out, err := strconv.ParseInt(val, 10, 64)
	if err != nil {
		logger.Error(err, "failed to parse overridden value from annotation, use default value", "annoValue", val, "defValue", defValue)
		return defValue
	}
	return out
}

func (c *Cluster) getOverriddenValueFromAnno(ctx context.Context, groupId string, baseKey, defValue string) string {
	logger := log.FromContext(ctx).WithValues("groupId", groupId, "baseKey", baseKey)
	val := c.Annotations[baseKey]
	// group-specific annotation has higher priority
	groupKey := baseKey + "-" + groupId
	if v := c.Annotations[groupKey]; v != "" {
		logger.Info("found group-specific override annotation", "annoKey", groupKey, "annoValue", v)
		val = v
	}
	if val == "" {
		return defValue
	}
	return val
}

func (c *Cluster) SharedBucketEnabled() bool {
	return c.Spec.SharedBucket != nil && c.Spec.SharedBucket.BucketURL != ""
}

func amendBugName(name string) string {
	name = strings.TrimSpace(name)
	return name
}

func (c *Cluster) GetJWKsSecretName() string {
	return fmt.Sprintf("%s-tidb-auth-token-jwks-secret", c.Name)
}

func (c *Cluster) GetAllComponentTypes() []ComponentType {
	return []ComponentType{
		ComponentPD,
		ComponentTiKV,
		ComponentTiDB,
		ComponentTiFlash,
		ComponentTiProxy,
	}
}

// GetAllComponentSpecs returns all components including extra groups of the cluster, excluding those with 0 replicas.
func (c *Cluster) GetAllComponentSpecs() []ClusterComponentSpecWrapped {
	var out []ClusterComponentSpecWrapped

	for _, each := range c.GetComponents() {
		out = append(out, &ClusterComponentSpecWrapper{
			ClusterComponent: each,
			cluster:          c,
			groupId:          c.GetDefaultGroupID(),
		})
	}

	for _, g := range c.Spec.ExtraGroups {
		if g.Deleted() {
			continue
		}

		for _, comp := range g.GetComponents() {
			out = append(out, &ClusterComponentSpecWrapper{
				ClusterComponent: comp,
				cluster:          c,
				groupId:          g.Name,
			})
		}
	}

	return out
}

func (c *Cluster) AllComponentSpecsGroupByGroupId() map[string][]ClusterComponentSpecWrapped {
	out := make(map[string][]ClusterComponentSpecWrapped)
	for _, each := range c.GetAllComponentSpecs() {
		out[each.GetGroupId()] = append(out[each.GetGroupId()], each)
	}
	return out
}

// GetAllComponentsV2 returns all components including extra groups of the cluster, excluding those with 0 replicas.
func (c *Cluster) GetAllComponentsV2() []ClusterComp {
	var out []ClusterComp

	for _, each := range c.GetAllComponentSpecs() {
		status, err := c.GetOrCreateComponentStatusInGroup(each.GetGroupId(), each.GetComponentType())
		if err != nil {
			panic(fmt.Sprintf("failed to get or create component status for group %s, component %s: %s", each.GetGroupId(), each.GetComponentType(), err))
		}

		out = append(out, &clusterComp{
			ClusterComponentSpecWrapped:   each,
			ClusterComponentStatusWrapped: status,
		})
	}

	return out
}

func (c *Cluster) AllComponentsV2GroupByGroupId() map[string][]ClusterComp {
	out := make(map[string][]ClusterComp)
	for _, each := range c.GetAllComponentsV2() {
		out[each.GetGroupId()] = append(out[each.GetGroupId()], each)
	}
	return out
}

// Deprecated Use GetAllComponentsV2 instead as it only returns components within the default group
// GetAllComponents returns all components excluding extra groups of the cluster, including those with 0 replicas.
func (c *Cluster) GetAllComponents() []ClusterComponent {
	return []ClusterComponent{
		&c.Spec.Cluster.PD,
		&c.Spec.Cluster.TiKV,
		&c.Spec.Cluster.TiDB,
		&c.Spec.Cluster.TiFlash,
		&c.Spec.Cluster.TiProxy,
	}
}

// TODO(ideascf): consider to rename it to `GetComponentsInDefaultGroup`
// GetComponents returns all components excluding extra groups of the cluster, excluding those with 0 replicas.
func (c *Cluster) GetComponents() []ClusterComponent {
	comps := []ClusterComponent{&c.Spec.Cluster.PD, &c.Spec.Cluster.TiKV, &c.Spec.Cluster.TiDB}
	if c.Spec.Cluster.TiFlash.Replicas > 0 {
		comps = append(comps, &c.Spec.Cluster.TiFlash)
	}
	if c.Spec.Cluster.TiProxy.Replicas > 0 {
		comps = append(comps, &c.Spec.Cluster.TiProxy)
	}
	return comps
}

func (c *Cluster) GetComponent(comp ComponentType) ClusterComponent {
	switch comp {
	case ComponentPD:
		return &c.Spec.Cluster.PD
	case ComponentTiProxy:
		return &c.Spec.Cluster.TiProxy
	case ComponentTiDB:
		return &c.Spec.Cluster.TiDB
	case ComponentTiKV:
		return &c.Spec.Cluster.TiKV
		// tiflash and tiflashLearner running in the same deployment
	case ComponentTiFlash, ComponentTiFlashLearner:
		return &c.Spec.Cluster.TiFlash
	default:
		panic(fmt.Sprintf("invalid component type: %s", comp))
	}
}

func (c *Cluster) GetComponentStatus(comp ComponentType) ClusterComponentStatus {
	if c.Status.DB == nil {
		return nil
	}
	switch comp {
	case ComponentPD:
		return c.Status.DB.PD
	case ComponentTiProxy:
		return c.Status.DB.TiProxy
	case ComponentTiDB:
		return c.Status.DB.TiDB
	case ComponentTiKV:
		return c.Status.DB.TiKV
		// tiflash and tiflashLearner running in the same deployment
	case ComponentTiFlash, ComponentTiFlashLearner:
		return c.Status.DB.TiFlash
	default:
		panic(fmt.Sprintf("invalid component type: %s", comp))
	}
}

// GetOrCreateComponentStatus gets or creates the component status in the default group
func (c *Cluster) GetOrCreateComponentStatus(comp ComponentType) ClusterComponentStatus {
	if c.Status.DB == nil {
		c.Status.DB = &DBStatus{}
	}

	switch comp {
	case ComponentPD:
		if c.Status.DB.PD == nil {
			c.Status.DB.PD = &PDStatus{}
		}
	case ComponentTiProxy:
		if c.Status.DB.TiProxy == nil {
			c.Status.DB.TiProxy = &TiProxyStatus{}
		}
	case ComponentTiDB:
		if c.Status.DB.TiDB == nil {
			c.Status.DB.TiDB = &TiDBStatus{}
		}
	case ComponentTiKV:
		if c.Status.DB.TiKV == nil {
			c.Status.DB.TiKV = &TiKVStatus{}
		}
		// tiflash and tiflashLearner running in the same deployment
	case ComponentTiFlash, ComponentTiFlashLearner:
		if c.Status.DB.TiFlash == nil {
			c.Status.DB.TiFlash = &TiFlashStatus{}
		}
	}
	return c.GetComponentStatus(comp)
}

// GetSuspendCondition returns the suspend cluster condition
func (c *Cluster) GetSuspendCondition() *ClusterCondition {
	for _, condition := range c.Status.Conditions {
		if condition.Type == ClusterSuspended {
			return &condition
		}
	}
	return nil
}

// Suspended returns true if all the components has been suspended
func (c *Cluster) Suspended() bool {
	return IsStatusConditionTrue(c.Status.Conditions, ClusterSuspended)
}

// Suspending returns true if any one of the components is suspending
func (c *Cluster) Suspending() bool {
	return (!c.PDStatusNil() && c.Status.DB.PD.Suspending()) ||
		(!c.TiDBStatusNil() && c.Status.DB.TiDB.Suspending()) ||
		(!c.TiKVStatusNil() && c.Status.DB.TiKV.Suspending()) ||
		(!c.TiFlashStatusNil() && c.Status.DB.TiFlash.Suspending())
}

func (c *Cluster) PDStatusNil() bool {
	if c.Status.DB == nil {
		return true
	}
	if c.Status.DB.PD == nil {
		return true
	}
	return false
}

func (c *Cluster) TiDBStatusNil() bool {
	if c.Status.DB == nil {
		return true
	}
	if c.Status.DB.TiDB == nil {
		return true
	}
	return false
}

func (c *Cluster) TiKVStatusNil() bool {
	if c.Status.DB == nil {
		return true
	}
	if c.Status.DB.TiKV == nil {
		return true
	}
	return false
}

func (c *Cluster) TiFlashStatusNil() bool {
	if c.Status.DB == nil {
		return true
	}
	if c.Status.DB.TiFlash == nil {
		return true
	}
	return false
}

func (c *Cluster) TiProxyStatusNil() bool {
	if c.Status.DB == nil {
		return true
	}
	if c.Status.DB.TiProxy == nil {
		return true
	}
	return false
}

func (c *Cluster) FullSynced() bool {
	return c.FullSyncedWithDetail() == nil
}

func (c *Cluster) FullSyncedWithDetail() error {
	if err := c.SyncedWithDetail(); err != nil {
		return err
	}
	if !c.ExtraGroupSync() {
		return fmt.Errorf("extra group is not synced")
	}
	if c.ConfigChanging() {
		return fmt.Errorf("config is changing")
	}

	return nil
}

// Synced make sure the status is synced as the spec.
// we can use it to make sure the update is applied when we update the replica, storage or instance type in the spec.
func (c *Cluster) Synced() bool {
	return c.SyncedWithDetail() == nil
}

func (c *Cluster) SyncedWithDetail() error {
	if c.Status.Phase != ClusterPhaseRunning {
		return fmt.Errorf("phase is not running")
	}

	// Updated the spec to pause, but the status may not be updated yet
	if c.Spec.Cluster.SuspendAction != nil && c.Spec.Cluster.SuspendAction.SuspendCompute {
		return fmt.Errorf("cluster is is suspending")
	}

	db := c.Status.DB
	if db == nil {
		return fmt.Errorf("status.db is not set")
	}

	if c.Spec.Cluster.TiDB.Replicas == 0 {
		if db.TiDB != nil && len(db.TiDB.Members) != 0 {
			return fmt.Errorf("replicas of tidb is 0 but status.db.tidb still contains members")
		}
	} else {
		if db.TiDB == nil {
			return fmt.Errorf("status.db.tidb is not set")
		}
		if db.TiDB.Status != ComponentNormal {
			return fmt.Errorf("status.db.tidb is not synced, current status is %s, not %s", db.TiDB.Status, ComponentNormal)
		}
		if len(db.TiDB.Members) != c.Spec.Cluster.TiDB.Replicas {
			return fmt.Errorf("status.db.tidb is not synced, expected replicas %d, actual %d", c.Spec.Cluster.TiDB.Replicas, len(db.TiDB.Members))
		}
		tidbInstanceTypePolicy := c.tiDBMemberInstanceTypePolicy("", c.Spec.Cluster.TiDB.InstanceType)
		for _, member := range db.TiDB.Members {
			if member.InstanceType != "" && !tidbInstanceTypePolicy.allows(member.InstanceType) {
				// NOTE: we treat `member.InstanceType == ""` as Synced as some providers not implment instance type scale.
				// check `InstanceType` rather than `InstanceTypeScalePhase` so that we can report Synced correctly before provider reconcile.
				return fmt.Errorf("status.db.tidb is not synced, expected instance type of %s is %s, actual %s", member.Name, c.Spec.Cluster.TiDB.InstanceType, member.InstanceType)
			}
		}
		if db.TiDB.InstanceTypeScalePhase != InstanceTypeScalePhaseNone {
			return fmt.Errorf("status.db.tidb is not synced, current instance type scale phase is %s", db.TiDB.InstanceTypeScalePhase)
		}
	}

	if c.Spec.Cluster.TiProxy.Replicas == 0 {
		if db.TiProxy != nil && len(db.TiProxy.Members) != 0 {
			return fmt.Errorf("replicas of tiproxy is 0 but status.db.tiproxy still contains members")
		}
	} else {
		if db.TiProxy == nil {
			return fmt.Errorf("status.db.tiproxy is not set")
		}
		if db.TiProxy.Status != ComponentNormal {
			return fmt.Errorf("status.db.tiproxy is not synced, current status is %s, not %s", db.TiProxy.Status, ComponentNormal)
		}
		if len(db.TiProxy.Members) != c.Spec.Cluster.TiProxy.Replicas {
			return fmt.Errorf("status.db.tiproxy is not synced, expected replicas %d, actual %d", c.Spec.Cluster.TiProxy.Replicas, len(db.TiProxy.Members))
		}
		for _, member := range db.TiProxy.Members {
			if member.InstanceType != "" && member.InstanceType != c.Spec.Cluster.TiProxy.InstanceType {
				return fmt.Errorf("status.db.tiproxy is not synced, expected instance type of %s is %s, actual %s", member.Name, c.Spec.Cluster.TiProxy.InstanceType, member.InstanceType)
			}
		}
		if db.TiProxy.InstanceTypeScalePhase != InstanceTypeScalePhaseNone {
			return fmt.Errorf("status.db.tiproxy is not synced, current instance type scale phase is %s", db.TiProxy.InstanceTypeScalePhase)
		}
	}

	mapper, err := RetrieveMapper(c)
	if err != nil {
		return fmt.Errorf("failed to retrieve CMEKStorageClassMapper: %w", err)
	}
	if c.Spec.Cluster.TiKV.Replicas == 0 {
		if db.TiKV != nil && len(db.TiKV.Members) != 0 {
			return fmt.Errorf("replicas of tikv is 0 but status.db.tikv still contains members")
		}
	} else {
		if db.TiKV == nil {
			return fmt.Errorf("status.db.tikv is not set")
		}
		if db.TiKV.Status != ComponentNormal {
			return fmt.Errorf("status.db.tikv is not synced, current status is %s, not %s", db.TiKV.Status, ComponentNormal)
		}
		if len(db.TiKV.Members) != c.Spec.Cluster.TiKV.Replicas {
			return fmt.Errorf("status.db.tikv is not synced, expected replicas %d, actual %d", c.Spec.Cluster.TiKV.Replicas, len(db.TiKV.Members))
		}
		tikvInstanceTypePolicy := c.tiKVMemberInstanceTypePolicy()
		if !c.Spec.Cluster.TiKV.StorageSize.IsZero() && !c.Spec.Cluster.TiKV.GetUseLocalStorage() {
			if db.TiKV.Volume != nil {
				if c.Spec.Cluster.TiKV.StorageSize.Cmp(db.TiKV.Volume.CurrentCapacity) != 0 {
					return fmt.Errorf("status.db.tikv.volume is not synced, expected storage size %s, actual %s", &c.Spec.Cluster.TiKV.StorageSize, &db.TiKV.Volume.CurrentCapacity)
				}
				if !db.TiKV.Volume.storageClassSynced(c.Spec.Cluster.TiKV.StorageClassName, mapper) {
					// TODO: add details
					return fmt.Errorf("status.db.tikv.volume is not synced, storage class is not syned, expected: %v, actual: %v", mapper.Name(c.Spec.Cluster.TiKV.StorageClassName), db.TiKV.Volume.CurrentStorageClass)
				}
			}
		}
		// check additional storage volumes (e.g raft volume) for tikv
		if len(c.Spec.Cluster.TiKV.StorageVolumes) > 0 {
			// NOTE: do not support remove existing storage volumes now
			if len(db.TiKV.StorageVolumes) != len(c.Spec.Cluster.TiKV.StorageVolumes) {
				return fmt.Errorf("status.db.tikv.storageVolumes is not synced, expected len %v, actual %v", len(c.Spec.Cluster.TiKV.StorageVolumes), len(db.TiKV.StorageVolumes))
			}
			// the spec and the status should all be sorted by volume name
			for i, volSpec := range c.Spec.Cluster.TiKV.StorageVolumes {
				volStatus := db.TiKV.StorageVolumes[i]
				if volSpec.Name != volStatus.Name || volSpec.StorageSize.Cmp(volStatus.CurrentCapacity) != 0 ||
					!volStatus.storageClassSynced(volSpec.StorageClassName, mapper) {
					return fmt.Errorf("status.db.tikv.storageVolumes[%v] is not synced, expected %v, actual %v", i, volSpec, volStatus)
				}
			}
		}
		for _, member := range db.TiKV.Members {
			if member.InstanceType != "" && !tikvInstanceTypePolicy.allows(member.InstanceType) {
				return fmt.Errorf("status.db.tikv is not synced, expected instance type of %s is %s, actual %s", member.Name, c.Spec.Cluster.TiKV.InstanceType, member.InstanceType)
			}
		}
		if db.TiKV.InstanceTypeScalePhase != InstanceTypeScalePhaseNone {
			return fmt.Errorf("status.db.tikv is not synced, current instance type scale phase is %s", db.TiKV.InstanceTypeScalePhase)
		}
	}

	if c.Spec.Cluster.PD.Replicas == 0 {
		if db.PD != nil && len(db.PD.Members) != 0 {
			return fmt.Errorf("replicas of pd is 0 but status.db.pd still contains members")
		}
	} else {
		if db.PD == nil {
			return fmt.Errorf("status.db.pd is not set")
		}
		if db.PD.Status != ComponentNormal {
			return fmt.Errorf("status.db.pd is not synced, current status is %s, not %s", db.PD.Status, ComponentNormal)
		}
		if len(db.PD.Members) != c.Spec.Cluster.PD.Replicas {
			return fmt.Errorf("status.db.pd is not synced, expected replicas %d, actual %d", c.Spec.Cluster.PD.Replicas, len(db.PD.Members))
		}
		if !c.Spec.Cluster.PD.StorageSize.IsZero() && !c.Spec.Cluster.PD.GetUseLocalStorage() {
			if db.PD.Volume != nil {
				if c.Spec.Cluster.PD.StorageSize.Cmp(db.PD.Volume.CurrentCapacity) != 0 {
					return fmt.Errorf("status.db.pd.volume is not synced, expected storage size %s, actual %s", &c.Spec.Cluster.PD.StorageSize, &db.PD.Volume.CurrentCapacity)
				}
				if !db.PD.Volume.storageClassSynced(c.Spec.Cluster.PD.StorageClassName, mapper) {
					// TODO: add details
					return fmt.Errorf("status.db.pd.volume is not synced, storage class is not syned")
				}
			}
		}
		for _, member := range db.PD.Members {
			if member.InstanceType != "" && member.InstanceType != c.Spec.Cluster.PD.InstanceType {
				return fmt.Errorf("status.db.pd is not synced, expected instance type of %s is %s, actual %s", member.Name, c.Spec.Cluster.PD.InstanceType, member.InstanceType)
			}
		}
		if db.PD.InstanceTypeScalePhase != InstanceTypeScalePhaseNone {
			return fmt.Errorf("status.db.pd is not synced, current instance type scale phase is %s", db.PD.InstanceTypeScalePhase)
		}
	}

	if c.Spec.Cluster.TiFlash.Replicas == 0 {
		if db.TiFlash != nil && len(db.TiFlash.Members) != 0 {
			return fmt.Errorf("replicas of tiflash is 0 but status.db.tiflash still contains members")
		}
	} else {
		if db.TiFlash == nil {
			return fmt.Errorf("status.db.tiflash is not set")
		}
		if db.TiFlash.Status != ComponentNormal {
			return fmt.Errorf("status.db.tiflash is not synced, current status is %s, not %s", db.TiFlash.Status, ComponentNormal)
		}
		if len(db.TiFlash.Members) != c.Spec.Cluster.TiFlash.Replicas {
			return fmt.Errorf("status.db.tiflash is not synced, expected replicas %d, actual %d", c.Spec.Cluster.TiFlash.Replicas, len(db.TiFlash.Members))
		}
		if !c.Spec.Cluster.TiFlash.StorageSize.IsZero() && !c.Spec.Cluster.TiFlash.GetUseLocalStorage() {
			if db.TiFlash.Volume != nil {
				if c.Spec.Cluster.TiFlash.StorageSize.Cmp(db.TiFlash.Volume.CurrentCapacity) != 0 {
					return fmt.Errorf("status.db.tiflash.volume is not synced, expected storage size %s, actual %s", &c.Spec.Cluster.TiFlash.StorageSize, &db.TiFlash.Volume.CurrentCapacity)
				}
				if !db.TiFlash.Volume.storageClassSynced(c.Spec.Cluster.TiFlash.StorageClassName, mapper) {
					// TODO: add details
					return fmt.Errorf("status.db.tiflash.volume is not synced, storage class is not syned")
				}
			}
		}
		for _, member := range db.TiFlash.Members {
			if member.InstanceType != "" && member.InstanceType != c.Spec.Cluster.TiFlash.InstanceType {
				return fmt.Errorf("status.db.tiflash is not synced, expected instance type of %s is %s, actual %s", member.Name, c.Spec.Cluster.TiFlash.InstanceType, member.InstanceType)
			}
		}
		if db.TiFlash.InstanceTypeScalePhase != InstanceTypeScalePhaseNone {
			return fmt.Errorf("status.db.tiflash is not synced, current instance type scale phase is %s", db.TiFlash.InstanceTypeScalePhase)
		}
	}

	return nil
}

func (c *Cluster) GetNetworkRef() types.NamespacedName {
	// if the networkName contains "/", it means it's a cross namespaced network.
	strs := strings.Split(c.Spec.NetworkName, "/")
	if len(strs) == 2 {
		return types.NamespacedName{Namespace: strs[0], Name: strs[1]}
	}
	return types.NamespacedName{Name: c.Spec.NetworkName, Namespace: c.Namespace}
}

func (c *Cluster) ClusterScopeName() string {
	return fmt.Sprintf("%s-%s", c.Namespace, c.Name)
}

func (c *Cluster) GetPublicSQLEndpointPort() int32 {
	return c.Spec.SqlEndpoints.GetPublicSQLEndpointPort()
}

func (c *Cluster) GetPrivateSQLEndpointPort() int32 {
	return c.Spec.SqlEndpoints.GetPrivateSQLEndpointPort()
}

// GetSQLEndpoints returns the corresponding sql endpoints by groupId.
func (c *Cluster) GetSQLEndpoints(groupId string) (*SqlEndpoints, bool) {
	if groupId == c.GetDefaultGroupID() {
		return &c.Spec.SqlEndpoints, true
	}

	extraGroup, ok := c.GetExtraGroupSpec(groupId)
	if !ok {
		return nil, false
	}
	return &extraGroup.SqlEndpoints, true
}

// GetSQLEndpointStatus returns the corresponding sql endpoints by groupId.
func (c *Cluster) GetSQLEndpointStatus(groupID string) (*SqlEndpointsStatus, bool) {
	if groupID == c.GetDefaultGroupID() {
		return &c.Status.SqlEndpoints, true
	}

	extraGroup, ok := c.GetExtraGroupStatus(groupID)
	if !ok {
		return nil, false
	}
	return &extraGroup.SqlEndpointsStatus, true
}

// GetComponentSpecInGroup returns the corresponding component by groupId.
func (c *Cluster) GetComponentSpecInGroup(groupId string, comp ComponentType) (ClusterComponentSpecWrapped, error) {
	if groupId == c.GetDefaultGroupID() || UngroupedClusterComponentTypes[comp] {
		return &ClusterComponentSpecWrapper{
			groupId:          c.GetDefaultGroupID(),
			cluster:          c,
			ClusterComponent: c.GetComponent(comp),
		}, nil
	}

	group, ok := c.GetExtraGroupSpec(groupId)
	if !ok {
		return nil, fmt.Errorf("TiDB group '%s' not found", groupId)
	}
	return &ClusterComponentSpecWrapper{
		groupId:          groupId,
		cluster:          c,
		ClusterComponent: group.GetComponent(comp),
	}, nil
}

func (c *Cluster) GetComponentStatusInGroup(groupId string, comp ComponentType) (ClusterComponentStatus, error) {
	if groupId == c.GetDefaultGroupID() || UngroupedClusterComponentTypes[comp] {
		return c.GetComponentStatus(comp), nil
	}

	group, ok := c.GetExtraGroup(groupId)
	if !ok {
		return nil, fmt.Errorf("TiDB group '%s' not found", groupId)
	}
	return group.GetComponentStatus(comp), nil
}

func (c *Cluster) GetOrCreateComponentStatusInGroup(groupId string, comp ComponentType) (ClusterComponentStatusWrapped, error) {
	var status ClusterComponentStatus
	if groupId == c.GetDefaultGroupID() || UngroupedClusterComponentTypes[comp] {
		status = c.GetOrCreateComponentStatus(comp)
	} else {
		group, ok := c.GetExtraGroup(groupId)
		if !ok {
			return nil, fmt.Errorf("TiDB group '%s' not found", groupId)
		}
		status = group.GetOrCreateComponentStatus(comp)

	}
	return &clusterComponentStatusWrapped{
		ClusterComponentStatus: status,
		cluster:                c,
		groupId:                groupId,
		compType:               comp,
	}, nil
}

// GetDefaultGroupID returns the default group id of the cluster.
func (c *Cluster) GetDefaultGroupID() string {
	return c.Annotations[DefaultLoadBalancerIDAnnotationKey]
}

// GetUpToDateReplicas returns the replicas count which has been up to date.
// some special cases:
// - scale up: the member in TiDB Cluster becomes disconnected delay and the new pod may already started in the new instance
// - scale out + scale up: scaled out replicas with new instance-type and become normal before rolling-update started
// A member can only be marked as up to date until the member becomes normal again in the new instance,
// but we can't handle this accurately and only ensure:
// - the progress never changed back
// - all members become normal when progress reached 100%
func (c *Cluster) GetUpToDateReplicas(comp ComponentType) int {
	db := c.Status.DB
	if db == nil {
		return 0 // "0" if no status exists yet
	}

	mapper, err := RetrieveMapper(c)
	if err != nil {
		// TODO(ideascf): handle this error properly
		// should not happen
		return 0
	}

	replicas := 0
	switch comp {
	case ComponentPD:
		if db.PD == nil {
			return replicas // "0" if no status exists yet
		}

		if !c.Spec.Cluster.PD.StorageSize.IsZero() && !c.Spec.Cluster.PD.GetUseLocalStorage() {
			if db.PD.Volume != nil && (db.PD.Volume.storageSizeSyncPending(c.Spec.Cluster.PD.StorageSize) ||
				db.PD.Volume.storageClassSyncPending(c.Spec.Cluster.PD.StorageClassName, mapper)) {
				replicas = 0 // storage size/class scale not started yet
				break
			}
		}

		itUpdatedReplicas := 0
		for _, member := range db.PD.Members {
			// When scale up + scale out, the new member will become normal before rolling-update started (then becomes unavailable),
			// in order to avoid progress decrease, we should tolerate the unavailable status.
			if (member.InstanceType == "" || member.InstanceType == c.Spec.Cluster.PD.InstanceType) &&
				(member.Status == MemberStatusNormal || member.Status == MemberStatusUnavailable) {
				itUpdatedReplicas++
			}
		}
		if itUpdatedReplicas == 0 {
			replicas = 0 // instance type scale not started yet
			break
		}

		replicas = int(db.PD.UpdatedReplicas)
		// as no volume status for each member to check whether its volume is up to date,
		// so we choose the min of updated-replicas (both StatefulSet and compute/instance) and volume-resized-count as the up to date replicas.
		if itUpdatedReplicas < replicas {
			replicas = itUpdatedReplicas
		}
		if db.PD.Volume != nil && !c.Spec.Cluster.PD.GetUseLocalStorage() && db.PD.Volume.getModifiedCount() < replicas {
			replicas = db.PD.Volume.getModifiedCount()
		}
		if replicas == c.Spec.Cluster.PD.Replicas && db.PD.Status != ComponentNormal {
			if c.Spec.Cluster.PD.Replicas >= len(db.PD.Members) {
				// HACK: do not reach 100% if still scaling (when scale out, this may be Unhealthy),
				// but for scaling in (only), it should not less than 100%.
				// As we scale-in first before upgrade (for instance-type or storage) in TiDB-Operator,
				// if `replicas == c.Spec.Cluster.PD.Replicas` && `.Spec.Cluster.PD.Replicas < len(db.PD.Members)`,
				// that should be scaling-in only.
				replicas = replicas - 1
			}
		}
	case ComponentTiKV:
		if db.TiKV == nil {
			return replicas
		}

		if !c.Spec.Cluster.TiKV.StorageSize.IsZero() && !c.Spec.Cluster.TiKV.GetUseLocalStorage() {
			if db.TiKV.Volume != nil && (db.TiKV.Volume.storageSizeSyncPending(c.Spec.Cluster.TiKV.StorageSize) ||
				db.TiKV.Volume.storageClassSyncPending(c.Spec.Cluster.TiKV.StorageClassName, mapper)) {
				replicas = 0
				break
			}
		}

		tikvInstanceTypePolicy := c.tiKVMemberInstanceTypePolicy()
		itUpdatedReplicas := 0
		for _, member := range db.TiKV.Members {
			if tikvInstanceTypePolicy.allows(member.InstanceType) &&
				(member.Status == MemberStatusNormal || member.Status == MemberStatusUnavailable) {
				itUpdatedReplicas++
			}
		}
		if itUpdatedReplicas == 0 {
			replicas = 0
			break
		}

		replicas = int(db.TiKV.UpdatedReplicas)
		if itUpdatedReplicas < replicas {
			replicas = itUpdatedReplicas
		}
		if db.TiKV.Volume != nil && !c.Spec.Cluster.TiKV.GetUseLocalStorage() && db.TiKV.Volume.getModifiedCount() < replicas {
			replicas = db.TiKV.Volume.getModifiedCount()
		}

		if replicas == c.Spec.Cluster.TiKV.Replicas && db.TiKV.Status != ComponentNormal {
			if c.Spec.Cluster.TiKV.Replicas >= len(db.TiKV.Members) {
				replicas = replicas - 1
			}
		}
	case ComponentTiDB:
		if db.TiDB == nil {
			return replicas
		}

		tidbInstanceTypePolicy := c.tiDBMemberInstanceTypePolicy("", c.Spec.Cluster.TiDB.InstanceType)
		itUpdatedReplicas := 0
		for _, member := range db.TiDB.Members {
			if tidbInstanceTypePolicy.allows(member.InstanceType) &&
				(member.Status == MemberStatusNormal || member.Status == MemberStatusUnavailable) {
				itUpdatedReplicas++
			}
		}
		if itUpdatedReplicas == 0 {
			replicas = 0
			break
		}

		replicas = int(db.TiDB.UpdatedReplicas)
		if itUpdatedReplicas < replicas {
			replicas = itUpdatedReplicas
		}
		// NOTE: After discuss with @csuzhangxc, one guarantee of this function annotated in function comment
		// "all members become normal when progress reached 100%" that is UNNECESSARY. All components modification of cluster
		// have been done don't mean the cluster modification is done. So we don't need to do this hack way to ensure it.
		// if replicas == c.Spec.Cluster.TiDB.Replicas && db.TiDB.Status != ComponentNormal {
		//	if c.Spec.Cluster.TiDB.Replicas >= len(db.TiDB.Members) {
		//		replicas = replicas - 1
		//	}
		// }
	case ComponentTiFlash:
		if db.TiFlash == nil {
			return replicas
		}

		if !c.Spec.Cluster.TiFlash.StorageSize.IsZero() && !c.Spec.Cluster.TiFlash.GetUseLocalStorage() {
			if db.TiFlash.Volume != nil && (db.TiFlash.Volume.storageSizeSyncPending(c.Spec.Cluster.TiFlash.StorageSize) ||
				db.TiFlash.Volume.storageClassSyncPending(c.Spec.Cluster.TiFlash.StorageClassName, mapper)) {
				replicas = 0
				break
			}
		}

		itUpdatedReplicas := 0
		for _, member := range db.TiFlash.Members {
			if (member.InstanceType == "" || member.InstanceType == c.Spec.Cluster.TiFlash.InstanceType) &&
				(member.Status == MemberStatusNormal || member.Status == MemberStatusUnavailable) {
				itUpdatedReplicas++
			}
		}
		if itUpdatedReplicas == 0 {
			replicas = 0
			break
		}

		replicas = int(db.TiFlash.UpdatedReplicas)
		if itUpdatedReplicas < replicas {
			replicas = itUpdatedReplicas
		}
		if db.TiFlash.Volume != nil && !c.Spec.Cluster.TiFlash.GetUseLocalStorage() && db.TiFlash.Volume.getModifiedCount() < replicas {
			replicas = db.TiFlash.Volume.getModifiedCount()
		}
		if replicas == c.Spec.Cluster.TiFlash.Replicas && db.TiFlash.Status != ComponentNormal {
			if c.Spec.Cluster.TiFlash.Replicas >= len(db.TiFlash.Members) {
				replicas = replicas - 1
			}
		}
	}

	return replicas
}

func (c *Cluster) GetStorageClassNames() []string {
	var scs []string
	for _, comp := range c.GetComponents() {
		sc, ok := comp.GetStorageClassName()
		if ok {
			scs = append(scs, sc)
		}
	}
	for _, vol := range c.Spec.Cluster.TiKV.StorageVolumes {
		scs = append(scs, vol.StorageClassName)
	}

	return scs
}

func (c *Cluster) GetStandbyNodeCnt(comp ComponentType, zone string) uint {
	value, ok := c.Annotations[AnnoStandbyNode]
	if !ok {
		return 0
	}

	standbyNode := AnnoValueStandbyNode{}
	if err := json.Unmarshal([]byte(value), &standbyNode); err != nil {
		return 0
	}

	return standbyNode[comp][zone]
}

// GetRemainingDeleteReplicas returns the replicas count which is still remaining to be deleted.
func (c *Cluster) GetRemainingDeleteReplicas(comp ComponentType) (count int) {
	db := c.Status.DB
	if db == nil {
		return 0 // "0" if no status exists yet
	}

	defer func() {
		if count < 0 {
			count = 0
		}
	}()

	switch comp {
	case ComponentPD:
		if db.PD == nil {
			return 0 // "0" if no status exists yet
		}
		return len(db.PD.Members) - c.Spec.Cluster.PD.Replicas
	case ComponentTiKV:
		if db.TiKV == nil {
			return 0
		}
		return len(db.TiKV.Members) - c.Spec.Cluster.TiKV.Replicas
	case ComponentTiDB:
		if db.TiDB == nil {
			return 0
		}
		return len(db.TiDB.Members) - c.Spec.Cluster.TiDB.Replicas
	case ComponentTiFlash:
		if db.TiFlash == nil {
			return 0
		}
		return len(db.TiFlash.Members) - c.Spec.Cluster.TiFlash.Replicas
	}
	return 0
}

// UpdateConfigRevision updates Cluster.Spec.ConfigRevisionRef and sets the version Guard if you require.
// setVersionGuard represents whether to set version change Guard
func (c *Cluster) UpdateConfigRevision(nextConfigRevision *ConfigRevision, setVersionGuard bool) {
	c.Spec.ConfigRevisionRef = &Reference{Name: nextConfigRevision.Name}
	if setVersionGuard {
		c.SetGuard(ClusterGuardVersionUpgrade, nextConfigRevision.Spec.Version)
	}
}

// CheckVersionGuard checks the cluster version in Guard against with version claimed in the next ConfigRevision, an error
// will raise if them mismatch.
func (c *Cluster) CheckVersionGuard(nextConfigRevision *ConfigRevision) error {
	versionInGuard, _ := c.GetGuard(ClusterGuardVersionUpgrade)
	if versionInGuard != nextConfigRevision.Spec.Version {
		return fmt.Errorf("the target version is %s, but defined in guard is %s", nextConfigRevision.Spec.Version, versionInGuard)
	}
	return nil
}

func (c *Cluster) GetGuard(guardName string) (string, bool) {
	value, ok := c.Annotations[GuardAnnotationPrefix+guardName]
	return value, ok
}

// SetGuard set a Guard into annotation and return the old one, return empty string if old one is not existing.
func (c *Cluster) SetGuard(guardName string, value string) string {
	if c.Annotations == nil {
		c.Annotations = make(map[string]string)
	}

	key := GuardAnnotationPrefix + guardName
	old := c.Annotations[key]
	c.Annotations[key] = value
	return old
}

func (c *Cluster) ConfigChanging() bool {
	return c.CurrentConfigChangeName() != ""
}

func (c *Cluster) CurrentConfigChangeName() string {
	if c.Annotations == nil {
		c.Annotations = make(map[string]string)
	}

	return c.Annotations[AnnoClusterCurrentConfigChangeName]
}

func (c *Cluster) SetConfigChange(configChangeName string) {
	if c.Annotations == nil {
		c.Annotations = make(map[string]string)
	}

	c.Annotations[AnnoClusterCurrentConfigChangeName] = configChangeName
}

func (c *Cluster) UnsetConfigChange(configChangeName string) bool {
	if c.Annotations[AnnoClusterCurrentConfigChangeName] == configChangeName {
		delete(c.Annotations, AnnoClusterCurrentConfigChangeName)
		return true
	}
	return false
}

func (c *Cluster) SetStandbyNodeCnt(comp ComponentType, zone string, cnt uint) error {
	value, ok := c.Annotations[AnnoStandbyNode]
	if !ok {
		value = `{}`
	}

	standbyNode := AnnoValueStandbyNode{}
	if err := json.Unmarshal([]byte(value), &standbyNode); err != nil {
		return fmt.Errorf("unmarshal standbyNode anno value: %w", err)
	}
	compValue, ok := standbyNode[comp]
	if !ok {
		compValue = map[string]uint{}
		standbyNode[comp] = compValue
	}
	standbyNode[comp][zone] = cnt

	data, err := json.Marshal(standbyNode)
	if err != nil {
		return fmt.Errorf("marshal standbyNode anno value: %w", err)
	}
	c.UpdateAnnotation(AnnoStandbyNode, string(data))
	return nil
}

func (c *Cluster) UpdateAnnotation(k, v string) {
	c.MutateAnnotations(func(anno map[string]string) {
		anno[k] = v
	})
}

// MutateAnnotations applies a mutation function to the cluster annotations safely.
func (c *Cluster) MutateAnnotations(mutator func(map[string]string)) {
	if mutator == nil {
		return
	}
	anno := c.GetAnnotations()
	if anno == nil {
		anno = make(map[string]string)
	}
	mutator(anno)
	c.SetAnnotations(anno)
}

func (c *Cluster) GetDNSRecordName(isPublicEndpoint bool, extraGroupStatus *ExtraGroupStatus) string {
	/*
		For Default TidbGroup:
			public endpoint:
				- AWS: nightly-ms-3200595-10026977408430179120-public
				- GCP: tidb.byapvm1y4oyi.clusters.dev.tidb-cloud.com
				- Azure: tidb.rndcji1y4l8w
			private endpoint:
				- AWS: nightly-ms-3200595-10026977408430179120-private-inpublic
				- GCP: private-tidb.byapvm1y4oyi.clusters.dev.tidb-cloud.com
				- Azure: private-tidb.rndcji1y4l8w
		For Extra TidbGroup:
			public endpoint:
				- AWS: tidb.madcmh1y63wc.clusters.dev.tidb-cloud.com
				- GCP: tidb.ghrpay1y6inc.clusters.dev.tidb-cloud.com
				- Azure: NOT SUPPORT EXTRA GROUP YET
			private endpoint:
				- AWS: private-tidb.madcmh1y63wc.clusters.dev.tidb-cloud.com
				- GCP: private-tidb.ghrpay1y6inc.clusters.dev.tidb-cloud.com
				- Azure: NOT SUPPORT EXTRA GROUP YET
	*/

	// As the DNS record CR naming style is really in a complex situation, we handle it in a dump but simple way,
	// use if...else... to handle the different situations instead of combining them into one. looks dump but easy to maintain..

	if extraGroupStatus == nil {
		// default group has default DNS record naming style
		if isPublicEndpoint {
			if c.Status.SqlEndpoints.PublicEndpoint != nil {
				switch c.Spec.Kubernetes.Provider {
				case ProviderTypeEKS:
					return fmt.Sprintf("%s-public", c.ClusterScopeName())
				case ProviderTypeGKE:
					return c.Status.SqlEndpoints.PublicEndpoint.Host
				case ProviderTypeAKS:
					fullDnsRecordName := c.Status.SqlEndpoints.PublicEndpoint.Host
					if subs := strings.Split(fullDnsRecordName, ".clusters."); len(subs) > 0 {
						return subs[0]
					}
				default:
					return c.Status.SqlEndpoints.PublicEndpoint.Host
				}
			}
		} else {
			if c.Status.SqlEndpoints.PrivateEndpoint != nil {
				switch c.Spec.Kubernetes.Provider {
				case ProviderTypeEKS:
					return fmt.Sprintf("%s-private-inpublic", c.ClusterScopeName())
				case ProviderTypeGKE:
					return c.Status.SqlEndpoints.PrivateEndpoint.Host
				case ProviderTypeAKS:
					fullDnsRecordName := c.Status.SqlEndpoints.PrivateEndpoint.Host
					if subs := strings.Split(fullDnsRecordName, ".clusters."); len(subs) > 0 {
						return subs[0]
					}
				default:
					return c.Status.SqlEndpoints.PrivateEndpoint.Host
				}
			}
		}
	} else {
		// extra group has another DNS record naming style...
		// And, Azure doesn't support extra group yet
		if isPublicEndpoint {
			switch c.Spec.Kubernetes.Provider {
			case ProviderTypeEKS:
				if extraGroupStatus.SqlEndpointsStatus.PublicEndpoint != nil {
					return extraGroupStatus.SqlEndpointsStatus.PublicEndpoint.Host
				}
			case ProviderTypeGKE:
				if extraGroupStatus.SqlEndpointsStatus.PublicEndpoint != nil {
					return extraGroupStatus.SqlEndpointsStatus.PublicEndpoint.Host
				}
			}
		} else {
			switch c.Spec.Kubernetes.Provider {
			case ProviderTypeEKS:
				if extraGroupStatus.SqlEndpointsStatus.PrivateEndpoint != nil {
					return extraGroupStatus.SqlEndpointsStatus.PrivateEndpoint.Host
				}
			case ProviderTypeGKE:
				if extraGroupStatus.SqlEndpointsStatus.PrivateEndpoint != nil {
					return extraGroupStatus.SqlEndpointsStatus.PrivateEndpoint.Host
				}
			}
		}
	}

	return ""
}

func (c *Cluster) IsDNSDisabled(groupName string) bool {
	if groupName == c.GetDefaultGroupID() {
		return c.Spec.SqlEndpoints.DNS.Disabled
	} else {
		for _, extraGroup := range c.Spec.ExtraGroups {
			if extraGroup.Name == groupName {
				return extraGroup.SqlEndpoints.DNS.Disabled
			}
		}
	}
	return false
}

func (v *VolumeStatus) getModifiedCount() int {
	if v.ModifiedCount == 0 {
		return v.ResizedCount
	}
	return v.ModifiedCount
}

func (v *VolumeStatus) storageSizeSyncPending(desired resource.Quantity) bool {
	modifiedCapacity := v.ModifiedCapacity
	if modifiedCapacity.IsZero() {
		modifiedCapacity = v.ResizedCapacity
	}
	return modifiedCapacity.Cmp(desired) != 0
}

func (v *VolumeStatus) storageClassSynced(desired string, mapper CMEKStorageClassMapperV2) bool {
	if v.CurrentStorageClass == "" || desired == "" {
		// special: no storage class in old volume status
		// special: no storage class specified in spec
		return true
	}
	return v.CurrentStorageClass == mapper.Name(desired)
}

func (v *VolumeStatus) storageClassSyncPending(desired string, mapper CMEKStorageClassMapperV2) bool {
	if v.ModifiedStorageClass == "" || desired == "" {
		// special: no storage class in old volume status
		// special: no storage class specified in spec
		return false
	}
	return v.ModifiedStorageClass != mapper.Name(desired)
}

func (s Service) IsEnabled() bool {
	return s.Enabled != nil && *s.Enabled
}

// +kubebuilder:object:root=false
// +kubebuilder:object:generate=false
type ClusterComponent interface {
	GetReplicas() int
	GetInstanceType() string
	GetComponentType() ComponentType
	GetStorageClassName() (string, bool)
	GetStorageSize() (*resource.Quantity, bool)
	GetUseLocalStorage() bool
	GetAdditionalStorages() []ComponentStorageVolume
	NeedSuspend(*SuspendAction) bool
	GetSuspendActions(*SuspendAction) *SuspendAction

	SetReplicas(replicas int)
	SetInstanceType(instanceType string)
	SetStorageClassName(className string)
	SetStorageSize(size resource.Quantity)
	SetUseLocalStorage(useLocalStorage bool)
	SetAdditionalStorages(volumes []ComponentStorageVolume)
}

// +kubebuilder:object:root=false
// +kubebuilder:object:generate=false
type ClusterComponentStatus interface {
	Suspending() bool
	Suspended() bool
	GetInstanceTypes() []string
	GetInstanceTypeScalePhase() InstanceTypeScalePhase
	GetUpdatedReplicas() int32
	GetComponentStatus() ComponentStatus
	GetVolumeStatus() *VolumeStatus
	GetConditions() []metav1.Condition

	SetInstanceTypeScalePhase(newPhase InstanceTypeScalePhase)
}

var (
	_ ClusterComponent = &PD{}
	_ ClusterComponent = &TiFlash{}
	_ ClusterComponent = &TiKV{}
	_ ClusterComponent = &TiDB{}
	_ ClusterComponent = &TiCDC{}
	_ ClusterComponent = &TiProxy{}
)

var (
	_ ClusterComponentStatus = &PDStatus{}
	_ ClusterComponentStatus = &TiFlashStatus{}
	_ ClusterComponentStatus = &TiKVStatus{}
	_ ClusterComponentStatus = &TiDBStatus{}
	_ ClusterComponentStatus = &TiProxyStatus{}
	_ ClusterComponentStatus = &TiCDCStatus{}
)

// +kubebuilder:object:root=false
// +kubebuilder:object:generate=false
type ClusterComponentSpecWrapped interface {
	GetCluster() *Cluster
	GetGroupId() string
	GetConfigVersion() string
	GetClusterComponent() ClusterComponent
	ClusterComponent
}

var _ ClusterComponentSpecWrapped = &ClusterComponentSpecWrapper{}

// +kubebuilder:object:root=false
// +kubebuilder:object:generate=false
type ClusterComponentSpecWrapper struct {
	groupId string
	cluster *Cluster
	ClusterComponent
}

func (c *ClusterComponentSpecWrapper) GetConfigVersion() string {
	if c.cluster.Spec.ConfigRevisionRef == nil {
		return ""
	}
	return c.cluster.Spec.ConfigRevisionRef.Name
}

func (c *ClusterComponentSpecWrapper) GetCluster() *Cluster {
	return c.cluster
}

func (c *ClusterComponentSpecWrapper) GetGroupId() string {
	return c.groupId
}

func (c *ClusterComponentSpecWrapper) GetClusterComponent() ClusterComponent {
	return c.ClusterComponent
}

// +kubebuilder:object:root=false
// +kubebuilder:object:generate=false
type ClusterComponentStatusWrapped interface {
	ClusterComponentStatus
	TransInstanceTypeScalePhase(ctx context.Context, newPhase InstanceTypeScalePhase)
}

var _ ClusterComponentStatusWrapped = &clusterComponentStatusWrapped{}

type clusterComponentStatusWrapped struct {
	groupId  string
	cluster  *Cluster
	compType ComponentType
	ClusterComponentStatus
}

// TransInstanceTypeScalePhase trans this component's scale up/down phase to next
func (w *clusterComponentStatusWrapped) TransInstanceTypeScalePhase(ctx context.Context, newPhase InstanceTypeScalePhase) {
	oldPhase := w.GetInstanceTypeScalePhase()
	logger := log.FromContext(ctx).WithValues("from", oldPhase, "to", newPhase, "group", w.groupId, "component", w.compType)

	if oldPhase == newPhase {
		return // no need to transfer
	}
	if (oldPhase == "" && newPhase == InstanceTypeScalePhaseCreatingNodes) ||
		(oldPhase == InstanceTypeScalePhaseCreatingNodes && newPhase == InstanceTypeScalePhaseCordoningNodes) ||
		(oldPhase == InstanceTypeScalePhaseCordoningNodes && newPhase == InstanceTypeScalePhaseRestartingPods) ||
		(oldPhase == InstanceTypeScalePhaseRestartingPods && newPhase == InstanceTypeScalePhaseDeletingNodes) ||
		(oldPhase == InstanceTypeScalePhaseDeletingNodes && newPhase == InstanceTypeScalePhaseNone) { // scale finished
		logger.Info("Transfer instance type scale phase")
		w.SetInstanceTypeScalePhase(newPhase)
		return
	}

	logger.Error(nil, "Invalid instance type scale phase transfer")
}

// +kubebuilder:object:root=false
// +kubebuilder:object:generate=false
type ClusterComp interface {
	ClusterComponentSpecWrapped
	ClusterComponentStatusWrapped
}

var _ ClusterComp = &clusterComp{}

type clusterComp struct {
	ClusterComponentSpecWrapped
	ClusterComponentStatusWrapped
}

func (s *PDStatus) GetComponentStatus() ComponentStatus {
	return s.Status
}

func (s *PDStatus) GetVolumeStatus() *VolumeStatus {
	return s.Volume
}

func (s *PDStatus) GetConditions() []metav1.Condition {
	return s.Conditions
}

func (s *PDStatus) Suspending() bool {
	return meta.IsStatusConditionFalse(s.Conditions, ComponentSuspended)
}

func (s *PDStatus) Suspended() bool {
	return meta.IsStatusConditionTrue(s.Conditions, ComponentSuspended)
}

func (s *PDStatus) GetInstanceTypes() []string {
	if s == nil {
		return nil
	}
	ret := make([]string, 0, len(s.Members))
	for _, member := range s.Members {
		if member.InstanceType != "" {
			ret = append(ret, member.InstanceType)
		}
	}
	return ret
}

func (s *PDStatus) GetInstanceTypeScalePhase() InstanceTypeScalePhase {
	if s == nil {
		return InstanceTypeScalePhaseNone
	}

	return s.InstanceTypeScalePhase
}

func (s *PDStatus) GetUpdatedReplicas() int32 {
	return s.UpdatedReplicas
}

func (s *PDStatus) SetInstanceTypeScalePhase(newPhase InstanceTypeScalePhase) {
	s.InstanceTypeScalePhase = newPhase
}

func (s *TiKVStatus) Suspending() bool {
	return meta.IsStatusConditionFalse(s.Conditions, ComponentSuspended)
}

func (s *TiKVStatus) Suspended() bool {
	return meta.IsStatusConditionTrue(s.Conditions, ComponentSuspended)
}

func (s *TiKVStatus) GetComponentStatus() ComponentStatus {
	return s.Status
}

func (s *TiKVStatus) GetVolumeStatus() *VolumeStatus {
	return s.Volume
}

func (s *TiKVStatus) GetConditions() []metav1.Condition {
	return s.Conditions
}

func (s *TiKVStatus) GetInstanceTypes() []string {
	if s == nil {
		return nil
	}
	ret := make([]string, 0, len(s.Members))
	for _, member := range s.Members {
		if member.InstanceType != "" {
			ret = append(ret, member.InstanceType)
		}
	}
	return ret
}

func (s *TiKVStatus) GetInstanceTypeScalePhase() InstanceTypeScalePhase {
	if s == nil {
		return InstanceTypeScalePhaseNone
	}
	return s.InstanceTypeScalePhase
}

func (s *TiKVStatus) GetUpdatedReplicas() int32 {
	return s.UpdatedReplicas
}

func (s *TiKVStatus) SetInstanceTypeScalePhase(newPhase InstanceTypeScalePhase) {
	s.InstanceTypeScalePhase = newPhase
}

func (s *TiDBStatus) Suspending() bool {
	return meta.IsStatusConditionFalse(s.Conditions, ComponentSuspended)
}

func (s *TiDBStatus) Suspended() bool {
	return meta.IsStatusConditionTrue(s.Conditions, ComponentSuspended)
}

func (s *TiDBStatus) GetComponentStatus() ComponentStatus {
	return s.Status
}

func (s *TiDBStatus) GetVolumeStatus() *VolumeStatus {
	return nil
}

func (s *TiDBStatus) GetConditions() []metav1.Condition {
	return s.Conditions
}

func (s *TiDBStatus) GetInstanceTypes() []string {
	if s == nil {
		return nil
	}
	ret := make([]string, 0, len(s.Members))
	for _, member := range s.Members {
		if member.InstanceType != "" {
			ret = append(ret, member.InstanceType)
		}
	}
	return ret
}

func (s *TiDBStatus) GetInstanceTypeScalePhase() InstanceTypeScalePhase {
	if s == nil {
		return InstanceTypeScalePhaseNone
	}

	return s.InstanceTypeScalePhase
}

func (s *TiDBStatus) GetUpdatedReplicas() int32 {
	return s.UpdatedReplicas
}

func (s *TiDBStatus) SetInstanceTypeScalePhase(newPhase InstanceTypeScalePhase) {
	s.InstanceTypeScalePhase = newPhase
}

func (s *TiFlashStatus) Suspending() bool {
	return meta.IsStatusConditionFalse(s.Conditions, ComponentSuspended)
}

func (s *TiFlashStatus) Suspended() bool {
	return meta.IsStatusConditionTrue(s.Conditions, ComponentSuspended)
}

func (s *TiFlashStatus) GetComponentStatus() ComponentStatus {
	return s.Status
}

func (s *TiFlashStatus) GetVolumeStatus() *VolumeStatus {
	return s.Volume
}

func (s *TiFlashStatus) GetConditions() []metav1.Condition {
	return s.Conditions
}
func (s *TiFlashStatus) GetInstanceTypes() []string {
	if s == nil {
		return nil
	}
	ret := make([]string, 0, len(s.Members))
	for _, member := range s.Members {
		if member.InstanceType != "" {
			ret = append(ret, member.InstanceType)
		}
	}
	return ret
}

func (s *TiFlashStatus) GetInstanceTypeScalePhase() InstanceTypeScalePhase {
	if s == nil {
		return InstanceTypeScalePhaseNone
	}
	return s.InstanceTypeScalePhase
}

func (s *TiFlashStatus) GetUpdatedReplicas() int32 {
	return s.UpdatedReplicas
}

func (s *TiFlashStatus) SetInstanceTypeScalePhase(newPhase InstanceTypeScalePhase) {
	s.InstanceTypeScalePhase = newPhase
}

func (s *TiCDCStatus) Suspending() bool {
	return false
}

func (s *TiCDCStatus) Suspended() bool {
	return false
}

func (s *TiCDCStatus) GetComponentStatus() ComponentStatus {
	return ComponentNormal
}

func (s *TiCDCStatus) GetVolumeStatus() *VolumeStatus {
	return nil
}

func (s *TiCDCStatus) GetConditions() []metav1.Condition {
	return nil
}
func (s *TiCDCStatus) GetInstanceTypes() []string {
	return []string{}
}

func (s *TiCDCStatus) GetInstanceTypeScalePhase() InstanceTypeScalePhase {
	if s == nil {
		return InstanceTypeScalePhaseNone
	}

	return InstanceTypeScalePhaseNone
}

func (s *TiCDCStatus) GetUpdatedReplicas() int32 {
	return 0
}

func (s *TiCDCStatus) SetInstanceTypeScalePhase(newPhase InstanceTypeScalePhase) {
	// do nothing
}

func (s *TiProxyStatus) Suspending() bool {
	return meta.IsStatusConditionFalse(s.Conditions, ComponentSuspended)
}

func (s *TiProxyStatus) Suspended() bool {
	return meta.IsStatusConditionTrue(s.Conditions, ComponentSuspended)
}

func (s *TiProxyStatus) GetComponentStatus() ComponentStatus {
	return s.Status
}

func (s *TiProxyStatus) GetVolumeStatus() *VolumeStatus {
	return nil
}

func (s *TiProxyStatus) GetConditions() []metav1.Condition {
	return s.Conditions
}

func (s *TiProxyStatus) GetInstanceTypes() []string {
	if s == nil {
		return nil
	}
	ret := make([]string, 0, len(s.Members))
	for _, member := range s.Members {
		if member.InstanceType != "" {
			ret = append(ret, member.InstanceType)
		}
	}
	return ret
}

func (s *TiProxyStatus) GetInstanceTypeScalePhase() InstanceTypeScalePhase {
	if s == nil {
		return InstanceTypeScalePhaseNone
	}

	return s.InstanceTypeScalePhase
}

func (s *TiProxyStatus) GetUpdatedReplicas() int32 {
	return s.UpdatedReplicas
}

func (s *TiProxyStatus) SetInstanceTypeScalePhase(newPhase InstanceTypeScalePhase) {
	s.InstanceTypeScalePhase = newPhase
}

func (c *TiProxy) NeedSuspend(base *SuspendAction) bool {
	action := c.GetSuspendActions(base)
	if action != nil && action.SuspendCompute {
		return true
	}
	return false
}

func (c *TiProxy) GetSuspendActions(base *SuspendAction) *SuspendAction {
	if c.SuspendAction != nil {
		return c.SuspendAction
	}
	return base
}

func (c *TiProxy) GetReplicas() int {
	return c.Replicas
}

func (c *TiProxy) GetInstanceType() string {
	return c.InstanceType
}

func (c *TiProxy) GetComponentType() ComponentType {
	return ComponentTiProxy
}

func (c *TiProxy) GetStorageClassName() (string, bool) {
	return "", false
}

func (c *TiProxy) GetStorageSize() (*resource.Quantity, bool) {
	return nil, false
}

func (c *TiProxy) GetUseLocalStorage() bool {
	return false
}

func (c *TiProxy) SetReplicas(replicas int) {
	c.Replicas = replicas
}

func (c *TiProxy) SetInstanceType(instanceType string) {
	c.InstanceType = instanceType
}

func (c *TiProxy) SetStorageClassName(className string) {}

func (c *TiProxy) SetStorageSize(size resource.Quantity) {}

func (c *TiProxy) SetUseLocalStorage(useLocalStorage bool) {}

func (c *PD) NeedSuspend(base *SuspendAction) bool {
	action := c.GetSuspendActions(base)
	if action != nil && action.SuspendCompute {
		return true
	}
	return false
}

func (c *PD) GetSuspendActions(base *SuspendAction) *SuspendAction {
	if c.SuspendAction != nil {
		return c.SuspendAction
	}
	return base
}

func (c *PD) GetReplicas() int {
	return c.Replicas
}

func (c *PD) GetInstanceType() string {
	return c.InstanceType
}

func (c *PD) GetComponentType() ComponentType {
	return ComponentPD
}

func (c *PD) GetStorageClassName() (string, bool) {
	return c.StorageClassName, true
}

func (c *PD) GetStorageSize() (*resource.Quantity, bool) {
	return &c.StorageSize, true
}

func (c *PD) GetUseLocalStorage() bool {
	return false
}

func (c *PD) SetReplicas(replicas int) {
	c.Replicas = replicas
}

func (c *PD) SetInstanceType(instanceType string) {
	c.InstanceType = instanceType
}

func (c *PD) SetStorageClassName(className string) {
	c.StorageClassName = className
}

func (c *PD) SetStorageSize(size resource.Quantity) {
	c.StorageSize = size
}

func (c *PD) SetUseLocalStorage(useLocalStorage bool) {}

func (c *TiKV) NeedSuspend(base *SuspendAction) bool {
	action := c.GetSuspendActions(base)
	if action != nil && action.SuspendCompute {
		return true
	}
	return false
}

func (c *TiKV) GetSuspendActions(base *SuspendAction) *SuspendAction {
	if c.SuspendAction != nil {
		return c.SuspendAction
	}
	return base
}

func (c *TiKV) GetReplicas() int {
	return c.Replicas
}

func (c *TiKV) GetInstanceType() string {
	return c.InstanceType
}

func (c *TiKV) GetComponentType() ComponentType {
	return ComponentTiKV
}

func (c *TiKV) GetStorageClassName() (string, bool) {
	return c.StorageClassName, true
}

func (c *TiKV) GetStorageSize() (*resource.Quantity, bool) {
	return &c.StorageSize, true
}

func (c *TiKV) GetUseLocalStorage() bool {
	if c.UseLocalStorage != nil {
		return *c.UseLocalStorage
	}
	return false
}

func (c *TiKV) GetAdditionalStorages() []ComponentStorageVolume {
	return c.StorageVolumes
}

func (c *TiKV) SetReplicas(replicas int) {
	c.Replicas = replicas
}

func (c *TiKV) SetInstanceType(instanceType string) {
	c.InstanceType = instanceType
}

func (c *TiKV) SetStorageClassName(className string) {
	c.StorageClassName = className
}

func (c *TiKV) SetStorageSize(size resource.Quantity) {
	c.StorageSize = size
}

func (c *TiKV) SetUseLocalStorage(useLocalStorage bool) {
	c.UseLocalStorage = &useLocalStorage
}

func (c *TiKV) SetAdditionalStorages(volumes []ComponentStorageVolume) {
	c.StorageVolumes = volumes
}

func (c *TiFlash) GetReplicas() int {
	return c.Replicas
}

func (c *TiFlash) NeedSuspend(base *SuspendAction) bool {
	action := c.GetSuspendActions(base)
	if action != nil && action.SuspendCompute {
		return true
	}
	return false
}

func (c *TiFlash) GetSuspendActions(base *SuspendAction) *SuspendAction {
	if c.SuspendAction != nil {
		return c.SuspendAction
	}
	return base
}

func (c *TiFlash) GetInstanceType() string {
	return c.InstanceType
}

func (c *TiFlash) GetComponentType() ComponentType {
	return ComponentTiFlash
}

func (c *TiFlash) GetStorageClassName() (string, bool) {
	return c.StorageClassName, true
}

func (c *TiFlash) GetStorageSize() (*resource.Quantity, bool) {
	return &c.StorageSize, true
}

func (c *TiFlash) GetUseLocalStorage() bool {
	if c.UseLocalStorage != nil {
		return *c.UseLocalStorage
	}
	return false
}

func (c *TiFlash) SetReplicas(replicas int) {
	c.Replicas = replicas
}

func (c *TiFlash) SetInstanceType(instanceType string) {
	c.InstanceType = instanceType
}

func (c *TiFlash) SetStorageClassName(className string) {
	c.StorageClassName = className
}

func (c *TiFlash) SetStorageSize(size resource.Quantity) {
	c.StorageSize = size
}

func (c *TiFlash) SetUseLocalStorage(useLocalStorage bool) {
	c.UseLocalStorage = &useLocalStorage
}

func (c *TiDB) NeedSuspend(base *SuspendAction) bool {
	action := c.GetSuspendActions(base)
	if action != nil && action.SuspendCompute {
		return true
	}
	return false
}

func (c *TiDB) GetSuspendActions(base *SuspendAction) *SuspendAction {
	if c.SuspendAction != nil {
		return c.SuspendAction
	}
	return base
}

func (c *TiDB) GetReplicas() int {
	return c.Replicas
}

func (c *TiDB) GetInstanceType() string {
	return c.InstanceType
}

func (c *TiDB) GetComponentType() ComponentType {
	return ComponentTiDB
}

func (c *TiDB) GetStorageClassName() (string, bool) {
	return "", false
}

func (c *TiDB) GetStorageSize() (*resource.Quantity, bool) {
	return nil, false
}

func (c *TiDB) GetUseLocalStorage() bool {
	return false
}

func (c *TiDB) SetReplicas(replicas int) {
	c.Replicas = replicas
}

func (c *TiDB) SetInstanceType(instanceType string) {
	c.InstanceType = instanceType
}

func (c *TiDB) SetStorageClassName(className string) {}

func (c *TiDB) SetStorageSize(size resource.Quantity) {}

func (c *TiDB) SetUseLocalStorage(useLocalStorage bool) {}

func (c *SqlEndpoints) GetPublicSQLEndpointPort() int32 {
	port := DefaultPublicSQLEndpointPort
	if c.Public.Port != nil {
		port = *c.Public.Port
	}
	return port
}

func (c *SqlEndpoints) GetPrivateSQLEndpointPort() int32 {
	port := DefaultPrivateSQLEndpointPort
	if c.Private != nil && c.Private.Port != nil {
		port = *c.Private.Port
	}
	return port
}

const (
	tidbServiceTargetPort    = 4000
	tiproxyServiceTargetPort = 6000
)

func (c *SqlEndpoints) GetTargetPort() intstr.IntOrString {
	if c.UseTiProxyAsLBTarget {
		return intstr.FromInt(tiproxyServiceTargetPort)
	}
	return intstr.FromInt(tidbServiceTargetPort)
}

func (c *SqlEndpoints) GetTargetComponent() ComponentType {
	if c.UseTiProxyAsLBTarget {
		return ComponentTiProxy
	} else {
		return ComponentTiDB
	}
}

// BuildClusterNamespace
// prefix like 'prod-'
func BuildClusterNamespace(prefix, projectID string) string {
	return fmt.Sprintf("%s%s", prefix, projectID)
}

func (g *ExtraGroupSpec) Deleted() bool {
	// For now, we only check the tidb replicas
	return g.TiDB.Replicas == 0
}

func (g *ExtraGroupSpec) IsDNSDisabled() bool {
	return g.SqlEndpoints.DNS.Disabled
}

// AppendGroup append new or merge existing group subnets, then sort it entirely by zone and subnetId.
func (gs AWSSubnetGroups) AppendGroup(newGroup AWSSubnetGroup) AWSSubnetGroups {
	targetIdx := -1
	// Search for an existing subnet group with the same name
	for i := range gs {
		// Index the group and merge subnets if the target group already exists
		if gs[i].Name != newGroup.Name {
			continue
		}

		targetIdx = i
		// Append and unique so that merge the subnets
		gs[i].Subnets = append(gs[i].Subnets, newGroup.Subnets...)
		gs[i].Subnets = lo.UniqBy(gs[i].Subnets, func(item AWSSubnet) string {
			return fmt.Sprintf("%s-%s", item.Zone, item.SubnetID)
		})

		break
	}

	// If no existing group was found, append the new group
	if targetIdx == -1 {
		// Append new group if not found
		gs = append(gs, newGroup)
		targetIdx = len(gs) - 1
	}

	// Sort the subnets in the target group to ensure consistent ordering
	// Sort(by zone first) to construct a static list with multi-subnets.
	sort.Slice(gs[targetIdx].Subnets, func(i, j int) bool {
		if gs[targetIdx].Subnets[i].Zone != gs[targetIdx].Subnets[j].Zone {
			return gs[targetIdx].Subnets[i].Zone < gs[targetIdx].Subnets[j].Zone
		}
		return gs[targetIdx].Subnets[i].SubnetID < gs[targetIdx].Subnets[j].SubnetID
	})

	return gs
}

func (gs AWSSubnetGroups) SubnetsFilterByGroupNameOrZones(
	names []AWSSubnetGroupName,
	zoneFilters []string,
) (zonesSet []string, subnetIds []string) {

	if len(names) <= 0 {
		return
	}

	// If given no zoneFilters, get subnets of any zones.
	isFilterByZone := false
	zoneKeys := map[string]struct{}{}
	if len(zoneFilters) > 0 {
		isFilterByZone = true
		for _, filter := range zoneFilters {
			zoneKeys[filter] = struct{}{}
		}
	}

	zonesResult := []string{}
	for _, group := range gs {
		// Filter by groupName first.
		if !slices.Contains(names, group.Name) {
			continue
		}

		for _, subnet := range group.Subnets {
			// !isFilterByZone -- get all subnets of any zones.
			// needed -- subnet of this zone is needed.
			if _, needed := zoneKeys[subnet.Zone]; !isFilterByZone || needed {
				zonesResult = append(zonesResult, subnet.Zone)
				subnetIds = append(subnetIds, subnet.SubnetID)
			}
		}
	}

	if len(zonesSet) > 0 || len(subnetIds) > 0 {
		zonesSet = lo.Uniq(zonesResult)
		sort.Strings(zonesSet)
	}

	if len(subnetIds) > 0 {
		sort.Strings(subnetIds)
	}

	return zonesSet, subnetIds
}
