package eks

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

	"github.com/aws/aws-sdk-go-v2/service/ec2"
	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
	"github.com/aws/aws-sdk-go/aws"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	"github.com/gogo/protobuf/proto"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	pkgaws "github.com/tidbcloud/infra-provider/regional-server/pkg/cloud/aws"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/consts"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/appbox/task"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/autoops"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/k8s"
	labelutil "github.com/tidbcloud/infra-provider/regional-server/pkg/labels"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/namer"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/nodepool"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/util"
	ec2v1beta1 "github.com/upbound/provider-aws/apis/ec2/v1beta1"
	eksv1beta1 "github.com/upbound/provider-aws/apis/eks/v1beta1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/pointer"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
	BottleRocket            = "bottlerocket"
	DefaultNodeMaxSize      = 10
	DefaultNodeMinSize      = 3
	DefaultInstanceType     = "t3.medium"
	DefaultSystemVolumeSize = 20

	NodeGroupStatusCreating     string = "CREATING"
	NodeGroupStatusActive       string = "ACTIVE"
	NodeGroupStatusCreateFailed string = "CREATE_FAILED"
)

type Nodegroup struct {
	*task.BaseTask
	awscli pkgaws.Client
}

func NewNodegroup(ctx *task.Context, awscli pkgaws.Client) *Nodegroup {
	return &Nodegroup{BaseTask: &task.BaseTask{Context: ctx}, awscli: awscli}
}

func (t *Nodegroup) Name() string {
	return "Nodegroup"
}

