package eks

import (
	"context"
	"fmt"

	"github.com/aws/aws-sdk-go-v2/service/ssm"
	"github.com/crossplane-contrib/provider-aws/apis/autoscaling/v1beta1"
	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	"github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	"github.com/crossplane-contrib/provider-aws/apis/eks/v1alpha1"
	eksv1beta1 "github.com/crossplane-contrib/provider-aws/apis/eks/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/hashicorp/go-multierror"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/util/retry"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"

	. "github.com/tidbcloud/eks-provider/controllers/k8sversion/common"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/cloud/aws"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/gitops"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/nodegroup"
	"github.com/tidbcloud/infra-provider/pkg/nodegroup/selfmanaged"
	"github.com/tidbcloud/infra-provider/pkg/util"
	crossplaneutil "github.com/tidbcloud/infra-provider/pkg/util/crossplane"
)

// getTiDBClusterNodeGroups returns the old and new k8s version node groups for a TiDB cluster.
// Since the TiDB cluster node groups may be migrated to self-managed node groups from managed node groups after upgrading,
// the function will return all of them.
func getTiDBClusterNodeGroups(ctx context.Context, infraCli client.Client, info *upgradeInfo) (oldNGs, newNGs []*manualv1alpha1.NodeGroup, oldASGs, newASGs []*v1beta1.AutoScalingGroup, err error) {
	oldNGs, newNGs, err = getTiDBClusterManagedNodeGroups(ctx, infraCli, info)
	if err != nil {
		return nil, nil, nil, nil, fmt.Errorf("failed to get managed node groups: %w", err)
	}

	oldASGs, newASGs, err = getTiDBClusterSelfManagedNodeGroups(ctx, infraCli, info)
	if len(newNGs) == 0 && err != nil {
		return nil, nil, nil, nil, fmt.Errorf("failed to get self-managed node groups: %w", err)
	}

	return oldNGs, newNGs, oldASGs, newASGs, nil
}

// getTiDBClusterManagedNodeGroups returns the tidb cluster's managed node groups those are currently in use and match the desired version.
func getTiDBClusterManagedNodeGroups(ctx context.Context, cli client.Client, info *upgradeInfo) (oldNGs []*manualv1alpha1.NodeGroup, newNGs []*manualv1alpha1.NodeGroup, err error) {
	list := &manualv1alpha1.NodeGroupList{}
	if err = cli.List(ctx, list, info.ClusterSelector()); err != nil {
		return nil, nil, fmt.Errorf("failed to list managed nodegroups: %w", err)
	}

	for i := range list.Items {
		var specInstanceType string
		com := list.Items[i].Labels[namer.ComponentSelectorKey]
		switch com {
		case string(cloudv1.ComponentPD):
			specInstanceType = info.cluster.Spec.Cluster.PD.InstanceType
		case string(cloudv1.ComponentTiKV):
			specInstanceType = info.cluster.Spec.Cluster.TiKV.InstanceType
		case string(cloudv1.ComponentTiFlash):
			specInstanceType = info.cluster.Spec.Cluster.TiFlash.InstanceType
		case string(cloudv1.ComponentTiDB):
			specInstanceType = info.cluster.Spec.Cluster.TiDB.InstanceType
		case string(cloudv1.ComponentTiProxy):
			specInstanceType = info.cluster.Spec.Cluster.TiProxy.InstanceType
		default:
			continue
		}

		for _, instanceType := range list.Items[i].Spec.ForProvider.InstanceTypes {
			// only include nodegroups matching the instance type for the current spec
			if instanceType == specInstanceType {
				if list.Items[i].Spec.ForProvider.Version != nil && *list.Items[i].Spec.ForProvider.Version == info.desiredK8sVersion {
					newNGs = append(newNGs, &list.Items[i])
				} else {
					oldNGs = append(oldNGs, &list.Items[i])
				}
				break
			}
		}
	}

	return oldNGs, newNGs, nil
}

