package eks

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

	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	ec2v1beta1 "github.com/upbound/provider-aws/apis/ec2/v1beta1"
	eksv1beta1 "github.com/upbound/provider-aws/apis/eks/v1beta1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	cloudaws "github.com/tidbcloud/infra-provider/regional-server/pkg/cloud/aws"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/constants"
	"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/nodepool/task"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/k8s"
	"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"
)

const (
	defaultNodeMinSize = 0
	defaultNodeMaxSize = 100
)

type nodeGroup struct {
	*task.BaseTask
}

func NewNodeGroup(ctx *task.Context) deploy.Task {
	return &nodeGroup{BaseTask: &task.BaseTask{Context: ctx}}
}

func (t *nodeGroup) Name() string {
	return "NodeGroup"
}

func (t *nodeGroup) Create(ctx context.Context) deploy.Result {
	t.BeginAt = ptr.To(time.Now())

	if t.APPBox.Status.IAM.NodeRoleARN == "" {
		return deploy.Panic(fmt.Errorf("node role is not ready"))
	}

	if t.APPBox.Status.KubernetesClusterName == "" {
		return deploy.Panic(fmt.Errorf("kubernetes cluster name is empty"))
	}

	// do not try to create nodegroups with the target version if the EKS is not upgraded to the target version
	if err := CheckEKSVersionReady(ctx, t.Local, t.APPBox.Status.KubernetesClusterName, t.APPBox.Spec.KubernetesConfig.Version); err != nil {
		return deploy.Error(err)
	}

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

	specLabels := map[string]*string{
		consts.LabelKeyNodeSpec:   ptr.To(nodepool.NodeSpecLabel(t.NodePool.Spec.CpuCore, t.NodePool.Spec.MemoryGi)),
		consts.K8sVersionLabelKey: ptr.To(t.NodePool.Spec.KubernetesVersion),
	}
	for k, v := range t.NodePool.Spec.Labels {
		val := v
		specLabels[k] = &val
	}

	taints := make([]eksv1beta1.TaintParameters, 0, len(t.NodePool.Spec.Taints)+2)
	userTaints := task.EnsureNodeTypeLabel(t.NodePool.Spec.Taints, task.NormalizeNodeType(t.NodePool.Spec.NodeType))
	if t.NodePool.Spec.WithLocalDisk {
		userTaints = task.EnsureLocalDiskTaint(userTaints)
	}
	for _, taint := range userTaints {
		effect := task.NormalizeTaintEffect(taint.Effect)
		crossplaneEffect := util.ToCrossplaneTaintEffect(corev1.TaintEffect(effect))
		if crossplaneEffect == "" {
			crossplaneEffect = consts.TaintEffectNoSchedule
		}
		taints = append(taints, eksv1beta1.TaintParameters{
			Effect: ptr.To(crossplaneEffect),
			Key:    ptr.To(taint.Key),
			Value:  ptr.To(taint.Value),
		})
	}

	if t.NodePool.Spec.WithLocalDisk {
		specLabels[consts.LabelKeyFastDiskNode] = ptr.To("pv-raid")
		specLabels[consts.LabelKeyHasLocalDisk] = ptr.To("true")
		specLabels[consts.LabelKeyLocalDiskSize] = ptr.To(fmt.Sprint(t.NodePool.Spec.LocalDiskSizeGi))
		metaLabels[consts.LabelKeyHasLocalDisk] = "true"
		metaLabels[consts.LabelKeyLocalDiskSize] = fmt.Sprint(t.NodePool.Spec.LocalDiskSizeGi)
	}

	zoneToSubnetID, zones, err := t.zoneSubnetIDs()
	if err != nil {
		return deploy.Error(err)
	}

	nodeRole := t.APPBox.Status.IAM.NodeRoleARN

	userdata, err := t.Config.LoadNodeUserData()
	if err != nil {
		return deploy.Error(err)
	}
	localDiskUserdata, err := t.Config.LoadLocalDiskNodeUserData()
	if err != nil {
		return deploy.Error(err)
	}
	selectedUserdata := userdata
	if t.NodePool.Spec.WithLocalDisk {
		selectedUserdata = localDiskUserdata
	}

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

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

	statusZones := zones
	if placementGroupEnabled(t.NodePool) {
		zone, subnetID, err := t.singleZoneSubnetID()
		if err != nil {
			return deploy.Error(err)
		}
		statusZones = []string{zone}
		for partitionIdx := 0; partitionIdx < placementGroupPartitionCount; partitionIdx++ {
			name := t.perPartitionNodegroupName(zone, partitionIdx, t.NodePool.Spec.InstanceType, t.NodePool.Spec.KubernetesVersion, t.NodePool.Spec.CpuCore, t.NodePool.Spec.MemoryGi)
			if err := t.createNodeGroupResources(ctx, name, subnetID, clusterSecurityGroupID, metaLabels, specLabels, taints, nodeRole, selectedUserdata, amiType, &partitionIdx); err != nil {
				return deploy.Error(err)
			}
		}
	} else {
		for _, zone := range zones {
			subnetID, ok := zoneToSubnetID[zone]
			if !ok || subnetID == "" {
				return deploy.Error(fmt.Errorf("missing subnet for zone %s", zone))
			}

			name := t.perZonePerInstanceTypeNodegroupName(zone, t.NodePool.Spec.InstanceType, t.NodePool.Spec.KubernetesVersion, t.NodePool.Spec.CpuCore, t.NodePool.Spec.MemoryGi)
			if err := t.createNodeGroupResources(ctx, name, subnetID, clusterSecurityGroupID, metaLabels, specLabels, taints, nodeRole, selectedUserdata, amiType, nil); err != nil {
				return deploy.Error(err)
			}
		}
	}

	t.NodePool.Status.KubernetesName = t.APPBox.Status.KubernetesClusterName
	t.NodePool.Status.NodeRole = nodeRole
	t.NodePool.Status.Zones = statusZones

	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.APPBox.Status.KubernetesClusterName}, cluster); err != nil {
		return "", fmt.Errorf("get eks cluster %s: %w", t.APPBox.Status.KubernetesClusterName, err)
	}
	if len(cluster.Status.AtProvider.VPCConfig) == 0 {
		return "", fmt.Errorf("eks cluster %s vpc config is empty", t.APPBox.Status.KubernetesClusterName)
	}
	clusterSecurityGroupID := ptr.Deref(cluster.Status.AtProvider.VPCConfig[0].ClusterSecurityGroupID, "")
	if clusterSecurityGroupID == "" {
		return "", fmt.Errorf("eks cluster %s cluster security group id is empty", t.APPBox.Status.KubernetesClusterName)
	}
	return clusterSecurityGroupID, nil
}

