package eks

import (
	"context"
	"encoding/binary"
	"fmt"
	"net"
	"sort"
	"strings"
	"time"

	"github.com/aws/aws-sdk-go-v2/service/ec2"
	"github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	"github.com/crossplane-contrib/provider-aws/apis/ec2/v1beta1"
	eksv1beta1 "github.com/crossplane-contrib/provider-aws/apis/eks/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	"github.com/hashicorp/go-multierror"
	"github.com/pingcap/errors"
	"k8s.io/apimachinery/pkg/api/equality"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	eksv1alpha1 "github.com/tidbcloud/eks-provider/api/v1alpha1"
	"github.com/tidbcloud/eks-provider/controllers/network/task"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/cloud/aws"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/util"
)

// VPC manage vpc.
type VPC struct {
	*task.BaseTask
	awsCli aws.Client
}

// NewVPC create VPC instance.
func NewVPC(ctx *task.Context, awsCli aws.Client) *VPC {
	return &VPC{BaseTask: &task.BaseTask{Context: ctx}, awsCli: awsCli}
}

var _ deploy.Task = &VPC{}

// Name implements Task interface.
func (t *VPC) Name() string {
	return "VPC"
}

// Create implements Task interface.
// Note: return deploy.Panic once VPC is not set up correctly.
func (t *VPC) Create(ctx context.Context) deploy.Result {
	// If network status already hibernated and not migrated network, skip vpc task.
	if t.Network.Status.Phase == cloudv1.NetworkPhaseHibernated && !eksv1alpha1.IsMigrate(t.Network) {
		return deploy.Done()
	}
	t.BeginAt = util.Pointer(time.Now())
	name := namer.UniqueIdentifier(t.Network.ObjectMeta)
	tags := util.AddResourceName(cloudv1.TagsFromLabels(t.Network.Labels), name)

	vpc := v1beta1.VPC{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "ec2.aws.crossplane.io/v1beta1",
			Kind:       "VPC",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: t.Namer.Labels(),
		},
		Spec: v1beta1.VPCSpec{
			ForProvider: v1beta1.VPCParameters{
				Region:             pointer.String(t.Network.Spec.Region),
				CIDRBlock:          t.Network.Spec.CIDR,
				EnableDNSSupport:   pointer.Bool(true),
				EnableDNSHostNames: pointer.Bool(true),
				Tags:               util.GetEC2V1BetaTags(tags),
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: name},
			},
		},
	}
	if eksv1alpha1.IsMigrate(t.Network) {
		spec, err := eksv1alpha1.EKSNetworkSpecForNetwork(t.Network)
		if err != nil {
			return deploy.Panic(err)
		}

		setExternalName(&vpc, spec.VpcId)
	}
	if err := k8s.Apply(ctx, t.Local, &vpc); err != nil {
		return deploy.Panic(err)
	}

	return t.createSubResources(ctx)
}

// PostCreate implements Task interface.
func (t *VPC) PostCreate(ctx context.Context) deploy.Result {
	return deploy.Done()
}

