//go:build metrics
// +build metrics

package monitoring

import (
	"path"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"

	"zotregistry.dev/zot/errors"
	"zotregistry.dev/zot/pkg/log"
)

const metricsNamespace = "zot"

var (
	httpConnRequests = promauto.NewCounterVec( //nolint: gochecknoglobals
		prometheus.CounterOpts{
			Namespace: metricsNamespace,
			Name:      "http_requests_total",
			Help:      "Total number of http request in zot",
		},
		[]string{"method", "code"},
	)
	httpRepoLatency = promauto.NewSummaryVec( //nolint: gochecknoglobals
		prometheus.SummaryOpts{
			Namespace: metricsNamespace,
			Name:      "http_repo_latency_seconds",
			Help:      "Latency of serving HTTP requests",
		},
		[]string{"repo"},
	)
	httpMethodLatency = promauto.NewHistogramVec( //nolint: gochecknoglobals
		prometheus.HistogramOpts{
			Namespace: metricsNamespace,
			Name:      "http_method_latency_seconds",
			Help:      "Latency of serving HTTP requests",
			Buckets:   GetDefaultBuckets(),
		},
		[]string{"method"},
	)
	repoStorageBytes = promauto.NewGaugeVec( //nolint: gochecknoglobals
		prometheus.GaugeOpts{
			Namespace: metricsNamespace,
			Name:      "repo_storage_bytes",
			Help:      "Storage used per zot repo",
		},
		[]string{"repo"},
	)
	uploadCounter = promauto.NewCounterVec( //nolint: gochecknoglobals
		prometheus.CounterOpts{
			Namespace: metricsNamespace,
			Name:      "repo_uploads_total",
			Help:      "Total number times an image was uploaded",
		},
		[]string{"repo"},
	)
	downloadCounter = promauto.NewCounterVec( //nolint: gochecknoglobals
		prometheus.CounterOpts{
			Namespace: metricsNamespace,
			Name:      "repo_downloads_total",
			Help:      "Total number times an image was downloaded",
		},
		[]string{"repo"},
	)
	serverInfo = promauto.NewGaugeVec( //nolint: gochecknoglobals
		prometheus.GaugeOpts{
			Namespace: metricsNamespace,
			Name:      "info",
			Help:      "Server general information",
		},
		[]string{"commit", "binaryType", "goVersion", "version"},
	)
	storageLockLatency = promauto.NewHistogramVec( //nolint: gochecknoglobals
		prometheus.HistogramOpts{
			Namespace: metricsNamespace,
			Name:      "storage_lock_latency_seconds",
			Help:      "Latency of serving HTTP requests",
			Buckets:   GetStorageLatencyBuckets(),
		},
		[]string{"storageName", "lockType"},
	)
	schedulerGenerators = promauto.NewCounter( //nolint: gochecknoglobals
		prometheus.CounterOpts{
			Namespace: metricsNamespace,
			Name:      "scheduler_generators_total",
			Help:      "Total number of generators registered in scheduler",
		},
	)
	schedulerGeneratorsStatus = promauto.NewGaugeVec( //nolint: gochecknoglobals
		prometheus.GaugeOpts{
			Namespace: metricsNamespace,
			Name:      "scheduler_generators_status",
			Help:      "Scheduler generators by priority & state",
		},
		[]string{"priority", "state"},
	)
	schedulerNumWorkers = promauto.NewGauge( //nolint: gochecknoglobals
		prometheus.GaugeOpts{ //nolint: promlinter
			Namespace: metricsNamespace,
			Name:      "scheduler_workers_total",
			Help:      "Total number of available workers to perform scheduler tasks",
		},
	)
	schedulerWorkers = promauto.NewGaugeVec( //nolint: gochecknoglobals
		prometheus.GaugeOpts{
			Namespace: metricsNamespace,
			Name:      "scheduler_workers",
			Help:      "Scheduler workers state",
		},
		[]string{"state"},
	)
	schedulerTasksQueue = promauto.NewGaugeVec( //nolint: gochecknoglobals
		prometheus.GaugeOpts{
			Namespace: metricsNamespace,
			Name:      "scheduler_tasksqueue_length",
			Help:      "Number of tasks waiting in the queue to pe processed by scheduler workers",
		},
		[]string{"priority"},
	)
	workersTasksDuration = promauto.NewHistogramVec( //nolint: gochecknoglobals
		prometheus.HistogramOpts{
			Namespace: metricsNamespace,
			Name:      "scheduler_workers_tasks_duration_seconds",
			Help:      "How long it takes for a worker to execute a task",
			Buckets:   GetDefaultBuckets(),
		},
		[]string{"name"},
	)
)

