package server

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/pingcap/kvproto/pkg/keyspacepb"
	"github.com/samber/lo"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow/dag"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
)

func (s *serverImpl) CreateTiDBGroup(ctx context.Context, tidbGroup *entity.TiDBGroup) (*entity.TiDBGroup, error) {
	// Validate tidb_group_id format
	if err := entity.ValidateLowercaseAlphanumericId("tidb_group_id", tidbGroup.GetId()); err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "%s", err.Error())
	}

	err := s.checkTiDBGroup(ctx, tidbGroup)
	if err != nil {
		tilog.Error(ctx, "failed to check tidb group", zap.Error(err))
		return nil, err
	}

	var dagExecId string
	var dagSubmitted bool
	if err := s.transactioner.Transaction(ctx, func(ctx context.Context) error {
		// check if the tidbGroup is locked
		unlocktidbGroupFunc, err := s.lock(ctx, tidbGroup.GetId())
		if err != nil {
			tilog.Error(ctx, "failed to lock tidbGroup", zap.String("tidbGroupId", tidbGroup.GetId()), zap.Error(err))
			return err
		}
		defer func() {
			if err := unlocktidbGroupFunc(); err != nil {
				tilog.Error(ctx, "failed to unlock tidbGroup", zap.String("tidbGroupId", tidbGroup.GetId()), zap.Error(err))
			}
		}()

		// get tidbcluster
		tidbCluster, err := s.tidbClusterRepo.Load(ctx, tidbGroup.GetClusterId(), false)
		if err != nil {
			tilog.Error(ctx, "failed to load tidb cluster", zap.String("clusterId", tidbGroup.GetClusterId()), zap.Error(err))
			return err
		}

		if tidbCluster.GetPhase() == apiv1.Cluster_UNAVAILABLE {
			tilog.Warn(ctx, "target tidbCluster is unavailable", zap.String("clusterId", tidbCluster.GetId()))
			return status.Errorf(codes.FailedPrecondition, "target tidbCluster is unavailable, please check the cluster status")
		}

		// set tidb group dataplane name and shared pool id and version
		tidbGroup.SharedPoolId = tidbCluster.GetSharedPoolId()
		tidbGroup.Version = tidbCluster.GetVersion()

		// check if the sharedPool is locked
		unlockSharedpoolFunc, err := s.lock(ctx, tidbCluster.GetSharedPoolId())
		if err != nil {
			tilog.Error(ctx, "failed to lock sharedPool", zap.String("sharedPoolId", tidbCluster.GetSharedPoolId()), zap.Error(err))
			return err
		}
		defer func() {
			if err := unlockSharedpoolFunc(); err != nil {
				tilog.Error(ctx, "failed to unlock sharedPool", zap.String("sharedPoolId", tidbCluster.GetSharedPoolId()), zap.Error(err))
			}
		}()

		// get shared pool
		sharedPool, err := s.sharedPoolRepo.Load(ctx, tidbCluster.GetSharedPoolId(), false)
		if err != nil {
			tilog.Error(ctx, "failed to load shared pool", zap.String("sharedPoolId", tidbCluster.GetSharedPoolId()), zap.Error(err))
			return status.Errorf(codes.Internal, "failed to load shared pool: %s", err)
		}
		if err := guardSharedPoolMutation(sharedPool, sharedPoolMutationBlockedWhilePaused); err != nil {
			return err
		}

		// merge sharedPool status from dag task
		err = s.mergeSharedPoolStatusFromDagTask(ctx, sharedPool)
		if err != nil {
			return status.Errorf(codes.Internal, "failed to merge sharedPool status from dag task: %s", err)
		}

		tidbGroup.DataplaneName = sharedPool.GetDataplaneName()

		// check tidb cluster
		switch tidbCluster.GetPhase() {
		case apiv1.Cluster_CREATING:
			tilog.Error(ctx, "target tidbCluster is creating", zap.String("clusterId", tidbCluster.GetId()))
			return status.Error(codes.FailedPrecondition, "target tidbCluster is creating, please try again later")
		case apiv1.Cluster_UNAVAILABLE:
			tilog.Warn(ctx, "target tidbCluster is unavailable", zap.String("clusterId", tidbCluster.GetId()))
			return status.Errorf(codes.FailedPrecondition, "target tidbCluster is unavailable, please check the tidb cluster status")
		default:
			err = s.checkTiDBGroupKeyspace(ctx, sharedPool, tidbGroup)
			if err != nil {
				tilog.Error(ctx, "failed to check tidb group keyspace", zap.Error(err))
				return err
			}
		}

		// check shared pool status
		switch sharedPool.GetPhase() {
		case apiv1.SharedPool_CREATING:
			tilog.Info(ctx, "sharedPool is still creating", zap.String("sharedPoolId", sharedPool.GetId()))
			return status.Errorf(codes.FailedPrecondition, "sharedPool is still creating, please wait until it's available")
		case apiv1.SharedPool_UNAVAILABLE:
			tilog.Warn(ctx, "sharedPool is unavailable", zap.String("sharedPoolId", sharedPool.GetId()))
			return status.Errorf(codes.FailedPrecondition, "sharedPool is unavailable, please check the shared pool status")
		default:
			// do nothing, shared pool is available
		}

		if tidbGroup.GetSqlEndpoint().GetDnsIdentity() == "" {
			if tidbGroup.GetSqlEndpoint() == nil {
				tidbGroup.SqlEndpoint = &entity.SQLEndpoint{}
			}
			tidbGroup.SqlEndpoint.DnsIdentity = newDefaultDNSIdentity()
		}

		// full new tidb group endpoint
		tidbGroup.SqlEndpoint = &entity.SQLEndpoint{
			DnsIdentity:              tidbGroup.GetSqlEndpoint().GetDnsIdentity(),
			DisablePublicDnsIdentity: tidbGroup.GetSqlEndpoint().GetDisablePublicDnsIdentity(),
			PublicLb: &entity.LoadBalancer{
				Enabled: tidbGroup.GetSqlEndpoint().GetPublicLb().GetEnabled(),
				Port:    tidbGroup.GetSqlEndpoint().GetPublicLb().GetPort(),
			},
			PrivateLb: &entity.LoadBalancer{
				Enabled:     tidbGroup.GetSqlEndpoint().GetPrivateLb().GetEnabled(),
				Port:        tidbGroup.GetSqlEndpoint().GetPrivateLb().GetPort(),
				IpAllowList: tidbGroup.GetSqlEndpoint().GetPrivateLb().GetIpAllowList(),
			},
		}

		var nodeSpecMap map[string][]*entity.NodeSpec
		var ruConfig *entity.RuConfig
		nodeSpecMap, err = s.genNodeSpecMap(ctx, sharedPool)
		if err != nil {
			tilog.Error(ctx, "failed to get node spec map", zap.String("dataplaneId", sharedPool.GetDataplaneName()), zap.Error(err))
			return status.Errorf(codes.Internal, "failed to get node spec map: %s", err)
		}

		if sharedPool.GetCapacityMode() != apiv1.CapacityMode_TOPOLOGY_DRIVEN {
			ruConfig, err = s.ruConfigRepo.GetRuConfig(ctx, sharedPool.GetId(), apiv1.RuConfig_SHARED_POOL)
			if err != nil {
				tilog.Error(ctx, "failed to get ru config", zap.String("sharedPoolId", sharedPool.GetId()), zap.Error(err))
				return status.Errorf(codes.Internal, "failed to get ru config: %s", err)
			}
		}

		// full new tidb group config with default tidb group config
		err = s.generatedTiDBGroup(ctx, sharedPool, tidbCluster, tidbGroup, nodeSpecMap, ruConfig)
		if err != nil {
			tilog.Error(ctx, "failed to generate tidb group", zap.Error(err))
			return err
		}

		var autoscalerTemplateArgs string
		if sharedPool.EnableCapacityPlanner {
			autoscalerTemplateArgs, err = buildTiDBGroupAutoscalerTemplateArgs(ctx, sharedPool, tidbGroup, nodeSpecMap, ruConfig)
			if err != nil {
				return status.Errorf(codes.Internal, "failed to build TiDBGroup autoscaler template args: %s", err)
			}
		}

		tidbGroup.Phase = apiv1.TiDBGroup_CREATING

		if err := s.tidbGroupRepo.Save(ctx, tidbGroup); err != nil {
			tilog.Error(ctx, "failed to save tidb group meta", zap.Error(err))
			return fmt.Errorf("failed to save tidb group meta: %w", err)
		}

		sharedPoolStr, err := protojson.Marshal(sharedPool)
		if err != nil {
			return status.Errorf(codes.Internal, "failed to marshal sharedPool: %s", err)
		}

		tidbGroupStr, err := protojson.Marshal(tidbGroup)
		if err != nil {
			return status.Errorf(codes.Internal, "failed to marshal tidb group: %s", err)
		}

		args := actions.ActionArgs{
			workflow.TenantIdKey:     actions.ActionArgString(tidbGroup.GetTenantId()),
			workflow.SharedPoolIdKey: actions.ActionArgString(sharedPool.GetId()),
			workflow.SharedPoolKey:   actions.ActionArgString(string(sharedPoolStr)),
			workflow.ComponentIdsKey: actions.ActionArgString(strings.Join([]string{
				tidbGroup.GetId(),
				entity.ResolveTiProxyID(sharedPool, tidbGroup.GetClusterId()),
				sharedPool.GetDefaultComponenId(),
			}, ",")),
			workflow.TiDBGroupKey:     actions.ActionArgString(string(tidbGroupStr)),
			workflow.TiDBGroupIdKey:   actions.ActionArgString(tidbGroup.GetId()),
			workflow.TiDBClusterIdKey: actions.ActionArgString(tidbGroup.GetClusterId()),
			workflow.DataplaneIdKey:   actions.ActionArgString(sharedPool.GetDataplaneName()),
			utils.TraceIdKey:          utils.GetTraceId(ctx),
		}
		if autoscalerTemplateArgs != "" {
			args[workflow.AutoscalerTemplateArgsKey] = actions.ActionArgString(autoscalerTemplateArgs)
		}

		dagExecId = utils.GeneratedDagExecId(ctx, fmt.Sprintf("create-tidbgroup-%s-%s", sharedPool.GetId(), tidbGroup.GetId()))
		err = s.dagExecutor.Submit(ctx, dag.PlanCreateTiDBGroup, args, dagExecId)
		if err != nil {
			return status.Errorf(codes.Internal, "failed to submit create tidb group dag: %s", err)
		}
		dagSubmitted = true
		return nil
	}); err != nil {
		if dagSubmitted {
			if abortErr := s.dagExecutor.Abort(ctx, dagExecId); abortErr != nil {
				tilog.Error(ctx, "failed to abort dag after transaction failure",
					zap.String("dagExecId", dagExecId),
					zap.Error(abortErr))
			}
		}
		return nil, err
	}

	return tidbGroup, nil
}

