package v1alpha5

import (
	"errors"
	"fmt"
	"net"
	"regexp"
	"slices"
	"strconv"
	"strings"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/aws/arn"
	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
	ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types"

	"github.com/hashicorp/go-version"
	"github.com/kris-nova/logger"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/util/validation"
	kubeletapis "k8s.io/kubelet/pkg/apis"

	"github.com/weaveworks/eksctl/pkg/utils"
	instanceutils "github.com/weaveworks/eksctl/pkg/utils/instance"
	"github.com/weaveworks/eksctl/pkg/utils/taints"
)

// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-blockdevicemapping-ebs.html
const (
	MinThroughput = DefaultNodeVolumeThroughput
	MaxThroughput = 1000
	MinIO1Iops    = DefaultNodeVolumeIO1IOPS
	MaxIO1Iops    = 64000
	MinIO2Iops    = DefaultNodeVolumeIO2IOPS
	MaxIO2Iops    = 256000
	MinGP3Iops    = DefaultNodeVolumeGP3IOPS
	MaxGP3Iops    = 16000
	OneDay        = 86400
)

var (
	// ErrClusterEndpointNoAccess indicates the config prevents API access
	ErrClusterEndpointNoAccess = errors.New("Kubernetes API access must have one of public or private clusterEndpoints enabled")

	// ErrClusterEndpointPrivateOnly warns private-only access requires changes
	// to AWS resource configuration in order to effectively use clients in the VPC
	ErrClusterEndpointPrivateOnly = errors.New("warning, having public access disallowed will subsequently interfere with some " +
		"features of eksctl. This will require running subsequent eksctl (and Kubernetes) " +
		"commands/API calls from within the VPC.  Running these in the VPC requires making " +
		"updates to some AWS resources.  See: " +
		"https://docs.aws.amazon.com/eks/latest/userguide/cluster-endpoint.html " +
		"for more details")

	ErrPodIdentityAgentNotInstalled = func(suggestion string) error {
		return fmt.Errorf("the %q addon must be installed to create pod identity associations; %s", PodIdentityAgentAddon, suggestion)
	}

	ErrUnsupportedInstanceTypes = func(instanceType, amiFamily, suggestion string) error {
		return fmt.Errorf("%s instance types are not supported for %s; %s", instanceType, amiFamily, suggestion)
	}

	GPUDriversWarning = func(amiFamily string) string {
		return fmt.Sprintf("%s does not ship with NVIDIA GPU drivers installed, hence won't support running GPU-accelerated workloads out of the box", amiFamily)
	}

	NeuronDeviceDriversWarning = func(amiFamily string) string {
		return fmt.Sprintf("%s does not ship with Neuron Devices drivers installed, hence won't support running inference-accelerated workloads out of the box", amiFamily)
	}
)

var (
	SupportedAmazonLinuxImages  = supportedAMIFamiliesForOS(IsAmazonLinuxImage)
	SupportedBottlerocketImages = supportedAMIFamiliesForOS(IsBottlerocketImage)
	SupportedUbuntuImages       = supportedAMIFamiliesForOS(IsUbuntuImage)
)

// NOTE: we don't use k8s.io/apimachinery/pkg/util/sets here to keep API package free of dependencies
type nameSet map[string]struct{}

func (s nameSet) checkUnique(path, name string) (bool, error) {
	if _, notUnique := s[name]; notUnique {
		return false, fmt.Errorf("%s %q is not unique", path, name)
	}
	s[name] = struct{}{}
	return true, nil
}

func setNonEmpty(field string) error {
	return fmt.Errorf("%s must be set and non-empty", field)
}

func (c *ClusterConfig) validateRemoteNetworkingConfig() error {
	rnc := c.RemoteNetworkConfig
	if rnc == nil {
		return nil
	}

	if c.IPv6Enabled() {
		return fmt.Errorf("remoteNetworkConfig is not supported on EKS cluster configured with IPv6 address family")
	}

	if c.AccessConfig.AuthenticationMode == ekstypes.AuthenticationModeConfigMap {
		return fmt.Errorf("remoteNetworkConfig requires authenticationMode to be either %q or %q", ekstypes.AuthenticationModeApiAndConfigMap, ekstypes.AuthenticationModeApi)
	}

	if len(rnc.RemoteNodeNetworks) == 0 {
		return setNonEmpty("remoteNetworkConfig.remoteNodeNetworks")
	}

	if rnc.VPCGatewayID.IsSet() {
		if c.VPC.ID != "" {
			return fmt.Errorf("remoteNetworkConfig.vpcGatewayID is not supported when using pre-existing VPC")
		}
		// vpcGatewayId must be either a virtual private gateway or a transit gateway
		if !rnc.VPCGatewayID.IsTransitGateway() && !rnc.VPCGatewayID.IsVirtualPrivateGateway() {
			return fmt.Errorf("invalid value %q provided for remoteNetworkConfig.vpcGatewayID; "+
				"only transit gateway (tgw-*) or virtual private gateway (vgw-*) IDs are supported", *rnc.VPCGatewayID)
		}
	}

	// credentials provider must be either SSM or IAM Roles Anywhere
	if !strings.EqualFold(*rnc.IAM.Provider, SSMProvider) &&
		!strings.EqualFold(*rnc.IAM.Provider, IRAProvider) {
		return fmt.Errorf("invalid value %q provided for remoteNetworkConfig.iam.provider; only %q and %q are supported",
			*rnc.IAM.Provider, SSMProvider, IRAProvider)
	}

	// CABundleCert should only be set of credentials provider is IAM Roles Anywhere
	if strings.EqualFold(*rnc.IAM.Provider, SSMProvider) && rnc.IAM.CABundleCert != nil {
		return fmt.Errorf("remoteNetworkConfig.iam.caBundleCert is not supported when using SSM credentials provider")
	}

	if strings.EqualFold(*rnc.IAM.Provider, IRAProvider) && rnc.IAM.CABundleCert == nil {
		return fmt.Errorf("remoteNetworkConfig.iam.caBundleCert is required when using IAMRolesAnywhere credentials provider")
	}

	if IsSetAndNonEmptyString(rnc.IAM.RoleARN) {
		logger.Warning("remoteNetworkConfig.iam.roleARN is set; eksctl will add a corresponding entry in aws-auth configmap; " +
			"but won't setup an additional SSM or IAMRolesAnywhere required config")
	}

	return nil
}

