package task

import (
	"context"
	"fmt"
	"regexp"
	"sort"
	"strings"

	vmoperatorv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"

	agentv2 "github.com/tidbcloud/observability/agent-control-server/api/v2"
	"github.com/tidbcloud/observability/agent-control-server/controllers/sharedpoolagent/metadata"
	"github.com/tidbcloud/observability/agent-control-server/pkg/agentbuild"
	"github.com/tidbcloud/observability/agent-control-server/pkg/consts"
	"github.com/tidbcloud/observability/agent-control-server/pkg/deploy"
	"github.com/tidbcloud/observability/agent-control-server/pkg/reconciler"
	"github.com/tidbcloud/observability/agent-control-server/pkg/util"
)

type VMPodScrapeTask struct {
	*reconciler.BaseTask[*agentv2.SharedPoolAgent]
	vmpodscrapeNames []string // for PostCreate check
}

func NewVMPodScrapeTask(ctx *reconciler.Context[*agentv2.SharedPoolAgent]) *VMPodScrapeTask {
	return &VMPodScrapeTask{
		BaseTask: &reconciler.BaseTask[*agentv2.SharedPoolAgent]{Context: ctx},
	}
}

func (t *VMPodScrapeTask) Name() string {
	return "VMPodScrape"
}

func (t *VMPodScrapeTask) ClusterMeta() deploy.ClusterMeta {
	return metadata.ClusterMeta(t.Agent)
}

func (t *VMPodScrapeTask) Create(ctx context.Context) deploy.Result {
	cli, err := t.Remote.GetClient(ctx)
	if err != nil {
		return deploy.Panic(err)
	}
	return deploy.PanicOrDone(t.create(ctx, cli))
}

func (t *VMPodScrapeTask) PostCreate(ctx context.Context) deploy.Result {
	cli, err := t.Remote.GetClient(ctx)
	if err != nil {
		return deploy.Panic(err)
	}
	for _, name := range t.vmpodscrapeNames {
		if err := agentbuild.CheckVMPodScrapeReady(ctx, cli, name, t.buildNamespace()); err != nil {
			return deploy.Panic(err)
		}
	}
	return deploy.Done()
}

func (t *VMPodScrapeTask) Delete(ctx context.Context) deploy.Result {
	cli, err := t.Remote.GetClient(ctx)
	if err != nil {
		return deploy.Panic(err)
	}
	vmpodscrapes, err := t.listVmpodscrapes(ctx, cli, client.MatchingLabels{"scrape-by": t.buildLabelScrapeBy()})
	if err != nil {
		return deploy.Panic(err)
	}
	for _, vmpodscrape := range vmpodscrapes {
		if err := t.deleteVmpodscrape(ctx, cli, vmpodscrape.Name, t.buildNamespace()); err != nil {
			return deploy.Panic(err)
		}
	}
	return deploy.Done()
}

func (t *VMPodScrapeTask) PostDelete(ctx context.Context) deploy.Result {
	cli, err := t.Remote.GetClient(ctx)
	if err != nil {
		return deploy.Panic(err)
	}
	vmpodscrapes, err := t.listVmpodscrapes(ctx, cli, client.MatchingLabels{"scrape-by": t.buildLabelScrapeBy()})
	if err != nil {
		return deploy.Panic(err)
	}
	if len(vmpodscrapes) > 0 {
		return deploy.Panic(fmt.Errorf("still have %d vmpodscrapes remaining", len(vmpodscrapes)))
	}
	return deploy.Done()
}

func (t *VMPodScrapeTask) create(ctx context.Context, cli client.Client) error {
	instanceIDs, err := t.buildInstanceIDs(ctx, cli)
	if err != nil {
		return err
	}
	if len(instanceIDs) == 0 {
		return nil
	}
	metricRelabelConfigs, err := t.buildMetricRelabelConfigs(ctx)
	if err != nil {
		return err
	}
	var vmpodscrapeNames []string // for later PostCreate check
	for instanceID := range instanceIDs {
		for name, tmpl := range t.Templates {
			if !strings.HasPrefix(name, "sharedpool/jobs/") {
				continue
			}
			if strings.Contains(name, "tiproxy") && !consts.IsTiProxyDeployTypeSharedTiProxy(t.Agent.Annotations) {
				continue
			}
			err := agentbuild.CreateOrUpdateVMPodScrape(ctx, cli, func() (*vmoperatorv1beta1.VMPodScrape, error) {
				v, err := agentbuild.BuildVMPodScrape(tmpl, map[string]string{
					"InstanceID":    instanceID,
					"Namespace":     t.buildNamespace(),
					"LabelScrapeBy": t.buildLabelScrapeBy(),
					"SharedPoolID":  t.Agent.Spec.SharedPoolID,
					"VMAccountID":   util.GetAnnotation(t.Agent, consts.AnnotationKeyO11yVMAccountID),
					"VMProjectID":   util.GetAnnotation(t.Agent, consts.AnnotationKeyO11yVMProjectID),
					"TenantID":      t.Agent.Spec.O11y.TenantID,
				})
				if err != nil {
					return nil, err
				}
				for n := range v.Spec.PodMetricsEndpoints {
					// Copy first to avoid mutating the underlying array of the existing slice during append.
					mergedMetricRelabelConfigs := append(
						[]*vmoperatorv1beta1.RelabelConfig{},
						v.Spec.PodMetricsEndpoints[n].MetricRelabelConfigs...,
					)
					v.Spec.PodMetricsEndpoints[n].MetricRelabelConfigs = append(mergedMetricRelabelConfigs, metricRelabelConfigs...)
				}
				vmpodscrapeNames = append(vmpodscrapeNames, v.Name)
				return v, nil
			})
			if err != nil {
				return err
			}
		}
	}
	t.vmpodscrapeNames = vmpodscrapeNames
	return t.deleteUnnecessaryVmpodscrapes(ctx, cli, instanceIDs)
}