func (t *Nodegroup) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())
	// If node role is ready, create the nodegroup CR first,
	// though it can not be reconciled when eks is not ready.
	if t.APPBox.Status.IAM.NodeRoleARN == "" {
		return deploy.Panic(fmt.Errorf("node role is not ready"))
	}

	targetVersion := t.APPBox.Spec.KubernetesConfig.Version

	// do not try to upgrade nodegroup if the EKS is not upgraded to the target version
	if err := CheckEKSVersionReady(ctx, t.Local, t.Cluster.GetName(), targetVersion); err != nil {
		return deploy.Error(err)
	}

	clusterSecurityGroupID, err := t.clusterSecurityGroupID(ctx)
	if err != nil {
		return deploy.Error(err)
	}

	labels := util.MergeMaps(t.Namer.Labels(), t.APPBox.Labels)

	// Extract admin node configuration using common utility function
	adminConfig := config.GetAdminNodeConfig(
		t.APPBox.Spec.KubernetesConfig.AdminNodeConfig,
		t.Config.APPBoxConfig.AdminNodeConfig,
		DefaultInstanceType,
		DefaultNodeMinSize,
		DefaultNodeMaxSize,
		DefaultSystemVolumeSize,
	)

	// Build desired set and create nodegroups, reusing computed names.
	// O11y pools share APPBox owner labels; include their names in desired so
	// cleanupStaleAdminNodegroups does not delete them. Enabled=false omits the
	// name so a previously created o11y pool is cleaned up.
	primaryName := t.nodegroupName()
	desired := map[string]struct{}{primaryName: {}}
	for i := range adminConfig.AdditionalInstanceTypes {
		desired[t.additionalNodegroupName(i)] = struct{}{}
	}

	// Create primary admin nodegroup
	if err := t.createNodegroupResources(ctx, nodegroupCreateParams{
		name:             primaryName,
		instanceType:     adminConfig.InstanceType,
		minSize:          adminConfig.MinSize,
		maxSize:          adminConfig.MaxSize,
		systemVolumeSize: adminConfig.SystemVolumeSize,
		component:        consts.LabelValComponentShared,
		amiTypeHint:      t.Config.APPBoxConfig.AdminNodeConfig.AMIType,
		targetVersion:    targetVersion,
		securityGroupID:  clusterSecurityGroupID,
		metaLabels:       labels,
	}); err != nil {
		return deploy.Error(err)
	}

	// Create additional admin nodegroups with minSize=0 for each extra instance type
	for i, instanceType := range adminConfig.AdditionalInstanceTypes {
		altName := t.additionalNodegroupName(i)
		if err := t.createNodegroupResources(ctx, nodegroupCreateParams{
			name:             altName,
			instanceType:     instanceType,
			minSize:          0,
			maxSize:          adminConfig.MaxSize,
			systemVolumeSize: adminConfig.SystemVolumeSize,
			component:        consts.LabelValComponentShared,
			amiTypeHint:      t.Config.APPBoxConfig.AdminNodeConfig.AMIType,
			targetVersion:    targetVersion,
			securityGroupID:  clusterSecurityGroupID,
			metaLabels:       labels,
		}); err != nil {
			return deploy.Error(fmt.Errorf("failed to create additional admin nodegroup for instance type %s: %w", instanceType, err))
		}
	}

	o11yPools, err := config.GetO11yNodePools(
		t.Config.APPBoxConfig.O11yNodeConfig,
		0, // default minSize=0 for o11y
		DefaultNodeMaxSize,
		DefaultSystemVolumeSize,
	)
	if err != nil {
		return deploy.Error(err)
	}
	for _, pool := range o11yPools {
		o11yName := t.o11yNodegroupName(pool.InstanceType)
		desired[o11yName] = struct{}{}
		if err := t.createNodegroupResources(ctx, nodegroupCreateParams{
			name:             o11yName,
			instanceType:     pool.InstanceType,
			minSize:          pool.MinSize,
			maxSize:          pool.MaxSize,
			systemVolumeSize: pool.SystemVolumeSize,
			component:        consts.LabelValComponentO11y,
			taints: []eksv1beta1.TaintParameters{{
				Key:    ptr.To(consts.TaintKeyDedicated),
				Value:  ptr.To(consts.TaintValueO11y),
				Effect: ptr.To(consts.TaintEffectNoSchedule),
			}},
			amiTypeHint:     pool.AMIType,
			targetVersion:   targetVersion,
			securityGroupID: clusterSecurityGroupID,
			metaLabels:      labels,
			extraNodeLabels: map[string]string{
				consts.LabelKeyNodeSpec: nodepool.NodeSpecLabel(uint32(pool.CPU), uint32(pool.MemoryGi)),
			},
		}); err != nil {
			return deploy.Error(fmt.Errorf("failed to create o11y nodegroup for instance type %s: %w", pool.InstanceType, err))
		}
	}

	if err := t.cleanupStaleAdminNodegroups(ctx, desired); err != nil {
		return deploy.Error(fmt.Errorf("failed to clean up stale admin/o11y nodegroups: %w", err))
	}

	return deploy.Done()
}

func (t *Nodegroup) clusterSecurityGroupID(ctx context.Context) (string, error) {
	cluster := &eksv1beta1.Cluster{}
	if err := t.Local.Get(ctx, types.NamespacedName{Name: t.Cluster.GetName()}, cluster); err != nil {
		return "", fmt.Errorf("get eks cluster %s: %w", t.Cluster.GetName(), err)
	}
	if len(cluster.Status.AtProvider.VPCConfig) == 0 {
		return "", fmt.Errorf("eks cluster %s vpc config is empty", t.Cluster.GetName())
	}
	clusterSecurityGroupID := ptr.Deref(cluster.Status.AtProvider.VPCConfig[0].ClusterSecurityGroupID, "")
	if clusterSecurityGroupID == "" {
		return "", fmt.Errorf("eks cluster %s cluster security group id is empty", t.Cluster.GetName())
	}
	return clusterSecurityGroupID, nil
}

