package eks

import (
	"context"
	"fmt"
	"strings"

	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	route53v1beta1 "github.com/upbound/provider-aws/apis/route53/v1beta1"
	route53v1beta2 "github.com/upbound/provider-aws/apis/route53/v1beta2"
	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/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"
	"github.com/tidbcloud/observability/agent-control-server/pkg/consts"
)

const (
	zoneNotReady   = "DNS Zone 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 {
	zone, err := t.createPrivateHostedZone(ctx)
	if err != nil {
		return deploy.Panic(err)
	}
	if t.PrivateLinkEndpoint.Status.EndpointAddress == "" {
		return deploy.Panic(fmt.Errorf("endpoint address is empty"))
	}

	regionAddress, zoneAddresses := t.PrivateLinkEndpoint.Status.EndpointAddress, t.PrivateLinkEndpoint.Status.ZonesAddresses
	labels := util.MergeMaps(t.VPC.Labels, map[string]string{v1alpha1.PrivateLinkEndpointNameKey: t.PrivateLinkEndpoint.Name})
	// add a base record to resolve regional endpoint address
	if err := t.createDNSRecord(ctx, zone.Name, "all", "*", regionAddress, labels); err != nil {
		return deploy.Panic(err)
	}

	for azID, recordName := range t.PrivateLinkEndpoint.Spec.DNSRecords {
		domainSuffix := fmt.Sprintf(".%s", t.PrivateLinkEndpoint.Spec.RootDomainName)
		azAddress, ok := zoneAddresses[azID]
		if !ok {
			return deploy.Panic(fmt.Errorf("az id %s is not found in endpoint zone addresses", azID))
		}

		recordName = strings.TrimSuffix(recordName, domainSuffix)
		if err := t.createDNSRecord(ctx, zone.Name, azID, fmt.Sprintf("*.%s", recordName), azAddress, labels); err != nil {
			return deploy.Panic(err)
		}
	}

	return deploy.Done()
}

func (t *dns) createPrivateHostedZone(ctx context.Context) (*route53v1beta1.Zone, error) {
	name := t.getHostedZoneName()
	tags := t.EffectiveCloudTags(consts.ProviderTypeEKS)
	tags = util.BuildAWSTagWithName(tags, name)
	labels := util.MergeMaps(t.VPC.Labels, tags, map[string]string{v1alpha1.PrivateLinkEndpointNameKey: t.PrivateLinkEndpoint.Name})
	// Create a private hosted zone
	zone := &route53v1beta1.Zone{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: labels,
		},
		Spec: route53v1beta1.ZoneSpec{
			ForProvider: route53v1beta1.ZoneParameters{
				Name:   ptr.To(t.PrivateLinkEndpoint.Spec.RootDomainName),
				Region: ptr.To(t.PrivateLinkEndpoint.Spec.Region),
				Tags:   util.ToMapPointer(tags),
				VPC: []route53v1beta1.VPCParameters{
					{VPCIDRef: &xpv1.Reference{Name: t.VPC.Status.VPC.Name}},
				},
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{
					Name: t.ProviderConfigName(),
				},
				ManagementPolicies: controllers.GetResourceManagementPolicies(),
			},
		},
	}
	if err := k8s.Apply(ctx, t.Local, zone); err != nil {
		return nil, err
	}
	return zone, nil
}

func (t *dns) createDNSRecord(ctx context.Context, zoneName, nameSuffix, recordName, address string, labels map[string]string) error {
	record := &route53v1beta2.Record{
		ObjectMeta: metav1.ObjectMeta{
			Name:   t.getRecordName(nameSuffix),
			Labels: labels,
		},
		Spec: route53v1beta2.RecordSpec{
			ForProvider: route53v1beta2.RecordParameters{
				Type:    ptr.To("CNAME"),
				Records: []*string{ptr.To(address)},
				Name:    ptr.To(recordName),
				Region:  ptr.To(t.PrivateLinkEndpoint.Spec.Region),
				TTL:     ptr.To(float64(300)),
				ZoneIDRef: &xpv1.Reference{
					Name: zoneName,
				},
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{
					Name: t.ProviderConfigName(),
				},
				ManagementPolicies: controllers.GetResourceManagementPolicies(),
			},
		},
	}
	return k8s.Apply(ctx, t.Local, record)
}

func (t *dns) PostCreate(ctx context.Context) deploy.Result {
	zone := &route53v1beta1.Zone{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.getHostedZoneName(),
		},
	}
	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))
	}

	recordList := &route53v1beta2.RecordList{}
	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 := &route53v1beta2.RecordList{}
	selector := client.MatchingLabels{v1alpha1.PrivateLinkEndpointNameKey: t.PrivateLinkEndpoint.Name}
	if err := t.Local.List(ctx, recordList, selector); err != nil {
		return deploy.Panic(err)
	}
	for _, record := range recordList.Items {
		if err := controllers.DeleteUpboundResource(ctx, t.Local, &record); err != nil {
			return deploy.Panic(err)
		}
	}

	zone := &route53v1beta1.Zone{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.getHostedZoneName(),
		},
	}
	return deploy.PanicOrDone(controllers.DeleteUpboundResource(ctx, t.Local, zone))
}

func (t *dns) PostDelete(ctx context.Context) deploy.Result {
	recordList := &route53v1beta2.RecordList{}
	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"))
	}

	zone := &route53v1beta1.Zone{}
	return deploy.ErrorOrDone(k8s.GetDeletedObject(ctx, t.Local, types.NamespacedName{Name: t.getHostedZoneName()}, zone))
}

func (t *dns) getHostedZoneName() string {
	return t.EndpointName()
}

func (t *dns) getRecordName(suffix string) string {
	return t.EndpointName() + "-" + suffix
}