// ValidateClusterConfig checks compatible fields of a given ClusterConfig
func ValidateClusterConfig(cfg *ClusterConfig) error {
	if IsDisabled(cfg.IAM.WithOIDC) && len(cfg.IAM.ServiceAccounts) > 0 {
		return fmt.Errorf("iam.withOIDC must be enabled explicitly for iam.serviceAccounts to be created")
	}

	saNames := nameSet{}
	for i, sa := range cfg.IAM.ServiceAccounts {
		path := fmt.Sprintf("iam.serviceAccounts[%d]", i)
		if sa.Name == "" {
			return fmt.Errorf("%s.name must be set", path)
		}
		if ok, err := saNames.checkUnique("<namespace>/<name> of "+path, sa.NameString()); !ok {
			return err
		}
		if !sa.WellKnownPolicies.HasPolicy() && len(sa.AttachPolicyARNs) == 0 && sa.AttachPolicy == nil && sa.AttachRoleARN == "" {
			return fmt.Errorf("%[1]s.wellKnownPolicies, %[1]s.attachPolicyARNs,%[1]s.attachRoleARN  or %[1]s.attachPolicy must be set", path)
		}
	}

	if err := cfg.validateKubernetesNetworkConfig(); err != nil {
		return err
	}

	if err := cfg.validateRemoteNetworkingConfig(); err != nil {
		return err
	}

	// names must be unique across both managed and unmanaged nodegroups
	ngNames := nameSet{}
	validateNg := func(ng *NodeGroupBase, path string) error {
		if ng.Name == "" {
			return fmt.Errorf("%s.name must be set", path)
		}
		if _, err := ngNames.checkUnique(path+".name", ng.Name); err != nil {
			return err
		}
		if cfg.PrivateCluster.Enabled && !ng.PrivateNetworking {
			return fmt.Errorf("%s.privateNetworking must be enabled for a fully-private cluster", path)
		}
		return nil
	}

	if err := validateIdentityProviders(cfg.IdentityProviders); err != nil {
		return err
	}

	var ngOutpostARN string
	for i, ng := range cfg.NodeGroups {
		path := fmt.Sprintf("nodeGroups[%d]", i)
		if err := validateNg(ng.NodeGroupBase, path); err != nil {
			return err
		}
		if ng.OutpostARN != "" {
			if ngOutpostARN != "" && ng.OutpostARN != ngOutpostARN {
				return fmt.Errorf("cannot create nodegroups in two different Outposts; got Outpost ARN %q and %q", ngOutpostARN, ng.OutpostARN)
			}
			ngOutpostARN = ng.OutpostARN
		}
	}

	for i, ng := range cfg.ManagedNodeGroups {
		path := fmt.Sprintf("managedNodeGroups[%d]", i)
		if err := validateNg(ng.NodeGroupBase, path); err != nil {
			return err
		}
	}

	if err := validateCloudWatchLogging(cfg); err != nil {
		return err
	}

	if err := validateAvailabilityZones(cfg.AvailabilityZones); err != nil {
		return err
	}

	if cfg.Outpost != nil {
		if cfg.Outpost.ControlPlaneOutpostARN == "" {
			return errors.New("outpost.controlPlaneOutpostARN is required for Outposts")
		}
		if err := validateOutpostARN(cfg.Outpost.ControlPlaneOutpostARN); err != nil {
			return err
		}

		if cfg.AccessConfig.AuthenticationMode != ekstypes.AuthenticationModeConfigMap {
			return fmt.Errorf("accessConfig.AuthenticationMode must be set to %s on Outposts", ekstypes.AuthenticationModeConfigMap)
		}
		if IsDisabled(cfg.AccessConfig.BootstrapClusterCreatorAdminPermissions) {
			return fmt.Errorf("accessConfig.BootstrapClusterCreatorAdminPermissions can't be set to false on Outposts")
		}
		if cfg.IPv6Enabled() {
			return errors.New("IPv6 is not supported on Outposts")
		}
		if len(cfg.Addons) > 0 {
			return errors.New("Addons are not supported on Outposts")
		}
		if len(cfg.IdentityProviders) > 0 {
			return errors.New("Identity Providers are not supported on Outposts")
		}
		if len(cfg.FargateProfiles) > 0 {
			return errors.New("Fargate is not supported on Outposts")
		}
		if cfg.Karpenter != nil {
			return errors.New("Karpenter is not supported on Outposts")
		}
		if cfg.SecretsEncryption != nil && cfg.SecretsEncryption.KeyARN != "" {
			return errors.New("KMS encryption is not supported on Outposts")
		}
		const zonesErr = "cannot specify %s on Outposts; the AZ defaults to the Outpost AZ"
		if len(cfg.AvailabilityZones) > 0 {
			return fmt.Errorf(zonesErr, "availabilityZones")
		}
		if len(cfg.LocalZones) > 0 {
			return fmt.Errorf(zonesErr, "localZones")
		}
		if cfg.GitOps != nil {
			return errors.New("GitOps is not supported on Outposts")
		}
		if cfg.IAM != nil && IsEnabled(cfg.IAM.WithOIDC) {
			return errors.New("iam.withOIDC is not supported on Outposts")
		}
		if cfg.VPC != nil {
			if IsEnabled(cfg.VPC.AutoAllocateIPv6) {
				return errors.New("autoAllocateIPv6 is not supported on Outposts")
			}
			if len(cfg.VPC.PublicAccessCIDRs) > 0 {
				return errors.New("publicAccessCIDRs is not supported on Outposts")
			}
		}
	} else if ngOutpostARN != "" && cfg.IsFullyPrivate() {
		return errors.New("nodeGroup.outpostARN is not supported on a fully-private cluster (privateCluster.enabled)")
	}

	if err := cfg.ValidateVPCConfig(); err != nil {
		return err
	}

	if err := ValidateSecretsEncryption(cfg); err != nil {
		return err
	}

	if err := validateIAMIdentityMappings(cfg); err != nil {
		return err
	}
	if err := validateAddonPodIdentityAssociations(cfg.Addons); err != nil {
		return err
	}
	if err := ValidateAutoModeConfig(cfg); err != nil {
		return err
	}

	if len(cfg.AccessConfig.AccessEntries) > 0 {
		switch cfg.AccessConfig.AuthenticationMode {
		case ekstypes.AuthenticationModeConfigMap:
			return fmt.Errorf("accessConfig.authenticationMode must be set to either %s or %s to use access entries",
				ekstypes.AuthenticationModeApiAndConfigMap, ekstypes.AuthenticationModeApi)
		}
		if err := validateAccessEntries(cfg.AccessConfig.AccessEntries); err != nil {
			return err
		}
	}

	if err := validateKarpenterConfig(cfg); err != nil {
		return fmt.Errorf("failed to validate Karpenter config: %w", err)
	}

	return nil
}

func validateKarpenterConfig(cfg *ClusterConfig) error {
	if cfg.Karpenter == nil {
		return nil
	}
	if cfg.Karpenter.Version == "" {
		return errors.New("version field is required if installing Karpenter is enabled")
	}

	v, err := version.NewVersion(cfg.Karpenter.Version)
	if err != nil {
		return fmt.Errorf("failed to parse Karpenter version %q: %w", cfg.Karpenter.Version, err)
	}

	supportedVersion, err := version.NewVersion(supportedKarpenterVersion)
	if err != nil {
		return fmt.Errorf("failed to parse supported Karpenter version %s: %w", supportedKarpenterVersion, err)
	}

	if v.LessThan(supportedVersion) {
		return fmt.Errorf("minimum supported version is %s", supportedKarpenterVersion)
	}

	if IsDisabled(cfg.IAM.WithOIDC) {
		return errors.New("iam.withOIDC must be enabled with Karpenter")
	}
	return nil
}

func validateCloudWatchLogging(clusterConfig *ClusterConfig) error {
	if !clusterConfig.HasClusterCloudWatchLogging() {
		if clusterConfig.CloudWatch != nil &&
			clusterConfig.CloudWatch.ClusterLogging != nil &&
			clusterConfig.CloudWatch.ClusterLogging.LogRetentionInDays != 0 {
			return errors.New("cannot set cloudWatch.clusterLogging.logRetentionInDays without enabling log types")
		}
		return nil
	}

	for i, logType := range clusterConfig.CloudWatch.ClusterLogging.EnableTypes {
		isUnknown := true
		for _, knownLogType := range SupportedCloudWatchClusterLogTypes() {
			if logType == knownLogType {
				isUnknown = false
			}
		}
		if isUnknown {
			return fmt.Errorf("log type %q (cloudWatch.clusterLogging.enableTypes[%d]) is unknown", logType, i)
		}
	}
	if logRetentionDays := clusterConfig.CloudWatch.ClusterLogging.LogRetentionInDays; logRetentionDays != 0 {
		for _, v := range LogRetentionInDaysValues {
			if v == logRetentionDays {
				return nil
			}
		}
		return fmt.Errorf("invalid value %d for logRetentionInDays; supported values are %v", logRetentionDays, LogRetentionInDaysValues)
	}

	return nil
}

// ValidateVPCConfig validates the vpc setting if it is defined.
func (c *ClusterConfig) ValidateVPCConfig() error {
	if c.VPC == nil {
		return nil
	}

	if err := c.ValidatePrivateCluster(); err != nil {
		return err
	}

	if err := c.ValidateClusterEndpointConfig(); err != nil {
		return err
	}

	if len(c.VPC.ExtraCIDRs) > 0 {
		cidrs, err := validateCIDRs(c.VPC.ExtraCIDRs)
		if err != nil {
			return err
		}
		c.VPC.ExtraCIDRs = cidrs
	}
	if len(c.VPC.PublicAccessCIDRs) > 0 {
		cidrs, err := validateCIDRs(c.VPC.PublicAccessCIDRs)
		if err != nil {
			return err
		}
		c.VPC.PublicAccessCIDRs = cidrs
	}
	if len(c.VPC.ExtraIPv6CIDRs) > 0 {
		if !c.IPv6Enabled() {
			return fmt.Errorf("cannot specify vpc.extraIPv6CIDRs with an IPv4 cluster")
		}
		cidrs, err := validateCIDRs(c.VPC.ExtraIPv6CIDRs)
		if err != nil {
			return err
		}
		c.VPC.ExtraIPv6CIDRs = cidrs
	}

	if c.VPC.SecurityGroup != "" && len(c.VPC.ControlPlaneSecurityGroupIDs) > 0 {
		return errors.New("only one of vpc.securityGroup and vpc.controlPlaneSecurityGroupIDs can be specified")
	}

	if (c.VPC.IPv6Cidr != "" || c.VPC.IPv6Pool != "") && !c.IPv6Enabled() {
		return fmt.Errorf("Ipv6Cidr and Ipv6CidrPool are only supported when IPFamily is set to IPv6")
	}

	if c.IPv6Enabled() && c.Status == nil {
		if IsEnabled(c.VPC.AutoAllocateIPv6) {
			return fmt.Errorf("auto allocate ipv6 is not supported with IPv6")
		}
		if err := c.ipv6CidrsValid(); err != nil {
			return err
		}
		if c.VPC.NAT != nil {
			return fmt.Errorf("setting NAT is not supported with IPv6")
		}
		if len(c.LocalZones) > 0 {
			return errors.New("localZones are not supported with IPv6")
		}
	}

	// manageSharedNodeSecurityGroupRules cannot be disabled if using eksctl managed security groups
	if c.VPC.SharedNodeSecurityGroup == "" && IsDisabled(c.VPC.ManageSharedNodeSecurityGroupRules) {
		return errors.New("vpc.manageSharedNodeSecurityGroupRules must be enabled when using eksctl-managed security groups")
	}

	if c.VPC.HostnameType != "" {
		if c.HasAnySubnets() {
			return errors.New("vpc.hostnameType is not supported with a pre-existing VPC")
		}
		var hostnameType ec2types.HostnameType
		found := false
		for _, h := range hostnameType.Values() {
			if h == ec2types.HostnameType(c.VPC.HostnameType) {
				found = true
				break
			}
		}
		if !found {
			return fmt.Errorf("invalid value %q for vpc.hostnameType; supported values are %v", c.VPC.HostnameType, hostnameType.Values())
		}
	}

	if len(c.LocalZones) > 0 {
		if c.VPC.ID != "" {
			return errors.New("localZones are not supported with a pre-existing VPC")
		}
		if c.VPC.NAT != nil && c.VPC.NAT.Gateway != nil && *c.VPC.NAT.Gateway == ClusterHighlyAvailableNAT {
			return fmt.Errorf("%s NAT gateway is not supported for localZones", ClusterHighlyAvailableNAT)
		}
	}

	return nil
}