type metricServer struct {
	enabled bool
	log     log.Logger
}

func GetDefaultBuckets() []float64 {
	return []float64{.05, .5, 1, 5, 30, 60, 600}
}

func GetStorageLatencyBuckets() []float64 {
	return []float64{.001, .01, 0.1, 1, 5, 10, 15, 30, 60}
}

func NewMetricsServer(enabled bool, log log.Logger) MetricServer {
	return &metricServer{
		enabled: enabled,
		log:     log,
	}
}

// implementing the MetricServer interface.
func (ms *metricServer) SendMetric(mfunc interface{}) {
	if ms.enabled {
		mfn, ok := mfunc.(func())
		if !ok {
			ms.log.Error().Err(errors.ErrInvalidMetric).
				Msgf("failed to cast type, expected '%T' but got '%T'", func() {}, mfunc)

			return
		}

		mfn()
	}
}

func (ms *metricServer) ForceSendMetric(mfunc interface{}) {
	mfn, ok := mfunc.(func())
	if !ok {
		ms.log.Error().Err(errors.ErrInvalidMetric).
			Msgf("failed to cast type, expected '%T' but got '%T'", func() {}, mfunc)

		return
	}

	mfn()
}

func (ms *metricServer) ReceiveMetrics() interface{} {
	return nil
}

func (ms *metricServer) IsEnabled() bool {
	return ms.enabled
}

func IncHTTPConnRequests(ms MetricServer, lvalues ...string) {
	ms.SendMetric(func() {
		httpConnRequests.WithLabelValues(lvalues...).Inc()
	})
}

func ObserveHTTPRepoLatency(ms MetricServer, path string, latency time.Duration) {
	ms.SendMetric(func() {
		match := re.FindStringSubmatch(path)

		if len(match) > 1 {
			httpRepoLatency.WithLabelValues(match[1]).Observe(latency.Seconds())
		} else {
			httpRepoLatency.WithLabelValues("N/A").Observe(latency.Seconds())
		}
	})
}

func ObserveHTTPMethodLatency(ms MetricServer, method string, latency time.Duration) {
	ms.SendMetric(func() {
		httpMethodLatency.WithLabelValues(method).Observe(latency.Seconds())
	})
}

func IncDownloadCounter(ms MetricServer, repo string) {
	ms.SendMetric(func() {
		downloadCounter.WithLabelValues(repo).Inc()
	})
}

func SetStorageUsage(ms MetricServer, rootDir, repo string) {
	ms.ForceSendMetric(func() {
		dir := path.Join(rootDir, repo)

		repoSize, err := GetDirSize(dir)
		if err == nil {
			repoStorageBytes.WithLabelValues(repo).Set(float64(repoSize))
		}
	})
}

func IncUploadCounter(ms MetricServer, repo string) {
	ms.SendMetric(func() {
		uploadCounter.WithLabelValues(repo).Inc()
	})
}

func SetServerInfo(ms MetricServer, lvalues ...string) {
	ms.ForceSendMetric(func() {
		serverInfo.WithLabelValues(lvalues...).Set(0)
	})
}

func ObserveStorageLockLatency(ms MetricServer, latency time.Duration, storageName, lockType string) {
	ms.SendMetric(func() {
		storageLockLatency.WithLabelValues(storageName, lockType).Observe(latency.Seconds())
	})
}

func IncSchedulerGenerators(ms MetricServer) {
	ms.ForceSendMetric(func() {
		schedulerGenerators.Inc()
	})
}

func SetSchedulerGenerators(ms MetricServer, gen map[string]map[string]uint64) {
	ms.SendMetric(func() {
		for priority, states := range gen {
			for state, value := range states {
				schedulerGeneratorsStatus.WithLabelValues(priority, state).Set(float64(value))
			}
		}
	})
}

func SetSchedulerNumWorkers(ms MetricServer, total int) {
	ms.SendMetric(func() {
		schedulerNumWorkers.Set(float64(total))
	})
}

func SetSchedulerWorkers(ms MetricServer, w map[string]int) {
	ms.SendMetric(func() {
		for state, value := range w {
			schedulerWorkers.WithLabelValues(state).Set(float64(value))
		}
	})
}

func SetSchedulerTasksQueue(ms MetricServer, tq map[string]int) {
	ms.SendMetric(func() {
		for priority, value := range tq {
			schedulerTasksQueue.WithLabelValues(priority).Set(float64(value))
		}
	})
}

func ObserveWorkersTasksDuration(ms MetricServer, taskName string, duration time.Duration) {
	ms.SendMetric(func() {
		workersTasksDuration.WithLabelValues(taskName).Observe(duration.Seconds())
	})
}
