package eks

import (
	"context"
	"fmt"

	"helm.sh/helm/v3/pkg/chart"
	"helm.sh/helm/v3/pkg/chart/loader"
	appv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"

	"github.com/tidbcloud/eks-provider/controllers/network/task"
	"github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/helm"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
)

const selinuxDSName = "ebs-csi-node-selinux"

type CSI struct {
	*task.BaseTask
	config *csiConfig
}

type csiConfig struct {
	helmChartPath string

	driver              config.ImageConfig
	provisioner         config.ImageConfig
	attacher            config.ImageConfig
	livenessProbe       config.ImageConfig
	resizer             config.ImageConfig
	nodeDriverRegistrar config.ImageConfig

	setNodeSelector bool
}

func (c *csiConfig) GetNamespace() string {
	return metav1.NamespaceSystem
}

func (c *csiConfig) ToHelmValues() map[string]interface{} {
	values := imageConfig(c.driver)
	values["sidecars"] = map[string]interface{}{
		"provisioner":         imageConfig(c.provisioner),
		"attacher":            imageConfig(c.attacher),
		"livenessProbe":       imageConfig(c.livenessProbe),
		"resizer":             imageConfig(c.resizer),
		"nodeDriverRegistrar": imageConfig(c.nodeDriverRegistrar),
	}
	values["enableVolumeResizing"] = true
	values["controller"] = map[string]interface{}{
		"serviceAccount": map[string]interface{}{
			"create": true,
		},
	}

	var nodeSelector map[string]string
	if c.setNodeSelector {
		nodeSelector = map[string]string{
			"tidbcloud.com/component": "shared",
		}
	}
	values["node"] = map[string]interface{}{
		"tolerateAllTaints": true,
		"nodeSelector":      nodeSelector,
	}

	return values
}

func (c *csiConfig) GetCRDs() ([]*apiextensionsv1.CustomResourceDefinition, error) {
	return nil, nil
}

func imageConfig(i config.ImageConfig) map[string]interface{} {
	return map[string]interface{}{
		"image": map[string]interface{}{
			"repository": i.Repo,
			"tag":        i.Tag,
		},
	}
}

func (c *csiConfig) GetName() string {
	return "csi"
}

func (c csiConfig) ToHelmChart() (result *chart.Chart, err error) {
	result, err = loader.Load(c.helmChartPath)
	if err != nil {
		return nil, fmt.Errorf("failed to load helm chart %q: %w", c.helmChartPath, err)
	}
	return
}

func NewCSI(ctx *task.Context) *CSI {
	return &CSI{
		BaseTask: &task.BaseTask{Context: ctx},
		config: &csiConfig{
			helmChartPath:       ctx.Config.Network.CSI.ChartPath,
			driver:              ctx.Config.Network.CSI.Driver,
			provisioner:         ctx.Config.Network.CSI.Provisioner,
			attacher:            ctx.Config.Network.CSI.Attacher,
			livenessProbe:       ctx.Config.Network.CSI.LivenessProbe,
			resizer:             ctx.Config.Network.CSI.Resizer,
			nodeDriverRegistrar: ctx.Config.Network.CSI.NodeDriverRegistrar,
		},
	}
}

func (t *CSI) Name() string {
	return "CSI"
}

// The next task: StorageClass relies on CSI task to succeed
func (t *CSI) Create(ctx context.Context) deploy.Result {
	if config.FeatureEnabled(t.Config.FeatureGates, config.SplitCSIDaemonSet, t.GetTenantID(), t.GetProjectID()) {
		t.config.setNodeSelector = true
	}
	return deploy.PanicOrDone(helm.Apply(ctx, t.Remote, t.config))
}

func (t *CSI) PostCreate(ctx context.Context) deploy.Result {
	if !config.FeatureEnabled(t.Config.FeatureGates, config.SplitCSIDaemonSet, t.GetTenantID(), t.GetProjectID()) {
		return deploy.Done()
	}

	t.Logger.Info("Creating another CSI driver DaemonSet with SELinux volumes")
	cli, err := t.Remote.GetClient(ctx)
	if err != nil {
		return deploy.Panic(err)
	}

	var ds appv1.DaemonSet
	if err := cli.Get(ctx, types.NamespacedName{
		Namespace: metav1.NamespaceSystem,
		Name:      "ebs-csi-node",
	}, &ds); err != nil {
		t.Logger.Error(err, "failed to get CSI driver DaemonSet")
		return deploy.Panic(err)
	}

	selinuxDS := buildDaemonSetWithSELinuxVolumes(&ds)
	return deploy.PanicOrDone(k8s.Apply(ctx, cli, selinuxDS))
}

func buildDaemonSetWithSELinuxVolumes(ds *appv1.DaemonSet) *appv1.DaemonSet {
	selinuxDS := appv1.DaemonSet{
		ObjectMeta: metav1.ObjectMeta{
			Name:      selinuxDSName,
			Namespace: ds.GetNamespace(),
		},
		Spec: ds.DeepCopy().Spec,
	}

	// Set labels and selectors.
	selinuxDS.Spec.Selector = &metav1.LabelSelector{
		MatchLabels: map[string]string{
			"app": selinuxDSName,
		},
	}
	selinuxDS.Spec.Template.ObjectMeta.Labels = map[string]string{
		"app": selinuxDSName,
	}

	// Select tidb nodes.
	selinuxDS.Spec.Template.Spec.NodeSelector = map[string]string{
		"kubernetes.io/os": "linux",
		"usedby":           "customer-tidb",
	}

	// Add SELinux volumes.
	directoryType := corev1.HostPathDirectory
	fileOrCreate := corev1.HostPathFileOrCreate
	selinuxDS.Spec.Template.Spec.Volumes = append(selinuxDS.Spec.Template.Spec.Volumes,
		corev1.Volume{
			Name: "selinuxfs",
			VolumeSource: corev1.VolumeSource{
				HostPath: &corev1.HostPathVolumeSource{
					Path: "/sys/fs/selinux",
					Type: &directoryType,
				},
			},
		},
		corev1.Volume{
			Name: "selinux-config",
			VolumeSource: corev1.VolumeSource{
				HostPath: &corev1.HostPathVolumeSource{
					Path: "/etc/selinux/config",
					Type: &fileOrCreate,
				},
			},
		},
	)
	for i := range selinuxDS.Spec.Template.Spec.Containers {
		container := &selinuxDS.Spec.Template.Spec.Containers[i]
		if container.Name == "ebs-plugin" {
			container.VolumeMounts = append(container.VolumeMounts,
				corev1.VolumeMount{
					Name:      "selinuxfs",
					MountPath: "/sys/fs/selinux",
				},
				corev1.VolumeMount{
					Name:      "selinux-config",
					MountPath: "/etc/selinux/config",
				},
			)
		}
	}

	return &selinuxDS
}
