package eks

import (
	"context"
	"fmt"
	"time"

	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/upbound/provider-aws/apis/ec2/v1beta1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/internet/task"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/k8s"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/namer"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/util"
)

type NAT struct {
	*task.BaseTask
	nats []*v1beta1.NATGateway
}

func NewNAT(ctx *task.Context) *NAT {
	return &NAT{
		BaseTask: &task.BaseTask{Context: ctx},
		nats:     []*v1beta1.NATGateway{},
	}
}

// Name implements Task interface.
// Returns the name of the task.
func (n *NAT) Name() string {
	return "NAT"
}

func (n *NAT) Create(ctx context.Context) deploy.Result {
	n.BeginAt = util.Pointer(time.Now())
	if n.IsHibernated() {
		return deploy.Done()
	}
	name := n.InternetName()
	tags := n.Internet.Labels
	zones := n.GetNodeZones()

	// Convert tags once before the loop for better performance
	tags = controllers.ConvertToCloudTags(tags, cloudv1.ProviderTypeEKS)
	tags = util.MergeMaps(tags, n.Config.AdditionalCloudTags)

	for _, zone := range zones {
		// BuildAWSTagWithName creates a copy and adds zone-specific Name tag
		zoneTags := util.BuildAWSTagWithName(tags, namer.PublicSubnetName(name, zone))
		sub, err := findPublicSubnetByZone(ctx, n.Local, n.VPC, zone)
		if err != nil {
			return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
		}

		ip := v1alpha1.FindIPAddressByName(n.Internet.Status.IPAddresses, namer.PublicSubnetName(name, zone))
		if ip == nil {
			return deploy.Panic(fmt.Errorf("no ip address found for subnet %s", namer.PublicSubnetName(name, zone))).WithRequeuAfter(defaultRequeuInterval)
		}

		nat, err := n.createNAT(ctx, zoneTags, name, zone, sub, ip)
		if err != nil {
			return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
		}

		n.nats = append(n.nats, nat)
	}
	return deploy.Done()
}

func (n *NAT) createNAT(ctx context.Context, tags map[string]string, name string, zone string, subnet *v1beta1.Subnet, ip *v1alpha1.IPAddress) (*v1beta1.NATGateway, error) {
	labels := util.MergeMaps(n.Internet.Labels, map[string]string{v1alpha1.InternetNameKey: n.Internet.Name})
	nat := &v1beta1.NATGateway{
		ObjectMeta: metav1.ObjectMeta{
			Name:   namer.PublicSubnetName(name, zone),
			Labels: labels,
		},
		Spec: v1beta1.NATGatewaySpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: n.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
			ForProvider: v1beta1.NATGatewayParameters_2{
				Region: ptr.To(n.Internet.Spec.Region),
				Tags:   util.ToMapPointer(tags),
				// we allow to change the allocationID even for migrated natgateway
				// if the ip changed, the natgateway need to update `.Spec.ForProvider.AllocationID`
				AllocationID: ptr.To(ip.AllocationID),
			},
			InitProvider: v1beta1.NATGatewayInitParameters_2{
				SubnetID:         ptr.To(meta.GetExternalName(subnet)),
				ConnectivityType: ptr.To("public"),
			},
		},
	}

	if err := controllerutil.SetControllerReference(subnet, nat, n.Local.Scheme()); err != nil {
		return nil, err
	}

	if err := k8s.Apply(ctx, n.Local, nat); err != nil {
		return nil, err
	}
	return nat, nil
}

