package metrics

import (
	"fmt"
	"sync"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-observability/metrics"
	"github.com/prometheus/client_golang/prometheus"
)

const (
	wcSubsystem = "writecache"
	wcShardID   = "shard_id"
	wcSuccess   = "success"
	wcStorage   = "storage"
	wcMode      = "mode"
)

type shardIDMode struct {
	shardID, mode string
}

type WriteCacheMetrics interface {
	AddGetDuration(shardID string, success bool, d time.Duration, storageType string)
	AddDeleteDuration(shardID string, success bool, d time.Duration, storageType string)
	AddPutDuration(shardID string, success bool, d time.Duration, storageType string)

	IncActualCount(shardID string, storageType string)
	DecActualCount(shardID string, storageType string)
	SetActualCount(shardID string, count uint64, storageType string)

	SetEstimateSize(shardID string, size uint64, storageType string)
	SetMode(shardID string, mode string)

	IncFlushCounter(shardID string, success bool, storageType string)
	IncEvictCounter(shardID string, storageType string)
}

type writeCacheMetrics struct {
	getDuration    *prometheus.HistogramVec
	putDuration    *prometheus.HistogramVec
	deleteDuration *prometheus.HistogramVec

	flushCounter *prometheus.CounterVec
	evictCounter *prometheus.CounterVec

	actualCount *prometheus.GaugeVec

	estimatedSize *prometheus.GaugeVec

	modeMetrics map[shardIDMode]prometheus.GaugeFunc
	modeValues  map[string]string
	modeMtx     sync.RWMutex
}

func newWriteCacheMetrics() *writeCacheMetrics {
	return &writeCacheMetrics{
		getDuration:    newWCMethodDurationCounter("get"),
		putDuration:    newWCMethodDurationCounter("put"),
		deleteDuration: newWCMethodDurationCounter("delete"),
		flushCounter:   newWCOperationCounterVec("flush", []string{wcShardID, wcStorage, wcSuccess}),
		evictCounter:   newWCOperationCounterVec("evict", []string{wcShardID, wcStorage}),
		actualCount:    newWCGaugeVec("actual_objects_count", "Actual objects count in writecache", []string{wcShardID, wcStorage}),
		estimatedSize:  newWCGaugeVec("estimated_size_bytes", "Estimated writecache size", []string{wcShardID, wcStorage}),
		modeMtx:        sync.RWMutex{},
		modeMetrics:    make(map[shardIDMode]prometheus.GaugeFunc),
		modeValues:     make(map[string]string),
	}
}

func (m *writeCacheMetrics) AddGetDuration(shardID string, success bool, d time.Duration, storageType string) {
	setWriteCacheDuration(m.getDuration, shardID, success, d, storageType)
}

func (m *writeCacheMetrics) AddDeleteDuration(shardID string, success bool, d time.Duration, storageType string) {
	setWriteCacheDuration(m.deleteDuration, shardID, success, d, storageType)
}

func (m *writeCacheMetrics) AddPutDuration(shardID string, success bool, d time.Duration, storageType string) {
	setWriteCacheDuration(m.putDuration, shardID, success, d, storageType)
}

func (m *writeCacheMetrics) IncActualCount(shardID string, storageType string) {
	m.actualCount.With(prometheus.Labels{
		wcShardID: shardID,
		wcStorage: storageType,
	}).Inc()
}

func (m *writeCacheMetrics) DecActualCount(shardID string, storageType string) {
	m.actualCount.With(prometheus.Labels{
		wcShardID: shardID,
		wcStorage: storageType,
	}).Dec()
}

func (m *writeCacheMetrics) SetActualCount(shardID string, count uint64, storageType string) {
	m.actualCount.With(prometheus.Labels{
		wcShardID: shardID,
		wcStorage: storageType,
	}).Set(float64(count))
}

func (m *writeCacheMetrics) SetEstimateSize(shardID string, size uint64, storageType string) {
	m.estimatedSize.With(prometheus.Labels{
		wcShardID: shardID,
		wcStorage: storageType,
	}).Set(float64(size))
}

func (m *writeCacheMetrics) SetMode(shardID string, mode string) {
	m.modeMtx.Lock()
	defer m.modeMtx.Unlock()

	m.modeValues[shardID] = mode
	key := shardIDMode{
		shardID: shardID,
		mode:    mode,
	}
	if _, found := m.modeMetrics[key]; found {
		return
	}

	metric := metrics.NewGaugeFunc(
		prometheus.GaugeOpts{
			Namespace: namespace,
			Subsystem: wcSubsystem,
			Name:      "writecache_mode",
			Help:      "Writecache mode value",
			ConstLabels: prometheus.Labels{
				wcShardID: shardID,
				wcMode:    mode,
			},
		}, func() float64 {
			m.modeMtx.RLock()
			defer m.modeMtx.RUnlock()

			value := m.modeValues[shardID]
			if value == mode {
				return 1
			}
			return 0
		})
	m.modeMetrics[key] = metric
}

func (m *writeCacheMetrics) IncFlushCounter(shardID string, success bool, storageType string) {
	m.flushCounter.With(prometheus.Labels{
		wcShardID: shardID,
		wcSuccess: fmt.Sprintf("%v", success),
		wcStorage: storageType,
	}).Inc()
}

func (m *writeCacheMetrics) IncEvictCounter(shardID string, storageType string) {
	m.evictCounter.With(prometheus.Labels{
		wcShardID: shardID,
		wcStorage: storageType,
	}).Inc()
}

func setWriteCacheDuration(m *prometheus.HistogramVec, shardID string, success bool, d time.Duration, storageType string) {
	m.With(
		prometheus.Labels{
			wcShardID: shardID,
			wcSuccess: fmt.Sprintf("%v", success),
			wcStorage: storageType,
		},
	).Observe(d.Seconds())
}

func newWCMethodDurationCounter(method string) *prometheus.HistogramVec {
	return metrics.NewHistogramVec(prometheus.HistogramOpts{
		Namespace: namespace,
		Subsystem: wcSubsystem,
		Name:      fmt.Sprintf("%s_req_duration_seconds", method),
		Help:      fmt.Sprintf("Accumulated %s request process duration", method),
	}, []string{wcShardID, wcSuccess, wcStorage})
}

func newWCOperationCounterVec(operation string, labels []string) *prometheus.CounterVec {
	return metrics.NewCounterVec(prometheus.CounterOpts{
		Namespace: namespace,
		Subsystem: wcSubsystem,
		Name:      fmt.Sprintf("%s_operation_count", operation),
		Help:      fmt.Sprintf("The number of %s operations processed", operation),
	}, labels)
}

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