package aks

import (
	"context"
	"fmt"

	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"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/privatelinkendpoint/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/util"
	azurenetworkv1beta1 "github.com/upbound/provider-azure/apis/network/v1beta1"
)

const (
	defaultRecordTTLSeconds = 3600

	zoneNotReady     = "Private DNS Zone not ready"
	vnetLinkNotReady = "VNet Link not ready"
	recordNotReady   = "DNS Record not ready"
)

type dns struct {
	*task.BaseTask
}

func NewDNS(ctx *task.Context) deploy.Task {
	return &dns{BaseTask: &task.BaseTask{Context: ctx}}
}

func (t *dns) Name() string {
	return "DNS"
}

func (t *dns) Create(ctx context.Context) deploy.Result {
	if t.PrivateLinkEndpoint.Status.EndpointAddress == "" {
		return deploy.Panic(fmt.Errorf("endpoint address is empty"))
	}
	if t.PrivateLinkEndpoint.Spec.RootDomainName == "" {
		return deploy.Panic(fmt.Errorf("root domain name is empty"))
	}
	resourceGroupName := t.VPC.Status.ResourceGroup.ID
	if resourceGroupName == "" {
		return deploy.Panic(fmt.Errorf("resource group is empty"))
	}
	virtualNetworkID := t.VPC.Status.VPC.ResourceID
	if virtualNetworkID == "" {
		return deploy.Panic(fmt.Errorf("vnet resource id is empty"))
	}

	tags := t.EffectiveCloudTags(consts.ProviderTypeAKS)
	labels := util.MergeMaps(t.VPC.Labels, map[string]string{v1alpha1.PrivateLinkEndpointNameKey: t.PrivateLinkEndpoint.Name})

	if err := t.applyPrivateDNSZone(ctx, resourceGroupName, labels, tags); err != nil {
		return deploy.Panic(err)
	}
	if err := t.applyVNetLink(ctx, resourceGroupName, virtualNetworkID, labels, tags); err != nil {
		return deploy.Panic(err)
	}
	// Base wildcard record to resolve the endpoint address
	if err := t.applyARecord(ctx, resourceGroupName, labels, tags); err != nil {
		return deploy.Panic(err)
	}

	return deploy.Done()
}

func (t *dns) PostCreate(ctx context.Context) deploy.Result {
	zone := &azurenetworkv1beta1.PrivateDNSZone{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.getDNSResourceName(),
		},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(zone), zone); err != nil {
		t.PrivateLinkEndpoint.Status.Message = zoneNotReady
		return deploy.Error(err)
	}
	if !controllers.IsResourceReady(zone.Status.ResourceStatus) {
		t.PrivateLinkEndpoint.Status.Message = zoneNotReady
		return deploy.Error(fmt.Errorf("%s: %s", zoneNotReady, zone.Name))
	}

	link := &azurenetworkv1beta1.PrivateDNSZoneVirtualNetworkLink{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.getDNSResourceName(),
		},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(link), link); err != nil {
		t.PrivateLinkEndpoint.Status.Message = vnetLinkNotReady
		return deploy.Error(err)
	}
	if !controllers.IsResourceReady(link.Status.ResourceStatus) {
		t.PrivateLinkEndpoint.Status.Message = vnetLinkNotReady
		return deploy.Error(fmt.Errorf("%s: %s", vnetLinkNotReady, link.Name))
	}

	recordList := &azurenetworkv1beta1.PrivateDNSARecordList{}
	selector := client.MatchingLabels{v1alpha1.PrivateLinkEndpointNameKey: t.PrivateLinkEndpoint.Name}
	if err := t.Local.List(ctx, recordList, selector); err != nil {
		t.PrivateLinkEndpoint.Status.Message = recordNotReady
		return deploy.Error(err)
	}
	if len(recordList.Items) == 0 {
		t.PrivateLinkEndpoint.Status.Message = recordNotReady
		return deploy.Error(fmt.Errorf(recordNotReady))
	}
	for _, record := range recordList.Items {
		if !controllers.IsResourceReady(record.Status.ResourceStatus) {
			t.PrivateLinkEndpoint.Status.Message = recordNotReady
			return deploy.Error(fmt.Errorf("%s: %s", recordNotReady, record.Name))
		}
	}
	return deploy.Done()
}

func (t *dns) Delete(ctx context.Context) deploy.Result {
	recordList := &azurenetworkv1beta1.PrivateDNSARecordList{}
	selector := client.MatchingLabels{v1alpha1.PrivateLinkEndpointNameKey: t.PrivateLinkEndpoint.Name}
	if err := t.Local.List(ctx, recordList, selector); err != nil {
		return deploy.Panic(err)
	}
	for i := range recordList.Items {
		if err := controllers.DeleteUpboundResource(ctx, t.Local, &recordList.Items[i]); err != nil {
			return deploy.Panic(err)
		}
	}

	link := &azurenetworkv1beta1.PrivateDNSZoneVirtualNetworkLink{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.getDNSResourceName(),
		},
	}
	if err := controllers.DeleteUpboundResource(ctx, t.Local, link); err != nil {
		return deploy.Panic(err)
	}

	zone := &azurenetworkv1beta1.PrivateDNSZone{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.getDNSResourceName(),
		},
	}
	if err := controllers.DeleteUpboundResource(ctx, t.Local, zone); err != nil {
		return deploy.Panic(err)
	}
	return deploy.Done()
}