func (c *ClusterConfig) unsupportedVPCCNIAddonVersion() (bool, error) {
	for _, addon := range c.Addons {
		if addon.Name == VPCCNIAddon {
			if addon.Version == "" {
				return false, nil
			}
			if addon.Version == "latest" {
				return false, nil
			}

			return versionLessThan(addon.Version, minimumVPCCNIVersionForIPv6)
		}
	}
	return false, nil
}

func versionLessThan(v1, v2 string) (bool, error) {
	v1Version, err := parseVersion(v1)
	if err != nil {
		return false, err
	}
	v2Version, err := parseVersion(v2)
	if err != nil {
		return false, err
	}
	return v1Version.LessThan(v2Version), nil
}

func parseVersion(v string) (*version.Version, error) {
	version, err := version.NewVersion(v)
	if err != nil {
		return nil, fmt.Errorf("failed to parse version %q: %w", v, err)
	}
	return version, nil
}

func (c *ClusterConfig) ipv6CidrsValid() error {
	if c.VPC.IPv6Cidr == "" && c.VPC.IPv6Pool == "" {
		return nil
	}

	if c.VPC.IPv6Cidr != "" && c.VPC.IPv6Pool != "" {
		if c.VPC.ID != "" {
			return fmt.Errorf("cannot provide VPC.IPv6Cidr when using a pre-existing VPC.ID")
		}
		return nil
	}
	return fmt.Errorf("Ipv6Cidr and Ipv6Pool must both be configured to use a custom IPv6 CIDR and address pool")
}

// addonContainsManagedAddons finds managed addons in the config and returns those it couldn't find.
func (c *ClusterConfig) addonContainsManagedAddons(addons []string) []string {
	var missing []string
	for _, a := range addons {
		found := false
		for _, add := range c.Addons {
			if strings.ToLower(add.Name) == a {
				found = true
				break
			}
		}
		if !found {
			missing = append(missing, a)
		}
	}
	return missing
}

func (c *ClusterConfig) getAddon(name string) *Addon {
	for _, addon := range c.Addons {
		if addon.Name == name {
			return addon
		}
	}
	return nil
}

// ValidateClusterEndpointConfig checks the endpoint configuration for potential issues
func (c *ClusterConfig) ValidateClusterEndpointConfig() error {
	if c.VPC.ClusterEndpoints != nil {
		if !c.HasClusterEndpointAccess() {
			return ErrClusterEndpointNoAccess
		}
		endpts := c.VPC.ClusterEndpoints

		if noAccess(endpts) {
			return ErrClusterEndpointNoAccess
		}
	}
	return nil
}

// ValidatePrivateCluster validates the private cluster config
func (c *ClusterConfig) ValidatePrivateCluster() error {
	if c.PrivateCluster.Enabled {
		if c.VPC != nil && c.VPC.ID != "" && len(c.VPC.Subnets.Private) == 0 {
			return errors.New("vpc.subnets.private must be specified in a fully-private cluster when a pre-existing VPC is supplied")
		}

		if additionalEndpoints := c.PrivateCluster.AdditionalEndpointServices; len(additionalEndpoints) > 0 {
			if c.PrivateCluster.SkipEndpointCreation {
				return errors.New("privateCluster.additionalEndpointServices cannot be set when privateCluster.skipEndpointCreation is true")
			}
			if err := ValidateAdditionalEndpointServices(additionalEndpoints); err != nil {
				return fmt.Errorf("invalid value in privateCluster.additionalEndpointServices: %w", err)
			}
		}

		if c.VPC != nil && c.VPC.ClusterEndpoints == nil {
			c.VPC.ClusterEndpoints = &ClusterEndpoints{}
		}
		if len(c.LocalZones) > 0 {
			return errors.New("localZones cannot be used in a fully-private cluster")
		}
		// public access is initially enabled to allow running operations that access the Kubernetes API
		if !c.IsControlPlaneOnOutposts() {
			c.VPC.ClusterEndpoints.PublicAccess = Enabled()
			c.VPC.ClusterEndpoints.PrivateAccess = Enabled()
		}
	}
	return nil
}

// validateKubernetesNetworkConfig validates the k8s network config
func (c *ClusterConfig) validateKubernetesNetworkConfig() error {
	// this check ensured the validation is only run on cluster creation
	if c.Status != nil {
		return nil
	}
	if c.KubernetesNetworkConfig == nil {
		return nil
	}
	if c.KubernetesNetworkConfig.ServiceIPv4CIDR != "" {
		if c.IPv6Enabled() {
			return errors.New("service IPv4 CIDR is not supported with IPv6")
		}
		serviceIP := c.KubernetesNetworkConfig.ServiceIPv4CIDR
		if _, _, err := net.ParseCIDR(serviceIP); serviceIP != "" && err != nil {
			return fmt.Errorf("invalid IPv4 CIDR for kubernetesNetworkConfig.serviceIPv4CIDR: %w", err)
		}
	}

	switch strings.ToLower(c.KubernetesNetworkConfig.IPFamily) {
	case strings.ToLower(IPV4Family), "":
	case strings.ToLower(IPV6Family):
		if !c.IsAutoModeEnabled() {
			if missing := c.addonContainsManagedAddons([]string{VPCCNIAddon, CoreDNSAddon, KubeProxyAddon}); len(missing) != 0 {
				return fmt.Errorf("the default core addons must be defined for IPv6; missing addon(s): %s; either define them or use EKS Auto Mode", strings.Join(missing, ", "))
			}

			// Check if at least one credential provider (Pod identity or IRSA) is configured
			if len(c.addonContainsManagedAddons([]string{PodIdentityAgentAddon})) != 0 && (c.IAM == nil || c.IAM != nil && IsDisabled(c.IAM.WithOIDC)) {
				return errors.New("either pod identity or oidc needs to be enabled if IPv6 is set; set either one or use EKS Auto Mode")
			}

			// If the pod identity addon is present, verify it is correctly configured for use by the VPC CNI addon
			// Assuming user intends to use pod identities if the pod identity agent addon is added.
			if len(c.addonContainsManagedAddons([]string{PodIdentityAgentAddon})) == 0 && !c.AddonsConfig.AutoApplyPodIdentityAssociations {
				vpcCNIAddonEntry := c.getAddon(VPCCNIAddon)

				if vpcCNIAddonEntry == nil {
					// should be unreachable
					return errors.New("the vpc-cni addon must be defined for IPv6; either define it or use EKS Auto Mode")
				}

				if !vpcCNIAddonEntry.UseDefaultPodIdentityAssociations &&
					(vpcCNIAddonEntry.PodIdentityAssociations == nil || len(*vpcCNIAddonEntry.PodIdentityAssociations) == 0) {
					return fmt.Errorf("Set one of: addonsConfig.autoApplyPodIdentityAssociations, useDefaultPodIdentityAssociations on the vpc-cni addon, apply a custom pod identity on the vpc-cni addon")
				}
			}
		}

		unsupportedVersion, err := c.unsupportedVPCCNIAddonVersion()
		if err != nil {
			return err
		}

		if unsupportedVersion {
			return fmt.Errorf("%s version must be at least version %s for IPv6", VPCCNIAddon, minimumVPCCNIVersionForIPv6)
		}

		if version, err := utils.CompareVersions(c.Metadata.Version, Version1_21); err != nil {
			return fmt.Errorf("failed to convert %s cluster version to semver: %w", c.Metadata.Version, err)
		} else if version == -1 {
			return fmt.Errorf("cluster version must be >= %s", Version1_21)
		}
	default:
		return fmt.Errorf("invalid value %q for ipFamily; allowed are %s and %s", c.KubernetesNetworkConfig.IPFamily, IPV4Family, IPV6Family)
	}

	return nil
}

// NoAccess returns true if neither public are private cluster endpoint access is enabled and false otherwise
func noAccess(ces *ClusterEndpoints) bool {
	return !(IsEnabled(ces.PublicAccess) || IsEnabled(ces.PrivateAccess))
}

// PrivateOnly returns true if public cluster endpoint access is disabled and private cluster endpoint access is enabled, and false otherwise
func PrivateOnly(ces *ClusterEndpoints) bool {
	return !*ces.PublicAccess && *ces.PrivateAccess
}