// getTiDBClusterSelfManagedNodeGroups returns the tidb cluster's self-managed node groups those are currently in use and match the desired version.
func getTiDBClusterSelfManagedNodeGroups(ctx context.Context, infraCli client.Client, info *upgradeInfo) (oldNGs []*v1beta1.AutoScalingGroup, newNGs []*v1beta1.AutoScalingGroup, err error) {
	allASGs, err := selfmanaged.GetTiDBClusterSelfManagedNodeGroups(ctx, infraCli, info.ClusterSelector(), info.cluster)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to get all self-managed nodegroups: %w", err)
	}

	for _, asg := range allASGs {
		if util.GetAnnotation(asg, selfmanaged.K8sVersionAnnotationKey) == info.desiredK8sVersion {
			newNGs = append(newNGs, asg)
		} else if info.cluster.Status.Phase == cloudv1.ClusterPhaseSuspend || (asg.Spec.ForProvider.DesiredCapacity != nil && *asg.Spec.ForProvider.DesiredCapacity > 0) {
			// Do not include the node group with desired capacity 0.
			// Otherwise, the number of old nodegroups will always be greater than the number of new nodegroups.
			// Since we will not create a new nodegroup from an old nodegroup with desired capacity 0.
			oldNGs = append(oldNGs, asg)
		}
	}

	return oldNGs, newNGs, nil
}

func isControlPlaneUpgraded(eks *eksv1beta1.Cluster, desiredVer, currentVer string) bool {
	return eks.Status.AtProvider.Status == eksv1beta1.ClusterStatusActive &&
		desiredVer != "" && desiredVer == currentVer && desiredVer == eks.Status.AtProvider.Version
}

func getEKSCR(ctx context.Context, cli client.Client, network *cloudv1.Network) (*eksv1beta1.Cluster, error) {
	var eks eksv1beta1.Cluster
	key := types.NamespacedName{Name: util.GetAnnotation(network, consts.AnnotationKeyDataplaneName)}
	if err := cli.Get(ctx, key, &eks); err != nil {
		return nil, err
	}
	return &eks, nil
}

// readyForUpgradeTiDBNodes checks if the eks cluster's control plane is upgraded.
func readyForUpgradeNodeGroups(ctx context.Context, cli client.Client, network *cloudv1.Network, desiredVer, currentVer string) bool {
	logger := log.FromContext(ctx)

	eks, err := getEKSCR(ctx, cli, network)
	if err != nil {
		logger.Error(err, "unable to get the eks CR")
		return false
	}

	logger.Info("eks info", "version", eks.Status.AtProvider.Version, "status", eks.Status.AtProvider.Status)
	return isControlPlaneUpgraded(eks, desiredVer, currentVer)
}

func getNewNodegroupCRName(oldNGLabels map[string]string, instanceType, region, version string) string {
	cluster := oldNGLabels["cluster"]
	component := oldNGLabels[namer.ComponentSelectorKey]
	zone := oldNGLabels[consts.NodeGroupZonesLabelKey]
	namespace := oldNGLabels[namer.LegacyNetworkNamespaceSelectorKey]

	ngName := util.FnvHash(namer.NodeGroupName(cluster, component, zone))
	instanceTypeHash := util.FnvHash(instanceType)
	ngName = "tidbcloud-" + ngName + "-" + instanceTypeHash

	identifier := namer.WithPrefix(ngName + nodegroup.NormalizeVersion(version))
	objMeta := metav1.ObjectMeta{
		Name:      "eks-" + region,
		Namespace: namespace,
	}

	return namer.UniqueIdentifier(objMeta, identifier)
}