// 1. create internet gateway.
// 2. create security group for eks.
// 3. create subnets and NAT gateway(ip and route table).
// for each zone create 3 subnets:
//   - nodes-<zone>: used for node group
//   - private-<zone>: used for private LB
//   - public-<zone>: used for public LB and accessing internet(while NAT now).
//
// for each zone create:
//
//	a Address(ipv4) for the NAT
//	a NAT in the public subnet
//
// for each zone create:
//   - a RouteTable for public subnets
//   - a RouteTable for private subnets(nodes-<zone> and private-<zone>)
//
// 4. write spec into network.
// TODO: split this LONG function into several functions.
// NOTE: Please return deploy.Panic() if meet error now to abort the following tasks.
func (t *VPC) createSubResources(ctx context.Context) deploy.Result {
	tags := cloudv1.TagsFromLabels(t.Network.Labels)

	name := namer.UniqueIdentifier(t.Network.ObjectMeta)
	key := types.NamespacedName{
		Name: name,
	}

	vpc := &v1beta1.VPC{}
	err := t.Local.Get(ctx, key, vpc)
	if err != nil {
		return deploy.Panic(err)
	}

	status := vpc.Status.AtProvider.VPCState
	if status != "available" {
		return deploy.Panic(fmt.Errorf("vpc status not available: %v", status)).WithRequeuAfter(time.Second * 3)
	}

	// 1. create internet gate.
	tags = util.AddResourceName(tags, name)
	ig := &v1beta1.InternetGateway{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
		Spec: v1beta1.InternetGatewaySpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: name},
			},
			ForProvider: v1beta1.InternetGatewayParameters{
				Region: pointer.String(t.Network.Spec.Region),
				VPCIDRef: &xpv1.Reference{
					Name: name,
				},
				Tags: util.GetEC2V1BetaTags(tags),
			},
		},
	}
	err = controllerutil.SetControllerReference(vpc, ig, t.Local.Scheme())
	if err != nil {
		return deploy.Panic(err)
	}

	_, err = controllerutil.CreateOrUpdate(ctx, t.Local, ig, func() error {
		if ig.CreationTimestamp.IsZero() {
			if v, ok := t.Network.Annotations[eksv1alpha1.NetworkInternetGatewayIDAnnKey]; ok {
				setExternalName(ig, v)
			}
		}
		return nil
	})
	if err != nil {
		return deploy.Panic(err)
	}

	// 2. create secure group
	sg := &v1beta1.SecurityGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
		Spec: v1beta1.SecurityGroupSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: name},
			},
			ForProvider: v1beta1.SecurityGroupParameters{
				Region:      t.Network.Spec.Region,
				GroupName:   name,
				Description: "secure group for eks",
				Egress: []v1beta1.IPPermission{
					// Allow all traffic
					{
						IPProtocol: "-1",
						IPRanges: []v1beta1.IPRange{
							{
								CIDRIP: "0.0.0.0/0",
							},
						},
					},
				},
				Tags: util.GetEC2V1BetaTags(util.AddResourceName(tags, name)),
				VPCIDRef: &xpv1.Reference{
					Name: name,
				},
			},
		},
	}

	ingress := []v1beta1.IPPermission{
		{
			IPProtocol: "tcp",
			FromPort:   pointer.Int32(443),
			ToPort:     pointer.Int32(443),
			IPRanges: []v1beta1.IPRange{
				{
					CIDRIP: "0.0.0.0/0",
				},
			},
		},
		// For metrics agents send data to o11y infra via interface endpoints.
		{
			IPProtocol: "tcp",
			FromPort:   pointer.Int32(80),
			ToPort:     pointer.Int32(80),
			IPRanges: []v1beta1.IPRange{
				{
					CIDRIP: "0.0.0.0/0",
				},
			},
		},
		// For metrics agents send data to seed vector via interface endpoint.
		{
			IPProtocol: "tcp",
			FromPort:   pointer.Int32(9090),
			ToPort:     pointer.Int32(9090),
			IPRanges: []v1beta1.IPRange{
				{
					CIDRIP: "0.0.0.0/0",
				},
			},
		},
		// All Private Internets, See http://www.faqs.org/rfcs/rfc1918.html for more detail
		// CDC X-Region create a VPC peering connection between the source network and the target network in the other region.
		// Add security group rules so that downstream PD can access upstream TiKV
		{
			// all protocols
			IPProtocol: "-1",
			IPRanges: []v1beta1.IPRange{
				{
					CIDRIP: "10.0.0.0/8",
				},
				{
					CIDRIP: "172.16.0.0/12",
				},
				{
					CIDRIP: "192.168.0.0/16",
				},
			},
		},
	}
	_, err = controllerutil.CreateOrUpdate(ctx, t.Local, sg, func() error {
		err = controllerutil.SetControllerReference(vpc, sg, t.Local.Scheme())
		if err != nil {
			return err
		}

		ruleForEp := t.addSecureGroupRuleForPrivateLinkEndPoint(ctx, name)
		if ruleForEp != nil {
			// support to access private link endpoint
			ingress = append(ingress, *ruleForEp)
		}
		sg.Spec.ForProvider.Ingress = ingress

		return nil
	})
	if err != nil {
		return deploy.Panic(err)
	}

	// create subnets
	zones, err := t.Config.FindRegionZones(t.Network.Spec.Region)
	if err != nil {
		return deploy.Panic(err)
	}

	workers, public, private, err := GetAWSCIDR(t.Network.Spec.CIDR, t.Network.Spec.Region, len(zones))
	if err != nil {
		return deploy.Panic(err)
	}

	eksName := namer.UniqueIdentifier(t.Network.ObjectMeta)

	var nodeSubnets []*v1beta1.Subnet
	var publicSubnets []*v1beta1.Subnet
	var natGateways []*v1beta1.NATGateway

	var natGatewayIDs []string
	var privateRouteTableIDs []string
	var publicRouteTableID string
	if v, ok := t.Network.Annotations[eksv1alpha1.NetworkNatGatewayIDsAnnKey]; ok {
		natGatewayIDs = strings.Split(v, ",")
	}
	if v, ok := t.Network.Annotations[eksv1alpha1.NetworkPrivateRouteTableIDsAnnKey]; ok {
		privateRouteTableIDs = strings.Split(v, ",")
	}
	if v, ok := t.Network.Annotations[eksv1alpha1.NetworkPublicRouteTableIDAnnKey]; ok {
		publicRouteTableID = v
	}

	for i := 0; i < len(workers); i++ {
		zone := zones[i]

		// node subnet
		nodeSubnet := &v1beta1.Subnet{
			ObjectMeta: metav1.ObjectMeta{
				Name: namer.NodesSubnetName(name, zone),
			},
			Spec: v1beta1.SubnetSpec{
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: name},
				},
				ForProvider: v1beta1.SubnetParameters{
					Region:           pointer.String(t.Network.Spec.Region),
					CIDRBlock:        workers[i],
					AvailabilityZone: pointer.String(zone),
					Tags:             util.GetEC2V1BetaTags(util.AddResourceName(tags, namer.NodesSubnetName(name, zone))),
					VPCIDRef: &xpv1.Reference{
						Name: name,
					},
				},
			},
		}
		_, err = controllerutil.CreateOrUpdate(ctx, t.Local, nodeSubnet, func() error {
			err := controllerutil.SetControllerReference(vpc, nodeSubnet, t.Local.Scheme())
			if err != nil {
				return err
			}

			if eksv1alpha1.IsMigrate(t.Network) {
				spec, err := eksv1alpha1.EKSNetworkSpecForNetwork(t.Network)
				if err != nil {
					return err
				}

				setExternalName(nodeSubnet, spec.SubnetIds[i])
			}
			return nil
		})
		if err != nil {
			return deploy.Panic(err)
		}
		nodeSubnets = append(nodeSubnets, nodeSubnet)

		// public subnet
		tags = util.AddResourceName(tags, namer.PublicSubnetName(name, zone))
		publicSubnettags := util.GetEC2V1BetaTags(tags)
		// https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.1/deploy/subnet_discovery/#public-subnets
		publicSubnettags = append(publicSubnettags,
			v1beta1.Tag{
				Key:   "kubernetes.io/role/elb",
				Value: "1",
			},
			v1beta1.Tag{
				Key:   eksClusterNameTagKey(eksName),
				Value: "owned",
			},
		)
		if name, ok := t.Network.Annotations[eksv1alpha1.ExtraK8SClusterNameAnnKey]; ok {
			publicSubnettags = append(publicSubnettags,
				v1beta1.Tag{
					Key:   eksClusterNameTagKey(name),
					Value: "1",
				},
			)
		}
		publicSubnet := &v1beta1.Subnet{
			TypeMeta: metav1.TypeMeta{
				APIVersion: "ec2.aws.crossplane.io/v1beta1",
				Kind:       "Subnet",
			},
			ObjectMeta: metav1.ObjectMeta{
				Name: namer.PublicSubnetName(name, zone),
			},
			Spec: v1beta1.SubnetSpec{
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: name},
				},
				ForProvider: v1beta1.SubnetParameters{
					Region:           pointer.String(t.Network.Spec.Region),
					CIDRBlock:        public[i],
					AvailabilityZone: pointer.String(zone),
					Tags:             publicSubnettags,
					VPCIDRef: &xpv1.Reference{
						Name: name,
					},
				},
			},
		}
		if eksv1alpha1.IsMigrate(t.Network) {
			spec, err := eksv1alpha1.EKSNetworkSpecForNetwork(t.Network)
			if err != nil {
				return deploy.Panic(err)
			}
			setExternalName(publicSubnet, spec.PublicSubnetIds[i])
		}
		if err = controllerutil.SetControllerReference(vpc, publicSubnet, t.Local.Scheme()); err != nil {
			return deploy.Panic(err)
		}
		if err = k8s.Apply(ctx, t.Local, publicSubnet); err != nil {
			return deploy.Panic(err)
		}
		publicSubnets = append(publicSubnets, publicSubnet)

		// private subnet
		tags = util.AddResourceName(tags, namer.PrivateSubnetName(name, zone))
		privateSubnetTags := util.GetEC2V1BetaTags(tags)
		// https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.1/deploy/subnet_discovery/#public-subnets
		privateSubnetTags = append(privateSubnetTags,
			v1beta1.Tag{
				Key:   "kubernetes.io/role/internal-elb",
				Value: "1",
			},
			v1beta1.Tag{
				Key:   eksClusterNameTagKey(eksName),
				Value: "owned",
			},
		)
		if name, ok := t.Network.Annotations[eksv1alpha1.ExtraK8SClusterNameAnnKey]; ok {
			privateSubnetTags = append(privateSubnetTags,
				v1beta1.Tag{
					Key:   fmt.Sprintf("kubernetes.io/cluster/%s", name),
					Value: "1",
				},
			)
		}
		privateSubnet := &v1beta1.Subnet{
			TypeMeta: metav1.TypeMeta{
				APIVersion: "ec2.aws.crossplane.io/v1beta1",
				Kind:       "Subnet",
			},
			ObjectMeta: metav1.ObjectMeta{
				Name: namer.PrivateSubnetName(name, zone),
			},
			Spec: v1beta1.SubnetSpec{
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: name},
				},
				ForProvider: v1beta1.SubnetParameters{
					Region:           pointer.String(t.Network.Spec.Region),
					CIDRBlock:        private[i],
					AvailabilityZone: pointer.String(zone),
					Tags:             privateSubnetTags,
					VPCIDRef: &xpv1.Reference{
						Name: name,
					},
				},
			},
		}
		if eksv1alpha1.IsMigrate(t.Network) {
			spec, err := eksv1alpha1.EKSNetworkSpecForNetwork(t.Network)
			if err != nil {
				return deploy.Panic(err)
			}
			setExternalName(privateSubnet, spec.PrivateSubnetIds[i])
		}
		if err = controllerutil.SetControllerReference(vpc, privateSubnet, t.Local.Scheme()); err != nil {
			return deploy.Panic(err)
		}
		if err = k8s.Apply(ctx, t.Local, privateSubnet); err != nil {
			return deploy.Panic(err)
		}

		tags = util.AddResourceName(tags, namer.PublicSubnetName(name, zone))
		// create NAT gateway(IP for NAT)
		ip := &v1beta1.Address{
			ObjectMeta: metav1.ObjectMeta{
				Name: namer.PublicSubnetName(name, zone),
			},
			Spec: v1beta1.AddressSpec{
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: name},
				},
				ForProvider: v1beta1.AddressParameters{
					Region: t.Network.Spec.Region,
					Domain: pointer.String("vpc"),
					Tags:   util.GetEC2V1BetaTags(tags),
				},
			},
		}
		err = controllerutil.SetControllerReference(privateSubnet, ip, t.Local.Scheme())
		if err != nil {
			return deploy.Panic(err)
		}
		_, err = controllerutil.CreateOrUpdate(ctx, t.Local, ip, func() error {
			return nil
		})
		if err != nil {
			return deploy.Panic(err)
		}

		allocationID := meta.GetExternalName(ip)
		publicSubnetID := meta.GetExternalName(publicSubnet)

		// TODO(zhengjiajin): remove this when provider-aws can get reference ID
		if allocationID == "" || publicSubnetID == "" {
			return deploy.Panic(fmt.Errorf("failed to get allocationID or publicSubnetID")).WithRequeuAfter(time.Second * 3)
		}

		tags = util.AddResourceName(tags, namer.PublicSubnetName(name, zone))
		nat := &v1beta1.NATGateway{
			ObjectMeta: metav1.ObjectMeta{
				Name: namer.PublicSubnetName(name, zone),
			},
			Spec: v1beta1.NATGatewaySpec{
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: name},
				},
				ForProvider: v1beta1.NATGatewayParameters{
					Region:           t.Network.Spec.Region,
					SubnetID:         pointer.String(publicSubnetID),
					ConnectivityType: "public",
					Tags:             util.GetEC2V1BetaTags(tags),
				},
			},
		}
		err = controllerutil.SetControllerReference(publicSubnet, nat, t.Local.Scheme())
		if err != nil {
			return deploy.Panic(err)
		}
		_, err = controllerutil.CreateOrUpdate(ctx, t.Local, nat, func() error {
			// If Address is recreated, we should update AllocationID.
			nat.Spec.ForProvider.AllocationID = pointer.String(allocationID)

			if nat.CreationTimestamp.IsZero() {
				if len(natGatewayIDs) > 0 {
					setExternalName(nat, natGatewayIDs[i])
				}
			}
			return nil
		})
		if err != nil {
			return deploy.Panic(err)
		}
		natGateways = append(natGateways, nat)

		natgatewayID := meta.GetExternalName(nat)
		internelGatewayID := meta.GetExternalName(ig)
		nodeSubnetID := meta.GetExternalName(nodeSubnet)
		privateubnetID := meta.GetExternalName(privateSubnet)
		// TODO(zhengjiajin): remove this when provider-aws can get reference ID
		if natgatewayID == "" || internelGatewayID == "" || nodeSubnetID == "" || privateubnetID == "" {
			return deploy.Panic(fmt.Errorf("failed to get natgatewayID or nodeSubnetID or internelGatewayID or privateubnetID")).WithRequeuAfter(time.Second * 3)
		}

		// create RouteTable one for public and one for private subnet.
		tags = util.AddResourceName(tags, namer.PrivateSubnetName(name, zone))
		privateRT := &v1beta1.RouteTable{
			TypeMeta: metav1.TypeMeta{
				APIVersion: "ec2.aws.crossplane.io/v1beta1",
				Kind:       "RouteTable",
			},
			ObjectMeta: metav1.ObjectMeta{
				Name: namer.PrivateSubnetName(name, zone),
			},
			Spec: v1beta1.RouteTableSpec{
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: name},
				},
				ForProvider: v1beta1.RouteTableParameters{
					Region: t.Network.Spec.Region,
					Associations: []v1beta1.Association{
						{
							SubnetIDRef: &xpv1.Reference{Name: nodeSubnet.Name},
						},
						{
							SubnetIDRef: &xpv1.Reference{Name: privateSubnet.Name},
						},
					},
					// Ignore routes in route table controller, we will create routes in route controller.
					// See https://github.com/crossplane-contrib/provider-aws/issues/1036#issuecomment-1003075859 for more detail
					IgnoreRoutes: pointer.Bool(true),
					Tags:         util.GetEC2V1BetaTags(tags),
					VPCIDRef:     &xpv1.Reference{Name: name},
				},
			},
		}
		if len(privateRouteTableIDs) > 0 {
			setExternalName(privateRT, privateRouteTableIDs[i])
		}
		if err = controllerutil.SetControllerReference(vpc, privateRT, t.Local.Scheme()); err != nil {
			return deploy.Panic(err)
		}
		if err = k8s.Apply(ctx, t.Local, privateRT); err != nil {
			return deploy.Panic(err)
		}

		if !nat.DeletionTimestamp.IsZero() {
			return deploy.Panic(fmt.Errorf("Wait nat gateway %s delete completed", nat.Name))
		}

		privateRoute := &v1alpha1.Route{
			TypeMeta: metav1.TypeMeta{
				APIVersion: "ec2.aws.crossplane.io/v1alpha1",
				Kind:       "Route",
			},
			ObjectMeta: metav1.ObjectMeta{
				Name: namer.PrivateSubnetName(name, zone),
			},
			Spec: v1alpha1.RouteSpec{
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: name},
				},
				ForProvider: v1alpha1.RouteParameters{
					Region:               t.Network.Spec.Region,
					DestinationCIDRBlock: pointer.String("0.0.0.0/0"),
					CustomRouteParameters: v1alpha1.CustomRouteParameters{
						NATGatewayIDRef: &xpv1.Reference{Name: nat.Name},
						RouteTableIDRef: &xpv1.Reference{Name: privateRT.Name},
					},
				},
			},
		}
		if err = k8s.Apply(ctx, t.Local, privateRoute); err != nil {
			return deploy.Panic(err)
		}
		if !privateRoute.DeletionTimestamp.IsZero() {
			return deploy.Panic(fmt.Errorf("Wait private route %s delete completed", privateRT.Name))
		}
	}

	for i := 0; i < len(workers); i++ {
		zone := zones[i]
		publicRouteTableName := namer.PublicSubnetName(name, zone)
		// intend to use the same route table for all public subnet(shoot use in this way)
		// since all using the same routes here.
		var associations []v1beta1.Association
		// gardener will create one public route association all public subnet
		// migrated network will follow gardener behavior
		createOnePublicRoute := false
		if publicRouteTableID != "" {
			createOnePublicRoute = true
			publicRouteTableName = vpcPublicRouteTableName(name)
			for _, publicSubnet := range publicSubnets {
				associations = append(associations, v1beta1.Association{SubnetIDRef: &xpv1.Reference{Name: publicSubnet.Name}})
			}
		} else {
			associations = []v1beta1.Association{
				{
					SubnetIDRef: &xpv1.Reference{Name: publicSubnets[i].Name},
				},
			}
		}

		tags = util.AddResourceName(tags, publicRouteTableName)
		publicRT := &v1beta1.RouteTable{
			TypeMeta: metav1.TypeMeta{
				APIVersion: "ec2.aws.crossplane.io/v1beta1",
				Kind:       "RouteTable",
			},
			ObjectMeta: metav1.ObjectMeta{
				Name: publicRouteTableName,
			},
			Spec: v1beta1.RouteTableSpec{
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: name},
				},
				ForProvider: v1beta1.RouteTableParameters{
					Region:       t.Network.Spec.Region,
					Associations: associations,
					// Ignore routes in route table controller, we will create routes in route controller.
					// See https://github.com/crossplane-contrib/provider-aws/issues/1036#issuecomment-1003075859 for more detail
					IgnoreRoutes: pointer.Bool(true),
					Tags:         util.GetEC2V1BetaTags(tags),
					VPCIDRef:     &xpv1.Reference{Name: name},
				},
			},
		}
		if len(publicRouteTableID) > 0 {
			setExternalName(publicRT, publicRouteTableID)
		}
		if err = controllerutil.SetControllerReference(vpc, publicRT, t.Local.Scheme()); err != nil {
			return deploy.Panic(err)
		}
		if err = k8s.Apply(ctx, t.Local, publicRT); err != nil {
			return deploy.Panic(err)
		}

		publicRoute := &v1alpha1.Route{
			TypeMeta: metav1.TypeMeta{
				APIVersion: "ec2.aws.crossplane.io/v1alpha1",
				Kind:       "Route",
			},
			ObjectMeta: metav1.ObjectMeta{
				Name: publicRouteTableName,
			},
			Spec: v1alpha1.RouteSpec{
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: name},
				},
				ForProvider: v1alpha1.RouteParameters{
					Region:               t.Network.Spec.Region,
					DestinationCIDRBlock: pointer.String("0.0.0.0/0"),
					CustomRouteParameters: v1alpha1.CustomRouteParameters{
						GatewayIDRef:    &xpv1.Reference{Name: ig.Name},
						RouteTableIDRef: &xpv1.Reference{Name: publicRT.Name},
					},
				},
			},
		}
		if err = k8s.Apply(ctx, t.Local, publicRoute); err != nil {
			return deploy.Panic(err)
		}

		if createOnePublicRoute {
			break
		}
	}

	var subnetIDs []string
	for _, subnet := range nodeSubnets {
		id := subnet.Status.AtProvider.SubnetID
		if id == "" {
			return deploy.Panic(errors.New("subnet not ready, 'Status.AtProvider.SubnetID' is empty"))
		}
		subnetIDs = append(subnetIDs, id)
	}

	// TODO check sg & subnets status ?

	needUpdate, err := writeNetworkSpec(t.Network, vpc, subnetIDs, sg, natGateways)
	if err != nil {
		return deploy.Panic(err)
	}
	if needUpdate {
		return deploy.Panic(nil).WithRequeuAfter(0)
	}

	return deploy.Done()
}