// cleanupStaleAdminNodegroups deletes APPBox-owned nodegroups (and their LaunchTemplates) that are NOT
// in the desired set (admin primary/additional + o11y when enabled).
// Both resource types are found independently by owner label selector.
func (t *Nodegroup) cleanupStaleAdminNodegroups(ctx context.Context, desired map[string]struct{}) error {
	patches := []k8s.ObjectPatch{
		k8s.RemoveAnnotationPatch(meta.AnnotationKeyExternalCreatePending),
		k8s.SetAnnotationPatch(consts.AnnotationKeyDeletionTrigger, time.Now().Format(time.RFC3339)),
	}
	ownerLabels := client.MatchingLabels(labelutil.NewOwnerLabels(t.APPBox))

	nodegroups := &eksv1beta1.NodeGroupList{}
	if err := t.Local.List(ctx, nodegroups, ownerLabels); err != nil {
		return err
	}
	for i := range nodegroups.Items {
		ng := &nodegroups.Items[i]
		if desired != nil {
			if _, ok := desired[ng.Name]; ok {
				continue
			}
		}
		if err := k8s.PatchObject(ctx, t.Local, ng, patches...); client.IgnoreNotFound(err) != nil {
			return err
		}
		if err := controllers.DeleteUpboundResource(ctx, t.Local, ng); err != nil {
			return err
		}
	}

	launchTemplates := &ec2v1beta1.LaunchTemplateList{}
	if err := t.Local.List(ctx, launchTemplates, ownerLabels); err != nil {
		return err
	}
	for i := range launchTemplates.Items {
		lt := &launchTemplates.Items[i]
		if desired != nil {
			if _, ok := desired[lt.Name]; ok {
				continue
			}
		}
		if err := k8s.PatchObject(ctx, t.Local, lt, patches...); client.IgnoreNotFound(err) != nil {
			return err
		}
		if err := controllers.DeleteUpboundResource(ctx, t.Local, lt); err != nil {
			return err
		}
	}
	return nil
}

// nodegroupCreateParams carries per-pool settings for admin and o11y nodegroups.
type nodegroupCreateParams struct {
	name             string
	instanceType     string
	minSize          int
	maxSize          int
	systemVolumeSize int64
	component        string
	taints           []eksv1beta1.TaintParameters
	amiTypeHint      string
	targetVersion    string
	securityGroupID  string
	metaLabels       map[string]string
	// extraNodeLabels are merged into the EKS NodeGroup ForProvider.Labels
	// (k8s node labels) after component / k8s-version. Admin paths leave this nil.
	extraNodeLabels map[string]string
}

