package dm_controller

import (
	"context"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/tidbcloud/serverless-service/internal/config"
	"github.com/tidbcloud/serverless-service/internal/domain/constant"
	"github.com/tidbcloud/serverless-service/internal/domain/entity"
	"github.com/tidbcloud/serverless-service/internal/domain/repo"
	"github.com/tidbcloud/serverless-service/internal/metric"
	"github.com/tidbcloud/serverless-service/internal/utils"
	"go.uber.org/zap"
)

var _ prometheus.Collector = &MigrationCollector{}

const maxErrMsgLabelLen = 1000

const recentFailDuration = 30 * time.Minute

type MigrationCollector struct {
	dmRepo repo.DMTaskRepo
}

func NewMigrationCollector(dmRepo repo.DMTaskRepo) *MigrationCollector {
	return &MigrationCollector{
		dmRepo: dmRepo,
	}
}

func (c *MigrationCollector) Describe(ch chan<- *prometheus.Desc) {
	ch <- metric.FailedMigrations
	ch <- metric.TransitionalDurationSeconds
}

func (c *MigrationCollector) Collect(ch chan<- prometheus.Metric) {
	ctx := context.WithValue(context.Background(), config.CtxWorkerNameKey, "MigrationCollector")
	logger := utils.GetWorkerLoggerFromContext(ctx)
	start := time.Now()
	defer func() {
		logger.Info("migrations metrics collect time", zap.Int64("time", time.Since(start).Milliseconds()))
	}()
	transitionalTasksTasks, err := c.dmRepo.ListTasks(ctx, &entity.DMTaskFilter{
		Stages: []constant.DMTaskStage{
			constant.DMTaskStageTaskCreating,
			constant.DMTaskStageSourceBounding,
			constant.DMTaskStageSourceCreating,
			constant.DMTaskStageDeleting,
		},
	})
	if err != nil {
		logger.Error("List transitional tasks failed", zap.Error(err))
		return
	}

	for _, task := range transitionalTasksTasks {
		switch task.Stage {
		case constant.DMTaskStageSourceCreating, constant.DMTaskStageSourceBounding, constant.DMTaskStageTaskCreating:
			duration := time.Since(task.CreatedAt).Seconds()
			ch <- prometheus.MustNewConstMetric(
				metric.TransitionalDurationSeconds,
				prometheus.GaugeValue,
				duration,
				task.TenantID,
				task.ClusterID,
				task.ID,
				string(task.Stage),
			)
		case constant.DMTaskStageDeleting:
			if task.DeletedTime != nil && !task.DeletedTime.IsZero() {
				duration := time.Since(*task.DeletedTime).Seconds()
				ch <- prometheus.MustNewConstMetric(
					metric.TransitionalDurationSeconds,
					prometheus.GaugeValue,
					duration,
					task.TenantID,
					task.ClusterID,
					task.ID,
					string(task.Stage),
				)
			}
		}
	}

	recentFailTime := time.Now().Add(-recentFailDuration)
	failedTasks, err := c.dmRepo.ListTasks(ctx, &entity.DMTaskFilter{
		Stages: []constant.DMTaskStage{
			constant.DMTaskStageFailed,
		},
		FailedTimeGTE: &recentFailTime,
	})
	if err != nil {
		logger.Error("List failed tasks failed", zap.Error(err))
		return
	}

	for _, task := range failedTasks {
		// Keep this metric as a recent-failure signal, not a persistent state alert.
		ch <- prometheus.MustNewConstMetric(
			metric.FailedMigrations,
			prometheus.GaugeValue,
			1,
			task.TenantID,
			task.ClusterID,
			task.ID,
			truncateLabel(task.ErrMsg, maxErrMsgLabelLen),
		)
	}

	eps, err := c.dmRepo.ListVpcEndpoints(ctx, &entity.VpcEndpointFilter{
		States: []constant.DMVpcEndpointState{constant.DMVpcEndpointStateDeleting},
	})
	if err != nil {
		logger.Error("List deleting endpoints failed", zap.Error(err))
		return
	}

	for _, ep := range eps {
		if ep.DeletedTime != nil && !ep.DeletedTime.IsZero() {
			duration := time.Since(*ep.DeletedTime).Seconds()
			ch <- prometheus.MustNewConstMetric(
				metric.EndpointTransitionalDurationSeconds,
				prometheus.GaugeValue,
				duration,
				ep.ClusterID,
				ep.ID,
				string(constant.DMVpcEndpointStateDeleting),
			)
		}
	}
}

func truncateLabel(value string, maxLen int) string {
	if maxLen <= 0 || value == "" {
		return ""
	}

	// Fast path: if the byte length is within maxLen, the rune length is also within maxLen.
	if len(value) <= maxLen {
		return value
	}

	// Walk runes until the maxLen-th rune and slice by byte index to avoid allocating []rune.
	runeCount := 0
	for i := range value {
		if runeCount == maxLen {
			return value[:i]
		}
		runeCount++
	}

	// String has fewer than maxLen runes.
	return value
}
