package ack

import (
	"context"
	"fmt"
	"time"

	alieipv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/eip/v1alpha1"
	alinatv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/natgateway/v1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"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"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
)

type snatEntryTask struct {
	*task.BaseTask
}

func NewSNATEntry(ctx *task.Context) deploy.Task {
	return &snatEntryTask{
		BaseTask: &task.BaseTask{Context: ctx},
	}
}

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

func (n *snatEntryTask) Create(ctx context.Context) deploy.Result {
	n.BeginAt = util.Pointer(time.Now())
	if len(n.Internet.Status.IPAddresses) == 0 {
		return deploy.Panic(fmt.Errorf("no ip address found")).WithRequeuAfter(time.Second * 30)
	}
	if len(n.Internet.Status.NATs) == 0 {
		return deploy.Panic(fmt.Errorf("no nat found")).WithRequeuAfter(time.Second * 30)
	}
	ipAddress := &n.Internet.Status.IPAddresses[0]
	nat := &n.Internet.Status.NATs[0]

	if err := n.associateNatEip(ctx, nat, ipAddress); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	for _, zone := range n.GetNodeZones() {
		if err := n.createSNatEntry(ctx, nat, ipAddress, zone); err != nil {
			return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
		}
	}

	return deploy.Done()
}

func (n *snatEntryTask) associateNatEip(ctx context.Context, nat *v1alpha1.NAT, ipAddress *v1alpha1.IPAddress) error {
	labels := util.MergeMaps(n.Internet.Labels, map[string]string{v1alpha1.InternetNameKey: n.Internet.Name})
	eipAssociation := &alieipv1alpha1.EIPAssociation{
		ObjectMeta: metav1.ObjectMeta{
			Name:   nat.Name,
			Labels: labels,
		},
		Spec: alieipv1alpha1.EIPAssociationSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: n.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
			ForProvider: alieipv1alpha1.EIPAssociationParameters{
				AllocationIDRef: &xpv1.Reference{Name: ipAddress.Name},
				InstanceID:      ptr.To(nat.NATGatewayID),
				InstanceType:    ptr.To(natInstanceType),
				Region:          ptr.To(n.Internet.Spec.Region),
			},
		},
	}
	return k8s.Apply(ctx, n.Local, eipAssociation)
}

func (n *snatEntryTask) createSNatEntry(ctx context.Context, nat *v1alpha1.NAT, ipAddress *v1alpha1.IPAddress, zone string) error {
	subnet := v1alpha1.FindSubnetByZone(n.VPC.Status.NodeSubnets, zone)
	if subnet == nil {
		return fmt.Errorf("no subnet found for zone %s", zone)
	}

	labels := util.MergeMaps(n.Internet.Labels, map[string]string{v1alpha1.InternetNameKey: n.Internet.Name})
	name := n.getSNATName(zone)
	snatEntry := &alinatv1alpha1.SNATEntry{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: labels,
		},
		Spec: alinatv1alpha1.SNATEntrySpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: n.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
			ForProvider: alinatv1alpha1.SNATEntryParameters{
				SnatEntryName:      ptr.To(name),
				SnatTableIDRef:     &xpv1.Reference{Name: nat.Name},
				SnatIPRef:          &xpv1.Reference{Name: ipAddress.Name},
				SourceVswitchIDRef: &xpv1.Reference{Name: subnet.Name},
				Region:             ptr.To(n.Internet.Spec.Region),
			},
		},
	}
	return k8s.Apply(ctx, n.Local, snatEntry)
}

func (n *snatEntryTask) getSNATName(zone string) string {
	return namer.PublicSubnetName(n.InternetName(), zone)
}

func (n *snatEntryTask) PostCreate(ctx context.Context) deploy.Result {
	ipAddress := n.Internet.Status.IPAddresses[0]
	nat := &n.Internet.Status.NATs[0]

	eipAssociationRes := &alieipv1alpha1.EIPAssociation{}
	if err := n.Local.Get(ctx, types.NamespacedName{Name: nat.Name}, eipAssociationRes); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	if !controllers.IsResourceReady(eipAssociationRes.Status.ResourceStatus) {
		return deploy.Panic(fmt.Errorf("eip association %s is not available", eipAssociationRes.Name)).WithRequeuAfter(time.Second * 30)
	}

	for _, zone := range n.GetNodeZones() {
		snatEntryRes := &alinatv1alpha1.SNATEntry{}
		if err := n.Local.Get(ctx, types.NamespacedName{Name: n.getSNATName(zone)}, snatEntryRes); err != nil {
			return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
		}
		if !controllers.IsResourceReady(snatEntryRes.Status.ResourceStatus) {
			return deploy.Panic(fmt.Errorf("snat entry %s is not available", snatEntryRes.Name)).WithRequeuAfter(time.Second * 30)
		}
	}

	n.Internet.Status.ExternalIPs = []string{ipAddress.IP}
	if err := n.InternetRW.UpdateStatus(ctx, n.Internet); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	return deploy.Done()
}

func (n *snatEntryTask) Delete(ctx context.Context) deploy.Result {
	if len(n.Internet.Status.NATs) == 0 {
		n.Logger.Info("no nat found, skip deleting")
		return deploy.Done()
	}
	nat := n.Internet.Status.NATs[0]
	eipAssociationRes := &alieipv1alpha1.EIPAssociation{
		ObjectMeta: metav1.ObjectMeta{
			Name: nat.Name,
		},
	}
	if err := controllers.DeleteUpboundResource(ctx, n.Local, eipAssociationRes); err != nil {
		return deploy.Error(err).WithRequeuAfter(time.Second * 30)
	}

	for _, zone := range n.GetNodeZones() {
		snatEntryRes := &alinatv1alpha1.SNATEntry{
			ObjectMeta: metav1.ObjectMeta{
				Name: n.getSNATName(zone),
			},
		}
		if err := controllers.DeleteUpboundResource(ctx, n.Local, snatEntryRes); err != nil {
			return deploy.Error(err).WithRequeuAfter(time.Second * 30)
		}
	}
	return deploy.Done()
}

func (n *snatEntryTask) PostDelete(ctx context.Context) deploy.Result {
	if len(n.Internet.Status.NATs) == 0 {
		n.Logger.Info("no nat found, skip deleting")
		return deploy.Done()
	}
	nat := n.Internet.Status.NATs[0]
	eipAssociationRes := &alieipv1alpha1.EIPAssociation{}
	if err := k8s.GetDeletedObject(ctx, n.Local, types.NamespacedName{Name: nat.Name}, eipAssociationRes); err != nil {
		return deploy.Error(err).WithRequeuAfter(time.Second * 30)
	}

	for _, zone := range n.GetNodeZones() {
		snatEntryRes := &alinatv1alpha1.SNATEntry{}
		if err := k8s.GetDeletedObject(ctx, n.Local, types.NamespacedName{Name: n.getSNATName(zone)}, snatEntryRes); err != nil {
			return deploy.Error(err).WithRequeuAfter(time.Second * 30)
		}
	}
	return deploy.Done()
}
