package mastermgr

import (
	"context"
	"fmt"
	"strings"

	"github.com/pingcap/errors"
	yamlv2 "gopkg.in/yaml.v2"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/yaml"

	"github.com/tidbcloud/eks-provider/controllers/utils"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"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/namer"
	"github.com/tidbcloud/infra-provider/pkg/o11y/metric_agent"
	"github.com/tidbcloud/infra-provider/pkg/o11y/prom_config"
	"github.com/tidbcloud/infra-provider/pkg/util"
	"github.com/tidbcloud/infra-provider/pkg/vpc_endpoint"
)

const (
	releaseNameLimit      = 53
	vmagentDeploymentName = "vmagent"
	// this configmap is deployed by pulumi
	externalTiflowConfigName = "external-prometheus-config-tiflow"
	externalConfigNamespace  = "infra"
	prometheusConfigKey      = "prometheus-config"
)

// externalLabels are labels that add to time series or alerts in Prometheus.
type externalLabels struct {
	// ClusterName is like `shoot/aws/us-west-2/shoot--nightly--10cfb004`.
	ClusterName string
	Project     string
	// ShootName is like `10cfb004`.
	ShootName    string
	SeedRegion   string
	SeedProvider string
	ShootInfra   string
	TenantID     string
	// ClusterID is like `tidb123456`.
	ClusterID string
}

// MetricsAgent collects tiflow clusters' metrics and sends them to o11y infra via VPC interface endpoint.
// It's deployed as a Deployment in the same namespace as the tiflow cluster.
type MetricsAgent struct {
	*BaseTask

	config *config.O11yAgentConfig
}

var _ deploy.Task = &MetricsAgent{}

func NewMetricsAgent(c *Context) *MetricsAgent {
	return &MetricsAgent{
		BaseTask: &BaseTask{Context: c},
		config: &config.O11yAgentConfig{
			HelmChartPath: c.Config.Network.O11yConfig.MetricsAgentConfig.HelmChartPath,
			Image:         c.Config.Network.O11yConfig.MetricsAgentConfig.Image,
		},
	}
}

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

func (t *MetricsAgent) Create(ctx context.Context) deploy.Result {
	if !t.HasActiveMaster() {
		return deploy.Done()
	}

	url, err := t.buildRemoteWriteURL()
	if err != nil {
		return deploy.Error(err)
	}

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

	promConfig, err := t.buildConfigMap(ctx, cli)
	if err != nil {
		return deploy.Error(err)
	}

	if err := t.setPriorityClass(ctx, cli); err != nil {
		return deploy.Error(err)
	}

	return deploy.ErrorOrDone(t.deployVMAgent(ctx, promConfig, url))
}

func (t *MetricsAgent) PostCreate(ctx context.Context) (res deploy.Result) {
	defer func() {
		metrics.UpdateO11yComponentHealthStatus(t.Network,
			t.Namer.GetTiflowNamespace(),
			t.Name(),
			res.IsDoneState())
	}()

	if !t.HasActiveMaster() {
		if !t.isVMAgentExists(ctx) {
			return deploy.Done()
		}
		return deploy.ErrorOrDone(t.uninstallAgent(ctx))
	}

	return deploy.ErrorOrDone(t.checkVMAgentReady(ctx))
}

func (t *MetricsAgent) Delete(ctx context.Context) (res deploy.Result) {
	if err := t.uninstallAgent(ctx); err != nil {
		t.Logger.Error(err, "uninstall VMAgent fail when call delete")
	}
	return deploy.Done()
}

func (t *MetricsAgent) isVMAgentExists(ctx context.Context) bool {
	cli, err := t.Remote.GetClient(ctx)
	if err != nil {
		return false
	}

	var app appsv1.Deployment
	if err := cli.Get(ctx, types.NamespacedName{
		Namespace: t.Namer.GetTiflowNamespace(),
		Name:      vmagentDeploymentName,
	}, &app); err != nil {
		if !apierrors.IsNotFound(err) {
			t.Logger.Info("get vmagent deployment fail", "err", err)
		}
		return false
	}

	return true
}

func (t *MetricsAgent) uninstallAgent(ctx context.Context) error {
	releaseName := metricsAgentReleaseName(t.Namer.GetTiflowNamespace())
	err := helm.Uninstall(ctx, t.Remote, t.config, releaseName)
	return errors.Annotate(err, "failed to helm uninstall MetricsAgent")
}