func (s *serverImpl) checkTiDBGroup(ctx context.Context, tidbGroup *entity.TiDBGroup) error {
	if tidbGroup.GetId() == "" {
		return status.Errorf(codes.InvalidArgument, "tidbGroup id is required")
	}

	if strings.Contains(tidbGroup.GetId(), "-") {
		return status.Errorf(codes.InvalidArgument, "tidbGroup id should not contain '-'")
	}

	if tidbGroup.GetTenantId() == "" {
		return status.Errorf(codes.InvalidArgument, "tidbGroup tenant id is required")
	}

	if tidbGroup.GetClusterId() == "" {
		return status.Errorf(codes.InvalidArgument, "tidbGroup cluster id is required")
	}

	// default type to DEFAULT if not specified
	switch tidbGroup.GetType() {
	case apiv1.TiDBGroupType_TIDB_GROUP_TYPE_UNSPECIFIED:
		tidbGroup.Type = apiv1.TiDBGroupType_DEFAULT
	case apiv1.TiDBGroupType_DEFAULT, apiv1.TiDBGroupType_INTERNAL:
		// valid, keep as-is
	default:
		return status.Errorf(codes.InvalidArgument, "invalid tidb group type: %s", tidbGroup.GetType().String())
	}

	// check endpoint
	if err := checkTidbGroupEndpoint(tidbGroup); err != nil {
		return err
	}

	if tidbGroup.GetSqlEndpoint().GetDnsIdentity() != "" {
		if err := validateDNSIdentity(tidbGroup.GetSqlEndpoint().GetDnsIdentity()); err != nil {
			return err
		}
	}

	// only allow create/update one tidb group in one shared pool same time
	dagIns, err := s.getRunningDagIns(ctx, []string{dag.PlanCreateTiDBGroup}, nil, nil, nil, []string{tidbGroup.GetId()})
	if err != nil {
		return status.Errorf(codes.Internal, "failed to get running dag instance: %s", err)
	}

	if len(dagIns) > 0 {
		return status.Errorf(codes.AlreadyExists, "tidb group creation is already running, please try again later")
	}

	// check tidb group already exists
	_, err = s.tidbGroupRepo.Load(ctx, tidbGroup.GetId(), true)
	if err == nil {
		tilog.Warn(ctx, "tidb group already exists", zap.String("tidbGroupId", tidbGroup.GetId()))
		return status.Errorf(codes.AlreadyExists, "tidb group id already exists")
	} else if status.Code(err) != codes.NotFound {
		return status.Errorf(codes.Internal, "failed to check tidb group id: %s", err)
	}

	return nil
}

