package tsm1

import (
	"sort"
	"sync"

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

// The following package variables act as singletons, to be shared by all Engine
// instantiations. This allows multiple Engines to be instantiated within the
// same process.
var (
	bms *blockMetrics
	mmu sync.RWMutex
)

// PrometheusCollectors returns all prometheus metrics for the tsm1 package.
func PrometheusCollectors() []prometheus.Collector {
	mmu.RLock()
	defer mmu.RUnlock()

	var collectors []prometheus.Collector
	if bms != nil {
		collectors = append(collectors, bms.compactionMetrics.PrometheusCollectors()...)
		collectors = append(collectors, bms.fileMetrics.PrometheusCollectors()...)
		collectors = append(collectors, bms.cacheMetrics.PrometheusCollectors()...)
	}
	return collectors
}

// namespace is the leading part of all published metrics for the Storage service.
const namespace = "storage"

const compactionSubsystem = "compactions" // sub-system associated with metrics for compactions.
const fileStoreSubsystem = "tsm_files"    // sub-system associated with metrics for TSM files.
const cacheSubsystem = "cache"            // sub-system associated with metrics for the cache.

// blockMetrics are a set of metrics concerned with tracking data about block storage.
type blockMetrics struct {
	labels prometheus.Labels
	*compactionMetrics
	*fileMetrics
	*cacheMetrics
}

// newBlockMetrics initialises the prometheus metrics for the block subsystem.
func newBlockMetrics(labels prometheus.Labels) *blockMetrics {
	return &blockMetrics{
		labels:            labels,
		compactionMetrics: newCompactionMetrics(labels),
		fileMetrics:       newFileMetrics(labels),
		cacheMetrics:      newCacheMetrics(labels),
	}
}

// PrometheusCollectors satisfies the prom.PrometheusCollector interface.
func (m *blockMetrics) PrometheusCollectors() []prometheus.Collector {
	var metrics []prometheus.Collector
	metrics = append(metrics, m.compactionMetrics.PrometheusCollectors()...)
	metrics = append(metrics, m.fileMetrics.PrometheusCollectors()...)
	metrics = append(metrics, m.cacheMetrics.PrometheusCollectors()...)
	return metrics
}

// compactionMetrics are a set of metrics concerned with tracking data about compactions.
type compactionMetrics struct {
	CompactionsActive  *prometheus.GaugeVec
	CompactionDuration *prometheus.HistogramVec
	CompactionQueue    *prometheus.GaugeVec

	// The following metrics include a ``"status" = {ok, error}` label
	Compactions *prometheus.CounterVec
}

// newCompactionMetrics initialises the prometheus metrics for compactions.
func newCompactionMetrics(labels prometheus.Labels) *compactionMetrics {
	names := []string{"level"} // All compaction metrics have a `level` label.
	for k := range labels {
		names = append(names, k)
	}
	sort.Strings(names)

	totalCompactionsNames := append(append([]string(nil), names...), []string{"reason", "status"}...)
	sort.Strings(totalCompactionsNames)

	return &compactionMetrics{
		Compactions: prometheus.NewCounterVec(prometheus.CounterOpts{
			Namespace: namespace,
			Subsystem: compactionSubsystem,
			Name:      "total",
			Help:      "Number of times cache snapshotted or TSM compaction attempted.",
		}, totalCompactionsNames),
		CompactionsActive: prometheus.NewGaugeVec(prometheus.GaugeOpts{
			Namespace: namespace,
			Subsystem: compactionSubsystem,
			Name:      "active",
			Help:      "Number of active compactions.",
		}, names),
		CompactionDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
			Namespace: namespace,
			Subsystem: compactionSubsystem,
			Name:      "duration_seconds",
			Help:      "Time taken for a successful compaction or snapshot.",
			// 30 buckets spaced exponentially between 5s and ~53 minutes.
			Buckets: prometheus.ExponentialBuckets(5.0, 1.25, 30),
		}, names),
		CompactionQueue: prometheus.NewGaugeVec(prometheus.GaugeOpts{
			Namespace: namespace,
			Subsystem: compactionSubsystem,
			Name:      "queued",
			Help:      "Number of queued compactions.",
		}, names),
	}
}

// PrometheusCollectors satisfies the prom.PrometheusCollector interface.
func (m *compactionMetrics) PrometheusCollectors() []prometheus.Collector {
	return []prometheus.Collector{
		m.Compactions,
		m.CompactionsActive,
		m.CompactionDuration,
		m.CompactionQueue,
	}
}

