package aks

import (
	"context"
	"fmt"
	"time"

	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	cloudv1 "github.com/tidbcloud/infra-api/api/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"
	"github.com/upbound/provider-azure/apis/network/v1beta1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
	// Each NAT gateway public IP address provides 64,512 SNAT ports to make outbound connections.
	// A NAT gateway can scale up to over 1 million SNAT ports.
	// Assume each VM uses 64 ports, then 2 NAT IP addresses can support 2,016 VMs.
	// https://learn.microsoft.com/en-us/azure/nat-gateway/nat-gateway-resource#snat-ports
	defaultAddressCount = 2
	// StaticAllocation is the static allocation method for public IP address.
	staticAllocation = "Static"
	// StandardSKU is the standard SKU for public IP address.
	standardSKU = "Standard"
	// defaultRequeuInterval is the default requeue interval for the task.
	defaultRequeuInterval = time.Second * 30
)

type IPAddress struct {
	*task.BaseTask
}

func NewIPAddress(ctx *task.Context) *IPAddress {
	return &IPAddress{BaseTask: &task.BaseTask{Context: ctx}}
}

func (t *IPAddress) Name() string {
	return "IPAddress"
}

func (t *IPAddress) Create(ctx context.Context) deploy.Result {
	tags := cloudv1.TagsFromLabels(t.Internet.Labels)
	for idx := 0; idx < defaultAddressCount; idx++ {
		addr := t.newAddress(t.newAddressName(idx), tags)
		if err := k8s.Apply(ctx, t.Local, addr); err != nil {
			return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
		}
	}
	return deploy.Done()
}

func (t *IPAddress) newAddressName(id int) string {
	return namer.UniqueIdentifier(t.Internet.ObjectMeta, namer.WithPrefix(fmt.Sprintf("addr-%d", id)))
}

func (t *IPAddress) newAddress(name string, tags map[string]string) *v1beta1.PublicIP {
	return &v1beta1.PublicIP{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
			Labels: map[string]string{
				v1alpha1.InternetNameKey: t.Internet.Name,
			},
		},
		Spec: v1beta1.PublicIPSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{
					Name: t.ProviderConfigName(),
				},
				ManagementPolicies: controllers.GetResourceManagementPolicies(),
			},
			ForProvider: v1beta1.PublicIPParameters{
				AllocationMethod:  ptr.To(staticAllocation),
				Location:          ptr.To(t.Internet.Spec.Region),
				ResourceGroupName: ptr.To(controllers.GetResourceGroupName(t.VPC, t.Config.ResourceNamePrefix)),
				Sku:               ptr.To(standardSKU),
				Tags:              util.ToMapPointer(tags),
			},
		},
	}
}

func (t *IPAddress) PostCreate(ctx context.Context) deploy.Result {
	ipAddresses := []v1alpha1.IPAddress{}
	for idx := 0; idx < defaultAddressCount; idx++ {
		name := t.newAddressName(idx)
		ip := &v1beta1.PublicIP{}
		if err := t.Local.Get(ctx, types.NamespacedName{Name: name}, ip); err != nil {
			return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
		}

		condition := ip.Status.GetCondition(xpv1.TypeReady)
		if condition.Status != corev1.ConditionTrue {
			return deploy.Panic(fmt.Errorf("publicIP %s is not ready", name)).WithRequeuAfter(defaultRequeuInterval)
		}

		addressStatus := v1alpha1.IPAddress{Name: name}
		if ip.Status.AtProvider.ID != nil {
			addressStatus.AllocationID = *ip.Status.AtProvider.ID
		}
		if ip.Status.AtProvider.IPAddress != nil {
			addressStatus.IP = *ip.Status.AtProvider.IPAddress
		}

		ipAddresses = append(ipAddresses, addressStatus)
	}
	t.Internet.Status.IPAddresses = ipAddresses

	return deploy.ErrorOrDone(t.InternetRW.UpdateStatus(ctx, t.Internet))
}

func (t *IPAddress) Delete(ctx context.Context) deploy.Result {
	ips := &v1beta1.PublicIPList{}
	if err := t.Local.List(ctx, ips, &client.ListOptions{
		LabelSelector: labels.SelectorFromSet(map[string]string{
			v1alpha1.InternetNameKey: t.Internet.Name,
		}),
	}); err != nil {
		return deploy.Error(err)
	}
	for _, ip := range ips.Items {
		if err := controllers.DeleteUpboundResource(ctx, t.Local, ptr.To(ip)); err != nil {
			return deploy.Error(err)
		}
	}

	return deploy.Done()
}
