package aks

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

	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	azurenetworkv1beta1 "github.com/upbound/provider-azure/apis/network/v1beta1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/util/errors"
	"k8s.io/apimachinery/pkg/util/intstr"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/eks-provider/controllers/cluster/task"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/applier"
	"github.com/tidbcloud/infra-provider/pkg/applier/mutatefuncs"
	"github.com/tidbcloud/infra-provider/pkg/controllers/common"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
)

const (
	aksPublicLoadBalancerName  = "kubernetes"
	aksPrivateLoadBalancerName = "kubernetes-internal"
)

var (
	defaultWaitDuration  = time.Second * 10
	maxServiceNameLength = 32
)

type Service struct {
	*task.BaseTask
}

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

func NewService(ctx *task.Context) *Service {
	return &Service{
		BaseTask: &task.BaseTask{Context: ctx},
	}
}

func (t *Service) Create(ctx context.Context) deploy.Result {
	remote := t.EnvProvider.GetClient()

	privateEp := t.newPrivateEndpoint()
	publicEp := t.newPublicEndpoint()

	wait, err := combine(
		t.ReconcileEndpointFunc(ctx, remote, privateEp, true),
		t.ReconcileEndpointFunc(ctx, remote, publicEp, false),
	)
	if err != nil {
		return deploy.Error(err)
	}
	if wait {
		return deploy.Error(fmt.Errorf("service is not ready")).WithRequeuAfter(defaultWaitDuration)
	}

	return deploy.Done()
}

func (t *Service) Delete(ctx context.Context) deploy.Result {
	remote := t.EnvProvider.GetClient()

	privateEp := t.newPrivateEndpoint()
	publicEp := t.newPublicEndpoint()

	return deploy.ErrorOrDone(errors.NewAggregate([]error{
		t.DeleteEndpoint(ctx, remote, privateEp),
		t.DeleteEndpoint(ctx, remote, publicEp),
	}))
}

func combine(fs ...func() (bool, error)) (wait bool, _ error) {
	errs := []error{}
	needWait := false
	for _, f := range fs {
		wait, err := f()
		if err != nil {
			errs = append(errs, err)
		}
		if wait {
			needWait = true
		}
	}

	return needWait, errors.NewAggregate(errs)
}

func (t *Service) DeleteEndpoint(ctx context.Context, remote client.Client, ep *endpoint) error {
	recordName := t.getDNSRecordName(ep.dnsName)
	if err := k8s.DeleteObjectCompletedWithHooks(ctx, t.Local,
		applier.NewClusterObject[*azurenetworkv1beta1.DNSARecord](recordName),
		k8s.HookAutoUnpauseCrossplaneCR,
	); err != nil {
		return err
	}

	if err := k8s.DeleteObjectCompleted(ctx, remote,
		applier.NewObject[*corev1.Service](t.Namer.GetTiDBNamespace(), ep.svcName),
	); err != nil {
		return err
	}

	return nil
}

type endpoint struct {
	isEnabled                bool
	dnsName                  string
	svcName                  string
	port                     int
	targetPort               intstr.IntOrString
	targetComponent          cloudv1.ComponentType
	loadBalancerSourceRanges []string
	setStatus                func(set bool)
	overlaySvc               func(svc *corev1.Service)
	resouceID                string
}

func (t *Service) newPublicEndpoint() *endpoint {
	ep := &endpoint{
		isEnabled:       t.Cluster.Spec.SqlEndpoints.Public.IsEnabled(),
		dnsName:         t.Namer.GetPublicEndpointAddress(),
		svcName:         t.Namer.GetPublicTiDBServiceName(),
		port:            int(t.Cluster.GetPublicSQLEndpointPort()),
		targetPort:      t.Cluster.Spec.SqlEndpoints.GetTargetPort(),
		targetComponent: t.Cluster.Spec.SqlEndpoints.GetTargetComponent(),
		// Disable white list of private service
		// We assume that all ips in vpc are allowed to visit private service
		loadBalancerSourceRanges: t.Cluster.Spec.SqlEndpoints.Public.LoadBalancerSourceRanges,
	}

	ep.setStatus = func(set bool) {
		if set {
			t.Cluster.Status.SqlEndpoints.PublicEndpoint = &cloudv1.SqlEndpointStatus{
				Host:       ep.dnsName,
				Port:       ep.port,
				ResourceID: ep.resouceID,
			}
		} else {
			t.Cluster.Status.SqlEndpoints.PublicEndpoint = nil
		}
	}

	ep.overlaySvc = func(svc *corev1.Service) {
		svc.Labels[common.Component] = common.Loadbalance
	}

	return ep
}

func (t *Service) newPrivateEndpoint() *endpoint {
	ep := &endpoint{
		isEnabled:       t.Cluster.Spec.SqlEndpoints.Private.IsEnabled(),
		dnsName:         t.Namer.GetPrivateEndpointAddress(),
		svcName:         t.Namer.GetPrivateTiDBServiceName(),
		port:            int(t.Cluster.GetPrivateSQLEndpointPort()),
		targetPort:      t.Cluster.Spec.SqlEndpoints.GetTargetPort(),
		targetComponent: t.Cluster.Spec.SqlEndpoints.GetTargetComponent(),
		// Disable white list of private service
		// We assume that all ips in vpc are allowed to visit private service
		loadBalancerSourceRanges: []string{"0.0.0.0/0"},
	}

	ep.setStatus = func(enable bool) {
		if enable {
			t.Cluster.Status.SqlEndpoints.PrivateEndpoint = &cloudv1.SqlEndpointStatus{
				Host:       ep.dnsName,
				Port:       ep.port,
				ResourceID: ep.resouceID,
			}
		} else {
			t.Cluster.Status.SqlEndpoints.PrivateEndpoint = nil
		}
	}

	ep.overlaySvc = func(svc *corev1.Service) {
		svc.Labels[common.Component] = common.PrivateLoadbalance

		// Use internal load balancer
		// See https://learn.microsoft.com/en-us/azure/aks/internal-lb?tabs=set-service-annotations#create-an-internal-load-balancer
		svc.Annotations["service.beta.kubernetes.io/azure-load-balancer-internal"] = "true"
	}

	return ep
}