func (t *VPC) addSecureGroupRuleForPrivateLinkEndPoint(ctx context.Context, name string) *v1beta1.IPPermission {
	groupId, err := t.queryEKSSecurityGroup(ctx, name)
	if err != nil || groupId == nil {
		return nil
	}

	return &v1beta1.IPPermission{
		IPProtocol: "tcp",
		FromPort:   pointer.Int32(0),
		ToPort:     pointer.Int32(65535),
		UserIDGroupPairs: []v1beta1.UserIDGroupPair{
			{
				GroupID: groupId,
			},
		},
	}
}

func (t *VPC) queryEKSSecurityGroup(ctx context.Context, name string) (*string, error) {
	var eks eksv1beta1.Cluster
	err := t.Local.Get(ctx, types.NamespacedName{Name: name}, &eks)
	if err != nil {
		if apierrors.IsNotFound(err) {
			return nil, nil
		} else {
			return nil, err
		}
	}

	if eks.Status.AtProvider.ResourcesVpcConfig.ClusterSecurityGroupID == "" {
		return nil, nil
	} else {
		return &eks.Status.AtProvider.ResourcesVpcConfig.ClusterSecurityGroupID, nil
	}
}

// Delete implements Task interface.
func (t *VPC) Delete(ctx context.Context) deploy.Result {
	name := namer.UniqueIdentifier(t.Network.ObjectMeta)

	// delete internet gateway.
	ig := &v1beta1.InternetGateway{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}
	err := k8s.DeleteObject(ctx, t.Local, ig)
	if err != nil {
		return deploy.Error(err)
	}

	// delete SecurityGroup
	sg := &v1beta1.SecurityGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}
	err = k8s.DeleteObject(ctx, t.Local, sg)
	if err != nil {
		return deploy.Error(err)
	}

	if _, ok := t.Network.Annotations[eksv1alpha1.NetworkPublicRouteTableIDAnnKey]; ok {
		publicRouteTableName := vpcPublicRouteTableName(name)
		publicRoute := &v1alpha1.Route{
			ObjectMeta: metav1.ObjectMeta{
				Name: publicRouteTableName,
			},
		}
		err = k8s.DeleteObject(ctx, t.Local, publicRoute)
		if err != nil {
			return deploy.Error(err)
		}
	}

	// delete subnet
	zones, err := t.Config.FindRegionZones(t.Network.Spec.Region)
	if err != nil {
		return deploy.Panic(err)
	}
	for i := 0; i < len(zones); i++ {
		zone := zones[i]

		subnet := &v1beta1.Subnet{
			ObjectMeta: metav1.ObjectMeta{
				Name: namer.NodesSubnetName(name, zone),
			},
		}
		err = k8s.DeleteObject(ctx, t.Local, subnet)
		if err != nil {
			return deploy.Error(err)
		}
		subnet = &v1beta1.Subnet{
			ObjectMeta: metav1.ObjectMeta{
				Name: namer.PublicSubnetName(name, zone),
			},
		}
		err = k8s.DeleteObject(ctx, t.Local, subnet)
		if err != nil {
			return deploy.Error(err)
		}
		subnet = &v1beta1.Subnet{
			ObjectMeta: metav1.ObjectMeta{
				Name: namer.PrivateSubnetName(name, zone),
			},
		}
		err = k8s.DeleteObject(ctx, t.Local, subnet)
		if err != nil {
			return deploy.Error(err)
		}

		ip := &v1beta1.Address{
			ObjectMeta: metav1.ObjectMeta{
				Name: namer.PublicSubnetName(name, zone),
			},
		}
		err = k8s.DeleteObject(ctx, t.Local, ip)
		if err != nil {
			return deploy.Error(err)
		}

		nat := &v1beta1.NATGateway{
			ObjectMeta: metav1.ObjectMeta{
				Name: namer.PublicSubnetName(name, zone),
			},
		}
		err = k8s.DeleteObject(ctx, t.Local, nat)
		if err != nil {
			return deploy.Error(err)
		}

		rt := &v1beta1.RouteTable{
			ObjectMeta: metav1.ObjectMeta{
				Name: namer.PrivateSubnetName(name, zone),
			},
		}
		err = k8s.DeleteObject(ctx, t.Local, rt)
		if err != nil {
			return deploy.Error(err)
		}

		re := &v1alpha1.Route{
			ObjectMeta: metav1.ObjectMeta{
				Name: namer.PrivateSubnetName(name, zone),
			},
		}
		err = k8s.DeleteObject(ctx, t.Local, re)
		if err != nil {
			return deploy.Error(err)
		}

		rt = &v1beta1.RouteTable{
			ObjectMeta: metav1.ObjectMeta{
				Name: namer.PublicSubnetName(name, zone),
			},
		}
		err = k8s.DeleteObject(ctx, t.Local, rt)
		if err != nil {
			return deploy.Error(err)
		}

		re = &v1alpha1.Route{
			ObjectMeta: metav1.ObjectMeta{
				Name: namer.PublicSubnetName(name, zone),
			},
		}
		err = k8s.DeleteObject(ctx, t.Local, re)
		if err != nil {
			return deploy.Error(err)
		}
	}

	// Delete all VPC endpoints, including the s3 gateway endpoint and interface endpoints.
	if err := deleteAllVPCEndpoints(ctx, t.Local, t.EKSNetworkSpec.VpcId); err != nil {
		return deploy.Error(err)
	}

	// delete vpc
	vpc := &v1beta1.VPC{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}
	return deploy.ErrorOrDone(k8s.DeleteObject(ctx, t.Local, vpc))
}