func (s *serverImpl) checkTiDBGroupKeyspace(ctx context.Context, sharedPool *entity.SharedPool, tidbGroup *entity.TiDBGroup) error {
	// check keyspace status
	keyspaceMeta, err := s.clustergatewayCli.GetKeySpace(ctx, sharedPool, tidbGroup.GetClusterId())
	if err != nil {
		return status.Errorf(codes.Internal, "failed to check keyspace status: %s", err)
	}

	if keyspaceMeta != nil && keyspaceMeta.GetState() != keyspacepb.KeyspaceState_ENABLED {
		return status.Errorf(codes.FailedPrecondition, "keyspace is not enabled, please check tidb cluster status")
	}

	return nil
}

func (s *serverImpl) generatedTiDBGroup(ctx context.Context, sharedPool *entity.SharedPool, tidbCluster *entity.TiDBCluster, tidbGroup *entity.TiDBGroup, nodeSpecMap map[string][]*entity.NodeSpec, ruConfig *entity.RuConfig) error {
	var tidbComp *entity.Component
	for _, comp := range sharedPool.GetComponentGroups() {
		if comp.GetComponentType() == apiv1.ComponentType_TIDB && comp.GetId() == tidbGroup.GetId() {
			tidbComp = comp
			break
		}
	}

	mode := entity.GetEffectiveCapacityMode(sharedPool.GetCapacityMode())
	if mode == apiv1.CapacityMode_TOPOLOGY_DRIVEN {
		if tidbComp != nil {
			return nil
		}

		topoComp, err := s.genTopologyDrivenTiDBGroupComponent(ctx, sharedPool, tidbCluster, tidbGroup, nodeSpecMap)
		if err != nil {
			return err
		}
		sharedPool.ComponentGroups = append(sharedPool.ComponentGroups, topoComp)
		return nil
	}

	// This helper owns creation only. Existing RCU components are updated by
	// regenerateTiDBGroupForRUScaleUp, whose narrower policy preserves the
	// post-create autoscaling and downscale ownership boundaries.
	if tidbComp != nil {
		return nil
	}

	// set tidb storage
	storages, err := s.genDefaultTiDBStorages(sharedPool.GetProvider().GetType(), tidbCluster.GetRequestUnit())
	if err != nil {
		tilog.Error(ctx, "failed to generate tidb storages", zap.Error(err))
		return err
	}

	// create new component with storages
	tidbComp = &entity.Component{
		Id:            tidbGroup.GetId(),
		ComponentType: apiv1.ComponentType_TIDB,
		Phase:         apiv1.Component_CREATING,
		Storages:      storages,
	}
	sharedPool.ComponentGroups = append(sharedPool.ComponentGroups, tidbComp)

	err = s.genDefaultTiDBComponent(ctx, sharedPool, tidbComp, tidbCluster.GetRequestUnit(), nodeSpecMap, ruConfig, false)
	if err != nil {
		tilog.Error(ctx, "failed to generate default tidb component", zap.Error(err))
		return err
	}

	return nil
}