func validateNodeGroupBase(np NodePool, path string, controlPlaneOnOutposts bool) error {
	ng := np.BaseNodeGroup()
	if ng.VolumeSize == nil {
		errCantSet := func(field string) error {
			return fmt.Errorf("%s.%s cannot be set without %s.volumeSize", path, field, path)
		}
		if IsSetAndNonEmptyString(ng.VolumeName) {
			return errCantSet("volumeName")
		}
		if IsEnabled(ng.VolumeEncrypted) {
			return errCantSet("volumeEncrypted")
		}
		if IsSetAndNonEmptyString(ng.VolumeKmsKeyID) {
			return errCantSet("volumeKmsKeyID")
		}
	}

	if err := validateVolumeOpts(ng, path, controlPlaneOnOutposts); err != nil {
		return err
	}

	if ng.VolumeEncrypted == nil || IsDisabled(ng.VolumeEncrypted) {
		if IsSetAndNonEmptyString(ng.VolumeKmsKeyID) {
			return fmt.Errorf("%s.volumeKmsKeyID can not be set without %s.volumeEncrypted enabled explicitly", path, path)
		}
	}
	if ng.MaxPodsPerNode < 0 {
		return fmt.Errorf("%s.maxPodsPerNode cannot be negative", path)
	}

	if IsEnabled(ng.DisablePodIMDS) && ng.IAM != nil {
		fmtFieldConflictErr := func(_ string) error {
			return fmt.Errorf("%s.disablePodIMDS and %s.iam.withAddonPolicies cannot be set at the same time", path, path)
		}
		if err := validateNodeGroupIAMWithAddonPolicies(ng.IAM.WithAddonPolicies, fmtFieldConflictErr); err != nil {
			return err
		}
	}

	if len(ng.AvailabilityZones) > 0 && len(ng.Subnets) > 0 {
		return fmt.Errorf("only one of %[1]s.subnets or %[1]s.availabilityZones should be set", path)
	}

	if ng.Placement != nil {
		if ng.Placement.GroupName == "" {
			return fmt.Errorf("%s.placement.groupName must be set and non-empty", path)
		}
	}

	if IsEnabled(ng.EFAEnabled) {
		if len(ng.AvailabilityZones) > 1 || len(ng.Subnets) > 1 {
			return fmt.Errorf("%s.efaEnabled nodegroups must have only one subnet or one availability zone", path)
		}
	}

	if ng.AMIFamily != "" {
		if !isSupportedAMIFamily(ng.AMIFamily) {
			if ng.AMIFamily == NodeImageFamilyWindowsServer20H2CoreContainer || ng.AMIFamily == NodeImageFamilyWindowsServer2004CoreContainer {
				return fmt.Errorf("AMI Family %s is deprecated. For more information, head to the Amazon documentation on Windows AMIs (https://docs.aws.amazon.com/eks/latest/userguide/eks-optimized-windows-ami.html)", ng.AMIFamily)
			}
			return fmt.Errorf("AMI Family %s is not supported - use one of: %s", ng.AMIFamily, strings.Join(SupportedAMIFamilies(), ", "))
		}
		if controlPlaneOnOutposts && ng.AMIFamily != NodeImageFamilyAmazonLinux2 {
			return fmt.Errorf("only %s is supported on local clusters", NodeImageFamilyAmazonLinux2)
		}
	}

	if ng.SSH != nil {
		if enableSSM := ng.SSH.EnableSSM; enableSSM != nil {
			if !*enableSSM {
				return errors.New("SSM agent is now built into EKS AMIs and cannot be disabled")
			}
			logger.Warning("SSM is now enabled by default; `ssh.enableSSM` is deprecated and will be removed in a future release")
		}
	}

	instanceType := SelectInstanceType(np)

	if ng.AMIFamily != NodeImageFamilyAmazonLinux2023 &&
		ng.AMIFamily != NodeImageFamilyAmazonLinux2 &&
		ng.AMIFamily != NodeImageFamilyBottlerocket &&
		ng.AMIFamily != "" {
		if instanceutils.IsNvidiaInstanceType(instanceType) {
			logger.Warning(GPUDriversWarning(ng.AMIFamily))
		}
		if ng.InstanceSelector != nil && !ng.InstanceSelector.IsZero() &&
			(ng.InstanceSelector.GPUs == nil || *ng.InstanceSelector.GPUs != 0) {
			logger.Warning("instance selector may/will select GPU instance types, " + GPUDriversWarning(ng.AMIFamily))
		}
		if ng.InstanceSelector != nil && !ng.InstanceSelector.IsZero() &&
			(ng.InstanceSelector.NeuronDevices == nil || *ng.InstanceSelector.NeuronDevices != 0) {
			logger.Warning("instance selector may/will select Neuron Device instance types, " + NeuronDeviceDriversWarning(ng.AMIFamily))
		}
	}

	if ng.AMIFamily != NodeImageFamilyAmazonLinux2 &&
		ng.AMIFamily != NodeImageFamilyAmazonLinux2023 &&
		ng.AMIFamily != NodeImageFamilyBottlerocket &&
		ng.AMIFamily != "" {
		// Only AL2, AL2023 and Bottlerocket support Inferentia hosts.
		if instanceutils.IsInferentiaInstanceType(instanceType) {
			return ErrUnsupportedInstanceTypes("Inferentia", ng.AMIFamily, fmt.Sprintf("please use %s instead", NodeImageFamilyAmazonLinux2))
		}
		// Only AL2, AL2023 and Bottlerocket support Trainium hosts.
		if instanceutils.IsTrainiumInstanceType(instanceType) {
			return ErrUnsupportedInstanceTypes("Trainium", ng.AMIFamily, fmt.Sprintf("please use %s instead", NodeImageFamilyAmazonLinux2))
		}
	}

	if ng.CapacityReservation != nil {
		if ng.CapacityReservation.CapacityReservationPreference != nil {
			if ng.CapacityReservation.CapacityReservationTarget != nil {
				return errors.New("only one of CapacityReservationPreference or CapacityReservationTarget may be specified at a time")
			}

			if *ng.CapacityReservation.CapacityReservationPreference != OpenCapacityReservation && *ng.CapacityReservation.CapacityReservationPreference != NoneCapacityReservation {
				return fmt.Errorf(`accepted values include "open" and "none"; got "%s"`, *ng.CapacityReservation.CapacityReservationPreference)
			}
		}

		if ng.CapacityReservation.CapacityReservationTarget != nil {
			if ng.CapacityReservation.CapacityReservationTarget.CapacityReservationID != nil && ng.CapacityReservation.CapacityReservationTarget.CapacityReservationResourceGroupARN != nil {
				return errors.New("only one of CapacityReservationID or CapacityReservationResourceGroupARN may be specified at a time")
			}
		}

		if ng.InstanceMarketOptions != nil {
			if ng.InstanceMarketOptions.MarketType != nil {
				if *ng.InstanceMarketOptions.MarketType != "capacity-block" {
					return fmt.Errorf(`only accepted value is "capacity-block"; got "%s"`, *ng.InstanceMarketOptions.MarketType)
				}
			}
		}
	} else {
		if ng.InstanceMarketOptions != nil {
			return errors.New("instanceMarketOptions cannot be set without capacityReservation")
		}
	}

	return nil
}

func validateVolumeOpts(ng *NodeGroupBase, path string, controlPlaneOnOutposts bool) error {
	if ng.VolumeType != nil {
		volumeType := *ng.VolumeType
		if ng.VolumeIOPS != nil && !(volumeType == NodeVolumeTypeIO1 || volumeType == NodeVolumeTypeIO2 || volumeType == NodeVolumeTypeGP3) {
			return fmt.Errorf("%s.volumeIOPS is only supported for %s, %s and %s volume types", path, NodeVolumeTypeIO1, NodeVolumeTypeIO2, NodeVolumeTypeGP3)
		}

		if volumeType == NodeVolumeTypeIO1 {
			if ng.VolumeIOPS != nil && !(*ng.VolumeIOPS >= MinIO1Iops && *ng.VolumeIOPS <= MaxIO1Iops) {
				return fmt.Errorf("value for %s.volumeIOPS must be within range %d-%d", path, MinIO1Iops, MaxIO1Iops)
			}
		}

		if volumeType == NodeVolumeTypeIO2 {
			if ng.VolumeIOPS != nil && !(*ng.VolumeIOPS >= MinIO2Iops && *ng.VolumeIOPS <= MaxIO2Iops) {
				return fmt.Errorf("value for %s.volumeIOPS must be within range %d-%d", path, MinIO2Iops, MaxIO2Iops)
			}
		}

		if ng.VolumeThroughput != nil && volumeType != NodeVolumeTypeGP3 {
			return fmt.Errorf("%s.volumeThroughput is only supported for %s volume type", path, NodeVolumeTypeGP3)
		}

		if controlPlaneOnOutposts && volumeType != NodeVolumeTypeGP2 {
			return fmt.Errorf("cannot set %q for %s.volumeType; only %q volume types are supported on Outposts", volumeType, path, NodeVolumeTypeGP2)
		}
	}

	if ng.VolumeType == nil || *ng.VolumeType == NodeVolumeTypeGP3 {
		if ng.VolumeIOPS != nil && !(*ng.VolumeIOPS >= MinGP3Iops && *ng.VolumeIOPS <= MaxGP3Iops) {
			return fmt.Errorf("value for %s.volumeIOPS must be within range %d-%d", path, MinGP3Iops, MaxGP3Iops)
		}

		if ng.VolumeThroughput != nil && !(*ng.VolumeThroughput >= MinThroughput && *ng.VolumeThroughput <= MaxThroughput) {
			return fmt.Errorf("value for %s.volumeThroughput must be within range %d-%d", path, MinThroughput, MaxThroughput)
		}
	}

	return nil
}

func validateIdentityProvider(idP IdentityProvider) error {
	switch idP := (idP.Inner).(type) {
	case *OIDCIdentityProvider:
		if idP.Name == "" {
			return setNonEmpty("name")
		}
		if idP.ClientID == "" {
			return setNonEmpty("clientID")
		}
		if idP.IssuerURL == "" {
			return setNonEmpty("issuerURL")
		}
	}
	return nil
}

func validateIdentityProviders(idPs []IdentityProvider) error {
	for k, idP := range idPs {
		if err := validateIdentityProvider(idP); err != nil {
			return fmt.Errorf("identityProviders[%d] is invalid: %w", k, err)
		}
	}
	return nil
}