// createNewNodegroup creates a new node group and a new launch template CR with the desired version, based on the given version.
func createNewManagedNodeGroups(ctx context.Context, infraCli client.Client, info *upgradeInfo, oldTiDBNodegroups []*manualv1alpha1.NodeGroup) error {
	var errs *multierror.Error

	for _, oldNG := range oldTiDBNodegroups {
		newName := getNewNodegroupCRName(oldNG.GetLabels(), oldNG.Spec.ForProvider.InstanceTypes[0], oldNG.Spec.ForProvider.Region, info.desiredK8sVersion)
		crLabels := util.AddResourceName(util.DeepCopyMap(oldNG.Labels), newName)

		var err error
		var oldLaunchTemplate ec2v1alpha1.LaunchTemplate
		if err = infraCli.Get(ctx, client.ObjectKey{Name: oldNG.Name}, &oldLaunchTemplate); err != nil {
			info.logger.Error(err, "unable to get the old launch template", "name", oldNG.Name)
			errs = multierror.Append(errs, err)
			continue
		}

		newLaunchTemplate := &ec2v1alpha1.LaunchTemplate{
			ObjectMeta: metav1.ObjectMeta{
				Name:   newName,
				Labels: crLabels,
			},
			Spec: ec2v1alpha1.LaunchTemplateSpec{
				ForProvider: ec2v1alpha1.LaunchTemplateParameters{
					Region:             oldLaunchTemplate.Spec.ForProvider.Region,
					LaunchTemplateName: pointer.String(newName),
					LaunchTemplateData: oldLaunchTemplate.Spec.ForProvider.LaunchTemplateData,
				},
				ResourceSpec: oldLaunchTemplate.Spec.ResourceSpec,
			},
		}
		if err = k8s.Apply(ctx, infraCli, newLaunchTemplate); err != nil {
			info.logger.Error(err, "unable to create new launch template", "name", newName)
			return err
		}

		newNg := &manualv1alpha1.NodeGroup{
			ObjectMeta: metav1.ObjectMeta{
				Name:   newName,
				Labels: crLabels,
			},
			Spec: oldNG.Spec,
		}
		newNg.Spec.ForProvider.Version = &info.desiredK8sVersion
		newNg.Spec.ForProvider.LaunchTemplate = &manualv1alpha1.LaunchTemplateSpecification{
			NameRef: &xpv1.Reference{
				Name: newName,
			},
		}
		if err = k8s.Apply(ctx, infraCli, newNg); err != nil {
			info.logger.Error(err, "unable to create new nodegroup", "name", newName)
			errs = multierror.Append(errs, err)
		}
	}

	return errs.ErrorOrNil()
}

func createNewSelfManagedNodeGroups(ctx context.Context, infraCli client.Client, ssmClient *ssm.Client, provider gitops.Provider, info *upgradeInfo, oldManagedNGs []*manualv1alpha1.NodeGroup, oldSelfManagedNGs []*v1beta1.AutoScalingGroup, systemConfiguration string) error {
	if len(oldManagedNGs) > 0 && len(oldSelfManagedNGs) > 0 {
		return fmt.Errorf("both managed and self-managed node groups exist, should not happen")
	}

	var errs *multierror.Error
	// amiIDs stores the ami id for each architecture.
	amiIDs := make(map[string]string, 2)

	// Create new version self-managed node groups based on the old version managed node groups.
	for i := range oldManagedNGs {
		var arch string
		amiType := oldManagedNGs[i].Spec.ForProvider.AMIType
		if amiType == nil {
			return fmt.Errorf("old managed node group %s does not have ami type", oldManagedNGs[i].Name)
		}
		switch *amiType {
		case nodegroup.X86AMIType, nodegroup.BottleRocketX86AMIType:
			arch = consts.AMD64ArchName
		case nodegroup.ARMAMIType:
			arch = consts.ARM64ArchName
		default:
			return fmt.Errorf("unknown ami type %s", *amiType)
		}

		var amiID string
		amiID, ok := amiIDs[arch]
		if !ok {
			amiIDPtr, err := aws.GetAMIID(ctx, ssmClient, selfmanaged.GetSSMName(info.desiredK8sVersion, arch))
			if err != nil {
				return fmt.Errorf("unable to get the new ami id: %w", err)
			}
			amiID = *amiIDPtr
		}

		errs = multierror.Append(errs, fromManagedToToSelfManaged(ctx, infraCli, info, oldManagedNGs[i], amiID, systemConfiguration))
	}

	// Create new version self-managed node groups based on the old version self-managed node groups.
	for i := range oldSelfManagedNGs {
		// Only create new self-managed node groups for those with desired capacity > 0.
		if oldSelfManagedNGs[i].Spec.ForProvider.DesiredCapacity != nil && *oldSelfManagedNGs[i].Spec.ForProvider.DesiredCapacity > 0 {
			it, err := getInstanceTypeFromSelfManagedNodeGroup(oldSelfManagedNGs[i], provider)
			if err != nil {
				return fmt.Errorf("failed to find instance type for asg %s: %w", oldSelfManagedNGs[i].Name, err)
			}

			var amiID string
			amiID, ok := amiIDs[it.Arch]
			if !ok {
				amiIDPtr, err := aws.GetAMIID(ctx, ssmClient, selfmanaged.GetSSMName(info.desiredK8sVersion, it.Arch))
				if err != nil {
					return fmt.Errorf("unable to get the new ami id: %w", err)
				}
				amiID = *amiIDPtr
			}

			errs = multierror.Append(errs, fromSelfManagedToSelfManaged(ctx, infraCli, info, oldSelfManagedNGs[i], amiID))
		}
	}

	return errs.ErrorOrNil()
}

