package frps

import (
	"context"
	"fmt"

	corev1 "k8s.io/api/core/v1"

	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/procedure"
	"github.com/tidbcloud/infra-provider/util/collections"

	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/intstr"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

const (
	frpsSvcName = "frps-service"
	public      = "public"
	internal    = "internal"
	external    = "external"
)

type frpsSvc struct{}

// Deploy deploys resource for the specified tenant.
func (f *frpsSvc) Deploy(ctx context.Context, baseCli client.Client, info ProcedureInfo) error {
	// svc is exposed to tenant.
	// tenant can connect to frps bind port to establish reverse proxy tunnel.
	svc := getFrpsService(info)
	_, err := controllerutil.CreateOrUpdate(ctx, baseCli, svc, func() error {
		mutateFrpsServiceSpec(svc, info)
		return nil
	})
	if err != nil {
		return err
	}
	// internalSvc is consumed by other components in base cluster.
	// it is resolved as tenant EKS's host.
	internalSvc := getFrpsInternalService(info)
	_, err = controllerutil.CreateOrUpdate(ctx, baseCli, internalSvc, func() error {
		mutateFrpsInternalServiceSpec(internalSvc, info)
		return nil
	})
	if err != nil {
		return err
	}
	// create LB type service to expose frps service to components external of the cluster if needed.
	if info.frps.Spec.ExternalTcpPorts != nil {
		externalSvc := getFrpsExternalService(info)
		_, err = controllerutil.CreateOrUpdate(ctx, baseCli, externalSvc, func() error {
			mutateFrpsExternalServiceSpec(externalSvc, info)
			return nil
		})
		if err != nil {
			return err
		}
	}
	return nil
}

func mutateFrpsServiceSpec(svc *corev1.Service, info ProcedureInfo) {
	labels := map[string]string{
		frpsLabelKey: frpsLabelValue,
		frpsNameKey:  info.frps.Name,
	}
	svc.Labels = collections.MergeMaps(info.frps.ObjectMeta.Labels, labels)
	svc.Spec.Type = corev1.ServiceTypeLoadBalancer
	foundPort := false
	for _, port := range svc.Spec.Ports {
		if int(port.Port) != info.frps.Spec.BindPort {
			continue
		}
		foundPort = true
		port.TargetPort = intstr.FromInt(info.frps.Spec.BindPort)
		break
	}
	if !foundPort {
		svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{
			TargetPort: intstr.FromInt(info.frps.Spec.BindPort),
			Port:       int32(info.frps.Spec.BindPort),
		})
	}
	svc.Spec.Selector = labels
	if svc.Annotations == nil {
		svc.Annotations = make(map[string]string)
	}
	svc.Annotations["service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled"] = "true"
	svc.Annotations["service.beta.kubernetes.io/aws-load-balancer-internal"] = "0.0.0.0/0"
	svc.Annotations["service.beta.kubernetes.io/aws-load-balancer-attributes"] = "load_balancing.cross_zone.enabled=true"
	svc.Annotations["service.beta.kubernetes.io/aws-load-balancer-type"] = "nlb"
}

func mutateFrpsInternalServiceSpec(svc *corev1.Service, info ProcedureInfo) {
	labels := map[string]string{
		frpsLabelKey: frpsLabelValue,
		frpsNameKey:  info.frps.Name,
	}
	svc.Labels = collections.MergeMaps(info.frps.ObjectMeta.Labels, labels)
	svc.Spec.Type = corev1.ServiceTypeClusterIP
	svc.Spec.Ports = make([]corev1.ServicePort, 0, len(svc.Spec.Ports))
	for _, port := range info.frps.Spec.TcpPorts {
		svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{
			Name:       fmt.Sprintf("tcp-port-%d", port),
			TargetPort: intstr.FromInt(port),
			Port:       int32(port),
		})
	}
	svc.Spec.Selector = labels
}

func mutateFrpsExternalServiceSpec(svc *corev1.Service, info ProcedureInfo) {
	labels := map[string]string{
		frpsLabelKey: frpsLabelValue,
		frpsNameKey:  info.frps.Name,
	}
	svc.Labels = collections.MergeMaps(info.frps.ObjectMeta.Labels, labels)
	// TODO: add subnet annotation to make the LB type service exposed on specific subnet
	// see: https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.2/guide/service/annotations/#subnets
	svc.Spec.Type = corev1.ServiceTypeLoadBalancer
	svc.Spec.Ports = make([]corev1.ServicePort, 0, len(svc.Spec.Ports))
	for _, port := range info.frps.Spec.ExternalTcpPorts {
		svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{
			Name:       fmt.Sprintf("tcp-port-%d", port),
			TargetPort: intstr.FromInt(port),
			Port:       int32(port),
		})
	}
	svc.Spec.Selector = labels
}

