package statsd

import (
	"fmt"
	"sort"
	"strings"

	logging "code.justin.tv/amzn/TwitchLogging"
	telemetry "code.justin.tv/amzn/TwitchTelemetry"
)

// metricBuilder takes in some telemetry.Dimesion and does all the heavily lifting to
// convert these dimensions into statsd metric names. This struct must be ProcessIdentifier
// aware in order to structure the metric name properly
type metricBuilder struct {
	distribution *telemetry.Distribution
	dimensionMap telemetry.DimensionSet
	nodeList     []string
	logger       logging.Logger
}

// TODO: use []*telemetry.Dimension when the upstream changes
func newMetricBuilder(distribution *telemetry.Distribution, logger logging.Logger) *metricBuilder {
	mb := &metricBuilder{
		distribution: distribution,
		logger:       logger,
	}
	// initialize helper data structures
	mb.generateDimensionMap()
	mb.generateMetricStructure()
	return mb
}

func (mb *metricBuilder) generateDimensionMap() {
	mb.dimensionMap = make(telemetry.DimensionSet)
	// initialize each ProcessIdentifier dimension to 'none', which will be the default
	// if not provided in the sample's dimensions
	mb.dimensionMap[telemetry.DimensionService] = noneString
	mb.dimensionMap[telemetry.DimensionStage] = noneString
	mb.dimensionMap[telemetry.DimensionSubstage] = noneString
	mb.dimensionMap[telemetry.DimensionRegion] = noneString
	// write all the given dimensions into the map
	for name, value := range mb.distribution.MetricID.Dimensions {
		// drop dimensions without a value
		if value != "" {
			// replace all '.' with '_'
			sanitizedName := strings.Replace(name, ".", "_", -1)
			sanitizedValue := strings.Replace(value, ".", "_", -1)
			mb.dimensionMap[sanitizedName] = sanitizedValue
		}
	}
}

func (mb *metricBuilder) generateMetricStructure() {
	extraDimensionNames := make([]string, 0)
	mb.nodeList = []string{
		telemetry.DimensionService,
		telemetry.DimensionStage,
		telemetry.DimensionSubstage,
		telemetry.DimensionRegion,
	}
	// Check for some additional special dimensions
	if mb.dimensionMap[telemetry.DimensionProcessAddress] != "" {
		mb.nodeList = append(mb.nodeList, telemetry.DimensionProcessAddress)
	}
	if mb.dimensionMap[telemetry.DimensionOperation] != "" {
		mb.nodeList = append(mb.nodeList, telemetry.DimensionOperation)
	}
	// find service identifier fields and put them in a special slice,
	// where if a ProcessIdentifier field is omitted (i.e. as part of a
	// specified rollup, it'll get dropped)
	for name := range mb.distribution.MetricID.Dimensions {
		switch name {
		case telemetry.DimensionService:
			continue
		case telemetry.DimensionStage:
			continue
		case telemetry.DimensionSubstage:
			continue
		case telemetry.DimensionRegion:
			continue
		case telemetry.DimensionProcessAddress:
			continue
		case telemetry.DimensionOperation:
			continue
		default:
			sanitizedName := strings.Replace(name, ".", "_", -1)
			extraDimensionNames = append(extraDimensionNames, sanitizedName)
		}
	}
	// sort and return
	sort.Strings(extraDimensionNames)
	mb.nodeList = append(mb.nodeList, extraDimensionNames...)
}

// expandMetricNames is the meat and potatoes of the metric builder. It uses the helper
// data structures to iterate through the base dimensions and each defined rollup
// to create a series of statsd metric names to emit
func (mb *metricBuilder) expandMetricNames() []string {
	metricNames := make([]string, 0, 1+len(mb.distribution.RollupDimensions))

	// Create the fully qualified metric name (no rollups) from the samples base dimensions
	var metricName string
	var err error
	metricName, err = mb.generateMetricName([]string{})
	if err != nil {
		// skip this metric
		mb.log("error creating base metric name, skipping", "err", err.Error())
	} else {
		metricNames = append(metricNames, metricName)
	}

	// Create a metric name for each specified rollup
	for _, rollupDimensions := range mb.distribution.RollupDimensions {
		metricName, err = mb.generateMetricName(rollupDimensions)
		if err != nil {
			// skip this metric
			mb.log("error creating rollup metric name, skipping", "err", err.Error())
			continue
		}
		metricNames = append(metricNames, metricName)
	}
	return metricNames

	// TODO: if a ProcessIdentifier field like SubStage is empty, but the AdditionalDimensions also wants you to
	// rollup on SubStage... that can lead to a situation where a particular metric name string is generated
	// twice and double sent... there should probably be a set to check that a particular metric name
	// isn't generated and emitted multiple times from a single sample.
}

// use the given rollup dimensions to create a statsd metric
// TODO: this could probably be cached so it doesnt need to be built every time!
func (mb *metricBuilder) generateMetricName(rollupDimensions []string) (string, error) {
	// remove dots from metric name
	sanitizedMetricName := strings.Replace(mb.distribution.MetricID.Name, ".", "_", -1)
	// start by stashing the dimensions to rollup so we can put them back later
	// and then overwrite the fields in the dimensionMap
	rollupBackup := make(map[string]string)
	var found bool
	var dimVal string
	for _, dimName := range rollupDimensions {
		if dimVal, found = mb.dimensionMap[dimName]; !found {
			// this error will be passed up and logged by the caller
			return "", fmt.Errorf("rollup given for undefined dimension '%s'", dimName)
		}
		rollupBackup[dimName] = dimVal
		mb.dimensionMap[dimName] = allString
	}

	var metricNameBuilder strings.Builder
	for _, nodeName := range mb.nodeList {
		// Get the value for this node, if one exists...
		// in theory it should always be found
		nodeValue, found := mb.dimensionMap[nodeName]
		if !found {
			// this error will be passed up and logged by the caller
			return "", fmt.Errorf("dimension '%s' not found when generating statsd metric", nodeName)
		}
		metricNameBuilder.WriteString(nodeName)
		metricNameBuilder.WriteString("/")
		metricNameBuilder.WriteString(nodeValue)
		metricNameBuilder.WriteString(".")
	}
	// now write in the metric name
	metricNameBuilder.WriteString(sanitizedMetricName)

	// replace the rollupBackup dimensions
	for k, v := range rollupBackup {
		mb.dimensionMap[k] = v
	}

	// TODO: if the value of i is less than the length of rollupDimensions,
	// it means some rollupDimension was specified but didnt exist in the original
	// dimensions on the sample... is that valid?
	// NOTE: that totally hoses this implementation!

	// Return what has been built
	return metricNameBuilder.String(), nil
}

// log logs if the logger is not nil
func (mb *metricBuilder) log(msg string, keyvals ...interface{}) {
	if mb.logger != nil {
		mb.logger.Log(msg, keyvals...)
	}
}
