package procedure

import (
	"context"

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

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"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"
	frpsInternalSvcName = "frps-internal-service"
	eksAPIServerPort    = 443
)

type frpsSvc struct {
	plClient frpsPrivateLinkClient
}

// 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.
	key := ResourceKey(info, frpsSvcName)
	svc := &corev1.Service{}
	svc.Name, svc.Namespace = key.Name, key.Namespace
	_, 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
	})
	return err
}

func mutateFrpsServiceSpec(svc *corev1.Service, info ProcedureInfo) {
	labels := map[string]string{frpsLabelKey: frpsLabelValue}
	svc.Labels = labels
	svc.Spec.Type = corev1.ServiceTypeLoadBalancer
	foundPort := false
	for _, port := range svc.Spec.Ports {
		if port.Port != frpsBindPort {
			continue
		}
		foundPort = true
		port.TargetPort = intstr.FromInt(frpsBindPort)
		break
	}
	if !foundPort {
		svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{
			TargetPort: intstr.FromInt(frpsBindPort),
			Port:       frpsBindPort,
		})
	}
	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-type"] = "nlb"
}

func mutateFrpsInternalServiceSpec(svc *corev1.Service, info ProcedureInfo) {
	labels := map[string]string{frpsLabelKey: frpsLabelValue}
	svc.Labels = labels
	svc.Spec.Type = corev1.ServiceTypeClusterIP
	svc.Spec.Ports = []corev1.ServicePort{
		{
			TargetPort: intstr.FromInt(eksAPIServerPort),
			Port:       eksAPIServerPort,
		},
	}
	svc.Spec.Selector = labels
}

func getFrpsService(info ProcedureInfo) *corev1.Service {
	key := ResourceKey(info, frpsSvcName)
	svc := &corev1.Service{}
	svc.Name = key.Name
	svc.Namespace = key.Namespace
	mutateFrpsServiceSpec(svc, info)
	return svc
}

func getFrpsInternalService(info ProcedureInfo) *corev1.Service {
	key := ResourceKey(info, frpsInternalSvcName)
	svc := &corev1.Service{}
	svc.Name = key.Name
	svc.Namespace = key.Namespace
	mutateFrpsInternalServiceSpec(svc, info)
	return svc
}

// Status retrieves the status for the deploying step.
func (f *frpsSvc) Status(ctx context.Context, baseCli, infraCli client.Reader, info ProcedureInfo) (bool, error) {
	internalSvc := &corev1.Service{}
	err := baseCli.Get(ctx, ResourceKey(info, frpsInternalSvcName), internalSvc)
	if err != nil {
		return false, err
	}
	// frps internal service is not ready until ClusterIP is assigned.
	if internalSvc.Spec.ClusterIP == "" {
		return false, nil
	}
	key := ResourceKey(info, f.Name())
	svc := &corev1.Service{}
	err = baseCli.Get(ctx, key, svc)
	if err != nil {
		return false, err
	}
	return f.isLBServiceReady(ctx, baseCli, info, svc)
}

// 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, error) {
	if len(svc.Status.LoadBalancer.Ingress) == 0 {
		return false, nil
	}
	nlbArn, err := f.plClient.getFrpsNlbArn(ctx, cli, info)
	if err != nil {
		return false, err
	}
	return nlbArn != "", nil
}

// 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, infraCli client.Client, info ProcedureInfo) error {
	err := baseCli.Delete(ctx, getFrpsService(info))
	if err != nil && !apierrors.IsNotFound(err) {
		return err
	}
	err = baseCli.Delete(ctx, getFrpsInternalService(info))
	if err != nil && !apierrors.IsNotFound(err) {
		return err
	}
	return nil
}

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

var _ Procedure = &frpsSvc{}
