package logr

import (
	"errors"

	"github.com/wiggin77/merror"
)

const (
	DefMetricsUpdateFreqMillis = 15000 // 15 seconds
)

// Counter is a simple metrics sink that can only increment a value.
// Implementations are external to Logr and provided via `MetricsCollector`.
type Counter interface {
	// Inc increments the counter by 1. Use Add to increment it by arbitrary non-negative values.
	Inc()
	// Add adds the given value to the counter. It panics if the value is < 0.
	Add(float64)
}

// Gauge is a simple metrics sink that can receive values and increase or decrease.
// Implementations are external to Logr and provided via `MetricsCollector`.
type Gauge interface {
	// Set sets the Gauge to an arbitrary value.
	Set(float64)
	// Add adds the given value to the Gauge. (The value can be negative, resulting in a decrease of the Gauge.)
	Add(float64)
	// Sub subtracts the given value from the Gauge. (The value can be negative, resulting in an increase of the Gauge.)
	Sub(float64)
}

// MetricsCollector provides a way for users of this Logr package to have metrics pushed
// in an efficient way to any backend, e.g. Prometheus.
// For each target added to Logr, the supplied MetricsCollector will provide a Gauge
// and Counters that will be called frequently as logging occurs.
type MetricsCollector interface {
	// QueueSizeGauge returns a Gauge that will be updated by the named target.
	QueueSizeGauge(target string) (Gauge, error)
	// LoggedCounter returns a Counter that will be incremented by the named target.
	LoggedCounter(target string) (Counter, error)
	// ErrorCounter returns a Counter that will be incremented by the named target.
	ErrorCounter(target string) (Counter, error)
	// DroppedCounter returns a Counter that will be incremented by the named target.
	DroppedCounter(target string) (Counter, error)
	// BlockedCounter returns a Counter that will be incremented by the named target.
	BlockedCounter(target string) (Counter, error)
}

// TargetWithMetrics is a target that provides metrics.
type TargetWithMetrics interface {
	EnableMetrics(collector MetricsCollector, updateFreqMillis int64) error
}

func (logr *Logr) getMetricsCollector() MetricsCollector {
	logr.mux.RLock()
	defer logr.mux.RUnlock()
	return logr.metrics
}

// SetMetricsCollector enables metrics collection by supplying a MetricsCollector.
// The MetricsCollector provides counters and gauges that are updated by log targets.
func (logr *Logr) SetMetricsCollector(collector MetricsCollector) error {
	if collector == nil {
		return errors.New("collector cannot be nil")
	}

	logr.mux.Lock()
	logr.metrics = collector
	logr.queueSizeGauge, _ = collector.QueueSizeGauge("_logr")
	logr.loggedCounter, _ = collector.LoggedCounter("_logr")
	logr.errorCounter, _ = collector.ErrorCounter("_logr")
	logr.mux.Unlock()

	logr.metricsInitOnce.Do(func() {
		logr.metricsDone = make(chan struct{})
		go logr.startMetricsUpdater()
	})

	merr := merror.New()

	logr.tmux.RLock()
	defer logr.tmux.RUnlock()
	for _, target := range logr.targets {
		if tm, ok := target.(TargetWithMetrics); ok {
			if err := tm.EnableMetrics(collector, logr.MetricsUpdateFreqMillis); err != nil {
				merr.Append(err)
			}
		}

	}
	return merr.ErrorOrNil()
}

func (logr *Logr) setQueueSizeGauge(val float64) {
	logr.mux.RLock()
	defer logr.mux.RUnlock()
	if logr.queueSizeGauge != nil {
		logr.queueSizeGauge.Set(val)
	}
}

func (logr *Logr) incLoggedCounter() {
	logr.mux.RLock()
	defer logr.mux.RUnlock()
	if logr.loggedCounter != nil {
		logr.loggedCounter.Inc()
	}
}

func (logr *Logr) incErrorCounter() {
	logr.mux.RLock()
	defer logr.mux.RUnlock()
	if logr.errorCounter != nil {
		logr.errorCounter.Inc()
	}
}