package frpc

import (
	"context"
	"encoding/json"
	"fmt"
	"path/filepath"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

const (
	frpcImageName  = "frpc"
	frpcLabelKey   = "app"
	frpcLabelValue = "frpc"
	frpcCommand    = frpcImageName
	frpcDeployName = "frpc-deployment"

	frpcConfigVolume    = "frpc-config"
	frpcConfigMap       = frpcConfigVolume
	frpcConfigMountPath = "/etc/frpc"
	frpcProxyName       = "eks"
)

type frpc struct {
	name string
}

func (f *frpc) FrpcDeployment(ctx context.Context, cli client.Client, frpcImage string) error {
	eksDeploy := getFrpcDeployment(eksFrpcName, frpcImage)
	_, err := controllerutil.CreateOrUpdate(ctx, cli, eksDeploy, func() error {
		mutateFrpcDeploymentSpec(eksDeploy, eksFrpcName, frpcImage)
		return nil
	})
	if err != nil {
		return err
	}
	proxyDeploy := getFrpcDeployment(proxyFrpcName, frpcImage)
	_, err = controllerutil.CreateOrUpdate(ctx, cli, proxyDeploy, func() error {
		mutateFrpcDeploymentSpec(proxyDeploy, proxyFrpcName, frpcImage)
		return nil
	})
	return err
}

func mutateFrpcDeploymentSpec(deployment *appsv1.Deployment, name, frpcImage string) {
	labels := map[string]string{frpcLabelKey: fmt.Sprintf("%s-%s", frpcLabelValue, name)}
	// TODO: add a node selector for a dedicated EKS node group
	deployment.Spec.Selector = &metav1.LabelSelector{}
	deployment.Spec.Selector.MatchLabels = labels
	deployment.Spec.Template.Spec.Volumes = []corev1.Volume{
		{
			Name: frpcConfigVolume,
			VolumeSource: corev1.VolumeSource{
				ConfigMap: &corev1.ConfigMapVolumeSource{
					LocalObjectReference: corev1.LocalObjectReference{
						Name: fmt.Sprintf("%s-%s", frpcConfigMapName, name),
					},
				},
			},
		},
	}
	deployment.Spec.Template.Spec.Containers = []corev1.Container{
		{
			Name:  frpcImageName,
			Image: frpcImage,
			VolumeMounts: []corev1.VolumeMount{
				{
					Name:      frpcConfigVolume,
					MountPath: frpcConfigMountPath,
				},
			},
			Command: []string{frpcCommand},
			Args: []string{
				"-c", filepath.Join(frpcConfigMountPath, frpcConfigFileName),
			},
			Ports: []corev1.ContainerPort{
				{
					Name:          "healthz",
					ContainerPort: frpcAdminPort,
				},
			},
			ReadinessProbe: &corev1.Probe{
				ProbeHandler: corev1.ProbeHandler{
					Exec: &corev1.ExecAction{
						Command: []string{
							"readiness",
							"-host", frpcAdminEndpoint,
							"-port", fmt.Sprintf("%d", frpcAdminPort),
							"-proxy", name,
						},
					},
				},
			},
		},
	}
	deployment.Spec.Template.Labels = labels
}

func getFrpcDeployment(name, frpcImage string) *appsv1.Deployment {
	deployment := &appsv1.Deployment{}
	deployment.Name = fmt.Sprintf("%s-%s", frpcDeployName, name)
	deployment.Namespace = metav1.NamespaceSystem
	mutateFrpcDeploymentSpec(deployment, name, frpcImage)
	return deployment
}

func (f *frpc) DeploymentData(ctx context.Context, cli client.Client) (string, error) {
	data := make(map[string]appsv1.Deployment)
	deploy := appsv1.Deployment{}
	err := cli.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-%s", frpcDeployName, eksFrpcName), Namespace: metav1.NamespaceSystem}, &deploy)
	if err != nil {
		return "", err
	}
	data[fmt.Sprintf("%s-%s", frpcDeployName, eksFrpcName)] = deploy
	deploy = appsv1.Deployment{}
	err = cli.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-%s", frpcDeployName, proxyFrpcName), Namespace: metav1.NamespaceSystem}, &deploy)
	if err != nil {
		return "", err
	}
	data[fmt.Sprintf("%s-%s", frpcDeployName, eksFrpcName)] = deploy
	buf, err := json.Marshal(data)
	if err != nil {
		return "", err
	}
	return string(buf), nil
}

func (f *frpc) FrpcReady(ctx context.Context, cli client.Client) (bool, error) {
	pods := &corev1.PodList{}
	err := cli.List(ctx, pods, client.InNamespace(metav1.NamespaceSystem), client.MatchingLabels{frpcLabelKey: frpcLabelValue})
	if err != nil {
		return false, err
	}
	for _, pod := range pods.Items {
		containerReady := false
		for _, cond := range pod.Status.Conditions {
			if cond.Type == corev1.ContainersReady {
				containerReady = true
			}
		}
		if !containerReady {
			return false, nil
		}
	}
	return true, nil
}

// NewFrpc returns the frpc deployer.
func NewFrpc() *frpc {
	return &frpc{}
}

var _ Frpc = &frpc{}