// createNodegroupResources creates a launch template and nodegroup (admin or o11y).
func (t *Nodegroup) createNodegroupResources(ctx context.Context, p nodegroupCreateParams) error {
	// Merge owner labels so all APPBox-owned nodegroup resources can be found by label selector.
	labels := util.MergeMaps(p.metaLabels, labelutil.NewOwnerLabels(t.APPBox))
	// TODO: use the right tags, since it's related to node labels.
	tags := maps.Clone(t.APPBox.Labels)
	// add node group name to master node asg tags
	// cluster-autoscaler will use this tag to find out the node group
	tags["tidbcloud.com/nodegroup-name"] = p.name
	tags = controllers.ConvertToCloudTags(tags, cloudv1.ProviderTypeEKS)
	tags = util.MergeMaps(tags, t.Config.AdditionalCloudTags)
	tags = util.BuildAWSTagWithName(tags, p.name)
	allTags := tags

	ngLaunchTemplate := ec2v1beta1.LaunchTemplate{
		ObjectMeta: metav1.ObjectMeta{
			Name:   p.name,
			Labels: labels,
		},
		Spec: ec2v1beta1.LaunchTemplateSpec{
			ForProvider: ec2v1beta1.LaunchTemplateParameters_2{
				Region: ptr.To(t.APPBox.Spec.Region),
				Name:   aws.String(p.name),
				MetadataOptions: []ec2v1beta1.LaunchTemplateMetadataOptionsParameters{
					{
						HTTPTokens: aws.String(string(ec2types.LaunchTemplateHttpTokensStateRequired)),
					},
				},
				Tags:                util.ToMapPointer(tags),
				VPCSecurityGroupIds: util.ToSlicePointer([]string{p.securityGroupID}),
				TagSpecifications: []ec2v1beta1.TagSpecificationsParameters{
					{
						ResourceType: aws.String(string(ec2types.ResourceTypeVolume)),
						Tags:         util.ToMapPointer(allTags),
					},
					{
						ResourceType: aws.String(string(ec2types.ResourceTypeInstance)),
						Tags:         util.ToMapPointer(allTags),
					},
					{
						ResourceType: aws.String(string(ec2types.ResourceTypeNetworkInterface)),
						Tags:         util.ToMapPointer(allTags),
					},
				},
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
		},
	}

	if bottlerocketEnabled(p.amiTypeHint, p.systemVolumeSize) {
		ngLaunchTemplate.Spec.ForProvider.BlockDeviceMappings = getBlockDeviceMappingsForBottlerocket(p.systemVolumeSize)
	}

	if err := k8s.Apply(ctx, t.Local, &ngLaunchTemplate); err != nil {
		return fmt.Errorf("failed apply launch template: %v", err)
	}

	ng := eksv1beta1.NodeGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name:   p.name,
			Labels: labels,
		},
		Spec: eksv1beta1.NodeGroupSpec{
			InitProvider: eksv1beta1.NodeGroupInitParameters{
				ScalingConfig: []eksv1beta1.ScalingConfigInitParameters{
					{
						DesiredSize: proto.Float64(float64(p.minSize)),
					},
				},
				LaunchTemplate: []eksv1beta1.LaunchTemplateInitParameters{
					{
						Version: ptr.To("$Latest"),
					},
				},
			},
			ForProvider: eksv1beta1.NodeGroupParameters{
				ClusterName:   ptr.To(t.Cluster.GetName()),
				Region:        ptr.To(t.APPBox.Spec.Region),
				InstanceTypes: util.ToSlicePointer([]string{p.instanceType}),
				ScalingConfig: []eksv1beta1.ScalingConfigParameters{
					{
						MaxSize: proto.Float64(float64(p.maxSize)),
						MinSize: proto.Float64(float64(p.minSize)),
					},
				},
				NodeRoleArn: ptr.To(t.APPBox.Status.IAM.NodeRoleARN),
				SubnetIds:   util.ToSlicePointer(t.nodeSubnetIDs()),
				Tags:        util.ToMapPointer(tags),
				// It will be set to the k8s node label
				// https://doc.crds.dev/github.com/crossplane/provider-aws/eks.aws.crossplane.io/NodeGroup/v1alpha1@v0.18.1#spec-forProvider-tags
				Labels: buildNodeGroupLabels(p.component, p.targetVersion, p.extraNodeLabels),
				Taint:  p.taints,
				LaunchTemplate: []eksv1beta1.LaunchTemplateParameters{
					{
						Name: ptr.To(p.name),
					},
				},
				CapacityType: ptr.To("ON_DEMAND"),     // Admin or o11y nodes always use ON_DEMAND capacity
				Version:      ptr.To(p.targetVersion), // set the version explicitly for later upgrade check
				// Do we need to support upgrade nodes in parallel (with the `updateConfig` field)?
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
		},
	}

	if err := t.setAMITypeByName(ctx, &ng, p.name, p.instanceType); err != nil {
		return err
	}

	return k8s.Apply(ctx, t.Local, &ng)
}

func (t *Nodegroup) nodeSubnetIDs() []string {
	subnets := []string{}
	for _, sn := range t.VPC.Status.NodeSubnets {
		subnets = append(subnets, sn.SubnetID)
	}
	return subnets
}

