package frps

import (
	"context"
	"path/filepath"

	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"

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

const (
	frpsDeployName = "frps-deployment"
	frpsImageName  = "frps"
	frpsLabelKey   = "app"
	frpsLabelValue = "eks-frps"
	frpsNameKey    = "name"
	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 := info.frps.Spec.FrpsImage
	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,
		frpsNameKey:  info.frps.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: frpsConfigVolume,
			VolumeSource: corev1.VolumeSource{
				ConfigMap: &corev1.ConfigMapVolumeSource{
					LocalObjectReference: corev1.LocalObjectReference{
						Name: namer.UniqueIdentifier(info.frps.ObjectMeta),
					},
				},
			},
		},
	}
	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 {
	deployment := &appsv1.Deployment{}
	deployment.Name = namer.UniqueIdentifier(info.frps.ObjectMeta)
	deployment.Namespace = info.namespace
	mutateFrpsDeploymentSpec(deployment, info, frpsImage)
	return deployment
}

// Status retrieves the status for the deploying step.
func (f *frpsDeployment) Status(ctx context.Context, baseCli client.Reader, _ client.Client, info ProcedureInfo) (bool, error) {
	deployment := &appsv1.Deployment{}
	err := baseCli.Get(ctx, types.NamespacedName{Name: namer.UniqueIdentifier(info.frps.ObjectMeta), Namespace: info.namespace}, 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 client.Client, info ProcedureInfo) error {
	return k8s.DeleteObject(ctx, baseCli, getFrpsDeployment(info, ""))
}

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

var _ Procedure = &frpsDeployment{}