func (t *MetricsAgent) checkVMAgentReady(ctx context.Context) error {
	cli, err := t.Remote.GetClient(ctx)
	if err != nil {
		return err
	}

	var app appsv1.Deployment
	if err := cli.Get(ctx, types.NamespacedName{
		Namespace: t.Namer.GetTiflowNamespace(),
		Name:      vmagentDeploymentName,
	}, &app); err != nil {
		return errors.Annotate(err, "failed to get MetricsAgent deployment")
	}

	if !utils.IsStatusConditionTrue(app.Status.Conditions, appsv1.DeploymentAvailable) {
		t.Logger.Error(fmt.Errorf("MetricsAgent is not ready"), "will not block cluster status to ready", "cluster", t.TiflowClusterMasterManager.Name)
	}

	return nil
}

func (t *MetricsAgent) buildConfigMap(ctx context.Context, cli client.Client) (*prom_config.PromConfig, error) {
	promConfigRaw, err := t.getConfigFromExternalTiflowConfig(ctx)
	if err != nil {
		return nil, err
	}

	return metric_agent.UnmarshalAndEnrichPromConfig(promConfigRaw, namer.LegacyBuildClusterNameByNetwork(t.Network),
		t.Namer.GetTiflowNamespace())
}

func (t *MetricsAgent) getConfigFromExternalTiflowConfig(ctx context.Context) (promConfigRaw string, err error) {
	promConfig := new(corev1.ConfigMap)
	err = t.Local.Get(ctx, types.NamespacedName{
		Namespace: externalConfigNamespace,
		Name:      externalTiflowConfigName,
	}, promConfig)
	if err != nil {
		return "", err
	}

	tenantID, projectID, _ := GetTiflowIDs(t.Network)
	promConfigRaw = populatePrometheusConfig(promConfig.Data[prometheusConfigKey], t.Namer.GetTiflowNamespace(),
		&externalLabels{
			ClusterName:  namer.LegacyBuildClusterNameByNetwork(t.Network),
			Project:      projectID,
			ShootName:    namer.LegacyBuildClusterNameByNetwork(t.Network),
			SeedRegion:   t.Network.Spec.Region,
			SeedProvider: t.Network.Spec.Provider,
			ShootInfra:   t.Network.Spec.Provider,
			TenantID:     tenantID,
			ClusterID:    t.Namer.GetTiflowNamespace(),
		})

	return
}

func populatePrometheusConfig(config string, namespace string, labels *externalLabels) (res string) {
	res = strings.ReplaceAll(config, "TIFLOW_NAMESPACE", namespace)
	res = strings.ReplaceAll(res, "CLUSTER_NAME", labels.ClusterName)
	res = strings.ReplaceAll(res, "CLUSTER_ID", labels.ClusterID)
	res = strings.ReplaceAll(res, "TENANT_ID", labels.TenantID)
	res = strings.ReplaceAll(res, "PROJECT", labels.Project)
	res = strings.ReplaceAll(res, "SHOOT_NAME", labels.ShootName)
	res = strings.ReplaceAll(res, "SHOOT_INFRA", labels.ShootInfra)
	res = strings.ReplaceAll(res, "SEED_REGION", labels.SeedRegion)
	res = strings.ReplaceAll(res, "SEED_PROVIDER", labels.SeedProvider)
	return
}

func (t *MetricsAgent) setPriorityClass(ctx context.Context, cli client.Client) error {
	if err := metric_agent.EnsureAgentPriorityClass(ctx, cli); err != nil {
		return errors.Annotate(err, "failed to ensure agent priority class")
	}

	return nil
}

func (t *MetricsAgent) deployVMAgent(ctx context.Context, promConfig *prom_config.PromConfig, url string) error {
	byteChartValues, err := buildDefaultVmagentChartValues(t.config.Image.Repo,
		t.config.Image.Tag,
		consts.MetricsAgentStorageClassName,
		promConfig,
		url)
	if err != nil {
		return errors.Annotate(err, "failed to build MetricsAgent's chart values")
	}
	var values map[string]interface{}
	if err := yaml.Unmarshal(byteChartValues, &values); err != nil {
		return errors.Annotate(err, "failed to unmarshal MetricsAgent's chart values")
	}
	namespace := t.Namer.GetTiflowNamespace()
	releaseName := metricsAgentReleaseName(t.Namer.GetTiflowNamespace())
	if err := helm.ApplyV2(ctx, t.Remote, t.config, releaseName, namespace, values); err != nil {
		return errors.Annotate(err, "failed to deploy MetricsAgent")
	}

	return nil
}