func (t *Nodegroup) setAMITypeByName(ctx context.Context, ng *eksv1beta1.NodeGroup, name, instanceType string) error {
	key := types.NamespacedName{
		Name: name,
	}
	var existNG eksv1beta1.NodeGroup
	if err := t.Local.Get(ctx, key, &existNG); err != nil {
		if apierrors.IsNotFound(err) {
			amiType, err := t.resolveAMIType(ctx, instanceType)
			if err != nil {
				return fmt.Errorf("failed to resolve AMI type for instance type %s: %w", instanceType, err)
			}
			ng.Spec.ForProvider.AMIType = util.Pointer(amiType)
			return nil
		}
		return err
	}

	// if ng is present, use the current AMIType to avoid node reboot.
	ng.Spec.ForProvider.AMIType = existNG.Spec.ForProvider.AMIType
	return nil
}

func (t *Nodegroup) resolveAMIType(ctx context.Context, instanceType string) (string, error) {
	cli := t.awscli.EC2(&pkgaws.AssumedRole{RoleARN: t.RoleARN()},
		pkgaws.WithRegion(t.APPBox.Spec.Region),
		pkgaws.WithRetry(3),
	)
	return t.AWSAMITypeGetter.GetAMIType(ctx, cli, instanceType)
}

func (t *Nodegroup) nodegroupName() string {
	return controllers.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithPrefix(t.Config.ResourceNamePrefix))
}

func (t *Nodegroup) additionalNodegroupName(index int) string {
	prefix := fmt.Sprintf("%s-alt%s", t.Config.ResourceNamePrefix, strconv.Itoa(index))
	return controllers.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithPrefix(prefix))
}

// o11yNodegroupName returns tidbcloud-o11y-{instanceType}-...
// (ResourceNamePrefix + "-o11y-" + instanceType with '.' replaced by '-').
func (t *Nodegroup) o11yNodegroupName(instanceType string) string {
	instanceTypePart := strings.ReplaceAll(instanceType, ".", "-")
	prefix := fmt.Sprintf("%s-o11y-%s", t.Config.ResourceNamePrefix, instanceTypePart)
	return controllers.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithPrefix(prefix))
}

// buildNodeGroupLabels builds EKS NodeGroup ForProvider.Labels (k8s node labels).
func buildNodeGroupLabels(component, targetVersion string, extra map[string]string) map[string]*string {
	labels := map[string]*string{
		consts.LabelKeyComponent: ptr.To(component),
		// NOTE: after upgraded this field, existing node will also be upgraded to this new label
		consts.K8sVersionLabelKey: ptr.To(targetVersion),
	}
	for k, v := range extra {
		labels[k] = ptr.To(v)
	}
	return labels
}

func (t *Nodegroup) PostCreate(ctx context.Context) deploy.Result {
	cli, err := t.Remote.GetClient(ctx)
	if err != nil {
		return deploy.Panic(fmt.Errorf("failed get remote client: %v", err))
	}

	if err := t.waitNodegroupReady(ctx, cli, t.nodegroupName(), "AdminNodeGroupNotReady"); err != nil {
		return deploy.Panic(err)
	}

	o11yPools, err := config.GetO11yNodePools(
		t.Config.APPBoxConfig.O11yNodeConfig,
		0, DefaultNodeMaxSize, DefaultSystemVolumeSize,
	)
	if err != nil {
		return deploy.Panic(err)
	}
	for _, pool := range o11yPools {
		name := t.o11yNodegroupName(pool.InstanceType)
		if err := t.waitNodegroupReady(ctx, cli, name, "O11yNodeGroupNotReady"); err != nil {
			return deploy.Panic(err)
		}
	}

	t.APPBox.Status.Conditions.SetCondition(metav1.Condition{
		Type:               v1alpha1.APPBoxConditionNodeGroupReady,
		Status:             metav1.ConditionTrue,
		Reason:             "NodeGroupReady",
		Message:            "NodeGroup admin/o11y creation succeeded",
		LastTransitionTime: metav1.Now(),
	})
	return deploy.Done()
}