func (t *VMPodScrapeTask) deleteUnnecessaryVmpodscrapes(ctx context.Context, cli client.Client, existingInstanceIDs map[string]struct{}) error {
	vmpodscrapes, err := t.listVmpodscrapes(ctx, cli, client.MatchingLabels{
		"scrape-by":       t.buildLabelScrapeBy(),
		"component-level": "sharedpool",
	})
	if err != nil {
		return err
	}
	for _, vmpodscrape := range vmpodscrapes {
		if len(vmpodscrape.Labels) > 0 {
			if id, ok := vmpodscrape.Labels["db-instance-id"]; ok {
				if _, ok := existingInstanceIDs[id]; ok {
					continue
				}
			}
		}
		if err := t.deleteVmpodscrape(ctx, cli, vmpodscrape.Name, t.buildNamespace()); err != nil {
			return err
		}
	}
	return nil
}

func (t *VMPodScrapeTask) deleteVmpodscrape(ctx context.Context, cli client.Client, name, namespace string) error {
	err := cli.Delete(ctx, &vmoperatorv1beta1.VMPodScrape{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
	})
	if client.IgnoreNotFound(err) == nil {
		return nil
	}
	if meta.IsNoMatchError(err) {
		return nil
	}
	return err
}

func (t *VMPodScrapeTask) listVmpodscrapes(ctx context.Context, cli client.Client, labels client.MatchingLabels) ([]vmoperatorv1beta1.VMPodScrape, error) {
	vmpodscrapes := &vmoperatorv1beta1.VMPodScrapeList{}
	err := cli.List(ctx, vmpodscrapes, client.InNamespace(t.buildNamespace()), labels)
	if err != nil {
		if meta.IsNoMatchError(err) {
			return nil, nil
		}
		return nil, fmt.Errorf("failed to list vmpodscrapes in namespace %s: %w", t.buildNamespace(), err)
	}
	return vmpodscrapes.Items, nil
}

var regexpPDPodNamePrefix = regexp.MustCompile(`^db-(\w+)-pd-`)

func (t *VMPodScrapeTask) buildInstanceIDs(ctx context.Context, cli client.Client) (map[string]struct{}, error) {
	pods := &corev1.PodList{}
	if err := cli.List(ctx, pods, client.InNamespace(t.buildNamespace()), client.MatchingLabels{
		"pingcap.com/component": "pd",
	}); err != nil {
		return nil, fmt.Errorf("failed to list pods: %w", err)
	}
	if len(pods.Items) == 0 {
		return nil, nil
	}
	instanceIDs := map[string]struct{}{}
	for _, pod := range pods.Items {
		matches := regexpPDPodNamePrefix.FindStringSubmatch(pod.Name)
		if len(matches) != 2 {
			continue
		}
		instanceIDs[matches[1]] = struct{}{}
	}
	return instanceIDs, nil
}

func (t *VMPodScrapeTask) buildMetricRelabelConfigs(ctx context.Context) ([]*vmoperatorv1beta1.RelabelConfig, error) {
	tidbgroupagents := &agentv2.TiDBGroupAgentList{}
	if err := t.Local.List(ctx, tidbgroupagents); err != nil {
		return nil, fmt.Errorf("failed to list tidbgroupagents: %w", err)
	}
	sort.SliceStable(tidbgroupagents.Items, func(i, j int) bool {
		if tidbgroupagents.Items[i].Spec.ClusterID != tidbgroupagents.Items[j].Spec.ClusterID {
			return tidbgroupagents.Items[i].Spec.ClusterID < tidbgroupagents.Items[j].Spec.ClusterID
		}
		return tidbgroupagents.Items[i].Name < tidbgroupagents.Items[j].Name
	})
	var metricRelabelConfigs []*vmoperatorv1beta1.RelabelConfig
	for _, tga := range tidbgroupagents.Items {
		if tga.Spec.SharedPoolID != t.Agent.Spec.SharedPoolID {
			continue
		}
		keyspaceName := tga.Spec.ClusterID
		vmAccountID := util.GetAnnotation(&tga, consts.AnnotationKeyO11yVMAccountID)
		vmProjectID := util.GetAnnotation(&tga, consts.AnnotationKeyO11yVMProjectID)
		if keyspaceName == "" || vmAccountID == "" || vmProjectID == "" {
			continue
		}
		metricRelabelConfigs = append(metricRelabelConfigs, []*vmoperatorv1beta1.RelabelConfig{{
			Action:       "replace",
			SourceLabels: []string{"keyspace_name"},
			Regex:        []string{keyspaceName},
			TargetLabel:  "tidb_cluster_id",
			Replacement:  &keyspaceName,
		}, {
			Action:       "replace",
			SourceLabels: []string{"keyspace_name"},
			Regex:        []string{keyspaceName},
			TargetLabel:  "vm_account_id",
			Replacement:  &vmAccountID,
		}, {
			Action:       "replace",
			SourceLabels: []string{"keyspace_name"},
			Regex:        []string{keyspaceName},
			TargetLabel:  "vm_project_id",
			Replacement:  &vmProjectID,
		}}...)
	}
	return metricRelabelConfigs, nil
}

func (t *VMPodScrapeTask) buildNamespace() string {
	return fmt.Sprintf("tidb%s", t.Agent.Spec.SharedPoolID)
}

func (t *VMPodScrapeTask) buildLabelScrapeBy() string {
	return fmt.Sprintf("tidb%s-o11y-vmagent", t.Agent.Spec.SharedPoolID)
}