func getFrpsService(info ProcedureInfo) *corev1.Service {
	svc := &corev1.Service{}
	svc.Name = namer.UniqueIdentifier(info.frps.ObjectMeta, namer.WithPrefix(public))
	svc.Namespace = info.namespace
	mutateFrpsServiceSpec(svc, info)
	return svc
}

func getFrpsInternalService(info ProcedureInfo) *corev1.Service {
	svc := &corev1.Service{}
	svc.Name = namer.UniqueIdentifier(info.frps.ObjectMeta, namer.WithPrefix(internal))
	svc.Namespace = info.namespace
	mutateFrpsInternalServiceSpec(svc, info)
	return svc
}

func getFrpsExternalService(info ProcedureInfo) *corev1.Service {
	svc := &corev1.Service{}
	svc.Name = namer.UniqueIdentifier(info.frps.ObjectMeta, namer.WithPrefix(external))
	svc.Namespace = info.namespace
	mutateFrpsExternalServiceSpec(svc, info)
	return svc
}

// Status retrieves the status for the deploying step.
func (f *frpsSvc) Status(ctx context.Context, baseCli client.Reader, infraCli client.Client, info ProcedureInfo) (bool, error) {
	internalSvc := &corev1.Service{}
	err := baseCli.Get(ctx, types.NamespacedName{Name: namer.UniqueIdentifier(info.frps.ObjectMeta, namer.WithPrefix(internal)), Namespace: info.namespace}, internalSvc)
	if err != nil {
		return false, err
	}
	// frps internal service is not ready until ClusterIP is assigned.
	if internalSvc.Spec.ClusterIP == "" {
		return false, nil
	}
	svc := &corev1.Service{}
	err = baseCli.Get(ctx, types.NamespacedName{Name: namer.UniqueIdentifier(info.frps.ObjectMeta, namer.WithPrefix(public)), Namespace: info.namespace}, svc)
	if err != nil {
		return false, err
	}
	if f.isLBServiceReady(ctx, baseCli, info, svc) {
		if err := f.addServiceNameAnnotation(ctx, baseCli, info); err != nil {
			return false, err
		}
		return true, nil
	}
	return false, nil
}

func (f *frpsSvc) addServiceNameAnnotation(ctx context.Context, base client.Reader, info ProcedureInfo) error {
	if info.frps.Annotations == nil {
		info.frps.Annotations = map[string]string{}
	}
	info.frps.Annotations[procedure.AnnotationKeyFrpsSvcName] = namer.UniqueIdentifier(info.frps.ObjectMeta, namer.WithPrefix(public))
	info.frps.Annotations[procedure.AnnotationKeyFrpsSvcNs] = info.namespace
	info.frps.Annotations[procedure.AnnotationKeyFrpsInternalSvcName] = namer.UniqueIdentifier(info.frps.ObjectMeta, namer.WithPrefix(internal))
	if info.frps.Spec.ExternalTcpPorts == nil {
		return nil
	}

	// annotate external svc name by domain name
	externalSvc := getFrpsExternalService(info)
	err := base.Get(ctx, types.NamespacedName{Name: externalSvc.Name, Namespace: externalSvc.Namespace}, externalSvc)
	if err != nil {
		return err
	}
	if len(externalSvc.Status.LoadBalancer.Ingress) == 0 {
		return fmt.Errorf("cannot find ingress for svc '%s/%s'", externalSvc.Namespace, externalSvc.Name)
	}
	info.frps.Annotations[procedure.AnnotationKeyFrpsExternalSvcName] = externalSvc.Status.LoadBalancer.Ingress[0].Hostname
	return nil
}

// isLBServiceReady return true if the LB type service is ready.
func (f *frpsSvc) isLBServiceReady(ctx context.Context, cli client.Reader, info ProcedureInfo, svc *corev1.Service) bool {
	if len(svc.Status.LoadBalancer.Ingress) == 0 {
		return false
	}
	return true
}

// Name returns the name of the step.
func (f *frpsSvc) Name() string {
	return frpsSvcName
}

// Cleanup cleans up the frps configmap.
func (f *frpsSvc) Cleanup(ctx context.Context, baseCli client.Client, info ProcedureInfo) error {
	if err := k8s.DeleteObject(ctx, baseCli, getFrpsService(info)); err != nil {
		return err
	}
	if err := k8s.DeleteObject(ctx, baseCli, getFrpsInternalService(info)); err != nil {
		return err
	}
	if err := k8s.DeleteObject(ctx, baseCli, getFrpsExternalService(info)); err != nil {
		return err
	}
	return nil
}

// NewFrpsSVC returns the procedure for frps service.
func NewFrpsSVC() *frpsSvc {
	return &frpsSvc{}
}

var _ Procedure = &frpsSvc{}