// waitNodegroupReady requires the named nodegroup to be ACTIVE and version-ready.
// On non-ACTIVE: runs Creating / CREATE_FAILED autoops, sets NodeGroupReady=false
// with notReadyReason, returns error. On version mismatch: Reason=VersionUpgrading.
func (t *Nodegroup) waitNodegroupReady(ctx context.Context, cli client.Client, name, notReadyReason string) error {
	var g eksv1beta1.NodeGroup
	key := types.NamespacedName{Name: name}
	if err := t.Local.Get(ctx, key, &g); err != nil {
		return err
	}
	status := g.Status.AtProvider.Status
	if status == nil {
		return fmt.Errorf("node group %s status is nil", name)
	}
	if *status != NodeGroupStatusActive {
		switch *status {
		case NodeGroupStatusCreating:
			if err := autoops.RemoveNodeGroupPendingAnnotationIfReady(ctx, t.Logger, t.Local, cli, &g); err != nil {
				return err
			}
		case NodeGroupStatusCreateFailed:
			if err := autoops.DeleteNodeGroupsIfIAMThrottled(ctx, t.Logger, t.Local, &g); err != nil {
				return err
			}
		}
		t.APPBox.Status.Conditions.SetCondition(metav1.Condition{
			Type:               v1alpha1.APPBoxConditionNodeGroupReady,
			Status:             metav1.ConditionFalse,
			Reason:             notReadyReason,
			Message:            fmt.Sprintf("NodeGroup has the state %s", *status),
			LastTransitionTime: metav1.Now(),
		})
		return fmt.Errorf("non-active nodegroup %s has the state %s", g.Name, *status)
	}

	if err := CheckNodeGroupVersionReady(&g, t.APPBox.Spec.KubernetesConfig.Version); err != nil {
		t.APPBox.Status.Conditions.SetCondition(metav1.Condition{
			Type:               v1alpha1.APPBoxConditionNodeGroupReady,
			Status:             metav1.ConditionFalse,
			Reason:             "VersionUpgrading",
			Message:            fmt.Sprintf("NodeGroup version is upgrading to %s", t.APPBox.Spec.KubernetesConfig.Version),
			LastTransitionTime: metav1.Now(),
		})
		return err
	}
	return nil
}

func (t *Nodegroup) Delete(ctx context.Context) deploy.Result {
	// Delete control plane
	if err := t.deleteAdminNodeResources(ctx); err != nil {
		return deploy.Panic(err)
	}
	// There might be node ENI leaks that happens occasionally
	// The leaked ENI also has securitygroup which is managed by EKS
	// If the EKS is deleted before the leaked ENI
	// then both the ENI and securitygroup will be leaked
	// And this would block VPC deletion
	// Delete ENIs that are available and belong to this VPC

	cli := t.awscli.EC2(&pkgaws.AssumedRole{RoleARN: t.RoleARN()})
	// list ENIs using ec2 DescribeNetworkInterfaces API
	req := ec2.DescribeNetworkInterfacesInput{
		Filters: []ec2types.Filter{
			{
				Name:   ptr.To("vpc-id"),
				Values: []string{t.VPC.Status.VPC.VPCID},
			},
			{
				Name:   ptr.To("status"),
				Values: []string{"available"},
			},
		},
	}
	paginator := ec2.NewDescribeNetworkInterfacesPaginator(cli, &req)
	for paginator.HasMorePages() {
		page, err := paginator.NextPage(ctx)
		if err != nil {
			return deploy.Panic(fmt.Errorf("failed to describe network interfaces: %v", err))
		}
		for _, eni := range page.NetworkInterfaces {
			t.Logger.Info("Deleting leaked ENI", "eni-id", *eni.NetworkInterfaceId)
			_, err := cli.DeleteNetworkInterface(ctx, &ec2.DeleteNetworkInterfaceInput{
				NetworkInterfaceId: eni.NetworkInterfaceId,
			})
			if err != nil {
				return deploy.Panic(fmt.Errorf("failed to delete leaked ENI %s: %v", *eni.NetworkInterfaceId, err))
			}
		}
	}

	return deploy.Done()
}