type unsupportedFieldError struct {
	ng    *NodeGroupBase
	path  string
	field string
}

func (ue *unsupportedFieldError) Error() string {
	return fmt.Sprintf("%s is not supported for %s nodegroups (path=%s.%s)", ue.field, ue.ng.AMIFamily, ue.path, ue.field)
}

// IsInvalidNameArg checks whether the name contains invalid characters
func IsInvalidNameArg(name string) bool {
	re := regexp.MustCompile(`[^a-zA-Z0-9\-]+`)
	return re.MatchString(name)
}

// errInvalidName error when invalid characters for a name is provided
func ErrInvalidName(name string) error {
	return fmt.Errorf("validation for %s failed, name must satisfy regular expression pattern: [a-zA-Z][-a-zA-Z0-9]*", name)
}

func validateNodeGroupName(name string) error {
	if name != "" && IsInvalidNameArg(name) {
		return ErrInvalidName(name)
	}

	return nil
}

// ValidateNodeGroup checks compatible fields of a given nodegroup
func ValidateNodeGroup(i int, ng *NodeGroup, cfg *ClusterConfig) error {
	normalizeAMIFamily(ng.BaseNodeGroup())
	path := fmt.Sprintf("nodeGroups[%d]", i)
	if err := validateNodeGroupBase(ng, path, cfg.IsControlPlaneOnOutposts()); err != nil {
		return err
	}

	if err := validateNodeGroupName(ng.Name); err != nil {
		return err
	}

	if ng.IAM != nil {
		if err := validateNodeGroupIAM(ng.IAM, ng.IAM.InstanceProfileARN, "instanceProfileARN", path); err != nil {
			return err
		}
		if err := validateNodeGroupIAM(ng.IAM, ng.IAM.InstanceRoleARN, "instanceRoleARN", path); err != nil {
			return err
		}
		if attachPolicyARNs := ng.IAM.AttachPolicyARNs; len(attachPolicyARNs) > 0 {
			for _, policyARN := range attachPolicyARNs {
				if _, err := arn.Parse(policyARN); err != nil {
					return fmt.Errorf("invalid ARN %q in %s.iam.attachPolicyARNs: %w", policyARN, path, err)
				}

			}
		}
		if err := validateDeprecatedIAMFields(ng.IAM); err != nil {
			return err
		}
	}

	if ng.AMI != "" && ng.AMIFamily == "" {
		return errors.New("when using a custom AMI, amiFamily needs to be explicitly set via config file or via --node-ami-family flag")
	}

	if ng.Bottlerocket != nil && ng.AMIFamily != NodeImageFamilyBottlerocket {
		return fmt.Errorf(`bottlerocket config can only be used with amiFamily "Bottlerocket" but found "%s" (path=%s.bottlerocket)`,
			ng.AMIFamily, path)
	}

	if ng.AMI != "" && ng.OverrideBootstrapCommand == nil &&
		ng.AMIFamily != NodeImageFamilyAmazonLinux2023 &&
		ng.AMIFamily != NodeImageFamilyBottlerocket &&
		!IsWindowsImage(ng.AMIFamily) {
		return fmt.Errorf("%[1]s.overrideBootstrapCommand is required when using a custom AMI based on %s (%[1]s.ami)", path, ng.AMIFamily)
	}

	if err := validateTaints(ng.Taints); err != nil {
		return err
	}

	if err := validateLabels(ng.Labels); err != nil {
		return err
	}

	if ng.SSH != nil {
		if err := validateNodeGroupSSH(ng.SSH); err != nil {
			return err
		}
	}

	fieldNotSupported := func(field string) error {
		return &unsupportedFieldError{
			ng:    ng.NodeGroupBase,
			path:  path,
			field: field,
		}
	}

	if IsWindowsImage(ng.AMIFamily) {
		if ng.KubeletExtraConfig != nil {
			return fieldNotSupported("kubeletExtraConfig")
		}
	} else if ng.AMIFamily == NodeImageFamilyBottlerocket {
		if ng.KubeletExtraConfig != nil {
			return fieldNotSupported("kubeletExtraConfig")
		}
		if ng.PreBootstrapCommands != nil {
			return fieldNotSupported("preBootstrapCommands")
		}
		if ng.OverrideBootstrapCommand != nil {
			return fieldNotSupported("overrideBootstrapCommand")
		}
		if ng.Bottlerocket != nil && ng.Bottlerocket.Settings != nil {
			if err := checkBottlerocketSettings(ng, path); err != nil {
				return err
			}
		}
	} else if err := validateNodeGroupKubeletExtraConfig(ng.KubeletExtraConfig); err != nil {
		return err
	}

	if err := validateInstanceTypeSupport(ng); err != nil {
		return err
	}

	if err := validateInstancesDistribution(ng); err != nil {
		return err
	}

	if err := validateCPUCredits(ng); err != nil {
		return err
	}

	if err := validateASGSuspendProcesses(ng); err != nil {
		return err
	}

	if ng.ContainerRuntime != nil {
		if ng.AMIFamily == NodeImageFamilyAmazonLinux2023 && *ng.ContainerRuntime != ContainerRuntimeContainerD {
			return fmt.Errorf("only %s is supported for container runtime on %s nodes", ContainerRuntimeContainerD, NodeImageFamilyAmazonLinux2023)
		}
		if *ng.ContainerRuntime != ContainerRuntimeDockerD && *ng.ContainerRuntime != ContainerRuntimeContainerD && *ng.ContainerRuntime != ContainerRuntimeDockerForWindows {
			return fmt.Errorf("only %s, %s and %s are supported for container runtime", ContainerRuntimeContainerD, ContainerRuntimeDockerD, ContainerRuntimeDockerForWindows)
		}
		if clusterVersion := cfg.Metadata.Version; clusterVersion != "" {
			isDockershimDeprecated, err := utils.IsMinVersion(DockershimDeprecationVersion, clusterVersion)
			if err != nil {
				return err
			}
			if *ng.ContainerRuntime != ContainerRuntimeContainerD && isDockershimDeprecated {
				return fmt.Errorf("only %s is supported for container runtime, starting with EKS version %s", ContainerRuntimeContainerD, DockershimDeprecationVersion)
			}
		}
		if ng.OverrideBootstrapCommand != nil {
			return fmt.Errorf("overrideBootstrapCommand overwrites container runtime setting; please use --container-runtime in the bootsrap script instead")
		}
	}

	if ng.MaxInstanceLifetime != nil {
		if *ng.MaxInstanceLifetime < OneDay {
			return fmt.Errorf("maximum instance lifetime must have a minimum value of 86,400 seconds (one day), but was: %d", *ng.MaxInstanceLifetime)
		}
	}

	if len(ng.LocalZones) > 0 && len(ng.AvailabilityZones) > 0 {
		return errors.New("cannot specify both localZones and availabilityZones")
	}

	if ng.OutpostARN != "" {
		if err := validateOutpostARN(ng.OutpostARN); err != nil {
			return err
		}
		if cfg.IsControlPlaneOnOutposts() && ng.OutpostARN != cfg.GetOutpost().ControlPlaneOutpostARN {
			return fmt.Errorf("nodeGroup.outpostARN must either be empty or match the control plane's Outpost ARN (%q != %q)", ng.OutpostARN, cfg.GetOutpost().ControlPlaneOutpostARN)
		}
	}

	if cfg.IsControlPlaneOnOutposts() || ng.OutpostARN != "" {
		if ng.InstanceSelector != nil && !ng.InstanceSelector.IsZero() {
			return errors.New("cannot specify instanceSelector for a nodegroup on Outposts")
		}
		const msg = "%s cannot be specified for a nodegroup on Outposts; the AZ defaults to the Outpost AZ"
		if len(ng.AvailabilityZones) > 0 {
			return fmt.Errorf(msg, "availabilityZones")
		}
		if len(ng.LocalZones) > 0 {
			return fmt.Errorf(msg, "localZones")
		}
	}

	return nil
}

// validateInstanceTypeSupport checks if the provided instance types are
// supported by the AMIFamily. If a custom AMI is provided then it will skip the
// validation, as it could be a derivative of a supported family type.
func validateInstanceTypeSupport(ng *NodeGroup) error {
	if IsAMI(ng.AMI) {
		return nil
	}
	instanceType := SelectInstanceType(ng)
	amiType := GetAMIType(ng.AMIFamily, instanceType, true /* strict, don't allows fallbacks */)
	if amiType == "" {
		return fmt.Errorf("%s instance types are not supported for unmanaged nodegroups with AMIFamily %s", instanceType, ng.AMIFamily)
	}
	return nil
}

func validateOutpostARN(val string) error {
	parsed, err := arn.Parse(val)
	if err != nil {
		return fmt.Errorf("invalid Outpost ARN: %w", err)
	}
	if parsed.Service != "outposts" {
		return fmt.Errorf("invalid Outpost ARN: %q", val)
	}
	return nil
}

