// Copyright 2021 Molecula Corp. All rights reserved.
package prometheus

import (
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/molecula/featurebase/v3/logger"
	"github.com/molecula/featurebase/v3/stats"
	"github.com/prometheus/client_golang/prometheus"
)

const (
	// namespace is prepended to each metric event name with "_"
	defaultNamespace = "general"
)

// Ensure client implements interface.
var _ stats.StatsClient = &prometheusClient{}

// Module-level mutex to avoid copying in WithTags()
var mu sync.Mutex

// prometheusClient represents a Prometheus implementation of pilosa.statsClient.
type prometheusClient struct {
	tags        []string
	logger      logger.Logger
	counters    map[string]prometheus.Counter
	counterVecs map[string]*prometheus.CounterVec
	gauges      map[string]prometheus.Gauge
	gaugeVecs   map[string]*prometheus.GaugeVec
	observers   map[string]prometheus.Observer
	summaryVecs map[string]*prometheus.SummaryVec
	namespace   string
}

// ClientOption is a functional option type for prometheusClient
type ClientOption func(c *prometheusClient)

// OptClientPrefix is a functional option on prometheusClient used to set the namespace
func OptClientNamespace(namespace string) ClientOption {
	return func(c *prometheusClient) {
		c.namespace = namespace
	}
}

// NewPrometheusClient returns a new instance of StatsClient.
func NewPrometheusClient(opts ...ClientOption) (*prometheusClient, error) {
	client := &prometheusClient{
		logger:      logger.NopLogger,
		counters:    make(map[string]prometheus.Counter),
		counterVecs: make(map[string]*prometheus.CounterVec),
		gauges:      make(map[string]prometheus.Gauge),
		gaugeVecs:   make(map[string]*prometheus.GaugeVec),
		observers:   make(map[string]prometheus.Observer),
		summaryVecs: make(map[string]*prometheus.SummaryVec),
		namespace:   defaultNamespace,
	}

	for _, opt := range opts {
		opt(client)
	}

	return client, nil
}

// Open no-op to satisfy interface
func (c *prometheusClient) Open() {}

// Close no-op to satisfy interface
func (c *prometheusClient) Close() error {
	return nil
}

// Tags returns a sorted list of tags on the client.
func (c *prometheusClient) Tags() []string {
	return c.tags
}

// labels returns an instance of prometheus.Labels with the value of the set tags.
func (c *prometheusClient) labels() prometheus.Labels {
	return tagsToLabels(c.tags, c.logger)
}

// WithTags returns a new client with additional tags appended.
func (c *prometheusClient) WithTags(tags ...string) stats.StatsClient {
	return &prometheusClient{
		tags:        unionStringSlice(c.tags, tags),
		logger:      c.logger,
		counters:    c.counters,
		counterVecs: c.counterVecs,
		gauges:      c.gauges,
		gaugeVecs:   c.gaugeVecs,
		observers:   c.observers,
		summaryVecs: c.summaryVecs,
		namespace:   c.namespace,
	}
}

// Count tracks the number of times something occurs per second.
func (c *prometheusClient) Count(name string, value int64, rate float64) {
	mu.Lock()
	defer mu.Unlock()

	var counter prometheus.Counter
	var ok bool
	name = strings.Replace(name, ".", "_", -1)
	labels := c.labels()
	opts := prometheus.CounterOpts{
		Namespace: c.namespace,
		Name:      name,
	}
	if len(labels) == 0 {
		counter, ok = c.counters[name]
		if !ok {
			counter = prometheus.NewCounter(opts)
			c.counters[name] = counter
			prometheus.MustRegister(counter)
		}
	} else {
		var counterVec *prometheus.CounterVec
		counterVec, ok = c.counterVecs[name]
		if !ok {
			counterVec = prometheus.NewCounterVec(
				opts,
				labelKeys(labels),
			)
			c.counterVecs[name] = counterVec
			prometheus.MustRegister(counterVec)
		}
		var err error
		counter, err = counterVec.GetMetricWith(labels)
		if err != nil {
			c.logger.Errorf("counterVec.GetMetricWith error: %s", err)
		}
	}
	if value == 1 {
		counter.Inc()
	} else {
		counter.Add(float64(value))
	}
}

// CountWithCustomTags tracks the number of times something occurs per second with custom tags.
func (c *prometheusClient) CountWithCustomTags(name string, value int64, rate float64, t []string) {
	c.WithTags(append(c.tags, t...)...).Count(name, value, rate)
}

