package eks

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
	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/applier"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/cloud/aws"
	"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/loadbalancer/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"
	route53v1beta2 "github.com/upbound/provider-aws/apis/route53/v1beta2"
	"gomodules.xyz/jsonpatch/v2"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

const (
	// This anno is set to avoid call AWS API multiple times
	// This anno is set in FullService task and maybe used in PrivateLink task
	annoKeyAWSLoadBalancerCanonicalHostedZoneID = "load-balancer.aws.cloud.pingcap.com/canonical-hosted-zone-id"
	annoKeyAWSLoadBalancerARN                   = "load-balancer.aws.cloud.pingcap.com/arn"
)

type DNS struct {
	*task.BaseTask
	awsCli aws.Client
}

func NewDNS(ctx *task.Context, awsCli aws.Client) deploy.Task {
	return &DNS{
		BaseTask: &task.BaseTask{Context: ctx},
		awsCli:   awsCli,
	}
}

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

func (t *DNS) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())
	remoteCli, err := t.GetRemoteClient(ctx)
	if err != nil {
		return deploy.Error(err)
	}
	svc := t.BuildServiceMeta(t.LoadBalancer)
	err = remoteCli.Get(ctx, client.ObjectKeyFromObject(svc), svc)
	if err != nil {
		return deploy.Error(err)
	}
	if len(svc.Status.LoadBalancer.Ingress) == 0 {
		return deploy.Error(fmt.Errorf("wait loadbalancer ingress not empty")).
			WithRequeuAfter(time.Second * 30)
	}
	address := svc.Status.LoadBalancer.Ingress[0].Hostname
	if address == "" {
		address = svc.Status.LoadBalancer.Ingress[0].IP
	}
	lbARN := t.LoadBalancer.Status.CloudResourceId
	if lbARN == "" {
		deploy.Error(fmt.Errorf("load balancer %s does not have cloud resource id", t.LoadBalancer.Name))
	}

	// hosted zone ID
	hostedZoneID, ok := svc.Annotations[annoKeyAWSLoadBalancerCanonicalHostedZoneID]
	if !ok {
		id, err := t.setAWSLoadBalancerCanonicalHostedZoneIDInAnno(ctx, remoteCli, svc, lbARN)
		if err != nil {
			return deploy.Error(err)
		}
		hostedZoneID = id
	}
	record := t.newAWSDNSRecord(address, hostedZoneID)
	// use controllerutil to create or update DNS record, avoid unexpected change for the existed resources
	if _, err := controllerutil.CreateOrUpdate(ctx, t.Local, record, func() error { return nil }); err != nil {
		return deploy.Error(err)
	}
	return deploy.Done()
}

func (t *DNS) PostCreate(ctx context.Context) deploy.Result {
	dnsName := t.GetDNSName(t.LoadBalancer, t.Config.DNSProviderConfig.AwsRoute53.Domain)
	t.LoadBalancer.Status.DNS = dnsName
	obj := applier.NewClusterObject[*route53v1beta2.Record](dnsName)
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil {
		return deploy.Error(err)
	}
	ready := controllers.IsResourceReady(obj.Status.ResourceStatus)
	if ready {
		if t.LoadBalancer.Spec.Type == v1alpha1.PublicLoadBalancerType && dnsName != "" {
			result := checkPublicDNSReachable(ctx, dnsName)
			t.recordPublicDNSProbeResult(result)
			if !result.reachable {
				if result.err != nil {
					return deploy.Error(fmt.Errorf("public dns is not reachable: %s: %w", result.reason, result.err)).
						WithRequeuAfter(publicDNSProbeRequeue)
				}
				return deploy.Error(fmt.Errorf("public dns is not reachable: %s", result.reason)).
					WithRequeuAfter(publicDNSProbeRequeue)
			}
		}
		t.LoadBalancer.Status.Phase = v1alpha1.LoadBalancerPhaseActive
		return deploy.Done()
	} else {
		return deploy.Error(fmt.Errorf("wait dns record ready")).
			WithRequeuAfter(time.Second * 30)
	}
}