func (t *dns) PostDelete(ctx context.Context) deploy.Result {
	recordList := &azurenetworkv1beta1.PrivateDNSARecordList{}
	selector := client.MatchingLabels{v1alpha1.PrivateLinkEndpointNameKey: t.PrivateLinkEndpoint.Name}
	if err := t.Local.List(ctx, recordList, selector); err != nil {
		return deploy.Error(err)
	}
	if len(recordList.Items) > 0 {
		return deploy.Error(fmt.Errorf("dns records still exist"))
	}

	if err := k8s.GetDeletedObject(ctx, t.Local, types.NamespacedName{Name: t.getDNSResourceName()}, &azurenetworkv1beta1.PrivateDNSZoneVirtualNetworkLink{}); err != nil {
		return deploy.Error(err)
	}

	if err := k8s.GetDeletedObject(ctx, t.Local, types.NamespacedName{Name: t.getDNSResourceName()}, &azurenetworkv1beta1.PrivateDNSZone{}); err != nil {
		return deploy.Error(err)
	}
	return deploy.Done()
}

func (t *dns) applyPrivateDNSZone(ctx context.Context, resourceGroupName string, labels, tags map[string]string) error {
	// NOTE: We currently create a dedicated Private DNS Zone per DNS resource name produced by t.getDNSResourceName().
	// This simplifies lifecycle management because the zone can be managed
	// together with the corresponding private endpoint resources.
	zone := &azurenetworkv1beta1.PrivateDNSZone{
		ObjectMeta: metav1.ObjectMeta{
			Name:   t.getDNSResourceName(),
			Labels: labels,
		},
		Spec: azurenetworkv1beta1.PrivateDNSZoneSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{
					Name: t.ProviderConfigName(),
				},
				ManagementPolicies: controllers.GetResourceManagementPolicies(),
			},
			ForProvider: azurenetworkv1beta1.PrivateDNSZoneParameters{
				ResourceGroupName: ptr.To(resourceGroupName),
				Tags:              util.ToMapPointer(tags),
			},
		},
	}
	// The Azure Private DNS Zone resource name IS the domain name (e.g. "privatelink.example.com").
	// Without this annotation, Crossplane would use the K8s object name as the Azure resource name,
	// which would result in an incorrect zone name and break DNS resolution entirely.
	v1alpha1.SetExternalName(zone, t.PrivateLinkEndpoint.Spec.RootDomainName)
	return k8s.Apply(ctx, t.Local, zone)
}

func (t *dns) applyVNetLink(ctx context.Context, resourceGroupName, virtualNetworkID string, labels, tags map[string]string) error {
	link := &azurenetworkv1beta1.PrivateDNSZoneVirtualNetworkLink{
		ObjectMeta: metav1.ObjectMeta{
			Name:   t.getDNSResourceName(),
			Labels: labels,
		},
		Spec: azurenetworkv1beta1.PrivateDNSZoneVirtualNetworkLinkSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{
					Name: t.ProviderConfigName(),
				},
				ManagementPolicies: controllers.GetResourceManagementPolicies(),
			},
			ForProvider: azurenetworkv1beta1.PrivateDNSZoneVirtualNetworkLinkParameters{
				PrivateDNSZoneName: ptr.To(t.PrivateLinkEndpoint.Spec.RootDomainName),
				ResourceGroupName:  ptr.To(resourceGroupName),
				VirtualNetworkID:   ptr.To(virtualNetworkID),
				Tags:               util.ToMapPointer(tags),
			},
		},
	}
	return k8s.Apply(ctx, t.Local, link)
}

func (t *dns) applyARecord(ctx context.Context, resourceGroupName string, labels, tags map[string]string) error {
	record := &azurenetworkv1beta1.PrivateDNSARecord{
		ObjectMeta: metav1.ObjectMeta{
			Name:   t.getDNSResourceName(),
			Labels: labels,
		},
		Spec: azurenetworkv1beta1.PrivateDNSARecordSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{
					Name: t.ProviderConfigName(),
				},
				ManagementPolicies: controllers.GetResourceManagementPolicies(),
			},
			ForProvider: azurenetworkv1beta1.PrivateDNSARecordParameters{
				Records:           []*string{ptr.To(t.PrivateLinkEndpoint.Status.EndpointAddress)},
				TTL:               ptr.To(float64(defaultRecordTTLSeconds)),
				ZoneName:          ptr.To(t.PrivateLinkEndpoint.Spec.RootDomainName),
				ResourceGroupName: ptr.To(resourceGroupName),
				Tags:              util.ToMapPointer(tags),
			},
		},
	}

	// External name maps to the Azure record set name; use it to support wildcard records like "*" and "*.foo".
	v1alpha1.SetExternalName(record, "*")
	return k8s.Apply(ctx, t.Local, record)
}

func (t *dns) getDNSResourceName() string {
	return fmt.Sprintf("dns-%s", t.PrivateLinkEndpoint.Name)
}