// Gauge sets the value of a metric.
func (c *prometheusClient) Gauge(name string, value float64, rate float64) {
	mu.Lock()
	defer mu.Unlock()

	var gauge prometheus.Gauge
	var ok bool
	name = strings.Replace(name, ".", "_", -1)
	labels := c.labels()
	opts := prometheus.GaugeOpts{
		Namespace: c.namespace,
		Name:      name,
	}
	if len(labels) == 0 {
		gauge, ok = c.gauges[name]
		if !ok {
			gauge = prometheus.NewGauge(opts)
			c.gauges[name] = gauge
			prometheus.MustRegister(gauge)
		}
	} else {
		var gaugeVec *prometheus.GaugeVec
		gaugeVec, ok = c.gaugeVecs[name]
		if !ok {
			gaugeVec = prometheus.NewGaugeVec(
				opts,
				labelKeys(labels),
			)
			c.gaugeVecs[name] = gaugeVec
			prometheus.MustRegister(gaugeVec)
		}
		var err error
		gauge, err = gaugeVec.GetMetricWith(labels)
		if err != nil {
			c.logger.Errorf("gaugeVec.GetMetricWith error: %s", err)
			return
		}
	}
	gauge.Set(float64(value))
}

// Histogram tracks statistical distribution of a metric.
func (c *prometheusClient) Histogram(name string, value float64, rate float64) {
	mu.Lock()
	defer mu.Unlock()

	var observer prometheus.Observer
	var ok bool
	name = strings.Replace(name, ".", "_", -1)
	labels := c.labels()
	opts := prometheus.SummaryOpts{
		Namespace:  c.namespace,
		Name:       name,
		Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
	}
	if len(labels) == 0 {
		observer, ok = c.observers[name]
		if !ok {
			summary := prometheus.NewSummary(opts)
			observer = summary
			c.observers[name] = observer
			prometheus.MustRegister(summary)
		}
	} else {
		var summaryVec *prometheus.SummaryVec
		summaryVec, ok = c.summaryVecs[name]
		if !ok {
			summaryVec = prometheus.NewSummaryVec(
				opts,
				labelKeys(labels),
			)
			c.summaryVecs[name] = summaryVec
			prometheus.MustRegister(summaryVec)
		}
		var err error
		observer, err = summaryVec.GetMetricWith(labels)
		if err != nil {
			c.logger.Errorf("summaryVec.GetMetricWith error: %s", err)
			return
		}
	}
	observer.Observe(value)
}

// Set tracks number of unique elements.
func (c *prometheusClient) Set(name string, value string, rate float64) {
	c.logger.Infof("prometheusClient.Set unimplemented: %s=%s", name, value)
}

// Timing tracks timing information for a metric.
func (c *prometheusClient) Timing(name string, value time.Duration, rate float64) {
	c.Histogram(name, value.Seconds(), rate)
}

// SetLogger sets the logger for client.
func (c *prometheusClient) SetLogger(logger logger.Logger) {
	c.logger = logger
}

// unionStringSlice returns a sorted set of tags which combine a & b.
func unionStringSlice(a, b []string) []string {
	// Sort both sets first.
	sort.Strings(a)
	sort.Strings(b)

	// Find size of largest slice.
	n := len(a)
	if len(b) > n {
		n = len(b)
	}

	// Exit if both sets are empty.
	if n == 0 {
		return nil
	}

	// Iterate over both in order and merge.
	other := make([]string, 0, n)
	for len(a) > 0 || len(b) > 0 {
		if len(a) == 0 {
			other, b = append(other, b[0]), b[1:]
		} else if len(b) == 0 {
			other, a = append(other, a[0]), a[1:]
		} else if a[0] < b[0] {
			other, a = append(other, a[0]), a[1:]
		} else if b[0] < a[0] {
			other, b = append(other, b[0]), b[1:]
		} else {
			other, a, b = append(other, a[0]), a[1:], b[1:]
		}
	}
	return other
}

func tagsToLabels(tags []string, logger logger.Logger) (labels prometheus.Labels) {
	labels = make(prometheus.Labels)
	for _, tag := range tags {
		tagParts := strings.SplitAfterN(tag, ":", 2)
		if len(tagParts) != 2 {
			// only process tags in "key:value" form
			logger.Errorf("invalid Prometheus label: %v\n", tag)
			continue
		}
		labels[tagParts[0][0:len(tagParts[0])-1]] = tagParts[1]
	}
	return labels
}

func labelKeys(labels prometheus.Labels) (keys []string) {
	keys = make([]string, len(labels))
	i := 0
	for k := range labels {
		keys[i] = k
		i++
	}
	return keys
}
