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

import (
	"sort"
	"time"

	"github.com/DataDog/datadog-go/statsd"
	"github.com/molecula/featurebase/v3/logger"
	"github.com/molecula/featurebase/v3/stats"
)

// StatsD protocol wrapper using the DataDog library that added Tags to the StatsD protocol
// statsD defailt host is "127.0.0.1:8125"

const (
	// bufferLen Stats lient buffer size.
	bufferLen = 1024
)

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

// statsClient represents a StatsD implementation of pilosa.statsClient.
type statsClient struct {
	client *statsd.Client
	tags   []string
	logger logger.Logger

	// prefix is appended to each metric event name
	prefix string
}

// NewStatsClient returns a new instance of StatsClient.
func NewStatsClient(host string, namespace string) (*statsClient, error) {
	c, err := statsd.NewBuffered(host, bufferLen)
	if err != nil {
		return nil, err
	}

	return &statsClient{
		client: c,
		logger: logger.NopLogger,
		prefix: namespace + ".",
	}, nil
}

// Open no-op
func (c *statsClient) Open() {}

// Close closes the connection to the agent.
func (c *statsClient) Close() error {
	return c.client.Close()
}

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

// WithTags returns a new client with additional tags appended.
func (c *statsClient) WithTags(tags ...string) stats.StatsClient {
	return &statsClient{
		client: c.client,
		tags:   unionStringSlice(c.tags, tags),
		logger: c.logger,
	}
}

// Count tracks the number of times something occurs per second.
func (c *statsClient) Count(name string, value int64, rate float64) {
	if err := c.client.Count(c.prefix+name, value, c.tags, rate); err != nil {
		c.logger.Errorf("statsd.StatsClient.Count error: %s", err)
	}
}

// CountWithCustomTags tracks the number of times something occurs per second with custom tags.
func (c *statsClient) CountWithCustomTags(name string, value int64, rate float64, t []string) {
	tags := append(c.tags, t...)
	if err := c.client.Count(c.prefix+name, value, tags, rate); err != nil {
		c.logger.Errorf("statsd.StatsClient.Count error: %s", err)
	}
}

// Gauge sets the value of a metric.
func (c *statsClient) Gauge(name string, value float64, rate float64) {
	if err := c.client.Gauge(c.prefix+name, value, c.tags, rate); err != nil {
		c.logger.Errorf("statsd.StatsClient.Gauge error: %s", err)
	}
}

// Histogram tracks statistical distribution of a metric.
func (c *statsClient) Histogram(name string, value float64, rate float64) {
	if err := c.client.Histogram(c.prefix+name, value, c.tags, rate); err != nil {
		c.logger.Errorf("statsd.StatsClient.Histogram error: %s", err)
	}
}

// Set tracks number of unique elements.
func (c *statsClient) Set(name string, value string, rate float64) {
	if err := c.client.Set(c.prefix+name, value, c.tags, rate); err != nil {
		c.logger.Errorf("statsd.StatsClient.Set error: %s", err)
	}
}

// Timing tracks timing information for a metric.
func (c *statsClient) Timing(name string, value time.Duration, rate float64) {
	if err := c.client.Timing(c.prefix+name, value, c.tags, rate); err != nil {
		c.logger.Errorf("statsd.StatsClient.Timing error: %s", err)
	}
}

// SetLogger sets the logger for client.
func (c *statsClient) 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
}