func (t *nodeGroup) PostCreate(ctx context.Context) deploy.Result {
	nodegroups := new(eksv1beta1.NodeGroupList)
	selector := client.MatchingLabels(labels.NewOwnerLabels(t.NodePool))
	if err := t.Local.List(ctx, nodegroups, selector); err != nil {
		return deploy.Error(err)
	}

	var totalReplicas uint32
	for _, ng := range nodegroups.Items {
		condition := ng.Status.GetCondition(xpv1.TypeReady)
		if condition.Status != corev1.ConditionTrue {
			t.NodePool.Status.Conditions = []metav1.Condition{task.ToMetaCondition(condition)}
			return deploy.Error(fmt.Errorf("waiting for nodegroup %s to be ready", ng.GetName())).WithRequeuAfter(deploy.DefaultRequeueAfter)
		}
		if len(ng.Status.AtProvider.ScalingConfig) > 0 && ng.Status.AtProvider.ScalingConfig[0].DesiredSize != nil {
			totalReplicas += uint32(ptr.Deref(ng.Status.AtProvider.ScalingConfig[0].DesiredSize, 0))
		}
		t.NodePool.Status.Conditions = []metav1.Condition{task.ToMetaCondition(condition)}
	}
	t.NodePool.Status.CurrentReplicas = totalReplicas
	return deploy.Done()
}

func (t *nodeGroup) Delete(ctx context.Context) deploy.Result {
	patches := []k8s.ObjectPatch{
		k8s.RemoveAnnotationPatch(meta.AnnotationKeyExternalCreatePending),
		k8s.SetAnnotationPatch(consts.AnnotationKeyDeletionTrigger, time.Now().Format(time.RFC3339)),
	}

	nodegroups := new(eksv1beta1.NodeGroupList)
	selector := client.MatchingLabels(labels.NewOwnerLabels(t.NodePool))
	if err := t.Local.List(ctx, nodegroups, selector); err != nil {
		return deploy.Error(err)
	}
	for i := range nodegroups.Items {
		ng := &nodegroups.Items[i]
		if err := k8s.PatchObject(ctx, t.Local, ng, patches...); err != nil {
			return deploy.Panic(err)
		}
		if err := controllers.DeleteUpboundResource(ctx, t.Local, ng); err != nil {
			return deploy.Panic(err)
		}
	}

	launchTemplates := new(ec2v1beta1.LaunchTemplateList)
	if err := t.Local.List(ctx, launchTemplates, selector); err != nil {
		return deploy.Error(err)
	}
	for i := range launchTemplates.Items {
		lt := &launchTemplates.Items[i]
		if err := k8s.PatchObject(ctx, t.Local, lt, patches...); err != nil {
			return deploy.Panic(err)
		}
		if err := controllers.DeleteUpboundResource(ctx, t.Local, lt); err != nil {
			return deploy.Panic(err)
		}
	}
	return deploy.Done()
}

