package metric

import (
	"context"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/pkgv2/logid"
	"github.com/tidbcloud/pkgv2/tracing"
	"github.com/tidbcloud/serverless-service/internal/domain/constant"
	"github.com/tidbcloud/serverless-service/internal/domain/entity"
	"github.com/tidbcloud/serverless-service/internal/domain/repo"
	"go.opentelemetry.io/otel"
	"go.uber.org/zap"
)

var BRTracer = otel.Tracer("br-metric-collector")

const maxRestoreRunningTime = 10 * time.Minute

const (
	RestoreJobRecoverCRFail     = "Recover CR failed. Please update the CR manually!"
	RestoreJobRecoverStatusFail = "Recover status failed. Please remove the custom status manually!"
)

var (
	BackupEventHandlerCounter = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Namespace: MetricNamespace,
			Subsystem: "backup",
			Name:      "event_handle_total",
			Help:      "Counter for backup event handle",
		},
		[]string{"event_id", "cluster_id", "event_type", "status"},
	)

	RestoreJobFailedGauge = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Namespace: MetricNamespace,
			Subsystem: "restore",
			Name:      "failed_number",
			Help:      "failed restore job",
		},
		[]string{"cluster_id", "restore_id"},
	)

	RestoreRecoverFailedGauge = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Namespace: MetricNamespace,
			Subsystem: "restore",
			Name:      "recover_failed_number",
			Help:      "failed to recover restore",
		},
		[]string{"cluster_id", "type"},
	)

	RestoreJobStatus = prometheus.NewDesc(
		prometheus.BuildFQName(MetricNamespace, "restore", "status"),
		"Status of restore job",
		[]string{"status"},
		nil)

	RestoreJobRunningTime = prometheus.NewDesc(
		prometheus.BuildFQName(MetricNamespace, "restore", "running_duration_seconds"),
		"Duration in seconds to running restore job",
		[]string{"cluster_id", "restore_id"},
		nil)
)

func init() {
	prometheus.MustRegister(BackupEventHandlerCounter)
	prometheus.MustRegister(RestoreJobFailedGauge)
	prometheus.MustRegister(RestoreRecoverFailedGauge)
}

// BackupRestoreMetricCollector Collector will refresh metrics every time.
// 1. Collector is suitable for the continuous alert when it is not recover. such as running long time.
// 2. NewGaugeVec is suitable for alert once. such as failed, it will not recover, we don't need continuous alert.
type BackupRestoreMetricCollector struct {
	restoreJobRepo repo.RestoreJobRepo
}

func NewBackupRestoreMetricCollector(restoreJobRepo repo.RestoreJobRepo) *BackupRestoreMetricCollector {
	return &BackupRestoreMetricCollector{
		restoreJobRepo: restoreJobRepo,
	}
}

func (c *BackupRestoreMetricCollector) Describe(ch chan<- *prometheus.Desc) {
	ch <- RestoreJobRunningTime
	ch <- RestoreJobStatus
}

func (c *BackupRestoreMetricCollector) Collect(ch chan<- prometheus.Metric) {
	ctx := logid.WithLogid(context.Background(), logid.GenLogid())
	var err error
	ctx, span := BRTracer.Start(ctx, "MetricsCollector.Collect")
	defer func() {
		tracing.EndSpan(span, err)
	}()

	logger := log.WithContextL(ctx)
	logger.Info("start to collect Export metric")

	stateCount, err := c.restoreJobRepo.GroupByState(ctx)
	if err != nil {
		log.Error(ctx, "failed to count export", zap.Error(err))
		return
	}
	for _, sc := range stateCount {
		ch <- prometheus.MustNewConstMetric(
			RestoreJobStatus,
			prometheus.GaugeValue,
			float64(sc.Count),
			string(sc.State),
		)
	}

	runningJobs, err := c.restoreJobRepo.List(ctx, entity.ListRestoreJobOptions{
		State: constant.RestoreJobStateRunning,
	})
	if err != nil {
		log.Error(ctx, "failed to list running restore jobs", zap.Error(err))
		return
	}
	for _, job := range runningJobs {
		duration := time.Since(job.CreatedAt)
		if duration > maxRestoreRunningTime {
			ch <- prometheus.MustNewConstMetric(
				RestoreJobRunningTime,
				prometheus.GaugeValue,
				duration.Seconds(),
				job.TargetClusterId,
				job.ID,
			)
			log.Warn(ctx, "restore job running too long", zap.String("restore id", job.ID))
		}
	}
	pendingJobs, err := c.restoreJobRepo.List(ctx, entity.ListRestoreJobOptions{
		State: constant.RestoreJobSatePending,
	})
	if err != nil {
		log.Error(ctx, "failed to list pending restore jobs", zap.Error(err))
		return
	}
	for _, job := range pendingJobs {
		duration := time.Since(job.CreatedAt)
		if duration > maxRestoreRunningTime {
			ch <- prometheus.MustNewConstMetric(
				RestoreJobRunningTime,
				prometheus.GaugeValue,
				duration.Seconds(),
				job.TargetClusterId,
				job.ID,
			)
			log.Warn(ctx, "restore job pending too long", zap.String("restore id", job.ID))
		}
	}
}
