package procedure

import (
	"context"
	"path/filepath"

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

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

const (
	frpsDeployName = "frps-deployment"
	frpsImageKey   = "frpsImage"
	frpsImageName  = "frps"
	frpsLabelKey   = "app"
	frpsLabelValue = "eks-frps"
	frpsCommand    = frpsImageName

	frpsConfigVolume    = "frps-config"
	frpsConfigMountPath = "/etc/frps"
)

type frpsDeployment struct{}

// Deploy deploys resource for the specified tenant.
func (f *frpsDeployment) Deploy(ctx context.Context, baseCli, _ client.Client, info ProcedureInfo) error {
	frpsImage, err := GetConfigWithKey(ctx, baseCli, info, frpsImageKey)
	if err != nil {
		return err
	}
	deploy := getFrpsDeployment(info, frpsImage)
	_, err = controllerutil.CreateOrUpdate(ctx, baseCli, deploy, func() error {
		mutateFrpsDeploymentSpec(deploy, info, frpsImage)
		return nil
	})
	return err
}

func mutateFrpsDeploymentSpec(deployment *appsv1.Deployment, info ProcedureInfo, frpsImage string) {
	labels := map[string]string{frpsLabelKey: frpsLabelValue}
	// 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: frpsConfigVolume,
			VolumeSource: corev1.VolumeSource{
				ConfigMap: &corev1.ConfigMapVolumeSource{
					LocalObjectReference: corev1.LocalObjectReference{
						Name: ResourceKey(info, frpsCmName).Name,
					},
				},
			},
		},
	}
	deployment.Spec.Template.Spec.Containers = []corev1.Container{
		{
			Name:  frpsImageName,
			Image: frpsImage,
			VolumeMounts: []corev1.VolumeMount{
				{
					Name:      frpsConfigVolume,
					MountPath: frpsConfigMountPath,
				},
			},
			Command: []string{frpsCommand},
			Args: []string{
				"-c", filepath.Join(frpsConfigMountPath, frpsConfigFileKey),
			},
		},
	}
	deployment.Spec.Template.Labels = labels
}

func getFrpsDeployment(info ProcedureInfo, frpsImage string) *appsv1.Deployment {
	key := ResourceKey(info, frpsDeployName)
	deployment := &appsv1.Deployment{}
	deployment.Name = key.Name
	deployment.Namespace = key.Namespace
	mutateFrpsDeploymentSpec(deployment, info, frpsImage)
	return deployment
}

// Status retrieves the status for the deploying step.
func (f *frpsDeployment) Status(ctx context.Context, baseCli, infraCli client.Reader, info ProcedureInfo) (bool, error) {
	key := ResourceKey(info, frpsDeployName)
	deployment := &appsv1.Deployment{}
	err := baseCli.Get(ctx, key, deployment)
	if err != nil {
		return false, err
	}
	return isDeploymentReady(deployment), nil
}

// isDeploymentReady return true if the deployment is ready.
func isDeploymentReady(deployment *appsv1.Deployment) bool {
	for _, cond := range deployment.Status.Conditions {
		if cond.Type != appsv1.DeploymentAvailable {
			continue
		}
		return cond.Status == corev1.ConditionTrue
	}
	return false
}

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

// Cleanup cleans up the frps deployment.
func (f *frpsDeployment) Cleanup(ctx context.Context, baseCli, infraCli client.Client, info ProcedureInfo) error {
	err := baseCli.Delete(ctx, getFrpsDeployment(info, ""))
	if err != nil && !apierrors.IsNotFound(err) {
		return err
	}
	return nil
}

// NewFrpsDeploy returns the procedure for frps deployment.
func NewFrpsDeploy() *frpsDeployment {
	return &frpsDeployment{}
}

var _ Procedure = &frpsDeployment{}