// validateLabels uses proper Kubernetes label validation,
// it's designed to make sure users don't pass invalid or disallowed labels,
// which would prevent kubelets to startup properly
func validateLabels(labels map[string]string) error {
	// compact version based on:
	// - https://github.com/kubernetes/kubernetes/blob/v1.13.2/cmd/kubelet/app/options/options.go#L257-L267
	// - https://github.com/kubernetes/kubernetes/blob/v1.13.2/pkg/kubelet/apis/well_known_labels.go
	// we cannot import those packages because they break other dependencies

	disallowedKubernetesLabels := []string{}

	for label := range labels {
		labelParts := strings.Split(label, "/")

		if len(labelParts) > 2 {
			return fmt.Errorf("node label key %q is of invalid format, can only use one '/' separator", label)
		}

		if errs := validation.IsQualifiedName(label); len(errs) > 0 {
			return fmt.Errorf("label %q is invalid - %v", label, errs)
		}
		if errs := validation.IsValidLabelValue(labels[label]); len(errs) > 0 {
			return fmt.Errorf("label %q has invalid value %q - %v", label, labels[label], errs)
		}

		if len(labelParts) == 2 {
			namespace := labelParts[0]
			if isKubernetesLabel(namespace) && !kubeletapis.IsKubeletLabel(label) {
				disallowedKubernetesLabels = append(disallowedKubernetesLabels, label)
			}
		}
	}

	if len(disallowedKubernetesLabels) > 0 {
		return fmt.Errorf("the following nodegroup labels are disallowed as they have reserved prefixes [kubernetes.io/, k8s.io/]: %v", disallowedKubernetesLabels)
	}
	return nil
}

func isKubernetesLabel(namespace string) bool {
	for _, domain := range []string{"kubernetes.io", "k8s.io"} {
		if namespace == domain || strings.HasSuffix(namespace, "."+domain) {
			return true
		}
	}
	return false
}

func validateNodeGroupIAMWithAddonPolicies(
	policies NodeGroupIAMAddonPolicies,
	fmtFieldConflictErr func(conflictingField string) error,
) error {
	prefix := "withAddonPolicies."
	if IsEnabled(policies.AutoScaler) {
		return fmtFieldConflictErr(prefix + "autoScaler")
	}
	if IsEnabled(policies.ExternalDNS) {
		return fmtFieldConflictErr(prefix + "externalDNS")
	}
	if IsEnabled(policies.CertManager) {
		return fmtFieldConflictErr(prefix + "certManager")
	}
	if IsEnabled(policies.ImageBuilder) {
		return fmtFieldConflictErr(prefix + "imageBuilder")
	}
	if IsEnabled(policies.AppMesh) {
		return fmtFieldConflictErr(prefix + "appMesh")
	}
	if IsEnabled(policies.AppMeshPreview) {
		return fmtFieldConflictErr(prefix + "appMeshPreview")
	}
	if IsEnabled(policies.EBS) {
		return fmtFieldConflictErr(prefix + "ebs")
	}
	if IsEnabled(policies.FSX) {
		return fmtFieldConflictErr(prefix + "fsx")
	}
	if IsEnabled(policies.EFS) {
		return fmtFieldConflictErr(prefix + "efs")
	}
	if IsEnabled(policies.AWSLoadBalancerController) {
		return fmtFieldConflictErr(prefix + "awsLoadBalancerController")
	}
	if IsEnabled(policies.DeprecatedALBIngress) {
		return fmtFieldConflictErr(prefix + "albIngress")
	}
	if IsEnabled(policies.XRay) {
		return fmtFieldConflictErr(prefix + "xRay")
	}
	if IsEnabled(policies.CloudWatch) {
		return fmtFieldConflictErr(prefix + "cloudWatch")
	}
	return nil
}

func validateDeprecatedIAMFields(iam *NodeGroupIAM) error {
	if IsEnabled(iam.WithAddonPolicies.DeprecatedALBIngress) {
		if IsEnabled(iam.WithAddonPolicies.AWSLoadBalancerController) {
			return fmt.Errorf(`"awsLoadBalancerController" and "albIngress" cannot both be configured, ` +
				`please use "awsLoadBalancerController" as "albIngress" is deprecated`)
		}
		logger.Warning("nodegroup.iam.withAddonPolicies.albIngress field is deprecated, please use awsLoadBalancerController instead")
	}

	return nil
}

func validateNodeGroupIAM(iam *NodeGroupIAM, value, fieldName, path string) error {
	if value != "" {
		fmtFieldConflictErr := func(conflictingField string) error {
			return fmt.Errorf("%s.iam.%s and %s.iam.%s cannot be set at the same time", path, fieldName, path, conflictingField)
		}

		if iam.InstanceRoleName != "" {
			return fmtFieldConflictErr("instanceRoleName")
		}
		if iam.AttachPolicy != nil {
			return fmtFieldConflictErr("attachPolicy")
		}
		if len(iam.AttachPolicyARNs) != 0 {
			return fmtFieldConflictErr("attachPolicyARNs")
		}
		if iam.InstanceRolePermissionsBoundary != "" {
			return fmtFieldConflictErr("instanceRolePermissionsBoundary")
		}
		if err := validateNodeGroupIAMWithAddonPolicies(iam.WithAddonPolicies, fmtFieldConflictErr); err != nil {
			return err
		}
	}
	return nil
}

