package eks

import (
	"context"
	"fmt"

	"github.com/crossplane-contrib/provider-aws/apis/ec2/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	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"

	"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"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/internet/task"
)

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 {
	if n.IsHibernated() {
		return deploy.Done()
	}
	name := n.InternetName()
	tags := cloudv1.TagsFromLabels(n.Internet.Labels)
	zones := n.GetZones()

	for _, zone := range zones {
		tags := util.AddResourceName(tags, namer.PublicSubnetName(name, zone))
		sub, err := findSubnetByZone(ctx, n.Local, n.VPC.Status.PublicSubnets, 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, tags, 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) {
	nat := &v1beta1.NATGateway{
		ObjectMeta: metav1.ObjectMeta{
			Name: namer.PublicSubnetName(name, zone),
			Labels: map[string]string{
				v1alpha1.InternetNameKey: n.InternetName(),
			},
		},
		Spec: v1beta1.NATGatewaySpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: n.ProviderConfigName()},
			},
			ForProvider: v1beta1.NATGatewayParameters{
				Region:           n.Internet.Spec.Region,
				SubnetID:         pointer.String(meta.GetExternalName(subnet)),
				ConnectivityType: "public",
				Tags:             util.GetEC2V1BetaTags(tags),
			},
		},
	}

	if err := controllerutil.SetControllerReference(subnet, nat, n.Local.Scheme()); err != nil {
		return nil, err
	}
	// we allow to change the allocationID even for migrated natgateway
	// if the ip changed, the natgateway need to update `.Spec.ForProvider.AllocationID`
	mutateFn := func() error {
		nat.Spec.ForProvider.AllocationID = pointer.String(ip.AllocationID)
		return nil
	}
	_, err := controllerutil.CreateOrUpdate(ctx, n.Local, nat, mutateFn)
	return nat, err
}

func (n *NAT) PostCreate(ctx context.Context) deploy.Result {
	if n.IsHibernated() {
		return deploy.Done()
	}
	records := []v1alpha1.NAT{}
	ips := []string{}
	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 natRes.Status.AtProvider.State != v1beta1.NatGatewayStatusAvailable {
			return deploy.Panic(fmt.Errorf("nat gateway %s is not available", nat.Name)).WithRequeuAfter(defaultRequeuInterval)
		}
		if len(natRes.Status.AtProvider.NatGatewayAddresses) == 0 {
			return deploy.Panic(fmt.Errorf("nat gateway %s has no public ip", nat.Name)).WithRequeuAfter(defaultRequeuInterval)
		}
		ips = append(ips, natRes.Status.AtProvider.NatGatewayAddresses[0].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.GetZones()
	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),
		},
	}

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

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

func findSubnetByZone(ctx context.Context, cli client.Client, subnets []v1alpha1.Subnet, zone string) (*v1beta1.Subnet, error) {
	record := v1alpha1.FindSubnetByZone(subnets, zone)
	if record == nil {
		return nil, fmt.Errorf("subnet not 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
}