// PostDelete implements Task interface.
func (t *VPC) PostDelete(ctx context.Context) deploy.Result {
	name := namer.UniqueIdentifier(t.Network.ObjectMeta)
	key := types.NamespacedName{
		Name: name,
	}
	vpc := &v1beta1.VPC{}

	if err := k8s.GetDeletedObject(ctx, t.Local, key, vpc); err != nil {
		t.Logger.Info("deleting security groups")
		// It means the VPC still exists.
		// Delete all security groups, especially the eks created sg,
		// which often blocks the deletion of the VPC.
		if e := t.deleteAllSecurityGroups(ctx, t.EKSNetworkSpec.VpcId); e != nil {
			return deploy.Error(fmt.Errorf("failed to delete all security groups: %w", e))
		}
		return deploy.Error(err)
	}
	return deploy.Done()
}

func writeNetworkSpec(
	network *cloudv1.Network,
	vpc *v1beta1.VPC,
	subnetIDs []string,
	sg *v1beta1.SecurityGroup,
	natGateways []*v1beta1.NATGateway,
) (needUpdate bool, err error) {
	spec, err := eksv1alpha1.EKSNetworkSpecForNetwork(network)
	if err != nil {
		return needUpdate, err
	}

	oldSpec := spec.DeepCopy()
	vpcID := meta.GetExternalName(vpc)
	spec.VpcId = vpcID
	spec.SecurityGroupId = sg.Status.AtProvider.SecurityGroupID
	spec.SubnetIds = subnetIDs

	var externalIPs []string
	natGatewayReadyNumber := 0
	for _, natGateway := range natGateways {
		if len(natGateway.Status.AtProvider.NatGatewayAddresses) == 0 {
			continue
		}

		natGatewayReadyNumber++
		for _, address := range natGateway.Status.AtProvider.NatGatewayAddresses {
			externalIPs = append(externalIPs, address.PublicIP)
		}
	}
	sort.Slice(externalIPs, func(i, j int) bool {
		return externalIPs[i] < externalIPs[j]
	})
	network.Status.ExternalIPs = externalIPs

	if !equality.Semantic.DeepEqual(spec, oldSpec) {
		needUpdate = true
		err = eksv1alpha1.SetEKSNetworkSpec(network, spec)
	}

	if spec.VpcId == "" || spec.SecurityGroupId == "" ||
		natGatewayReadyNumber != len(natGateways) {
		return needUpdate, fmt.Errorf("resources not ready, vpcID: %v, security group id: %v, natGateWayNumber is: %v",
			spec.VpcId, spec.SecurityGroupId, natGatewayReadyNumber)
	}

	return needUpdate, err
}