// buildRemoteWriteURL check if o11y infra is setup and return available remote write URL.
func (t *MetricsAgent) buildRemoteWriteURL() (string, error) {
	if t.isO11yInfraSetup() {
		builder := metric_agent.NewWriteURLBuilder(util.GetAnnotation(t.TiflowClusterMasterManager, consts.O11yServiceDNSNameAnnotationKey))
		if t.Config.Predicate.MatchLabels[cloudv1.ProviderTypeLabel] == consts.ProviderTypeGKE {
			return builder.ForGCP(util.GetAnnotation(t.TiflowClusterMasterManager, consts.MetricInterfaceEndpointNameAnnotationKey)).
				BuildRemoteWrite(util.GetAnnotation(t.TiflowClusterMasterManager, consts.MetricsO11yIDAnnotationKey)), nil
		}
		return builder.ForAWS().
			BuildRemoteWrite(util.GetAnnotation(t.TiflowClusterMasterManager, consts.MetricsO11yIDAnnotationKey)), nil
	}
	return "", fmt.Errorf("o11y infra is not setup")
}

func (t *MetricsAgent) isO11yInfraSetup() bool {
	return util.GetAnnotation(t.TiflowClusterMasterManager, consts.O11yIDAnnotationKey) != "" &&
		util.GetAnnotation(t.TiflowClusterMasterManager, consts.O11yServiceDNSNameAnnotationKey) != "" &&
		!vpc_endpoint.ShouldUpdateEndpoint(t.TiflowClusterMasterManager.Annotations, t.Config.Network.O11yConfig.Version)
}

// metricsAgentReleaseName returns a valid release name.
// Since we deploy a metrics agent for each Tiflow namespace,
// we use the namespace name to generate the release name,
// which may exceed the max length limit.
func metricsAgentReleaseName(ns string) string {
	s := fmt.Sprintf("%s-%s", ns, vmagentDeploymentName)
	return util.Truncate(s, releaseNameLimit)
}

func buildDefaultVmagentChartValues(imageRepo, imageTag, storageClass string, promConfig *prom_config.PromConfig, remoteWriteUrl string) ([]byte, error) {
	if promConfig == nil {
		promConfig = &prom_config.PromConfig{}
	}
	values := metric_agent.ChartValues{
		ReplicaCount: 1,
		Deployment: metric_agent.Deployment{
			Enabled: true,
			// Metric Agent deployment pod use one pvc and share state file between multiple replicaset. The agent process also lock the state file to prevent other agent to process concurrently.
			// If we use rollingupdate strategy, the newer agent will never be ready because the old one won't release the file lock.
			Strategy: map[string]interface{}{
				"type": "Recreate",
			},
		},
		Statefulset: metric_agent.Statefulset{
			Enabled: false,
		},
		RemoteWriteUrls: []string{remoteWriteUrl},
		ExtraArgs: map[string]string{
			"envflag.enable":                                 "true",
			"envflag.prefix":                                 "VM_",
			"loggerFormat":                                   "json",
			"remoteWrite.maxDiskUsagePerURL":                 "2000MiB",
			"remoteWrite.queues":                             "1",
			"promscrape.config.strictParse":                  "false",
			"promscrape.suppressDuplicateScrapeTargetErrors": "true",
		},
		Resources: metric_agent.Resources{
			Requests: &metric_agent.Resource{
				Memory: "250Mi",
			},
			Limits: &metric_agent.Resource{
				Memory: "1000Mi",
			},
		},
		Persistence: metric_agent.Persistence{
			Enabled:          true,
			StorageClassName: storageClass,
			AccessModes:      []string{"ReadWriteOnce"},
			Size:             "4Gi",
		},
		Config: *promConfig,
		Image: metric_agent.Image{
			Repository: imageRepo,
			Tag:        imageTag,
		},
		PriorityClassName: metric_agent.ClusterCriticalPriorityClassName,
	}
	return yamlv2.Marshal(values)
}