func (t *Service) ReconcileEndpointFunc(ctx context.Context, remote client.Client, ep *endpoint, isPrivateLB bool) func() (wait bool, _ error) {
	return func() (wait bool, _ error) {
		if !ep.isEnabled {
			if err := t.DeleteEndpoint(ctx, remote, ep); err != nil {
				return false, err
			}

			ep.setStatus(false)

			return false, nil
		}
		svc, err := applier.NewApplier(remote, mutatefuncs.MutateService).
			Expected(t.NewService(ep)).
			Apply(ctx)
		if err != nil {
			return false, err
		}

		if len(svc.Status.LoadBalancer.Ingress) == 0 {
			return true, nil
		}

		address := svc.Status.LoadBalancer.Ingress[0].Hostname
		if address == "" {
			address = svc.Status.LoadBalancer.Ingress[0].IP
		}

		if _, err := applier.NewApplier(t.Local, mutatefuncs.MutateAzureDNSRecord).
			Expected(t.NewDNSRecord(ep.dnsName, address)).
			Apply(ctx); err != nil {
			return false, err
		}

		ep.resouceID = t.getAKSLoadBalancerResourceID(string(svc.GetUID()), isPrivateLB)

		ep.setStatus(true)

		return false, nil
	}
}

func (t *Service) getAKSLoadBalancerResourceID(svcUID string, isPrivateLB bool) string {
	// Azure will add an character 'a' in the head and remove all the '-' to construct load balancer name, add 'a' and then limit the length to 32
	// https://github.com/kubernetes-sigs/cloud-provider-azure/blob/06ec1846c13c3ea6b366a5b888d0fff6a28a77a2/pkg/provider/azure_loadbalancer.go#L4144
	frontendIPConfigurationID := strings.ReplaceAll("a"+svcUID, "-", "")
	if len(frontendIPConfigurationID) > maxServiceNameLength {
		frontendIPConfigurationID = frontendIPConfigurationID[:maxServiceNameLength]
	}

	subscriptionID := t.Cluster.Spec.Kubernetes.Account
	resourceGroupName := t.Cluster.Spec.Kubernetes.Cloud.Azure.NodeResourceGroup
	// Name of the load balancer, when using internal load balancer, the name is kubernetes-internal
	loadBalancerName := aksPublicLoadBalancerName
	if isPrivateLB {
		loadBalancerName = aksPrivateLoadBalancerName
	}

	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/frontendIPConfigurations/%s", subscriptionID, resourceGroupName, loadBalancerName, frontendIPConfigurationID)
}

// For Azure DNS, the dns record doesn't contain the zone domain name
func (t *Service) getDNSRecordName(dnsName string) string {
	return strings.TrimSuffix(dnsName, fmt.Sprintf(".%s", t.Config.FullManage.PublicHostedZoneDomain))
}

func (t *Service) NewDNSRecord(dnsName, address string) *azurenetworkv1beta1.DNSARecord {
	recordName := t.getDNSRecordName(dnsName)
	rrs := applier.NewClusterObject[*azurenetworkv1beta1.DNSARecord](recordName)

	rrs.Spec = azurenetworkv1beta1.DNSARecordSpec{
		ResourceSpec: xpv1.ResourceSpec{
			ProviderConfigReference: &xpv1.Reference{
				Name: t.Config.ControlplaneAzureProviderConfig,
			},
		},
		ForProvider: azurenetworkv1beta1.DNSARecordParameters{
			Records: []*string{
				ptr.To(address),
			},
			ZoneName:          ptr.To(t.Config.FullManage.PublicHostedZoneDomain),
			ResourceGroupName: ptr.To(t.Config.FullManage.AzurePublicHostedResourceGroupName),
			TTL:               ptr.To(float64(60)),
		},
	}

	return rrs
}

func (t *Service) NewService(ep *endpoint) *corev1.Service {
	svc := applier.NewObject[*corev1.Service](t.Namer.GetTiDBNamespace(), ep.svcName)

	tags := common.ConcatenateTags(cloudv1.TagsFromLabels(t.Cluster.Labels),
		map[string]string{
			common.Cluster: t.Cluster.Name,
			common.UsedBy:  common.CustomerTiDB,
		},
	)

	annotation := map[string]string{}

	svc.Labels = tags
	svc.Annotations = annotation
	svc.Spec = corev1.ServiceSpec{
		Type:                  corev1.ServiceTypeLoadBalancer,
		SessionAffinity:       corev1.ServiceAffinityNone,
		ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyTypeLocal,
		Ports: []corev1.ServicePort{
			{
				Name:       "mysql-client",
				Port:       int32(ep.port),
				TargetPort: ep.targetPort,
				Protocol:   corev1.ProtocolTCP,
			},
		},
		LoadBalancerSourceRanges: ep.loadBalancerSourceRanges,
		Selector:                 t.Namer.WithComponent(string(ep.targetComponent)).GetPodLabels(),
	}

	ep.overlaySvc(svc)

	return svc
}