// deleteAllVPCEndpoints deletes all VPC endpoints in the specified VPC.
func deleteAllVPCEndpoints(ctx context.Context, cli client.Client, vpcId string) error {
	var endpointList v1alpha1.VPCEndpointList
	if err := cli.List(ctx, &endpointList, client.MatchingFields(map[string]string{
		"spec.forProvider.vpcId": vpcId,
	})); err != nil {
		return err
	}

	var errs error
	for _, ep := range endpointList.Items {
		if err := k8s.DeleteObject(ctx, cli, &ep); err != nil {
			errs = multierror.Append(errs, errors.Annotate(err, fmt.Sprintf("failed to delete vpc endpoint: %s", ep.Name)))
		}
	}

	return errs
}

func (t *VPC) deleteAllSecurityGroups(ctx context.Context, vpcId string) error {
	cli := t.awsCli.EC2(aws.GetRoleFromEKSNetworkSpec(t.EKSNetworkSpec), aws.WithRegion(t.Network.Spec.Region))

	output, err := cli.GetSecurityGroupsForVpc(ctx, &ec2.GetSecurityGroupsForVpcInput{
		VpcId: pointer.String(vpcId),
	})
	if err != nil {
		return fmt.Errorf("failed to describe security groups: %w", err)
	}
	t.Logger.Info("describe security groups", "count", len(output.SecurityGroupForVpcs))

	for _, sg := range output.SecurityGroupForVpcs {
		if sg.GroupId != nil && *sg.GroupName != "default" {
			t.Logger.Info("deleting the security group", "id", *sg.GroupId, "name", *sg.GroupName)
			if _, err := cli.DeleteSecurityGroup(context.Background(), &ec2.DeleteSecurityGroupInput{
				GroupId: sg.GroupId,
			}); err != nil {
				return fmt.Errorf("failed to delete security group %s: %w", *sg.GroupId, err)
			}
		}
	}
	return nil
}

