package metrics

import (
	"fmt"
	"strings"
	"time"

	"github.com/prometheus/client_golang/prometheus"
)

const objectSubsystem = "object"

type (
	methodCount struct {
		success metric[prometheus.Counter]
		total   metric[prometheus.Counter]
	}

	objectServiceMetrics struct {
		getCounter       methodCount
		putCounter       methodCount
		headCounter      methodCount
		searchCounter    methodCount
		deleteCounter    methodCount
		rangeCounter     methodCount
		rangeHashCounter methodCount

		getDuration       metric[prometheus.Counter]
		putDuration       metric[prometheus.Counter]
		headDuration      metric[prometheus.Counter]
		searchDuration    metric[prometheus.Counter]
		deleteDuration    metric[prometheus.Counter]
		rangeDuration     metric[prometheus.Counter]
		rangeHashDuration metric[prometheus.Counter]

		putPayload metric[prometheus.Counter]
		getPayload metric[prometheus.Counter]

		shardMetrics   metric[*prometheus.GaugeVec]
		shardsReadonly metric[*prometheus.GaugeVec]
	}
)

const (
	shardIDLabelKey     = "shard"
	counterTypeLabelKey = "type"
	containerIDLabelKey = "cid"
)

func newObjectMethodCallCounter(name string) methodCount {
	return methodCount{
		success: newCounter(prometheus.CounterOpts{
			Namespace: namespace,
			Subsystem: objectSubsystem,
			Name:      fmt.Sprintf("%s_req_count_success", name),
			Help:      fmt.Sprintf("The number of successful %s requests processed", name),
		}),
		total: newCounter(prometheus.CounterOpts{
			Namespace: namespace,
			Subsystem: objectSubsystem,
			Name:      fmt.Sprintf("%s_req_count", name),
			Help:      fmt.Sprintf("Total number of %s requests processed", name),
		}),
	}
}

func (m methodCount) mustRegister() {
	mustRegister(m.success)
	mustRegister(m.total)
}

func (m methodCount) Inc(success bool) {
	m.total.value.Inc()
	if success {
		m.success.value.Inc()
	}
}

func newObjectServiceMetrics() objectServiceMetrics {
	return objectServiceMetrics{
		getCounter:        newObjectMethodCallCounter("get"),
		putCounter:        newObjectMethodCallCounter("put"),
		headCounter:       newObjectMethodCallCounter("head"),
		searchCounter:     newObjectMethodCallCounter("search"),
		deleteCounter:     newObjectMethodCallCounter("delete"),
		rangeCounter:      newObjectMethodCallCounter("range"),
		rangeHashCounter:  newObjectMethodCallCounter("range_hash"),
		getDuration:       newObjectMethodDurationCounter("get"),
		putDuration:       newObjectMethodDurationCounter("put"),
		headDuration:      newObjectMethodDurationCounter("head"),
		searchDuration:    newObjectMethodDurationCounter("search"),
		deleteDuration:    newObjectMethodDurationCounter("delete"),
		rangeDuration:     newObjectMethodDurationCounter("range"),
		rangeHashDuration: newObjectMethodDurationCounter("range_hash"),
		putPayload:        newObjectMethodPayloadCounter("put"),
		getPayload:        newObjectMethodPayloadCounter("get"),
		shardMetrics:      newObjectGaugeVector("counter", "Objects counters per shards", []string{shardIDLabelKey, counterTypeLabelKey}),
		shardsReadonly:    newObjectGaugeVector("readonly", "Shard state", []string{shardIDLabelKey}),
	}
}

func newObjectMethodPayloadCounter(method string) metric[prometheus.Counter] {
	return newCounter(prometheus.CounterOpts{
		Namespace: namespace,
		Subsystem: objectSubsystem,
		Name:      fmt.Sprintf("%s_payload", method),
		Help:      fmt.Sprintf("Accumulated payload size at object %s method", strings.ReplaceAll(method, "_", " ")),
	})
}

func newObjectMethodDurationCounter(method string) metric[prometheus.Counter] {
	return newCounter(prometheus.CounterOpts{
		Namespace: namespace,
		Subsystem: objectSubsystem,
		Name:      fmt.Sprintf("%s_req_duration", method),
		Help:      fmt.Sprintf("Accumulated %s request process duration", strings.ReplaceAll(method, "_", " ")),
	})
}