// ValidateManagedNodeGroup validates a ManagedNodeGroup and sets some defaults
func ValidateManagedNodeGroup(index int, ng *ManagedNodeGroup) error {
	normalizeAMIFamily(ng.BaseNodeGroup())
	path := fmt.Sprintf("managedNodeGroups[%d]", index)

	if err := validateNodeGroupBase(ng, path, false); err != nil {
		return err
	}

	if ng.IAM != nil {
		if err := validateNodeGroupIAM(ng.IAM, ng.IAM.InstanceRoleARN, "instanceRoleARN", path); err != nil {
			return err
		}

		errNotSupported := func(field string) error {
			return fmt.Errorf("%s is not supported for Managed Nodes (%s.%s)", field, path, field)
		}

		if ng.IAM.InstanceProfileARN != "" {
			return errNotSupported("instanceProfileARN")
		}
	}

	if ng.OutpostARN != "" {
		return errors.New("Outposts is not supported for managed nodegroups")
	}

	// TODO fix error messages to not use CLI flags
	if ng.MinSize == nil {
		if ng.DesiredCapacity == nil {
			defaultNodeCount := DefaultNodeCount
			ng.MinSize = &defaultNodeCount
		} else {
			ng.MinSize = ng.DesiredCapacity
		}
	} else if ng.DesiredCapacity != nil && *ng.DesiredCapacity < *ng.MinSize {
		return fmt.Errorf("cannot use --nodes-min=%d and --nodes=%d at the same time", *ng.MinSize, *ng.DesiredCapacity)
	}

	// Ensure MaxSize is set, as it is required by the ASG CFN resource
	if ng.MaxSize == nil {
		if ng.DesiredCapacity == nil {
			ng.MaxSize = ng.MinSize
		} else {
			ng.MaxSize = ng.DesiredCapacity
		}
		// MaxSize needs to be greater or equal to 1
		if *ng.MaxSize == 0 {
			defaultMaxSize := DefaultMaxSize
			ng.MaxSize = &defaultMaxSize
		}
	} else if ng.DesiredCapacity != nil && *ng.DesiredCapacity > *ng.MaxSize {
		return fmt.Errorf("cannot use --nodes-max=%d and --nodes=%d at the same time", *ng.MaxSize, *ng.DesiredCapacity)
	} else if *ng.MaxSize < *ng.MinSize {
		return fmt.Errorf("cannot use --nodes-min=%d and --nodes-max=%d at the same time", *ng.MinSize, *ng.MaxSize)
	}

	if ng.DesiredCapacity == nil {
		ng.DesiredCapacity = ng.MinSize
	}

	if ng.UpdateConfig != nil {
		if ng.UpdateConfig.MaxUnavailable == nil && ng.UpdateConfig.MaxUnavailablePercentage == nil {
			return fmt.Errorf("invalid UpdateConfig: maxUnavailable or maxUnavailablePercentage must be defined")
		}
		if ng.UpdateConfig.MaxUnavailable != nil && ng.UpdateConfig.MaxUnavailablePercentage != nil {
			return fmt.Errorf("cannot use maxUnavailable=%d and maxUnavailablePercentage=%d at the same time", *ng.UpdateConfig.MaxUnavailable, *ng.UpdateConfig.MaxUnavailablePercentage)
		}
		if aws.ToInt(ng.UpdateConfig.MaxUnavailable) > aws.ToInt(ng.MaxSize) {
			return fmt.Errorf("maxUnavailable=%d cannot be greater than maxSize=%d", *ng.UpdateConfig.MaxUnavailable, *ng.MaxSize)
		}
	}

	if IsEnabled(ng.SecurityGroups.WithLocal) || IsEnabled(ng.SecurityGroups.WithShared) {
		return fmt.Errorf("securityGroups.withLocal and securityGroups.withShared are not supported for managed nodegroups (%s.securityGroups)", path)
	}

	if ng.InstanceType != "" {
		if len(ng.InstanceTypes) > 0 {
			return fmt.Errorf("only one of instanceType or instanceTypes can be specified (%s)", path)
		}
		if !ng.InstanceSelector.IsZero() {
			return fmt.Errorf("cannot set instanceType when instanceSelector is specified (%s)", path)
		}
	}

	if ng.AMIFamily == NodeImageFamilyBottlerocket {
		fieldNotSupported := func(field string) error {
			return &unsupportedFieldError{
				ng:    ng.NodeGroupBase,
				path:  path,
				field: field,
			}
		}
		if ng.PreBootstrapCommands != nil {
			return fieldNotSupported("preBootstrapCommands")
		}
		if ng.OverrideBootstrapCommand != nil {
			return fieldNotSupported("overrideBootstrapCommand")
		}
	}

	// Windows doesn't use overrideBootstrapCommand, as it always uses bootstrapping script that comes with Windows AMIs
	if IsWindowsImage(ng.AMIFamily) {
		fieldNotSupported := func(field string) error {
			return &unsupportedFieldError{
				ng:    ng.NodeGroupBase,
				path:  path,
				field: field,
			}
		}
		if ng.OverrideBootstrapCommand != nil {
			return fieldNotSupported("overrideBootstrapCommand")
		}
	}

	if err := validateTaints(ng.Taints); err != nil {
		return err
	}

	if err := validateLabels(ng.Labels); err != nil {
		return err
	}

	switch {
	case ng.LaunchTemplate != nil:
		if ng.LaunchTemplate.ID == "" {
			return fmt.Errorf("launchTemplate.id is required if launchTemplate is set (%s.%s)", path, "launchTemplate")
		}

		if ng.LaunchTemplate.Version != nil {
			// TODO support `latest` and `default`
			versionNumber, err := strconv.ParseInt(*ng.LaunchTemplate.Version, 10, 64)
			if err != nil {
				return fmt.Errorf("invalid launch template version: %w", err)
			}
			if versionNumber < 1 {
				return fmt.Errorf("launchTemplate.version must be >= 1 (%s.launchTemplate.version)", path)
			}
		}

		if ng.InstanceType != "" || ng.AMI != "" || IsEnabled(ng.SSH.Allow) || IsEnabled(ng.SSH.EnableSSM) || len(ng.SSH.SourceSecurityGroupIDs) > 0 ||
			ng.VolumeSize != nil || len(ng.PreBootstrapCommands) > 0 || ng.OverrideBootstrapCommand != nil ||
			len(ng.SecurityGroups.AttachIDs) > 0 || ng.InstanceName != "" || ng.InstancePrefix != "" || ng.MaxPodsPerNode != 0 ||
			IsDisabled(ng.DisableIMDSv1) || IsEnabled(ng.DisablePodIMDS) || ng.Placement != nil {

			incompatibleFields := []string{
				"instanceType", "ami", "ssh.allow", "ssh.enableSSM", "ssh.sourceSecurityGroupIds", "securityGroups",
				"volumeSize", "instanceName", "instancePrefix", "maxPodsPerNode", "disableIMDSv1",
				"disablePodIMDS", "preBootstrapCommands", "overrideBootstrapCommand", "placement",
			}
			return fmt.Errorf("cannot set %s in managedNodeGroup when a launch template is supplied", strings.Join(incompatibleFields, ", "))
		}

	case ng.AMI != "":
		if !IsAMI(ng.AMI) {
			return fmt.Errorf("invalid AMI %q (%s.%s)", ng.AMI, path, "ami")
		}
		if ng.AMIFamily == "" {
			return errors.New("when using a custom AMI, amiFamily needs to be explicitly set via config file or via --node-ami-family flag")
		}
		if !IsAmazonLinuxImage(ng.AMIFamily) && !IsBottlerocketImage(ng.AMIFamily) && !IsUbuntuImage(ng.AMIFamily) {
			return fmt.Errorf("cannot set amiFamily to %s when using a custom AMI for managed nodes, only %s are supported", ng.AMIFamily,
				strings.Join(slices.Concat(SupportedAmazonLinuxImages, SupportedBottlerocketImages, SupportedUbuntuImages), ", "))
		}
		if ng.OverrideBootstrapCommand == nil && ng.AMIFamily != NodeImageFamilyAmazonLinux2023 && ng.AMIFamily != NodeImageFamilyBottlerocket {
			return fmt.Errorf("%[1]s.overrideBootstrapCommand is required when using a custom AMI based on %s (%[1]s.ami)", path, ng.AMIFamily)
		}
		notSupportedWithCustomAMIErr := func(field string) error {
			return fmt.Errorf("%s.%s is not supported when using a custom AMI (%s.ami)", path, field, path)
		}
		if ng.MaxPodsPerNode != 0 {
			return notSupportedWithCustomAMIErr("maxPodsPerNode")
		}
		if ng.SSH != nil && IsEnabled(ng.SSH.EnableSSM) {
			return notSupportedWithCustomAMIErr("enableSSM")
		}
		if ng.ReleaseVersion != "" {
			return notSupportedWithCustomAMIErr("releaseVersion")
		}

	case ng.OverrideBootstrapCommand != nil && ng.AMIFamily != NodeImageFamilyAmazonLinux2023:
		return fmt.Errorf("%s.overrideBootstrapCommand can only be set when a custom AMI (%s.ami) is specified", path, path)
	}

	return nil
}

func normalizeAMIFamily(ng *NodeGroupBase) {
	for _, family := range SupportedAMIFamilies() {
		if strings.EqualFold(ng.AMIFamily, family) {
			ng.AMIFamily = family
			return
		}
	}
}

func validateInstancesDistribution(ng *NodeGroup) error {
	hasInstanceSelector := ng.InstanceSelector != nil && !ng.InstanceSelector.IsZero()
	if ng.InstancesDistribution == nil && !hasInstanceSelector {
		return nil
	}

	if ng.InstanceType != "" && ng.InstanceType != "mixed" {
		makeError := func(featureStr string) error {
			return fmt.Errorf(`instanceType should be "mixed" or unset when using the %s feature`, featureStr)
		}
		if ng.InstancesDistribution != nil {
			return makeError("instances distribution")
		}
		return makeError("instance selector")
	}

	if ng.InstancesDistribution == nil {
		return nil
	}

	distribution := ng.InstancesDistribution
	if len(distribution.InstanceTypes) == 0 && !hasInstanceSelector {
		return fmt.Errorf("at least two instance types have to be specified for mixed nodegroups")
	}

	if !hasInstanceSelector {
		uniqueInstanceTypes := make(map[string]struct{})
		for _, instanceType := range distribution.InstanceTypes {
			uniqueInstanceTypes[instanceType] = struct{}{}
		}

		if len(uniqueInstanceTypes) > 20 {
			return fmt.Errorf("mixed nodegroups should have between 1 and 20 different instance types")
		}
	}

	if distribution.OnDemandBaseCapacity != nil && *distribution.OnDemandBaseCapacity < 0 {
		return fmt.Errorf("onDemandBaseCapacity should be 0 or more")
	}

	if distribution.OnDemandPercentageAboveBaseCapacity != nil && (*distribution.OnDemandPercentageAboveBaseCapacity < 0 || *distribution.OnDemandPercentageAboveBaseCapacity > 100) {
		return fmt.Errorf("percentageAboveBase should be between 0 and 100")
	}

	if distribution.SpotInstancePools != nil && (*distribution.SpotInstancePools < 1 || *distribution.SpotInstancePools > 20) {
		return fmt.Errorf("spotInstancePools should be between 1 and 20")
	}

	if distribution.SpotInstancePools != nil && distribution.SpotAllocationStrategy != nil && *distribution.SpotAllocationStrategy == SpotAllocationStrategyCapacityOptimized {
		return fmt.Errorf("spotInstancePools cannot be specified when also specifying spotAllocationStrategy: %s", SpotAllocationStrategyCapacityOptimized)
	}

	if distribution.SpotInstancePools != nil && distribution.SpotAllocationStrategy != nil && (*distribution.SpotAllocationStrategy == SpotAllocationStrategyCapacityOptimized || *distribution.SpotAllocationStrategy == SpotAllocationStrategyCapacityOptimizedPrioritized) {
		return fmt.Errorf("spotInstancePools cannot be specified when also specifying spotAllocationStrategy: %s", SpotAllocationStrategyCapacityOptimizedPrioritized)
	}

	if distribution.SpotAllocationStrategy != nil {
		if err := validateSpotAllocationStrategy(*distribution.SpotAllocationStrategy); err != nil {
			return err
		}
	}

	return nil
}

func validateCPUCredits(ng *NodeGroup) error {
	isTInstance := false
	instanceTypes := []string{ng.InstanceType}

	if ng.CPUCredits == nil {
		return nil
	}

	if ng.InstancesDistribution != nil {
		instanceTypes = ng.InstancesDistribution.InstanceTypes
	}

	for _, instanceType := range instanceTypes {
		if strings.HasPrefix(instanceType, "t") {
			isTInstance = true
		}
	}

	if !isTInstance {
		return fmt.Errorf("cpuCredits option set for nodegroup, but it has no t2/t3 instance types")
	}

	if strings.ToLower(*ng.CPUCredits) != "unlimited" && strings.ToLower(*ng.CPUCredits) != "standard" {
		return fmt.Errorf("cpuCredits option accepts only one of 'standard' or 'unlimited'")
	}

	return nil
}

func validateASGSuspendProcesses(ng *NodeGroup) error {
	// Processes list taken from here: https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_SuspendProcesses.html
	for _, proc := range ng.ASGSuspendProcesses {
		switch proc {
		case
			"Launch",
			"Terminate",
			"AddToLoadBalancer",
			"AlarmNotification",
			"AZRebalance",
			"HealthCheck",
			"InstanceRefresh",
			"ReplaceUnhealthy",
			"ScheduledActions":
			continue
		default:
			return fmt.Errorf("asgSuspendProcesses contains invalid process name '%s'", proc)
		}
	}
	return nil
}

