package slackscot

import (
	"go.opentelemetry.io/otel/label"
	"go.opentelemetry.io/otel/metric"
	"time"
)

const (
	newMsgType    = "new"
	updateMsgType = "edit"
	deleteMsgType = "delete"
)

// instrumenter holds data for core instrumentation
type instrumenter struct {
	appName       string
	coreMetrics   coreMetrics
	pluginMetrics map[string]pluginMetrics
	meter         metric.Meter
}

// coreMetrics holds core slackscot metrics
type coreMetrics struct {
	msgsSeen                   metric.BoundInt64Counter
	msgsProcessed              map[string]metric.BoundInt64Counter
	msgProcessingLatencyMillis map[string]metric.BoundInt64ValueRecorder
	msgDispatchLatencyMillis   metric.BoundInt64ValueRecorder
	slackLatencyMillis         metric.Int64ValueObserver
}

// pluginMetrics holds metrics specific to a plugin
type pluginMetrics struct {
	processingTimeMillis metric.BoundInt64ValueRecorder
	reactionCount        metric.BoundInt64Counter
}

// newInstrumenter creates a new core instrumenter
func newInstrumenter(appName string, meter metric.Meter, latencyCallback metric.Int64ObserverFunc) (ins *instrumenter, err error) {
	ins = new(instrumenter)

	defaultLabels := []label.KeyValue{label.String("name", appName)}

	msgSeen, err := meter.NewInt64Counter("msgSeen")
	if err != nil {
		return nil, err
	}

	slackLatency, err := meter.NewInt64ValueObserver("slackLatencyMillis", latencyCallback)
	if err != nil {
		return nil, err
	}

	dispatchLatency, err := meter.NewInt64ValueRecorder("msgDispatchLatencyMillis")
	if err != nil {
		return nil, err
	}

	msgProcessed, err := newBoundCounterByMsgType("msgProcessed", appName, meter)
	if err != nil {
		return nil, err
	}

	msgProcessingLatencyMillis, err := newBoundValueRecorderByMsgType("msgProcessingLatencyMillis", appName, meter)
	if err != nil {
		return nil, err
	}

	ins.coreMetrics = coreMetrics{msgsSeen: msgSeen.Bind(defaultLabels...),
		msgsProcessed:              msgProcessed,
		msgProcessingLatencyMillis: msgProcessingLatencyMillis,
		msgDispatchLatencyMillis:   dispatchLatency.Bind(defaultLabels...),
		slackLatencyMillis:         slackLatency}

	ins.appName = appName
	ins.pluginMetrics = make(map[string]pluginMetrics)

	ins.meter = meter
	return ins, nil
}

// newBoundValueRecorderByMsgType creates a set of BoundInt64Counter by message type
func newBoundCounterByMsgType(counterName string, appName string, meter metric.Meter) (boundCounter map[string]metric.BoundInt64Counter, err error) {
	boundCounter = make(map[string]metric.BoundInt64Counter)

	c, err := meter.NewInt64Counter(counterName)
	if err != nil {
		return nil, err
	}

	boundCounter[newMsgType] = c.Bind(label.String("name", appName), label.String("msgType", newMsgType))
	boundCounter[updateMsgType] = c.Bind(label.String("name", appName), label.String("msgType", updateMsgType))
	boundCounter[deleteMsgType] = c.Bind(label.String("name", appName), label.String("msgType", deleteMsgType))

	return boundCounter, nil
}

// newBoundValueRecorderByMsgType creates a set of BoundInt64ValueRecorder by message type
func newBoundValueRecorderByMsgType(ValueRecorderName string, appName string, meter metric.Meter) (boundValueRecorder map[string]metric.BoundInt64ValueRecorder, err error) {
	boundValueRecorder = make(map[string]metric.BoundInt64ValueRecorder)

	m, err := meter.NewInt64ValueRecorder(ValueRecorderName)
	if err != nil {
		return nil, err
	}

	boundValueRecorder[newMsgType] = m.Bind(label.String("name", appName), label.String("msgType", newMsgType))
	boundValueRecorder[updateMsgType] = m.Bind(label.String("name", appName), label.String("msgType", updateMsgType))
	boundValueRecorder[deleteMsgType] = m.Bind(label.String("name", appName), label.String("msgType", deleteMsgType))

	return boundValueRecorder, nil
}

// getOrCreatePluginMetrics returns an existing pluginMetrics for a plugin or creates a new one, if necessary
func (ins *instrumenter) getOrCreatePluginMetrics(pluginName string) (pm pluginMetrics, err error) {
	if _, ok := ins.pluginMetrics[pluginName]; !ok {
		pm, err = newPluginMetrics(ins.appName, pluginName, ins.meter)
		if err != nil {
			return pm, err
		}
		ins.pluginMetrics[pluginName] = pm
	}

	return ins.pluginMetrics[pluginName], nil
}

// newPluginMetrics returns a new pluginMetrics instance for a plugin
func newPluginMetrics(appName string, pluginName string, meter metric.Meter) (pm pluginMetrics, err error) {
	c, err := meter.NewInt64Counter("reactionCount")
	if err != nil {
		return pm, err
	}
	m, err := meter.NewInt64ValueRecorder("processingTimeMillis")
	if err != nil {
		return pm, err
	}

	pm.reactionCount = c.Bind(label.String("name", appName), label.String("plugin", pluginName))
	pm.processingTimeMillis = m.Bind(label.String("name", appName), label.String("plugin", pluginName))

	return pm, nil
}

type timed func()

// measure returns the execution duration of a timed function
func measure(operation timed) (d time.Duration) {
	before := time.Now()

	operation()

	return time.Now().Sub(before)
}