// fileMetrics are a set of metrics concerned with tracking data about compactions.
type fileMetrics struct {
	DiskSize *prometheus.GaugeVec
	Files    *prometheus.GaugeVec
}

// newFileMetrics initialises the prometheus metrics for tracking files on disk.
func newFileMetrics(labels prometheus.Labels) *fileMetrics {
	var names []string
	for k := range labels {
		names = append(names, k)
	}
	sort.Strings(names)

	return &fileMetrics{
		DiskSize: prometheus.NewGaugeVec(prometheus.GaugeOpts{
			Namespace: namespace,
			Subsystem: fileStoreSubsystem,
			Name:      "disk_bytes",
			Help:      "Number of bytes TSM files using on disk.",
		}, names),
		Files: prometheus.NewGaugeVec(prometheus.GaugeOpts{
			Namespace: namespace,
			Subsystem: fileStoreSubsystem,
			Name:      "total",
			Help:      "Number of files.",
		}, names),
	}
}

// PrometheusCollectors satisfies the prom.PrometheusCollector interface.
func (m *fileMetrics) PrometheusCollectors() []prometheus.Collector {
	return []prometheus.Collector{
		m.DiskSize,
		m.Files,
	}
}

// cacheMetrics are a set of metrics concerned with tracking data about the TSM Cache.
type cacheMetrics struct {
	MemSize          *prometheus.GaugeVec
	DiskSize         *prometheus.GaugeVec
	SnapshotsActive  *prometheus.GaugeVec
	Age              *prometheus.GaugeVec
	SnapshottedBytes *prometheus.CounterVec

	// The following metrics include a ``"status" = {ok, error, dropped}` label
	WrittenBytes *prometheus.CounterVec
	Writes       *prometheus.CounterVec
}

// newCacheMetrics initialises the prometheus metrics for compactions.
func newCacheMetrics(labels prometheus.Labels) *cacheMetrics {
	var names []string
	for k := range labels {
		names = append(names, k)
	}
	sort.Strings(names)

	writeNames := append(append([]string(nil), names...), "status")
	sort.Strings(writeNames)

	return &cacheMetrics{
		MemSize: prometheus.NewGaugeVec(prometheus.GaugeOpts{
			Namespace: namespace,
			Subsystem: cacheSubsystem,
			Name:      "inuse_bytes",
			Help:      "In-memory size of cache.",
		}, names),
		DiskSize: prometheus.NewGaugeVec(prometheus.GaugeOpts{
			Namespace: namespace,
			Subsystem: cacheSubsystem,
			Name:      "disk_bytes",
			Help:      "Number of bytes on disk used by snapshot data.",
		}, names),
		SnapshotsActive: prometheus.NewGaugeVec(prometheus.GaugeOpts{
			Namespace: namespace,
			Subsystem: cacheSubsystem,
			Name:      "snapshots_active",
			Help:      "Number of active concurrent snapshots (>1 when splitting the cache).",
		}, names),
		Age: prometheus.NewGaugeVec(prometheus.GaugeOpts{
			Namespace: namespace,
			Subsystem: cacheSubsystem,
			Name:      "age_seconds",
			Help:      "Age in seconds of the current cache (time since last snapshot or initialisation).",
		}, names),
		SnapshottedBytes: prometheus.NewCounterVec(prometheus.CounterOpts{
			Namespace: namespace,
			Subsystem: cacheSubsystem,
			Name:      "snapshot_bytes",
			Help:      "Number of bytes snapshotted.",
		}, names),
		WrittenBytes: prometheus.NewCounterVec(prometheus.CounterOpts{
			Namespace: namespace,
			Subsystem: cacheSubsystem,
			Name:      "written_bytes",
			Help:      "Number of bytes successfully written to the Cache.",
		}, writeNames),
		Writes: prometheus.NewCounterVec(prometheus.CounterOpts{
			Namespace: namespace,
			Subsystem: cacheSubsystem,
			Name:      "writes_total",
			Help:      "Number of writes to the Cache.",
		}, writeNames),
	}
}

// PrometheusCollectors satisfies the prom.PrometheusCollector interface.
func (m *cacheMetrics) PrometheusCollectors() []prometheus.Collector {
	return []prometheus.Collector{
		m.MemSize,
		m.DiskSize,
		m.SnapshotsActive,
		m.Age,
		m.SnapshottedBytes,
		m.WrittenBytes,
		m.Writes,
	}
}