package metrics

import (
	"strconv"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/shard/mode"
	"git.frostfs.info/TrueCloudLab/frostfs-observability/metrics"
	"github.com/prometheus/client_golang/prometheus"
)

type EngineMetrics interface {
	AddMethodDuration(method string, d time.Duration)
	AddToContainerSize(cnrID string, size int64)
	DeleteContainerSize(cnrID string)
	DeleteContainerCount(cnrID string)
	IncErrorCounter(shardID string)
	ClearErrorCounter(shardID string)
	DeleteShardMetrics(shardID string)
	AddToObjectCounter(shardID, objectType string, delta int)
	SetObjectCounter(shardID, objectType string, v uint64)
	AddToPayloadCounter(shardID string, size int64)
	SetMode(shardID string, mode mode.Mode)
	SetContainerObjectCounter(shardID, contID, objectType string, v uint64)
	IncContainerObjectCounter(shardID, contID, objectType string)
	SubContainerObjectCounter(shardID, contID, objectType string, v uint64)
	IncRefillObjectsCount(shardID, path string, size int, success bool)
	SetRefillPercent(shardID, path string, percent uint32)
	SetRefillStatus(shardID, path, status string)
	SetEvacuationInProgress(shardID string, value bool)

	WriteCache() WriteCacheMetrics
	GC() GCMetrics
}

type engineMetrics struct {
	methodDuration *prometheus.HistogramVec
	objectCounter  *prometheus.GaugeVec
	containerSize  *prometheus.GaugeVec
	payloadSize    *prometheus.GaugeVec
	errorCounter   *prometheus.GaugeVec
	mode           *shardIDModeValue
	contObjCounter *prometheus.GaugeVec

	refillStatus         *shardIDPathModeValue
	refillObjCounter     *prometheus.GaugeVec
	refillPayloadCounter *prometheus.GaugeVec
	refillPercentCounter *prometheus.GaugeVec
	evacuationInProgress *shardIDModeValue

	gc         *gcMetrics
	writeCache *writeCacheMetrics
}

func newEngineMetrics() *engineMetrics {
	return &engineMetrics{
		containerSize: newEngineGaugeVector("container_size_bytes", "Accumulated size of all objects in a container", []string{containerIDLabelKey}),
		payloadSize:   newEngineGaugeVector("payload_size_bytes", "Accumulated size of all objects in a shard", []string{shardIDLabel}),
		errorCounter:  newEngineGaugeVector("errors_total", "Shard's error counter", []string{shardIDLabel}),
		methodDuration: metrics.NewHistogramVec(prometheus.HistogramOpts{
			Namespace: namespace,
			Subsystem: engineSubsystem,
			Name:      "request_duration_seconds",
			Help:      "Duration of Engine requests",
		}, []string{methodLabel}),
		objectCounter: newEngineGaugeVector("objects_total",
			"Objects counters per shards. DEPRECATED: Will be deleted in next releasese, use frostfs_node_engine_container_objects_total metric.",
			[]string{shardIDLabel, typeLabel}),
		gc:                   newGCMetrics(),
		writeCache:           newWriteCacheMetrics(),
		mode:                 newShardIDMode(engineSubsystem, "mode_info", "Shard mode"),
		contObjCounter:       newEngineGaugeVector("container_objects_total", "Count of objects for each container", []string{shardIDLabel, containerIDLabelKey, typeLabel}),
		refillStatus:         newShardIDPathMode(engineSubsystem, "resync_metabase_status", "Resync from blobstore to metabase status"),
		refillObjCounter:     newEngineGaugeVector("resync_metabase_objects_total", "Count of objects resynced from blobstore to metabase", []string{shardIDLabel, pathLabel, successLabel}),
		refillPayloadCounter: newEngineGaugeVector("resync_metabase_objects_size_bytes", "Size of objects resynced from blobstore to metabase", []string{shardIDLabel, pathLabel, successLabel}),
		refillPercentCounter: newEngineGaugeVector("resync_metabase_complete_percent", "Percent of resynced from blobstore to metabase completeness", []string{shardIDLabel, pathLabel}),
		evacuationInProgress: newShardIDMode(engineSubsystem, "evacuation_in_progress", "Shard evacuation in progress"),
	}
}

func newEngineGaugeVector(name, help string, labels []string) *prometheus.GaugeVec {
	return metrics.NewGaugeVec(prometheus.GaugeOpts{
		Namespace: namespace,
		Subsystem: engineSubsystem,
		Name:      name,
		Help:      help,
	}, labels)
}

func (m *engineMetrics) AddMethodDuration(method string, d time.Duration) {
	m.methodDuration.With(prometheus.Labels{
		methodLabel: method,
	}).Observe(d.Seconds())
}

