package common

import (
	"context"
	"fmt"
	"strings"
	"time"

	"helm.sh/helm/v3/pkg/chart"
	"helm.sh/helm/v3/pkg/chart/loader"
	appsv1 "k8s.io/api/apps/v1"
	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"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/metrics"
	"github.com/tidbcloud/infra-provider/pkg/util"
)

const daemonSetName = "node-exporter-prometheus-node-exporter"

// NodeExporter is a task to deploy node exporter,
// which is a Prometheus exporter for hardware and OS metrics exposed by *NIX kernels.
type NodeExporter struct {
	*task.BaseTask
	config *NodeExporterConfig
}

type NodeExporterConfig struct {
	helmChartPath          string
	image                  config.ImageConfig
	enableEthtoolCollecter bool
}

func (c *NodeExporterConfig) GetNamespace() string {
	return task.ObservabilityNamespace
}

func (c *NodeExporterConfig) GetName() string {
	return "node-exporter"
}

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

func (c *NodeExporterConfig) 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 (c *NodeExporterConfig) ToHelmValues() map[string]interface{} {
	values := map[string]interface{}{
		"image": map[string]string{
			"registry":   c.image.Registry,
			"repository": c.image.Repo,
			"tag":        c.image.Tag,
		},
		"resources": map[string]interface{}{
			"requests": map[string]string{
				"cpu":    "100m",
				"memory": "200Mi",
			},
			"limits": map[string]string{
				"cpu":    "250m",
				"memory": "400Mi",
			},
		},
	}

	if c.enableEthtoolCollecter {
		values["extraArgs"] = []string{
			"--collector.ethtool", // Exposes network interface information and network driver statistics
		}
	}

	return values
}

func NewNodeExporter(ctx *task.Context) *NodeExporter {
	return &NodeExporter{
		BaseTask: &task.BaseTask{Context: ctx},
		config: &NodeExporterConfig{
			helmChartPath:          ctx.Config.Network.NodeExporter.ChartPath,
			image:                  ctx.Config.Network.NodeExporter.Image,
			enableEthtoolCollecter: config.FeatureEnabled(ctx.Config.FeatureGates, config.EnableEthtoolCollecter, ctx.GetTenantID(), ctx.GetProjectID()),
		},
	}
}

var _ helm.Package = &NodeExporterConfig{}

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

func (t *NodeExporter) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())

	cli, err := t.Remote.GetClient(ctx)
	if err != nil {
		return deploy.Panic(err)
	}

	// It is incompatible from 3.x to 4.x node-exporter helm charts
	// Ignore existing node-exporter ds to avoid compatibility issues
	var app appsv1.DaemonSet
	if err := cli.Get(ctx, types.NamespacedName{
		Namespace: task.ObservabilityNamespace,
		Name:      daemonSetName,
	}, &app); err != nil {
		if apierrors.IsNotFound(err) {
			return deploy.ErrorOrDone(helm.Apply(ctx, t.Remote, t.config))
		}
		return deploy.Error(fmt.Errorf("failed to get NodeExporter Daemonset: %v", err))
	}

	// If the node-exporter is 3.x, ignore it.
	if strings.HasPrefix(app.GetLabels()["helm.sh/chart"], "prometheus-node-exporter-3.") {
		return deploy.Done()
	}

	return deploy.ErrorOrDone(helm.Apply(ctx, t.Remote, t.config))
}

func (t *NodeExporter) PostCreate(ctx context.Context) (res deploy.Result) {
	defer func() {
		metrics.UpdateO11yComponentHealthStatus(t.Network, task.ObservabilityNamespace, t.Name(), res.IsDoneState())
	}()

	cli, err := t.Remote.GetClient(ctx)
	if err != nil {
		return deploy.Panic(err)
	}

	var app appsv1.DaemonSet
	if err := cli.Get(ctx, types.NamespacedName{
		Namespace: task.ObservabilityNamespace,
		Name:      daemonSetName,
	}, &app); err != nil {
		return deploy.Error(fmt.Errorf("failed to get NodeExporter Daemonset: %v", err))
	}

	if app.Status.NumberReady != app.Status.DesiredNumberScheduled {
		t.Logger.Error(fmt.Errorf("NodeExporter is not ready"), "Will not block network status to normal", "network", t.Network.Name)
	}

	return deploy.Done()
}

func (t *NodeExporter) Delete(ctx context.Context) deploy.Result {
	return deploy.Done()
}

func (t *NodeExporter) PostDelete(ctx context.Context) deploy.Result {
	return deploy.Done()
}