func getInstanceTypeFromSelfManagedNodeGroup(asg *v1beta1.AutoScalingGroup, provider gitops.Provider) (cloudv1.InstanceType, error) {
	if asg == nil {
		return cloudv1.InstanceType{}, fmt.Errorf("the self-managed node group is nil")
	}
	if asg.Status.AtProvider.Instances == nil || len(asg.Status.AtProvider.Instances) == 0 {
		return cloudv1.InstanceType{}, fmt.Errorf("the self-managed node group %s has no instances", asg.Name)
	}

	return provider.FindInstanceType(*asg.Status.AtProvider.Instances[0].InstanceType)
}

func fromManagedToToSelfManaged(ctx context.Context, infraCli client.Client, info *upgradeInfo, oldNG *manualv1alpha1.NodeGroup, amiID string, systemConfiguration string) error {
	instanceType := oldNG.Spec.ForProvider.InstanceTypes[0]

	var oldLT ec2v1alpha1.LaunchTemplate
	if err := infraCli.Get(ctx, types.NamespacedName{Name: oldNG.Name}, &oldLT); err != nil {
		return fmt.Errorf("unable to get the old launch template: %w", err)
	}

	asgName := getNewNodegroupCRName(oldNG.GetLabels(), instanceType, oldNG.Spec.ForProvider.Region, info.desiredK8sVersion)

	newLT, newNG := selfmanaged.FromManagedToSelfManaged(&oldLT, oldNG, info.eks, asgName, amiID, info.desiredK8sVersion, systemConfiguration)
	if err := k8s.Apply(ctx, infraCli, newLT); err != nil {
		return fmt.Errorf("unable to create new launch template: %w", err)
	}
	return k8s.Apply(ctx, infraCli, newNG)
}

func fromSelfManagedToSelfManaged(ctx context.Context, infraCli client.Client, info *upgradeInfo, ng *v1beta1.AutoScalingGroup, amiID string) error {
	var oldLT ec2v1alpha1.LaunchTemplate
	if err := infraCli.Get(ctx, types.NamespacedName{Name: ng.Name}, &oldLT); err != nil {
		return fmt.Errorf("unable to get the old launch template: %w", err)
	}

	instanceType := oldLT.Spec.ForProvider.LaunchTemplateData.InstanceType
	if instanceType == nil {
		return fmt.Errorf("instance type not found for launch template %s", oldLT.Name)
	}

	asgName := getNewNodegroupCRName(ng.GetLabels(), *instanceType, info.network.Spec.Region, info.desiredK8sVersion)

	newLT, newNG := selfmanaged.FromSelfManagedToSelfManaged(&oldLT, ng, info.eks.Name, asgName, amiID, info.desiredK8sVersion)
	if err := k8s.Apply(ctx, infraCli, newLT); err != nil {
		return fmt.Errorf("unable to create new launch template: %w", err)
	}

	return k8s.Apply(ctx, infraCli, newNG)
}

// allNodeGroupsUpgraded checks if all node groups are upgraded to the desired version and their status are active.
func allNodeGroupsReady(ngs []*manualv1alpha1.NodeGroup, asgs []*v1beta1.AutoScalingGroup, desiredVersion string) bool {
	for _, ng := range ngs {
		if ng.Status.AtProvider.Version != desiredVersion ||
			ng.Status.AtProvider.Status != manualv1alpha1.NodeGroupStatusActive ||
			!IsCRReadyAndSynced(ng.Status.Conditions) {
			return false
		}
	}

	for _, asg := range asgs {
		if util.GetAnnotation(asg, selfmanaged.K8sVersionAnnotationKey) != desiredVersion || !crossplaneutil.IsReadyAndSynced(asg.Status.Conditions) {
			return false
		}
	}
	return true
}

// allTiDBNodesUpgraded checks if all TiDB nodes are upgraded to the desired version.
// Since the cluster may use managed nodegroups or self-managed nodegroups, both need to be checked.
func allTiDBNodesUpgraded(info *upgradeInfo, ngs []*manualv1alpha1.NodeGroup, asgs []*v1beta1.AutoScalingGroup) bool {
	if info.cluster.Status.Phase == cloudv1.ClusterPhaseSuspend {
		if info.useSelfManaged {
			status, err := cloudv1.EKSClusterStatusFromCluster(info.cluster)
			return err == nil && status.NodeVersion == info.desiredK8sVersion
		}

		return len(ngs) > 0 && allNodeGroupsReady(ngs, asgs, info.desiredK8sVersion)
	}

	// If the cluster is not suspended, compare control plane and node versions to determine if nodes are upgraded.
	return info.currentNodeVersion != "" && info.currentCPVersion == info.currentNodeVersion
}