// regenerateTiDBGroupForRUScaleUp intentionally uses a narrower policy than
// TiDB group creation:
//   - only a strict upward RU tier transition may change NodeSpec;
//   - NodeCount, MinNodeCount, and MaxNodeCount stay untouched because
//     autoscaling owns replica counts after creation;
//   - RU decreases stay with RcuScaleDownJob and its downscale guards.
//
// Comparing the old and new mapped tiers also means a same-tier RU update does
// not reconcile historical spec drift or pick up a later mapping change. That
// tradeoff is deliberate: repair/backfill should be explicit instead of being
// coupled to UpdateTiDBCluster.
func (s *serverImpl) regenerateTiDBGroupForRUScaleUp(
	ctx context.Context,
	sharedPool *entity.SharedPool,
	oldRequestUnit *entity.RequestUnit,
	tidbCluster *entity.TiDBCluster,
	tidbGroup *entity.TiDBGroup,
	nodeSpecMap map[string][]*entity.NodeSpec,
	ruConfig *entity.RuConfig,
) (bool, error) {
	var tidbComp *entity.Component
	for _, comp := range sharedPool.GetComponentGroups() {
		if comp.GetComponentType() == apiv1.ComponentType_TIDB && comp.GetId() == tidbGroup.GetId() {
			tidbComp = comp
			break
		}
	}

	if tidbComp == nil {
		return false, s.generatedTiDBGroup(ctx, sharedPool, tidbCluster, tidbGroup, nodeSpecMap, ruConfig)
	}
	if entity.GetEffectiveCapacityMode(sharedPool.GetCapacityMode()) == apiv1.CapacityMode_TOPOLOGY_DRIVEN {
		tilog.Info(ctx, "skip tidb group RU spec scale-up",
			zap.String("reason", "topology_driven"),
			zap.String("sharedPoolId", sharedPool.GetId()),
			zap.String("tidbClusterId", tidbCluster.GetId()),
			zap.String("tidbGroupId", tidbGroup.GetId()),
			zap.Uint32("oldMaxRU", oldRequestUnit.GetMax()),
			zap.Uint32("newMaxRU", tidbCluster.GetRequestUnit().GetMax()),
		)
		return false, nil
	}

	oldTierSpec, err := ruConfig.GetNodeSpec(apiv1.ComponentType_TIDB, oldRequestUnit.GetMax())
	if err != nil {
		tilog.Warn(ctx, "failed to resolve old RU tier for tidb group spec scale-up",
			zap.String("sharedPoolId", sharedPool.GetId()),
			zap.String("tidbClusterId", tidbCluster.GetId()),
			zap.String("tidbGroupId", tidbGroup.GetId()),
			zap.Uint32("oldMaxRU", oldRequestUnit.GetMax()),
			zap.Error(err),
		)
		return false, status.Errorf(codes.Internal, "%v", err)
	}
	newTierSpec, err := ruConfig.GetNodeSpec(apiv1.ComponentType_TIDB, tidbCluster.GetRequestUnit().GetMax())
	if err != nil {
		tilog.Warn(ctx, "failed to resolve new RU tier for tidb group spec scale-up",
			zap.String("sharedPoolId", sharedPool.GetId()),
			zap.String("tidbClusterId", tidbCluster.GetId()),
			zap.String("tidbGroupId", tidbGroup.GetId()),
			zap.Uint32("newMaxRU", tidbCluster.GetRequestUnit().GetMax()),
			zap.Error(err),
		)
		return false, status.Errorf(codes.Internal, "%v", err)
	}
	if !isStrictNodeSpecUpgrade(oldTierSpec, newTierSpec) {
		reason := "mixed_dimension_tier_transition"
		switch {
		case oldTierSpec.GetCpu() == newTierSpec.GetCpu() && oldTierSpec.GetMemoryGi() == newTierSpec.GetMemoryGi():
			reason = "same_tier"
		case newTierSpec.GetCpu() <= oldTierSpec.GetCpu() && newTierSpec.GetMemoryGi() <= oldTierSpec.GetMemoryGi():
			reason = "downward_tier"
		}
		tilog.Info(ctx, "skip tidb group RU spec scale-up",
			zap.String("reason", reason),
			zap.String("sharedPoolId", sharedPool.GetId()),
			zap.String("tidbClusterId", tidbCluster.GetId()),
			zap.String("tidbGroupId", tidbGroup.GetId()),
			zap.Uint32("oldMaxRU", oldRequestUnit.GetMax()),
			zap.Uint32("newMaxRU", tidbCluster.GetRequestUnit().GetMax()),
			zap.Uint32("oldTierCpu", oldTierSpec.GetCpu()),
			zap.Uint32("oldTierMemoryGi", oldTierSpec.GetMemoryGi()),
			zap.Uint32("newTierCpu", newTierSpec.GetCpu()),
			zap.Uint32("newTierMemoryGi", newTierSpec.GetMemoryGi()),
		)
		return false, nil
	}

	targetNodeSpec, err := getNodeSpec(ctx, newTierSpec, nodeSpecMap, false)
	if err != nil {
		return false, err
	}
	currentNodeSpec := tidbComp.GetNodeQuota().GetNodeSpec()
	if !isStrictNodeSpecUpgrade(currentNodeSpec, targetNodeSpec) {
		tilog.Info(ctx, "skip tidb group RU spec scale-up",
			zap.String("reason", "target_not_larger_than_current"),
			zap.String("sharedPoolId", sharedPool.GetId()),
			zap.String("tidbClusterId", tidbCluster.GetId()),
			zap.String("tidbGroupId", tidbGroup.GetId()),
			zap.Uint32("oldMaxRU", oldRequestUnit.GetMax()),
			zap.Uint32("newMaxRU", tidbCluster.GetRequestUnit().GetMax()),
			zap.Uint32("currentCpu", currentNodeSpec.GetCpu()),
			zap.Uint32("currentMemoryGi", currentNodeSpec.GetMemoryGi()),
			zap.Uint32("targetCpu", targetNodeSpec.GetCpu()),
			zap.Uint32("targetMemoryGi", targetNodeSpec.GetMemoryGi()),
		)
		return false, nil
	}
	if tidbComp.GetPhase() != apiv1.Component_AVAILABLE {
		tilog.Info(ctx, "skip tidb group RU spec scale-up",
			zap.String("reason", "component_not_available"),
			zap.String("sharedPoolId", sharedPool.GetId()),
			zap.String("tidbClusterId", tidbCluster.GetId()),
			zap.String("tidbGroupId", tidbGroup.GetId()),
			zap.String("componentPhase", tidbComp.GetPhase().String()),
			zap.Uint32("currentCpu", currentNodeSpec.GetCpu()),
			zap.Uint32("currentMemoryGi", currentNodeSpec.GetMemoryGi()),
			zap.Uint32("targetCpu", targetNodeSpec.GetCpu()),
			zap.Uint32("targetMemoryGi", targetNodeSpec.GetMemoryGi()),
		)
		return false, status.Errorf(codes.FailedPrecondition, "tidb %s is not available", entity.GetComponentGroupName(tidbComp))
	}

	nodeQuota, ok := proto.Clone(tidbComp.GetNodeQuota()).(*apiv1.NodeQuota)
	if !ok {
		return false, status.Errorf(codes.Internal, "failed to clone tidb node quota")
	}
	nodeQuota.NodeSpec = targetNodeSpec
	tidbComp.NodeQuota = nodeQuota
	tidbComp.Phase = apiv1.Component_MODIFYING
	tilog.Info(ctx, "scale up tidb group node spec for RU tier transition",
		zap.String("sharedPoolId", sharedPool.GetId()),
		zap.String("tidbClusterId", tidbCluster.GetId()),
		zap.String("tidbGroupId", tidbGroup.GetId()),
		zap.Uint32("oldMaxRU", oldRequestUnit.GetMax()),
		zap.Uint32("newMaxRU", tidbCluster.GetRequestUnit().GetMax()),
		zap.Uint32("oldCpu", currentNodeSpec.GetCpu()),
		zap.Uint32("oldMemoryGi", currentNodeSpec.GetMemoryGi()),
		zap.Uint32("newCpu", targetNodeSpec.GetCpu()),
		zap.Uint32("newMemoryGi", targetNodeSpec.GetMemoryGi()),
	)
	return true, nil
}