func (t *Nodegroup) PostDelete(ctx context.Context) deploy.Result {
	// Check all admin nodegroups and launch templates are gone by label selector.
	ownerLabels := client.MatchingLabels(labelutil.NewOwnerLabels(t.APPBox))
	nodegroups := &eksv1beta1.NodeGroupList{}
	if err := t.Local.List(ctx, nodegroups, ownerLabels); err != nil {
		return deploy.PanicOrDone(err)
	}
	if len(nodegroups.Items) > 0 {
		return deploy.PanicOrDone(fmt.Errorf("admin nodegroups still exist: %d remaining", len(nodegroups.Items)))
	}
	launchTemplates := &ec2v1beta1.LaunchTemplateList{}
	if err := t.Local.List(ctx, launchTemplates, ownerLabels); err != nil {
		return deploy.PanicOrDone(err)
	}
	if len(launchTemplates.Items) > 0 {
		return deploy.PanicOrDone(fmt.Errorf("admin launch templates still exist: %d remaining", len(launchTemplates.Items)))
	}
	return deploy.Done()
}

func (t *Nodegroup) deleteAdminNodeResources(ctx context.Context) error {
	// Delete all admin nodegroups by label selector (not by names derived from config).
	// This handles the case where AdditionalInstanceTypes was changed before deletion.
	return t.cleanupStaleAdminNodegroups(ctx, nil)
}

func bottlerocketEnabled(amiTypeHint string, systemVolumeSize int64) bool {
	return strings.Contains(strings.ToLower(amiTypeHint), BottleRocket) && systemVolumeSize > 0
}

func getBlockDeviceMappingsForBottlerocket(systemVolumeSize int64) []ec2v1beta1.BlockDeviceMappingsParameters {
	return []ec2v1beta1.BlockDeviceMappingsParameters{
		{
			DeviceName: pointer.String("/dev/xvda"),
			EBS: []ec2v1beta1.EBSParameters{
				{
					VolumeSize: pointer.Float64(20),
					VolumeType: pointer.String("gp3"),
					Iops:       pointer.Float64(3000),
					Throughput: pointer.Float64(125),
				},
			},
		},
		{
			DeviceName: pointer.String("/dev/xvdb"),
			EBS: []ec2v1beta1.EBSParameters{
				{
					VolumeSize: pointer.Float64(float64(systemVolumeSize)),
					VolumeType: pointer.String("gp3"),
					Iops:       pointer.Float64(3000),
					Throughput: pointer.Float64(125),
				},
			},
		},
	}
}

// CheckNodeGroupVersionReady checks if the node group version is ready.
// It returns an error if the node group is currently upgrading (Spec.Version != Status.Version)
// NOTE: this function does not check if the node group is active.
func CheckNodeGroupVersionReady(ng *eksv1beta1.NodeGroup, targetVersion string) error {
	if ng.Spec.ForProvider.Version != nil && ng.Status.AtProvider.Version != nil &&
		*ng.Status.AtProvider.Version != *ng.Spec.ForProvider.Version {
		return fmt.Errorf("node group %s is upgrading from version %s to %s",
			ng.Name, *ng.Status.AtProvider.Version, *ng.Spec.ForProvider.Version)
	}
	if ng.Status.AtProvider.Version != nil && *ng.Status.AtProvider.Version != targetVersion {
		return fmt.Errorf("node group %s version %s is not ready for target version %s",
			ng.Name, *ng.Status.AtProvider.Version, targetVersion)
	}
	return nil
}