func deleteNodeGroupAndLaunchTemplate(ctx context.Context, infraCli client.Client, ng client.Object, ngName string) error {
	var errs *multierror.Error

	if err := k8s.DeleteObject(ctx, infraCli, ng); err != nil {
		errs = multierror.Append(errs, fmt.Errorf("failed to delete the old nodegroup %s: %w", ngName, err))
	}

	var oldLaunchTemplate ec2v1alpha1.LaunchTemplate
	if err := infraCli.Get(ctx, client.ObjectKey{Name: ngName}, &oldLaunchTemplate); err == nil {
		if e := k8s.DeleteObject(ctx, infraCli, &oldLaunchTemplate); e != nil {
			errs = multierror.Append(errs, fmt.Errorf("failed to delete the old launch template %s: %w", oldLaunchTemplate.Name, e))
		}
	}

	return errs.ErrorOrNil()
}

// updateNodeGroupVersion updates the node group CR's version and release version.
func updateNodeGroupVersion(ctx context.Context, infraCli client.Client, ng *manualv1alpha1.NodeGroup, desiredVersion string) error {
	ng.Spec.ForProvider.Version = pointer.String(desiredVersion)
	ng.Spec.ForProvider.ReleaseVersion = nil
	return infraCli.Update(ctx, ng)
}

func cordonNodes(ctx context.Context, cli client.Client, nodes corev1.NodeList) error {
	var errs *multierror.Error

	for _, node := range nodes.Items {
		if !node.Spec.Unschedulable {
			if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
				var currentNode corev1.Node
				if err := cli.Get(ctx, types.NamespacedName{Name: node.Name}, &currentNode); err != nil {
					return err
				}
				currentNode.Spec.Unschedulable = true
				return cli.Update(ctx, &currentNode)
			}); err != nil {
				errs = multierror.Append(errs, err)
			}
		}
	}

	return errs.ErrorOrNil()
}

// cordonTiDBNodesByNodeGroups cordon tidb nodes that belong to the given managed node groups and self-managed node groups.
// So that after restarting pods, the pods will be scheduled to the new version nodes.
func cordonTiDBNodesByNodeGroups(ctx context.Context, cli client.Client, clusterID string, oldNGs []*manualv1alpha1.NodeGroup, oldASGs []*v1beta1.AutoScalingGroup) error {
	var errs *multierror.Error

	for _, ng := range oldNGs {
		var nodeList corev1.NodeList
		if err := cli.List(ctx, &nodeList, client.MatchingLabelsSelector{
			Selector: labels.Set{
				"tidbcloud-cluster": clusterID,
				// only managed node groups' nodes have this label
				"eks.amazonaws.com/nodegroup": ng.Name,
			}.AsSelector(),
		}); err != nil {
			return err
		}

		if err := cordonNodes(ctx, cli, nodeList); err != nil {
			errs = multierror.Append(errs, err)
		}
	}

	for _, asg := range oldASGs {
		var nodeList corev1.NodeList
		if err := cli.List(ctx, &nodeList, client.MatchingLabelsSelector{
			Selector: labels.Set{
				"tidbcloud-cluster": clusterID,
				"Name":              asg.Name,
			}.AsSelector(),
		}); err != nil {
			return err
		}

		if err := cordonNodes(ctx, cli, nodeList); err != nil {
			errs = multierror.Append(errs, err)
		}
	}

	return errs.ErrorOrNil()
}