func setExternalName(obj metav1.Object, name string) {
	if obj.GetAnnotations() == nil {
		obj.SetAnnotations(map[string]string{})
	}
	if name == "" {
		panic("empty name")
	}
	obj.GetAnnotations()[meta.AnnotationKeyExternalName] = name
}

func vpcPublicRouteTableName(vpcName string) string {
	return vpcName + "-public"
}

func eksClusterNameTagKey(clusterName string) string {
	return fmt.Sprintf("kubernetes.io/cluster/%s", clusterName)
}

func GetAWSCIDR(cidr string, region string, zones int) (workers []string, publicLBs []string, internalLBs []string, err error) {
	for i := 0; i < zones; i++ {
		zoneCIDR, err := getSubCIDR(cidr, 2, i)
		if err != nil {
			return nil, nil, nil, err
		}

		// split by one bit, worker take halve
		workerCIDR, err := getSubCIDR(zoneCIDR, 1, 0)
		if err != nil {
			return nil, nil, nil, err
		}
		workers = append(workers, workerCIDR)

		left, err := getSubCIDR(zoneCIDR, 1, 1)
		if err != nil {
			return nil, nil, nil, err
		}

		// split the left part for public and internal LB
		public, err := getSubCIDR(left, 1, 0)
		if err != nil {
			return nil, nil, nil, err
		}
		publicLBs = append(publicLBs, public)

		internal, err := getSubCIDR(left, 1, 1)
		if err != nil {
			return nil, nil, nil, err
		}
		internalLBs = append(internalLBs, internal)
	}
	// The first 3 return values are the same type, so we use named result parameters and naked return
	return // nolint:nakedret
}

func getSubCIDR(cidr string, subBits int, index int) (string, error) {
	_, ipNet, err := net.ParseCIDR(cidr)
	if err != nil {
		return "", errors.AddStack(err)
	}

	ipInt := binary.BigEndian.Uint32(ipNet.IP)

	ones, _ := ipNet.Mask.Size()
	if ones+subBits > 32 {
		return "", fmt.Errorf("host bits is %d, but want to split by %d bits", 32-ones, subBits)
	}

	index <<= (32 - ones - subBits)

	data := make([]byte, 4)
	binary.BigEndian.PutUint32(data, ipInt|uint32(index))

	sub := fmt.Sprintf("%s/%d", net.IP(data).String(), ones+subBits)
	return sub, nil
}