func (t *nodeGroup) PostDelete(ctx context.Context) deploy.Result {
	nodegroups := new(eksv1beta1.NodeGroupList)
	selector := client.MatchingLabels(labels.NewOwnerLabels(t.NodePool))
	if err := t.Local.List(ctx, nodegroups, selector); err != nil {
		return deploy.Panic(err)
	}
	if len(nodegroups.Items) > 0 {
		return deploy.Panic(fmt.Errorf("nodegroups not deleted: %d remaining", len(nodegroups.Items)))
	}

	launchTemplates := new(ec2v1beta1.LaunchTemplateList)
	if err := t.Local.List(ctx, launchTemplates, selector); err != nil {
		return deploy.Panic(err)
	}
	if len(launchTemplates.Items) > 0 {
		return deploy.Panic(fmt.Errorf("launch templates not deleted: %d remaining", len(launchTemplates.Items)))
	}
	return deploy.Done()
}

func (t *nodeGroup) createNodeGroupResources(
	ctx context.Context,
	name string,
	subnetID string,
	clusterSecurityGroupID string,
	metaLabels map[string]string,
	specLabels map[string]*string,
	taints []eksv1beta1.TaintParameters,
	nodeRole string,
	selectedUserdata string,
	amiType string,
	partitionIdx *int,
) error {
	commonTags := maps.Clone(t.NodePool.Labels)
	commonTags["tidbcloud.com/nodegroup-name"] = name
	tags := controllers.ConvertToCloudTags(commonTags, consts.ProviderTypeEKS)
	tags = util.MergeMaps(tags, t.Config.AdditionalCloudTags)
	tags = util.BuildAWSTagWithName(tags, name)

	ngLaunchTemplate := &ec2v1beta1.LaunchTemplate{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: metaLabels,
		},
		Spec: ec2v1beta1.LaunchTemplateSpec{
			ForProvider: ec2v1beta1.LaunchTemplateParameters_2{
				Region: ptr.To(t.APPBox.Spec.Region),
				Name:   ptr.To(name),
				MetadataOptions: []ec2v1beta1.LaunchTemplateMetadataOptionsParameters{
					{
						HTTPTokens: ptr.To(string(ec2types.LaunchTemplateHttpTokensStateRequired)),
					},
				},
				BlockDeviceMappings: getBlockDeviceMappingsForBottlerocket(t.Config.APPBoxConfig.WorkerNodeConfig.GetSystemVolumeSize()),
				Tags:                util.ToMapPointer(tags),
				VPCSecurityGroupIds: util.ToSlicePointer([]string{clusterSecurityGroupID}),
				TagSpecifications: []ec2v1beta1.TagSpecificationsParameters{
					{
						ResourceType: ptr.To(string(ec2types.ResourceTypeVolume)),
						Tags:         util.ToMapPointer(tags),
					},
					{
						ResourceType: ptr.To(string(ec2types.ResourceTypeInstance)),
						Tags:         util.ToMapPointer(tags),
					},
					{
						ResourceType: ptr.To(string(ec2types.ResourceTypeNetworkInterface)),
						Tags:         util.ToMapPointer(tags),
					},
				},
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
		},
	}
	if selectedUserdata != "" {
		ngLaunchTemplate.Spec.ForProvider.UserData = ptr.To(selectedUserdata)
	}
	nodeLabels := maps.Clone(specLabels)
	if partitionIdx != nil {
		partition := strconv.Itoa(*partitionIdx)
		nodeLabels["tidbcloud.com/pg-partition"] = ptr.To(partition)
		ngLaunchTemplate.Spec.ForProvider.Placement = []ec2v1beta1.PlacementParameters{
			{
				GroupName: ptr.To(namer.PlacementGroupName(t.NodePool.Spec.InstanceType, appBoxPlacementGroupIdentity(t.APPBox))),
				// AWS placement group partition numbers are 1-based, while the Kubernetes topology label remains 0-based.
				PartitionNumber: ptr.To(float64(*partitionIdx + 1)),
			},
		}
	}
	if err := k8s.Apply(ctx, t.Local, ngLaunchTemplate); err != nil {
		return err
	}

	ng := &eksv1beta1.NodeGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: metaLabels,
		},
		Spec: eksv1beta1.NodeGroupSpec{
			ForProvider: eksv1beta1.NodeGroupParameters{
				ClusterName:   ptr.To(t.APPBox.Status.KubernetesClusterName),
				Region:        ptr.To(t.APPBox.Spec.Region),
				InstanceTypes: []*string{ptr.To(t.NodePool.Spec.InstanceType)},
				ScalingConfig: []eksv1beta1.ScalingConfigParameters{
					{
						MaxSize: ptr.To(float64(defaultNodeMaxSize)),
						MinSize: ptr.To(float64(defaultNodeMinSize)),
					},
				},
				AMIType:      ptr.To(amiType),
				NodeRoleArn:  ptr.To(nodeRole),
				SubnetIds:    util.ToSlicePointer([]string{subnetID}),
				Tags:         util.ToMapPointer(tags),
				Labels:       nodeLabels,
				Taint:        taints,
				CapacityType: ptr.To("ON_DEMAND"),
				Version:      ptr.To(t.NodePool.Spec.KubernetesVersion),
				LaunchTemplate: []eksv1beta1.LaunchTemplateParameters{
					{
						Name: ptr.To(name),
					},
				},
			},
			InitProvider: eksv1beta1.NodeGroupInitParameters{
				ScalingConfig: []eksv1beta1.ScalingConfigInitParameters{
					{
						DesiredSize: ptr.To(float64(0)),
					},
				},
				LaunchTemplate: []eksv1beta1.LaunchTemplateInitParameters{
					{
						Version: ptr.To("$Latest"),
					},
				},
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
		},
	}

	ng.Spec.ForProvider.Tags[constants.TagNodeGroupSpecCPU] = ptr.To(strconv.Itoa(int(t.NodePool.Spec.CpuCore)))
	ng.Spec.ForProvider.Tags[constants.TagNodeGroupSpecMemoryGi] = ptr.To(strconv.Itoa(int(t.NodePool.Spec.MemoryGi)))
	availableCPU := int(t.NodePool.Spec.AvailableCpuMilliCore / 1000)
	if availableCPU <= 0 {
		availableCPU = int(t.NodePool.Spec.CpuCore)
	}
	ng.Spec.ForProvider.Tags[constants.TagNodeGroupAvailableCPU] = ptr.To(strconv.Itoa(availableCPU))
	availableMemoryMi := uint64(t.NodePool.Spec.AvailableMemoryMi)
	if availableMemoryMi == 0 {
		availableMemoryMi = nodepool.ComputeAvailableMemory(uint64(t.NodePool.Spec.MemoryGi) * 1024)
	}
	ng.Spec.ForProvider.Tags[constants.TagNodeGroupAvailableMemoryMi] = ptr.To(strconv.FormatUint(availableMemoryMi, 10))

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

