package common

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

	route53v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/route53/v1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/go-logr/logr"
	"github.com/samber/lo"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/util/errors"
	"k8s.io/apimachinery/pkg/util/intstr"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/eks-provider/controllers/cluster/task"
	"github.com/tidbcloud/eks-provider/controllers/cluster/task/utils"
	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/consts"
	"github.com/tidbcloud/infra-provider/pkg/controllers/common"
	route53pause "github.com/tidbcloud/infra-provider/pkg/crossplane/route53"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
)

const (
	defaultWaitDuration = time.Second * 10

	// GCP
	annoKeyForwardingRuleName = consts.AnnoKeyGKEOriginForwardingRuleName
)

type FullService struct {
	*task.BaseTask
	needsPauseRequeue bool
}

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

func NewFullService(ctx *task.Context) *FullService {
	return &FullService{
		BaseTask: &task.BaseTask{Context: ctx},
	}
}

func (t *FullService) Create(ctx context.Context) deploy.Result {
	t.needsPauseRequeue = false
	if t.TCGetter == nil || t.TCGetter.IsNil() {
		return deploy.Fatal(fmt.Errorf("tidb cluster is not ready yet"))
	}

	remote := t.EnvProvider.GetClient()

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

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

	return deploy.Done()
}

func (t *FullService) 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 (t *FullService) DeleteEndpoint(ctx context.Context, remote client.Client, ep *endpoint) error {
	if err := k8s.DeleteObjectCompletedWithHooks(ctx, t.Local,
		applier.NewClusterObject[*route53v1alpha1.ResourceRecordSet](ep.dnsName),
		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
	// used for tidbgroup
	groupName    string
	svcNamespace string

	dnsName                  string
	dnsDisabled              bool
	svcName                  string
	port                     int
	targetPort               intstr.IntOrString
	targetComponent          cloudv1.ComponentType
	loadBalancerSourceRanges []string
	setStatus                func(set bool)
	overlaySvc               func(svc *corev1.Service)
	resourceID               string
}

func (t *FullService) newPublicEndpoint() *endpoint {
	ep := &endpoint{
		isEnabled:       t.Cluster.Spec.SqlEndpoints.Public.IsEnabled(),
		dnsName:         t.Namer.GetPublicEndpointAddress(),
		dnsDisabled:     t.Cluster.IsDNSDisabled(t.Cluster.GetDefaultGroupID()),
		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:       utils.GetEffectiveHostName(ep.dnsName, ep.dnsDisabled),
				Port:       ep.port,
				ResourceID: ep.resourceID,
			}
		} else {
			t.Cluster.Status.SqlEndpoints.PublicEndpoint = nil
		}
	}

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

		// Use external load balancer
		// See https://cloud.google.com/kubernetes-engine/docs/concepts/service-load-balancer-parameters
		svc.Annotations["networking.gke.io/load-balancer-type"] = "External"
	}

	return ep
}

func (t *FullService) newPrivateEndpoint() *endpoint {
	ep := &endpoint{
		isEnabled:       t.Cluster.Spec.SqlEndpoints.Private.IsEnabled(),
		dnsName:         t.Namer.GetPrivateEndpointAddress(),
		svcName:         t.Namer.GetPrivateTiDBServiceName(),
		dnsDisabled:     t.Cluster.IsDNSDisabled(t.Cluster.GetDefaultGroupID()),
		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:       utils.GetEffectiveHostName(ep.dnsName, ep.dnsDisabled),
				Port:       ep.port,
				ResourceID: ep.resourceID,
			}
		} 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://cloud.google.com/kubernetes-engine/docs/concepts/service-load-balancer-parameters
		svc.Annotations["networking.gke.io/load-balancer-type"] = "Internal"
	}

	return ep
}

func (t *FullService) ReconcileEndpointFunc(ctx context.Context, remote client.Client, ep *endpoint) 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
		}
		// TODO(ideascf): the `mutatefuncs.MutateService` just overwrite the `t.NewService(ep).Annotations`
		// into the *current* service object got from k8s api server. This may cause some existing annotations annotated
		// by 3-rd services like the gcp lb controller to be lost.
		// However, it actually don't lose anything. This is because the applier will compare the hash computed from the expected obj
		// with the value in the current object's annotation. The hash string wouldn't be changed if we don't change the expected obj as
		// it's calculated from the expected obj, even if the expected obj is different from the current obj.
		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
		}

		frName, err := reconcileFRName(ctx, t.Logger, remote, svc)
		if err != nil {
			return false, err
		}
		ep.resourceID = frName

		if ep.dnsDisabled {
			if err := k8s.DeleteObjectCompletedWithHooks(ctx, t.Local,
				applier.NewClusterObject[*route53v1alpha1.ResourceRecordSet](ep.dnsName),
				k8s.HookAutoUnpauseCrossplaneCR,
			); err != nil {
				return false, err
			}
		} else {
			if err := t.applyRoute53RecordAndPause(ctx, t.NewDNSRecord(ep.dnsName, address)); err != nil {
				return false, err
			}
		}

		ep.setStatus(true)

		return false, nil
	}
}