func (m *engineMetrics) AddToContainerSize(cnrID string, size int64) {
	m.containerSize.With(prometheus.Labels{containerIDLabelKey: cnrID}).Add(float64(size))
}

func (m *engineMetrics) DeleteContainerSize(cnrID string) {
	m.containerSize.DeletePartialMatch(prometheus.Labels{containerIDLabelKey: cnrID})
}

func (m *engineMetrics) DeleteContainerCount(cnrID string) {
	m.contObjCounter.DeletePartialMatch(prometheus.Labels{containerIDLabelKey: cnrID})
}

func (m *engineMetrics) AddToPayloadCounter(shardID string, size int64) {
	m.payloadSize.With(prometheus.Labels{shardIDLabel: shardID}).Add(float64(size))
}

func (m *engineMetrics) IncErrorCounter(shardID string) {
	m.errorCounter.With(prometheus.Labels{shardIDLabel: shardID}).Inc()
}

func (m *engineMetrics) ClearErrorCounter(shardID string) {
	m.errorCounter.With(prometheus.Labels{shardIDLabel: shardID}).Set(0)
}

func (m *engineMetrics) DeleteShardMetrics(shardID string) {
	m.errorCounter.Delete(prometheus.Labels{shardIDLabel: shardID})
	m.payloadSize.Delete(prometheus.Labels{shardIDLabel: shardID})
	m.objectCounter.DeletePartialMatch(prometheus.Labels{shardIDLabel: shardID})
	m.contObjCounter.DeletePartialMatch(prometheus.Labels{shardIDLabel: shardID})
	m.refillObjCounter.DeletePartialMatch(prometheus.Labels{shardIDLabel: shardID})
	m.refillPayloadCounter.DeletePartialMatch(prometheus.Labels{shardIDLabel: shardID})
	m.refillPercentCounter.DeletePartialMatch(prometheus.Labels{shardIDLabel: shardID})
	m.mode.Delete(shardID)
	m.refillStatus.DeleteByShardID(shardID)
	m.evacuationInProgress.Delete(shardID)
}

func (m *engineMetrics) AddToObjectCounter(shardID, objectType string, delta int) {
	m.objectCounter.With(
		prometheus.Labels{
			shardIDLabel: shardID,
			typeLabel:    objectType,
		},
	).Add(float64(delta))
}

func (m *engineMetrics) SetObjectCounter(shardID, objectType string, v uint64) {
	m.objectCounter.With(
		prometheus.Labels{
			shardIDLabel: shardID,
			typeLabel:    objectType,
		},
	).Set(float64(v))
}

func (m *engineMetrics) SetContainerObjectCounter(shardID, contID, objectType string, v uint64) {
	m.contObjCounter.With(
		prometheus.Labels{
			shardIDLabel:        shardID,
			containerIDLabelKey: contID,
			typeLabel:           objectType,
		},
	).Set(float64(v))
}

func (m *engineMetrics) IncContainerObjectCounter(shardID, contID, objectType string) {
	m.contObjCounter.With(
		prometheus.Labels{
			shardIDLabel:        shardID,
			containerIDLabelKey: contID,
			typeLabel:           objectType,
		},
	).Inc()
}

func (m *engineMetrics) SubContainerObjectCounter(shardID, contID, objectType string, v uint64) {
	m.contObjCounter.With(
		prometheus.Labels{
			shardIDLabel:        shardID,
			containerIDLabelKey: contID,
			typeLabel:           objectType,
		},
	).Sub(float64(v))
}

func (m *engineMetrics) SetMode(shardID string, mode mode.Mode) {
	m.mode.SetMode(shardID, mode.String())
}

func (m *engineMetrics) WriteCache() WriteCacheMetrics {
	return m.writeCache
}

func (m *engineMetrics) GC() GCMetrics {
	return m.gc
}

func (m *engineMetrics) IncRefillObjectsCount(shardID, path string, size int, success bool) {
	m.refillObjCounter.With(
		prometheus.Labels{
			shardIDLabel: shardID,
			pathLabel:    path,
			successLabel: strconv.FormatBool(success),
		},
	).Inc()
	m.refillPayloadCounter.With(
		prometheus.Labels{
			shardIDLabel: shardID,
			pathLabel:    path,
			successLabel: strconv.FormatBool(success),
		},
	).Add(float64(size))
}

func (m *engineMetrics) SetRefillPercent(shardID, path string, percent uint32) {
	m.refillPercentCounter.With(prometheus.Labels{
		shardIDLabel: shardID,
		pathLabel:    path,
	}).Set(float64(percent))
}

func (m *engineMetrics) SetRefillStatus(shardID, path, status string) {
	m.refillStatus.SetMode(shardID, path, status)
}

func (m *engineMetrics) SetEvacuationInProgress(shardID string, value bool) {
	m.evacuationInProgress.SetMode(shardID, strconv.FormatBool(value))
}