func isStrictNodeSpecUpgrade(current, target *entity.NodeSpec) bool {
	if current == nil || target == nil {
		return false
	}
	return target.GetCpu() >= current.GetCpu() &&
		target.GetMemoryGi() >= current.GetMemoryGi() &&
		(target.GetCpu() > current.GetCpu() || target.GetMemoryGi() > current.GetMemoryGi())
}

func (s *serverImpl) genTopologyDrivenTiDBGroupComponent(ctx context.Context, sharedPool *entity.SharedPool, tidbCluster *entity.TiDBCluster, tidbGroup *entity.TiDBGroup, nodeSpecMap map[string][]*entity.NodeSpec) (*entity.Component, error) {
	if tidbGroup.TopologySpec == nil {
		return nil, status.Errorf(codes.InvalidArgument, "topology spec is required for creating tidb group %s in TOPOLOGY_DRIVEN mode", tidbGroup.GetId())
	}

	var source *entity.Component
	for _, comp := range tidbGroup.TopologySpec.GetComponents() {
		if comp != nil && comp.GetComponentType() == apiv1.ComponentType_TIDB {
			source = comp
			break
		}
	}
	if source == nil {
		return nil, status.Errorf(codes.InvalidArgument, "topology driven tidb component not found in request topology spec for tidb group %s", tidbGroup.GetId())
	}

	cloned, ok := proto.Clone(source).(*apiv1.Component)
	if !ok {
		return nil, status.Errorf(codes.Internal, "failed to clone topology tidb component")
	}

	tidbComp := cloned
	tidbComp.Id = tidbGroup.GetId()
	tidbComp.Phase = apiv1.Component_CREATING

	if tidbComp.GetNodeQuota() == nil || tidbComp.GetNodeQuota().GetNodeSpec() == nil {
		return nil, status.Errorf(codes.InvalidArgument, "topology driven tidb component for tidb group %s is missing node quota or node spec", tidbGroup.GetId())
	}
	if tidbComp.GetNodeQuota().GetNodeCount() == 0 {
		return nil, status.Errorf(codes.InvalidArgument, "topology driven tidb component for tidb group %s has empty node count", tidbGroup.GetId())
	}
	if tidbComp.GetNodeQuota().GetMinNodeCount() == 0 {
		tidbComp.GetNodeQuota().MinNodeCount = tidbComp.GetNodeQuota().GetNodeCount()
	}
	if tidbComp.GetNodeQuota().GetMaxNodeCount() == 0 {
		tidbComp.GetNodeQuota().MaxNodeCount = tidbComp.GetNodeQuota().GetNodeCount()
	}

	resolvedSpec, err := getNodeSpec(ctx, tidbComp.GetNodeQuota().GetNodeSpec(), nodeSpecMap, tidbComp.GetNodeQuota().GetNodeSpec().GetWithLocalDisk())
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "node spec %s not found for tidb group %s: %s", entity.GetComponentNodeSpec(tidbComp.GetNodeQuota().GetNodeSpec()), tidbGroup.GetId(), err)
	}
	tidbComp.GetNodeQuota().NodeSpec = resolvedSpec

	if len(tidbComp.GetStorages()) == 0 {
		storages, err := s.genDefaultTiDBStorages(sharedPool.GetProvider().GetType(), tidbCluster.GetRequestUnit())
		if err != nil {
			return nil, err
		}
		tidbComp.Storages = storages
	}
	for _, storage := range tidbComp.GetStorages() {
		if storage.GetName() == apiv1.Component_Storage_SLOWLOG && storage.GetMountPath() == "" {
			storage.MountPath = entity.AuditAndSlowLogPath
		}
	}

	tilog.Info(ctx, "inherit tidb group component from topology spec",
		zap.String("tidbGroupId", tidbGroup.GetId()),
		zap.Uint32("nodeCount", tidbComp.GetNodeQuota().GetNodeCount()),
		zap.Uint32("cpu", tidbComp.GetNodeQuota().GetNodeSpec().GetCpu()),
		zap.Uint32("memoryGi", tidbComp.GetNodeQuota().GetNodeSpec().GetMemoryGi()))

	return tidbComp, nil
}

const (
	randomLen = 6
	// 36 = len(0-9) + len(a-z)
	formatBase = 36
)

var timestampStartPoint = time.Date(2022, time.January, 0, 0, 0, 0, 0, time.UTC)

func newDefaultDNSIdentity() string {
	n := time.Now().UTC().Unix() - timestampStartPoint.Unix()
	s := strconv.FormatInt(n, formatBase)

	return lo.RandomString(randomLen, lo.LowerCaseLettersCharset) + s
}