func (t *FullService) applyRoute53RecordAndPause(ctx context.Context, desired *route53v1alpha1.ResourceRecordSet) error {
	paused, err := route53pause.IsPaused(ctx, t.Local, desired.GetName())
	if err != nil {
		return err
	}
	if paused {
		return nil
	}
	if _, err := applier.NewApplier(t.Local, mutatefuncs.MutateAWSDNSResourceRecordSet).
		Expected(desired).
		Apply(ctx); err != nil {
		return err
	}
	needsRequeue, err := route53pause.ReconcilePause(ctx, t.Local, desired.GetName())
	if err != nil {
		return err
	}
	t.needsPauseRequeue = t.needsPauseRequeue || needsRequeue
	return nil
}

// This name generation is only for legacy ilb with finalizer gke.networking.io/l4-ilb-v1
func generateNewTargetServiceName(uid string) string {
	// See https://github.com/kubernetes/cloud-provider/blob/release-1.31/cloud.go#L84-L98
	targetService := strings.ReplaceAll("a"+uid, "-", "")
	if len(targetService) > maxServiceNameLength {
		targetService = targetService[:maxServiceNameLength]
	}
	return targetService
}

func reconcileFRName(ctx context.Context, logger logr.Logger, remote client.Client, svc *corev1.Service) (string, error) {
	// Get the first non-empty annotation
	frName, existedAnno := lo.Coalesce(
		// The annotation exists if ILB subsetting enabled.
		svc.Annotations[consts.AnnoKeyGKESubsettingILBForwardingRule],
		// annoKeyForwardingRuleName is set in FullService task
		svc.Annotations[annoKeyForwardingRuleName],
	)
	// If not found, generate new legacy FR name
	if !existedAnno {
		frName = generateNewTargetServiceName(string(svc.GetUID()))
	}

	// Initialize or refresh annotation
	if svc.Annotations[annoKeyForwardingRuleName] != frName {
		logger.Info("Updating ILB Forwarding Rule name", "origin", svc.Annotations[annoKeyForwardingRuleName], "new", frName)
		if err := k8s.PatchObject(ctx, remote, svc, newForwardingRulePatch(frName)); err != nil {
			return "", err
		}
	}

	return frName, nil
}

func newForwardingRulePatch(name string) k8s.ObjectPatch {
	return newAnnoPatch(annoKeyForwardingRuleName, name)
}

func (t *FullService) NewDNSRecord(dnsName, address string) *route53v1alpha1.ResourceRecordSet {
	rrs := applier.NewClusterObject[*route53v1alpha1.ResourceRecordSet](dnsName)

	rrs.Spec = route53v1alpha1.ResourceRecordSetSpec{
		ResourceSpec: xpv1.ResourceSpec{
			ProviderConfigReference: &xpv1.Reference{
				Name: t.Config.ControlplaneAWSProviderConfig,
			},
		},
		ForProvider: route53v1alpha1.ResourceRecordSetParameters{
			ResourceRecords: []route53v1alpha1.ResourceRecord{
				{
					Value: address,
				},
			},
			Type:   "A",
			TTL:    pointer.Int64(60),
			ZoneID: pointer.String(t.Config.FullManage.PublicHostedZoneID),
		},
	}

	return rrs
}

func (t *FullService) 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 := make(map[string]string)
	if !t.Cluster.FeatureEnabled(consts.ClusterFeatureRemoveNEGAnnotation) {
		// TODO: Remove this annotation for existing clusters, it will not affect connectivity.
		// To use standalone NEG, we need to enable the gke subsetting, instead of using this annotation.
		// See https://cloud.google.com/kubernetes-engine/docs/how-to/internal-load-balancing#gke-subsetting
		annotation["cloud.google.com/neg"] = fmt.Sprintf(`{"exposed_ports": {"%d":{}}}`, ep.port)
	}

	// NOTE: these labels should be attached onto real google resource,
	// but I can't find the way to do it.
	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
}