func (t *nodeGroup) perZonePerInstanceTypeNodegroupName(zone, instanceType, k8sVersion string, cpu, memory uint32) string {
	namespacedName := t.APPBox.Namespace + t.APPBox.Name
	resourceNamePrefix := t.Config.ResourceNamePrefix
	if t.NodePool.Spec.ResourceNamePrefix != "" {
		resourceNamePrefix = t.NodePool.Spec.ResourceNamePrefix
	}
	prefix := resourceNamePrefix + "-" + strings.ReplaceAll(instanceType, ".", "-")
	if t.NodePool.Annotations[consts.AnnotationKeyBuiltinNodePool] != "true" {
		prefix = prefix + "-" + fmt.Sprintf("%dc%dg", cpu, memory)
	}
	suffix := strings.ReplaceAll(zone, ",", "-") + "-" + strings.ReplaceAll(k8sVersion, ".", "-")
	return namer.NewName(namespacedName, prefix, suffix, -1)
}

func (t *nodeGroup) perPartitionNodegroupName(zone string, partitionIdx int, instanceType, k8sVersion string, cpu, memory uint32) string {
	namespacedName := t.APPBox.Namespace + t.APPBox.Name
	resourceNamePrefix := t.Config.ResourceNamePrefix
	if t.NodePool.Spec.ResourceNamePrefix != "" {
		resourceNamePrefix = t.NodePool.Spec.ResourceNamePrefix
	}
	prefix := resourceNamePrefix + "-" + strings.ReplaceAll(instanceType, ".", "-")
	if t.NodePool.Annotations[consts.AnnotationKeyBuiltinNodePool] != "true" {
		prefix = prefix + "-" + fmt.Sprintf("%dc%dg", cpu, memory)
	}
	suffix := fmt.Sprintf("%s-p%d-%s", strings.ReplaceAll(zone, ",", "-"), partitionIdx, strings.ReplaceAll(k8sVersion, ".", "-"))
	return namer.NewName(namespacedName, prefix, suffix, -1)
}