func (n *NAT) PostCreate(ctx context.Context) deploy.Result {
	if n.IsHibernated() {
		return deploy.Done()
	}
	records := make([]v1alpha1.NAT, 0, len(n.nats))
	ips := make([]string, 0, len(n.nats))
	for _, nat := range n.nats {
		natRes := &v1beta1.NATGateway{}
		if err := n.Local.Get(ctx, types.NamespacedName{Name: nat.Name}, natRes); err != nil {
			return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
		}

		if !controllers.IsResourceReady(natRes.Status.ResourceStatus) {
			return deploy.Panic(fmt.Errorf("nat gateway %s is not available", nat.Name)).WithRequeuAfter(defaultRequeuInterval)
		}
		if natRes.Status.AtProvider.PublicIP == nil {
			return deploy.Panic(fmt.Errorf("nat gateway %s has no public ip", nat.Name)).WithRequeuAfter(defaultRequeuInterval)
		}
		ips = append(ips, *natRes.Status.AtProvider.PublicIP)
		records = append(records, v1alpha1.NAT{
			NATGatewayID: meta.GetExternalName(natRes),
			Name:         natRes.Name,
		})
	}

	n.Internet.Status.NATs = records
	n.Internet.Status.ExternalIPs = ips
	if err := n.InternetRW.UpdateStatus(ctx, n.Internet); err != nil {
		return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
	}
	return deploy.Done()
}

func (n *NAT) Delete(ctx context.Context) deploy.Result {
	// FIXME(cwen0): use config for zones in delete task may cause problem
	zones := n.GetNodeZones()
	name := n.InternetName()
	for _, zone := range zones {
		if err := n.deleteNAT(ctx, name, zone); err != nil {
			return deploy.Error(err)
		}
	}
	return deploy.Done()
}

func (n *NAT) deleteNAT(ctx context.Context, name string, zone string) error {
	nat := &v1beta1.NATGateway{
		ObjectMeta: metav1.ObjectMeta{
			Name: namer.PublicSubnetName(name, zone),
		},
	}

	if err := n.Local.Get(ctx, client.ObjectKeyFromObject(nat), nat); client.IgnoreNotFound(err) != nil {
		return err
	}

	nat.Spec.ResourceSpec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionAll}
	if err := k8s.PatchObject(ctx, n.Local, nat, controllers.ManagementPoliciesPatch); client.IgnoreNotFound(err) != nil {
		return err
	}

	return k8s.DeleteObject(ctx, n.Local, nat)
}

func (n *NAT) PostDelete(ctx context.Context) deploy.Result {
	return deploy.Done()
}

func findPublicSubnetByZone(ctx context.Context, cli client.Client, vpc *v1alpha1.VPC, zone string) (*v1beta1.Subnet, error) {
	record := v1alpha1.FindSubnetByZone(vpc.Status.PublicSubnets, zone)
	if record == nil {
		return nil, fmt.Errorf("no public subnet found in zone %s", zone)
	}
	subRes := &v1beta1.Subnet{}
	key := types.NamespacedName{
		Name: record.Name,
	}
	if err := cli.Get(ctx, key, subRes); err != nil {
		return nil, err
	}
	return subRes, nil
}

func findPrivateSubnetByZone(ctx context.Context, cli client.Client, vpc *v1alpha1.VPC, zone string) (*v1beta1.Subnet, error) {
	record := v1alpha1.FindSubnetByZone(vpc.Status.PrivateSubnets, zone)
	if record == nil {
		return nil, fmt.Errorf("no private subnet found in zone %s", zone)
	}
	subRes := &v1beta1.Subnet{}
	key := types.NamespacedName{
		Name: record.Name,
	}
	if err := cli.Get(ctx, key, subRes); err != nil {
		return nil, err
	}
	return subRes, nil
}

func findNodeSubnetByZone(ctx context.Context, cli client.Client, vpc *v1alpha1.VPC, zone string) (*v1beta1.Subnet, error) {
	record := v1alpha1.FindSubnetByZone(vpc.Status.NodeSubnets, zone)
	if record == nil {
		return nil, fmt.Errorf("no node subnet found in zone %s", zone)
	}
	subRes := &v1beta1.Subnet{}
	key := types.NamespacedName{
		Name: record.Name,
	}
	if err := cli.Get(ctx, key, subRes); err != nil {
		return nil, err
	}
	return subRes, nil
}