func newObjectGaugeVector(name, help string, labels []string) metric[*prometheus.GaugeVec] {
	return newGaugeVec(prometheus.GaugeOpts{
		Namespace: namespace,
		Subsystem: objectSubsystem,
		Name:      name,
		Help:      help,
	}, labels)
}

func (m objectServiceMetrics) register() {
	m.getCounter.mustRegister()
	m.putCounter.mustRegister()
	m.headCounter.mustRegister()
	m.searchCounter.mustRegister()
	m.deleteCounter.mustRegister()
	m.rangeCounter.mustRegister()
	m.rangeHashCounter.mustRegister()

	mustRegister(m.getDuration)
	mustRegister(m.putDuration)
	mustRegister(m.headDuration)
	mustRegister(m.searchDuration)
	mustRegister(m.deleteDuration)
	mustRegister(m.rangeDuration)
	mustRegister(m.rangeHashDuration)

	mustRegister(m.putPayload)
	mustRegister(m.getPayload)

	mustRegister(m.shardMetrics)
	mustRegister(m.shardsReadonly)
}

func (m objectServiceMetrics) IncGetReqCounter(success bool) {
	m.getCounter.Inc(success)
}

func (m objectServiceMetrics) IncPutReqCounter(success bool) {
	m.putCounter.Inc(success)
}

func (m objectServiceMetrics) IncHeadReqCounter(success bool) {
	m.headCounter.Inc(success)
}

func (m objectServiceMetrics) IncSearchReqCounter(success bool) {
	m.searchCounter.Inc(success)
}

func (m objectServiceMetrics) IncDeleteReqCounter(success bool) {
	m.deleteCounter.Inc(success)
}

func (m objectServiceMetrics) IncRangeReqCounter(success bool) {
	m.rangeCounter.Inc(success)
}

func (m objectServiceMetrics) IncRangeHashReqCounter(success bool) {
	m.rangeHashCounter.Inc(success)
}

func (m objectServiceMetrics) AddGetReqDuration(d time.Duration) {
	m.getDuration.value.Add(float64(d))
}

func (m objectServiceMetrics) AddPutReqDuration(d time.Duration) {
	m.putDuration.value.Add(float64(d))
}

func (m objectServiceMetrics) AddHeadReqDuration(d time.Duration) {
	m.headDuration.value.Add(float64(d))
}

func (m objectServiceMetrics) AddSearchReqDuration(d time.Duration) {
	m.searchDuration.value.Add(float64(d))
}

func (m objectServiceMetrics) AddDeleteReqDuration(d time.Duration) {
	m.deleteDuration.value.Add(float64(d))
}

func (m objectServiceMetrics) AddRangeReqDuration(d time.Duration) {
	m.rangeDuration.value.Add(float64(d))
}

func (m objectServiceMetrics) AddRangeHashReqDuration(d time.Duration) {
	m.rangeHashDuration.value.Add(float64(d))
}

func (m objectServiceMetrics) AddPutPayload(ln int) {
	m.putPayload.value.Add(float64(ln))
}

func (m objectServiceMetrics) AddGetPayload(ln int) {
	m.getPayload.value.Add(float64(ln))
}

func (m objectServiceMetrics) AddToObjectCounter(shardID, objectType string, delta int) {
	m.shardMetrics.value.With(
		prometheus.Labels{
			shardIDLabelKey:     shardID,
			counterTypeLabelKey: objectType,
		},
	).Add(float64(delta))
}

func (m objectServiceMetrics) SetObjectCounter(shardID, objectType string, v uint64) {
	m.shardMetrics.value.With(
		prometheus.Labels{
			shardIDLabelKey:     shardID,
			counterTypeLabelKey: objectType,
		},
	).Set(float64(v))
}

func (m objectServiceMetrics) SetReadonly(shardID string, readonly bool) {
	var flag float64
	if readonly {
		flag = 1
	}
	m.shardsReadonly.value.With(
		prometheus.Labels{
			shardIDLabelKey: shardID,
		},
	).Set(flag)
}