package ack

import (
	"context"
	"fmt"
	"sort"
	"time"

	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/consts"
	"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"
)

const (
	natType         = "Enhanced"
	natPaymentType  = "PayAsYouGo"
	natChargeType   = "PayByLcu"
	natNetworkType  = "internet"
	natInstanceType = "Nat"
	natEIPBindMode  = "NAT"
)

type natTask struct {
	*task.BaseTask
}

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

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

func (n *natTask) Create(ctx context.Context) deploy.Result {
	n.BeginAt = util.Pointer(time.Now())

	natZone := n.getNATZone()
	natName := n.getNATName(natZone)
	tags := controllers.ConvertToCloudTags(n.Internet.Labels, consts.ProviderTypeACK)
	tags = util.MergeMaps(tags, n.Config.AdditionalCloudTags)

	if err := n.createNAT(ctx, natName, natZone, tags); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	return deploy.Done()
}

func (n *natTask) createNAT(ctx context.Context, name, zone string, tags map[string]string) error {
	subnet := v1alpha1.FindSubnetByZone(n.VPC.Status.PublicSubnets, 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})
	nat := &alinatv1alpha1.NATGateway{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: labels,
		},
		Spec: alinatv1alpha1.NATGatewaySpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: n.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
			ForProvider: alinatv1alpha1.NATGatewayParameters{
				NATGatewayName:     ptr.To(name),
				Region:             ptr.To(n.Internet.Spec.Region),
				VPCIDRef:           &xpv1.Reference{Name: n.VPCName()},
				VswitchIDRef:       &xpv1.Reference{Name: subnet.Name},
				NATType:            ptr.To(natType),
				EIPBindMode:        ptr.To(natEIPBindMode),
				PaymentType:        ptr.To(natPaymentType),
				InternetChargeType: ptr.To(natChargeType),
				NetworkType:        ptr.To(natNetworkType),
				Tags:               util.ToMapPointer(tags),
			},
		},
	}
	return k8s.Apply(ctx, n.Local, nat)
}

func (n *natTask) getNATName(zone string) string {
	return namer.PublicSubnetName(n.InternetName(), zone)
}

func (n *natTask) PostCreate(ctx context.Context) deploy.Result {
	natZone := n.getNATZone()

	natRes := &alinatv1alpha1.NATGateway{}
	if err := n.Local.Get(ctx, types.NamespacedName{Name: n.getNATName(natZone)}, natRes); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	if !controllers.IsResourceReady(natRes.Status.ResourceStatus) {
		return deploy.Panic(fmt.Errorf("nat gateway %s is not available", natRes.Name)).WithRequeuAfter(time.Second * 30)
	}

	n.Internet.Status.NATs = []v1alpha1.NAT{
		{
			Name:         natRes.Name,
			NATGatewayID: *natRes.Status.AtProvider.ID,
		},
	}
	if err := n.InternetRW.UpdateStatus(ctx, n.Internet); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	return deploy.Done()
}

func (n *natTask) Delete(ctx context.Context) deploy.Result {
	natZone := n.getNATZone()
	natRes := &alinatv1alpha1.NATGateway{
		ObjectMeta: metav1.ObjectMeta{
			Name: n.getNATName(natZone),
		},
	}
	if err := controllers.DeleteUpboundResource(ctx, n.Local, natRes); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	return deploy.Done()
}

func (n *natTask) PostDelete(ctx context.Context) deploy.Result {
	natZone := n.getNATZone()
	natRes := &alinatv1alpha1.NATGateway{}
	if err := k8s.GetDeletedObject(ctx, n.Local, types.NamespacedName{Name: n.getNATName(natZone)}, natRes); err != nil {
		return deploy.Error(err).WithRequeuAfter(time.Second * 30)
	}
	return deploy.Done()
}

func (n *natTask) getNATZone() string {
	zones := n.GetNodeZones()
	sort.Slice(zones, func(i, j int) bool { return zones[i] < zones[j] })
	return zones[0]
}