func (t *DNS) recordPublicDNSProbeResult(result publicDNSProbeResult) {
	if t.LoadBalancer.Annotations == nil {
		t.LoadBalancer.Annotations = map[string]string{}
	}
	if result.reachable {
		t.LoadBalancer.Annotations[consts.AnnotationKeyPublicDNSReachable] = publicDNSReachable
		t.LoadBalancer.Annotations[consts.AnnotationKeyPublicDNSReason] = ""
	} else {
		t.LoadBalancer.Annotations[consts.AnnotationKeyPublicDNSReachable] = publicDNSUnreachable
		t.LoadBalancer.Annotations[consts.AnnotationKeyPublicDNSReason] = result.reason
	}
}

func (t *DNS) Delete(ctx context.Context) deploy.Result {
	dnsName := t.GetDNSName(t.LoadBalancer, t.Config.DNSProviderConfig.AwsRoute53.Domain)
	pc := applier.NewClusterObject[*route53v1beta2.Record](dnsName)
	return deploy.PanicOrDone(k8s.DeleteObject(ctx, t.Local, pc))
}

func (t *DNS) newAWSDNSRecord(address string, elbHostedZoneId string) *route53v1beta2.Record {
	dnsName := t.GetDNSName(t.LoadBalancer, t.Config.DNSProviderConfig.AwsRoute53.Domain)
	// make it competible with the old version in lb management
	rrs := applier.NewClusterObject[*route53v1beta2.Record](dnsName)
	rrs.Annotations = map[string]string{}
	rrs.Labels = t.LoadBalancer.Spec.Tags
	rrs.Spec = route53v1beta2.RecordSpec{
		ResourceSpec: xpv1.ResourceSpec{
			ProviderConfigReference: &xpv1.Reference{
				Name: getProviderConfigName(t.Config.DNSProviderConfig.AwsRoute53.AccountID),
			},
		},
		ForProvider: route53v1beta2.RecordParameters{
			Region: ptr.To(t.Config.RegionConfig.Name),
			Name:   ptr.To(dnsName),
			Alias: &route53v1beta2.AliasParameters{
				Name:                 ptr.To(fmt.Sprintf("dualstack.%s", address)),
				ZoneID:               ptr.To(elbHostedZoneId),
				EvaluateTargetHealth: ptr.To(false),
			},
			Type:   ptr.To("A"),
			ZoneID: ptr.To(t.Config.DNSProviderConfig.AwsRoute53.ZoneId),
		},
	}

	return rrs
}

func (t *DNS) setAWSLoadBalancerCanonicalHostedZoneIDInAnno(ctx context.Context, remote client.Client, svc *corev1.Service, arn string) (string, error) {
	hostedZoneID, err := t.getLoadBalancerCanonicalHostedZoneID(ctx, arn)
	if err != nil {
		return "", err
	}

	if err := k8s.PatchObject(ctx, remote, svc, newHostedZoneIDPatch(hostedZoneID)); err != nil {
		return "", err
	}

	return hostedZoneID, nil
}

func (t *DNS) getLoadBalancerCanonicalHostedZoneID(ctx context.Context, arn string) (string, error) {
	c := t.awsCli.ELBV2(&aws.AssumedRole{
		RoleARN: getAWSAssumedRoleForSDK(t.VPC),
	})
	result, err := c.DescribeLoadBalancers(ctx, &elasticloadbalancingv2.DescribeLoadBalancersInput{
		LoadBalancerArns: []string{arn},
	})
	if err != nil {
		return "", err
	}
	if len(result.LoadBalancers) == 0 || result.LoadBalancers[0].CanonicalHostedZoneId == nil {
		return "", fmt.Errorf("NLB not found for arn %s", arn)
	}
	return *result.LoadBalancers[0].CanonicalHostedZoneId, nil
}

func newARNPatch(arn string) k8s.ObjectPatch {
	return newAnnoPatch(annoKeyAWSLoadBalancerARN, arn)
}

func newHostedZoneIDPatch(hostedZoneID string) k8s.ObjectPatch {
	return newAnnoPatch(annoKeyAWSLoadBalancerCanonicalHostedZoneID, hostedZoneID)
}

func newAnnoPatch(k, v string) k8s.ObjectPatch {
	return func(obj client.Object) *jsonpatch.Operation {
		anno, op := obj.GetAnnotations(), "replace"
		if anno == nil || anno[k] == "" {
			op = "add"
		}
		return &jsonpatch.Operation{
			Operation: op,
			Path:      `/metadata/annotations/` + strings.ReplaceAll(k, "/", "~1"),
			Value:     v,
		}
	}
}

func getProviderConfigName(accountID string) string {
	return fmt.Sprintf("dns-%s", accountID)
}