func (t *nodeGroup) zoneSubnetIDs() (map[string]string, []string, error) {
	zoneToSubnetID := map[string]string{}
	for _, sn := range t.VPC.Status.NodeSubnets {
		if sn.AvailabilityZone == "" || sn.SubnetID == "" {
			continue
		}
		zoneToSubnetID[sn.AvailabilityZone] = sn.SubnetID
	}

	zones := t.GetNodeZones()
	if len(zones) == 0 {
		zoneSet := make(map[string]struct{}, len(zoneToSubnetID))
		for zone := range zoneToSubnetID {
			zoneSet[zone] = struct{}{}
		}
		for zone := range zoneSet {
			zones = append(zones, zone)
		}
		sort.Strings(zones)
	}
	if len(zones) == 0 {
		return nil, nil, fmt.Errorf("no node subnets or zones found for nodepool %s", t.NodePool.Name)
	}

	for _, zone := range zones {
		subnetID, ok := zoneToSubnetID[zone]
		if !ok || subnetID == "" {
			return nil, nil, fmt.Errorf("missing subnet for zone %s", zone)
		}
	}

	return zoneToSubnetID, zones, nil
}

func (t *nodeGroup) singleZoneSubnetID() (string, string, error) {
	zoneToSubnetID, zones, err := t.zoneSubnetIDs()
	if err != nil {
		return "", "", err
	}
	if len(zones) != 1 {
		return "", "", fmt.Errorf("placement group enabled nodepool %s requires exactly one node zone, got %d", t.NodePool.Name, len(zones))
	}
	return zones[0], zoneToSubnetID[zones[0]], nil
}

func (t *nodeGroup) resolveAMIType(ctx context.Context) (string, error) {
	cli := t.AWSClient.EC2(&cloudaws.AssumedRole{RoleARN: t.RoleARN()},
		cloudaws.WithRegion(t.APPBox.Spec.Region),
		cloudaws.WithRetry(3),
	)
	return t.AWSAMITypeGetter.GetAMIType(ctx, cli, t.NodePool.Spec.InstanceType)
}

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

// CheckEKSVersionReady checks if the EKS cluster version is ready for the target version.
// It returns an error if:
// 1. The EKS cluster is currently upgrading (Spec.Version != Status.Version)
// 2. The EKS cluster's current version doesn't match the target version (not started upgrading yet)
// NOTE: if the EKS cluster is creating (without Status.Version), it is considered ready for the target version.
func CheckEKSVersionReady(ctx context.Context, cli client.Client, clusterName string, targetVersion string) error {
	var eksCluster eksv1beta1.Cluster
	err := cli.Get(ctx, types.NamespacedName{Name: clusterName}, &eksCluster)
	if err != nil {
		return fmt.Errorf("failed to get EKS cluster: %w", err)
	}

	if eksCluster.Spec.ForProvider.Version == nil || eksCluster.Status.AtProvider.Version == nil ||
		eksCluster.Status.AtProvider.Status == nil || *eksCluster.Status.AtProvider.Status == "CREATING" {
		// the EKS cluster is creating, it is considered ready for the target version.
		// so that we can continue to create the other resources.
		return nil
	}

	// eksCluster.Status.AtProvider.Status: One of CREATING, ACTIVE, DELETING, FAILED
	// NO UPDATING status in the EKS cluster status, so we can only check the version.

	if *eksCluster.Status.AtProvider.Version != *eksCluster.Spec.ForProvider.Version {
		return fmt.Errorf("EKS cluster is upgrading from version %s to %s, waiting for upgrade to complete",
			*eksCluster.Status.AtProvider.Version, *eksCluster.Spec.ForProvider.Version)
	}

	if *eksCluster.Status.AtProvider.Version != targetVersion {
		return fmt.Errorf("EKS cluster version %s is not ready for target version %s, waiting for EKS upgrade",
			*eksCluster.Status.AtProvider.Version, targetVersion)
	}

	return nil
}