func validateNodeGroupSSH(SSH *NodeGroupSSH) error {
	numSSHFlagsEnabled := countEnabledFields(
		SSH.PublicKeyPath,
		SSH.PublicKey,
		SSH.PublicKeyName)

	if numSSHFlagsEnabled > 1 {
		return errors.New("only one of publicKeyName, publicKeyPath or publicKey can be specified for SSH per node-group")
	}
	return nil
}

func countEnabledFields(fields ...*string) int {
	count := 0
	for _, flag := range fields {
		if flag != nil && *flag != "" {
			count++
		}
	}
	return count
}

func validateNodeGroupKubeletExtraConfig(kubeletConfig *InlineDocument) error {
	if kubeletConfig == nil {
		return nil
	}

	var kubeletForbiddenFields = map[string]struct{}{
		"kind":               {},
		"apiVersion":         {},
		"address":            {},
		"clusterDomain":      {},
		"authentication":     {},
		"authorization":      {},
		"serverTLSBootstrap": {},
	}

	for k := range *kubeletConfig {
		if _, exists := kubeletForbiddenFields[k]; exists {
			return fmt.Errorf("cannot override %q in kubelet config, as it's critical to eksctl functionality", k)
		}
	}
	return nil
}

func isSupportedAMIFamily(imageFamily string) bool {
	for _, image := range SupportedAMIFamilies() {
		if imageFamily == image {
			return true
		}
	}
	return false
}

func supportedAMIFamiliesForOS(isOSImage func(string) bool) []string {
	amiFamilies := []string{}
	for _, image := range SupportedAMIFamilies() {
		if isOSImage(image) {
			amiFamilies = append(amiFamilies, image)
		}
	}
	return amiFamilies
}

func IsAmazonLinuxImage(imageFamily string) bool {
	switch imageFamily {
	case NodeImageFamilyAmazonLinux2023,
		NodeImageFamilyAmazonLinux2:
		return true

	default:
		return false
	}
}

func IsBottlerocketImage(imageFamily string) bool {
	switch imageFamily {
	case NodeImageFamilyBottlerocket:
		return true

	default:
		return false
	}
}

func IsUbuntuImage(imageFamily string) bool {
	switch imageFamily {
	case NodeImageFamilyUbuntuPro2404,
		NodeImageFamilyUbuntu2404,
		NodeImageFamilyUbuntuPro2204,
		NodeImageFamilyUbuntu2204,
		NodeImageFamilyUbuntuPro2004,
		NodeImageFamilyUbuntu2004:
		return true

	default:
		return false
	}
}

// IsWindowsImage reports whether the AMI family is for Windows
func IsWindowsImage(imageFamily string) bool {
	switch imageFamily {
	case NodeImageFamilyWindowsServer2019CoreContainer,
		NodeImageFamilyWindowsServer2019FullContainer,
		NodeImageFamilyWindowsServer2022CoreContainer,
		NodeImageFamilyWindowsServer2022FullContainer:
		return true

	default:
		return false
	}
}

func validateCIDRs(cidrs []string) ([]string, error) {
	var validCIDRs []string
	for _, cidr := range cidrs {
		_, ipNet, err := net.ParseCIDR(cidr)
		if err != nil {
			return nil, err
		}
		validCIDRs = append(validCIDRs, ipNet.String())
	}
	return validCIDRs, nil
}

func validateTaints(ngTaints []NodeGroupTaint) error {
	for _, t := range ngTaints {
		if err := taints.Validate(corev1.Taint{
			Key:    t.Key,
			Value:  t.Value,
			Effect: t.Effect,
		}); err != nil {
			return err
		}
	}
	return nil
}

// ReservedProfileNamePrefix defines the Fargate profile name prefix reserved
// for AWS, and which therefore, cannot be used by users. AWS' API should
// reject the creation of profiles starting with this prefix, but we eagerly
// validate this client-side.
const ReservedProfileNamePrefix = "eks-"

// Validate validates this FargateProfile object.
func (fp FargateProfile) Validate() error {
	if fp.Name == "" {
		return errors.New("invalid Fargate profile: empty name")
	}
	if strings.HasPrefix(fp.Name, ReservedProfileNamePrefix) {
		return fmt.Errorf("invalid Fargate profile %q: name should NOT start with %q", fp.Name, ReservedProfileNamePrefix)
	}
	if len(fp.Selectors) == 0 {
		return fmt.Errorf("invalid Fargate profile %q: no profile selector", fp.Name)
	}
	for i, selector := range fp.Selectors {
		if err := selector.Validate(); err != nil {
			return fmt.Errorf("invalid Fargate profile %q: invalid profile selector at index #%v: %w", fp.Name, i, err)
		}
	}
	return nil
}

// Validate validates this FargateProfileSelector object.
func (fps FargateProfileSelector) Validate() error {
	if fps.Namespace == "" {
		return errors.New("empty namespace")
	}
	return nil
}

func checkBottlerocketSettings(ng *NodeGroup, path string) error {
	overlapErr := func(kubernetesField, ngField string) error {
		return fmt.Errorf("invalid Bottlerocket setting: use %[1]s.%[2]s instead (path=%[1]s.bottlerocket.settings.kubernetes.%[3]s)", path, ngField, kubernetesField)
	}

	// Dig into kubernetes settings if provided.
	kubeVal, ok := (*ng.Bottlerocket.Settings)["kubernetes"]
	if !ok {
		return nil
	}

	kube, ok := kubeVal.(map[string]interface{})
	if !ok {
		return errors.New("invalid kubernetes settings provided: expected a map of settings")
	}

	checkMapping := map[string]string{
		"node-labels": "labels",
		"node-taints": "taints",
		"max-pods":    "maxPodsPerNode",
	}

	for checkKey, shouldUse := range checkMapping {
		_, ok := kube[checkKey]
		if ok {
			return overlapErr(checkKey, shouldUse)
		}
	}

	if _, ok := kube["cluster-dns-ip"]; ok && ng.ClusterDNS != "" {
		return fmt.Errorf("only one of %[1]s.bottlerocket.settings.kubernetes.cluster-dns-ip or %[1]s.clusterDNS can be set", path)
	}

	return nil
}

func validateAvailabilityZones(azList []string) error {
	count := len(azList)
	switch {
	case count == 0:
		return nil
	case count < MinRequiredAvailabilityZones:
		return ErrTooFewAvailabilityZones(azList)
	default:
		return nil
	}
}

func ErrTooFewAvailabilityZones(azs []string) error {
	return fmt.Errorf("only %d zone(s) specified %v, %d are required (can be non-unique)", len(azs), azs, MinRequiredAvailabilityZones)
}

func ValidateSecretsEncryption(clusterConfig *ClusterConfig) error {
	if clusterConfig.SecretsEncryption == nil {
		return nil
	}

	if clusterConfig.SecretsEncryption.KeyARN == "" {
		return errors.New("field secretsEncryption.keyARN is required for enabling secrets encryption")
	}

	if _, err := arn.Parse(clusterConfig.SecretsEncryption.KeyARN); err != nil {
		return fmt.Errorf("invalid ARN in secretsEncryption.keyARN: %q: %w", clusterConfig.SecretsEncryption.KeyARN, err)
	}
	return nil
}

// ToPodIdentityAssociationID extracts the pod identity association ID from piaARN.
// The ARN is of the format: arn:aws:eks:us-west-2:000:podidentityassociation/cluster/a-d3dw7wfvxtoatujeg.
func ToPodIdentityAssociationID(piaARN string) (string, error) {
	parsed, err := arn.Parse(piaARN)
	if err != nil {
		return "", fmt.Errorf("parsing ARN %q: %w", piaARN, err)
	}
	parts := strings.Split(parsed.Resource, "/")
	if len(parts) != 3 {
		return "", fmt.Errorf("unexpected pod identity association ARN format: %q", parsed.String())
	}
	return parts[len(parts)-1], nil
}

func validateIAMIdentityMappings(clusterConfig *ClusterConfig) error {
	for _, mapping := range clusterConfig.IAMIdentityMappings {
		if err := mapping.Validate(); err != nil {
			return err
		}
	}
	return nil
}

func validateAddonPodIdentityAssociations(addons []*Addon) error {
	for _, addon := range addons {
		makeAddonErr := func(msg string) error {
			return fmt.Errorf("%s (addon: %s)", msg, addon.Name)
		}
		if addon.PodIdentityAssociations != nil {
			for _, pia := range *addon.PodIdentityAssociations {
				if pia.WellKnownPolicies.HasPolicy() {
					return makeAddonErr("wellKnownPolicies is not supported for addon.podIdentityAssociations; use addon.useDefaultPodIdentityAssociations instead")
				}
				if pia.Tags != nil {
					return makeAddonErr("tags is not supported for addon.podIdentityAssociations")
				}
			}
		}
		if addon.UseDefaultPodIdentityAssociations {
			if addon.HasPodIDsSet() {
				return makeAddonErr("cannot specify both addon.useDefaultPodIdentityAssociations and addon.podIdentityAssociations")
			}
			if addon.ServiceAccountRoleARN != "" || addon.WellKnownPolicies.HasPolicy() || len(addon.AttachPolicy) > 0 || len(addon.AttachPolicyARNs) > 0 {
				return makeAddonErr("cannot specify serviceAccountRoleARN, wellKnownPolicies, attachPolicy or attachPolicyARNs" +
					" when addon.useDefaultPodIdentityAssociations is set")
			}
		}
	}
	return nil
}