// areAllTiDBPodsOnNewNodeGroups checks if all tidb component pods are on the newly created node groups' nodes.
func areAllTiDBPodsOnNewNodeGroups(ctx context.Context, cli client.Client, namespace string, newNGs []*manualv1alpha1.NodeGroup, newASGs []*v1beta1.AutoScalingGroup) (result bool, ngNamePodMap map[string]string, err error) {
	logger := log.FromContext(ctx)

	if len(newNGs) > 0 && len(newASGs) > 0 {
		return false, nil, fmt.Errorf("both managed and self-managed node groups exist, should not happen")
	}

	// List all pods that are managed by tidb-operator and in the given namespace.
	var podList corev1.PodList
	if err = cli.List(ctx, &podList,
		client.InNamespace(namespace),
		client.MatchingLabelsSelector{
			Selector: labels.SelectorFromSet(map[string]string{
				consts.ManagedByLabelKey: "tidb-operator",
			}),
		},
	); err != nil || len(podList.Items) == 0 {
		return false, nil, fmt.Errorf("unable to list the tidb pods: %v; the number of pods: %d", err, len(podList.Items))
	}

	var errs *multierror.Error
	ngNamePodMap = map[string]string{}
	for _, pod := range podList.Items {
		_, exists := ComponentSet[pod.Labels[consts.ComponentLabelKey]]
		if !exists || pod.Spec.NodeName == "" {
			continue
		}

		var node corev1.Node
		if err = cli.Get(ctx, types.NamespacedName{Name: pod.Spec.NodeName}, &node); err != nil {
			logger.Error(err, "unable to get the node for pod", "node", pod.Spec.NodeName, "pod", pod.Name, "namespace", pod.Namespace)
			errs = multierror.Append(errs, err)
			continue
		}

		ngName := node.Labels["Name"]
		if ngName == "" {
			ngName = node.Labels["eks.amazonaws.com/nodegroup"]
		}
		ngNamePodMap[ngName] = pod.Name
	}
	if err = errs.ErrorOrNil(); err != nil {
		return false, nil, err
	}

	// Check if all the pods are on the new node groups' nodes.
	result = true
	for _, ng := range newNGs {
		// Only check those node groups who have nodes.
		// For example:
		// - Before the upgrade, the cluster has two node groups of tidb: ng1 (az-1) and ng2 (az-2).
		// - The cluster scale-in one tidb, so only ng1 has one node, ng2's desired size is 0.
		// - During the upgrade, we will also create a new version node group for ng2.
		if ng.Status.AtProvider.ScalingConfig.DesiredSize != nil && *ng.Status.AtProvider.ScalingConfig.DesiredSize > 0 {
			if _, exists := ngNamePodMap[ng.Name]; !exists {
				logger.Info("no pods on the new managed nodegroup", "nodegroup", ng.Name)
				result = false
				break
			}
		} else {
			logger.Info("the new managed nodegroup has no nodes, skip", "nodegroup", ng.Name)
		}
	}
	for _, asg := range newASGs {
		if asg.Spec.ForProvider.DesiredCapacity != nil && *asg.Spec.ForProvider.DesiredCapacity > 0 {
			if _, exists := ngNamePodMap[asg.Name]; !exists {
				logger.Info("no pods on the new self-managed nodegroup", "nodegroup", asg.Name)
				result = false
				break
			}
		} else {
			logger.Info("the new self-managed nodegroup has no nodes, skip", "nodegroup", asg.Name)
		}
	}

	return result, ngNamePodMap, nil
}

func addonCRName(eksName, addon string) string {
	return addon + "-" + eksName
}

func getAddonCR(network *cloudv1.Network, eksName, addon, version string) *v1alpha1.Addon {
	return &v1alpha1.Addon{
		ObjectMeta: metav1.ObjectMeta{
			Name: addonCRName(eksName, addon),
			Labels: map[string]string{
				"eks":                 eksName,
				"addon":               addon,
				cloudv1.TenantTagKey:  network.Labels[cloudv1.TenantTagKey],
				cloudv1.ProjectTagKey: network.Labels[cloudv1.ProjectTagKey],
			},
		},
		Spec: v1alpha1.AddonSpec{
			ResourceSpec: xpv1.ResourceSpec{
				// To prevent add-ons from being accidentally deleted and causing tidb service interruption,
				// use orphan deletion policy.
				// It's safe since:
				//  - the external resource is not billed
				//  - it can be overwritten by the new add-on CR
				//  - it will be deleted when eks is deleted
				DeletionPolicy:          xpv1.DeletionOrphan,
				ProviderConfigReference: &xpv1.Reference{Name: namer.UniqueIdentifier(network.ObjectMeta)},
			},
			ForProvider: v1alpha1.AddonParameters{
				Region:           network.Spec.Region,
				AddonName:        pointer.String(addon),
				AddonVersion:     pointer.String(version),
				ResolveConflicts: pointer.String("Overwrite"),
				CustomAddonParameters: v1alpha1.CustomAddonParameters{
					ClusterNameRef: &xpv1.Reference{
						Name: eksName,
					},
				},
			},
		},
	}
}
