package statsd

import (
	"strconv"
	"strings"
)

var (
	gaugeSymbol        = []byte("g")
	countSymbol        = []byte("c")
	histogramSymbol    = []byte("h")
	distributionSymbol = []byte("d")
	setSymbol          = []byte("s")
	timingSymbol       = []byte("ms")
)

const (
	tagSeparatorSymbol  = ","
	nameSeparatorSymbol = ":"
	cardSeparatorSymbol = "|"
)

func appendHeader(buffer []byte, namespace string, name string) []byte {
	if namespace != "" {
		buffer = append(buffer, namespace...)
	}
	buffer = append(buffer, name...)
	buffer = append(buffer, ':')
	return buffer
}

func appendRate(buffer []byte, rate float64) []byte {
	if rate < 1 {
		buffer = append(buffer, "|@"...)
		buffer = strconv.AppendFloat(buffer, rate, 'f', -1, 64)
	}
	return buffer
}

func appendWithoutNewlines(buffer []byte, s string) []byte {
	// fastpath for strings without newlines
	if strings.IndexByte(s, '\n') == -1 {
		return append(buffer, s...)
	}

	for _, b := range []byte(s) {
		if b != '\n' {
			buffer = append(buffer, b)
		}
	}
	return buffer
}

func appendTags(buffer []byte, globalTags []string, tags []string) []byte {
	if len(globalTags) == 0 && len(tags) == 0 {
		return buffer
	}
	buffer = append(buffer, "|#"...)
	firstTag := true

	for _, tag := range globalTags {
		if !firstTag {
			buffer = append(buffer, tagSeparatorSymbol...)
		}
		buffer = appendWithoutNewlines(buffer, tag)
		firstTag = false
	}
	for _, tag := range tags {
		if !firstTag {
			buffer = append(buffer, tagSeparatorSymbol...)
		}
		buffer = appendWithoutNewlines(buffer, tag)
		firstTag = false
	}
	return buffer
}

func appendTagsAggregated(buffer []byte, globalTags []string, tags string) []byte {
	if len(globalTags) == 0 && tags == "" {
		return buffer
	}

	buffer = append(buffer, "|#"...)
	firstTag := true

	for _, tag := range globalTags {
		if !firstTag {
			buffer = append(buffer, tagSeparatorSymbol...)
		}
		buffer = appendWithoutNewlines(buffer, tag)
		firstTag = false
	}
	if tags != "" {
		if !firstTag {
			buffer = append(buffer, tagSeparatorSymbol...)
		}
		buffer = appendWithoutNewlines(buffer, tags)
	}
	return buffer
}

func appendFloatMetric(buffer []byte, typeSymbol []byte, namespace string, globalTags []string, name string, value float64, tags []string, rate float64, precision int, originDetection bool) []byte {
	buffer = appendHeader(buffer, namespace, name)
	buffer = strconv.AppendFloat(buffer, value, 'f', precision, 64)
	buffer = append(buffer, '|')
	buffer = append(buffer, typeSymbol...)
	buffer = appendRate(buffer, rate)
	buffer = appendTags(buffer, globalTags, tags)
	buffer = appendContainerID(buffer)
	buffer = appendExternalEnv(buffer, originDetection)
	return buffer
}

func appendIntegerMetric(buffer []byte, typeSymbol []byte, namespace string, globalTags []string, name string, value int64, tags []string, rate float64, originDetection bool) []byte {
	buffer = appendHeader(buffer, namespace, name)
	buffer = strconv.AppendInt(buffer, value, 10)
	buffer = append(buffer, '|')
	buffer = append(buffer, typeSymbol...)
	buffer = appendRate(buffer, rate)
	buffer = appendTags(buffer, globalTags, tags)
	buffer = appendContainerID(buffer)
	buffer = appendExternalEnv(buffer, originDetection)
	return buffer
}

func appendStringMetric(buffer []byte, typeSymbol []byte, namespace string, globalTags []string, name string, value string, tags []string, rate float64, originDetection bool) []byte {
	buffer = appendHeader(buffer, namespace, name)
	buffer = append(buffer, value...)
	buffer = append(buffer, '|')
	buffer = append(buffer, typeSymbol...)
	buffer = appendRate(buffer, rate)
	buffer = appendTags(buffer, globalTags, tags)
	buffer = appendContainerID(buffer)
	buffer = appendExternalEnv(buffer, originDetection)
	return buffer
}

func appendGauge(buffer []byte, namespace string, globalTags []string, name string, value float64, tags []string, rate float64, originDetection bool) []byte {
	return appendFloatMetric(buffer, gaugeSymbol, namespace, globalTags, name, value, tags, rate, -1, originDetection)
}

func appendCount(buffer []byte, namespace string, globalTags []string, name string, value int64, tags []string, rate float64, originDetection bool) []byte {
	return appendIntegerMetric(buffer, countSymbol, namespace, globalTags, name, value, tags, rate, originDetection)
}

func appendHistogram(buffer []byte, namespace string, globalTags []string, name string, value float64, tags []string, rate float64, originDetection bool) []byte {
	return appendFloatMetric(buffer, histogramSymbol, namespace, globalTags, name, value, tags, rate, -1, originDetection)
}

func appendDistribution(buffer []byte, namespace string, globalTags []string, name string, value float64, tags []string, rate float64, originDetection bool) []byte {
	return appendFloatMetric(buffer, distributionSymbol, namespace, globalTags, name, value, tags, rate, -1, originDetection)
}

func appendSet(buffer []byte, namespace string, globalTags []string, name string, value string, tags []string, rate float64, originDetection bool) []byte {
	return appendStringMetric(buffer, setSymbol, namespace, globalTags, name, value, tags, rate, originDetection)
}

func appendTiming(buffer []byte, namespace string, globalTags []string, name string, value float64, tags []string, rate float64, originDetection bool) []byte {
	return appendFloatMetric(buffer, timingSymbol, namespace, globalTags, name, value, tags, rate, 6, originDetection)
}

func escapedEventTextLen(text string) int {
	return len(text) + strings.Count(text, "\n")
}

func appendEscapedEventText(buffer []byte, text string) []byte {
	for _, b := range []byte(text) {
		if b != '\n' {
			buffer = append(buffer, b)
		} else {
			buffer = append(buffer, "\\n"...)
		}
	}
	return buffer
}

func appendEvent(buffer []byte, event *Event, globalTags []string, originDetection bool) []byte {
	escapedTextLen := escapedEventTextLen(event.Text)

	buffer = append(buffer, "_e{"...)
	buffer = strconv.AppendInt(buffer, int64(len(event.Title)), 10)
	buffer = append(buffer, tagSeparatorSymbol...)
	buffer = strconv.AppendInt(buffer, int64(escapedTextLen), 10)
	buffer = append(buffer, "}:"...)
	buffer = append(buffer, event.Title...)
	buffer = append(buffer, '|')
	if escapedTextLen != len(event.Text) {
		buffer = appendEscapedEventText(buffer, event.Text)
	} else {
		buffer = append(buffer, event.Text...)
	}

	if !event.Timestamp.IsZero() {
		buffer = append(buffer, "|d:"...)
		buffer = strconv.AppendInt(buffer, int64(event.Timestamp.Unix()), 10)
	}

	if len(event.Hostname) != 0 {
		buffer = append(buffer, "|h:"...)
		buffer = append(buffer, event.Hostname...)
	}

	if len(event.AggregationKey) != 0 {
		buffer = append(buffer, "|k:"...)
		buffer = append(buffer, event.AggregationKey...)
	}

	if len(event.Priority) != 0 {
		buffer = append(buffer, "|p:"...)
		buffer = append(buffer, event.Priority...)
	}

	if len(event.SourceTypeName) != 0 {
		buffer = append(buffer, "|s:"...)
		buffer = append(buffer, event.SourceTypeName...)
	}

	if len(event.AlertType) != 0 {
		buffer = append(buffer, "|t:"...)
		buffer = append(buffer, string(event.AlertType)...)
	}

	buffer = appendTags(buffer, globalTags, event.Tags)
	buffer = appendContainerID(buffer)
	buffer = appendExternalEnv(buffer, originDetection)
	return buffer
}

func appendEscapedServiceCheckText(buffer []byte, text string) []byte {
	for i := 0; i < len(text); i++ {
		if text[i] == '\n' {
			buffer = append(buffer, "\\n"...)
		} else if text[i] == 'm' && i+1 < len(text) && text[i+1] == ':' {
			buffer = append(buffer, "m\\:"...)
			i++
		} else {
			buffer = append(buffer, text[i])
		}
	}
	return buffer
}

func appendServiceCheck(buffer []byte, serviceCheck *ServiceCheck, globalTags []string, originDetection bool) []byte {
	buffer = append(buffer, "_sc|"...)
	buffer = append(buffer, serviceCheck.Name...)
	buffer = append(buffer, '|')
	buffer = strconv.AppendInt(buffer, int64(serviceCheck.Status), 10)

	if !serviceCheck.Timestamp.IsZero() {
		buffer = append(buffer, "|d:"...)
		buffer = strconv.AppendInt(buffer, int64(serviceCheck.Timestamp.Unix()), 10)
	}

	if len(serviceCheck.Hostname) != 0 {
		buffer = append(buffer, "|h:"...)
		buffer = append(buffer, serviceCheck.Hostname...)
	}

	buffer = appendTags(buffer, globalTags, serviceCheck.Tags)

	if len(serviceCheck.Message) != 0 {
		buffer = append(buffer, "|m:"...)
		buffer = appendEscapedServiceCheckText(buffer, serviceCheck.Message)
	}

	buffer = appendContainerID(buffer)
	buffer = appendExternalEnv(buffer, originDetection)
	return buffer
}

func appendSeparator(buffer []byte) []byte {
	return append(buffer, '\n')
}

func appendContainerID(buffer []byte) []byte {
	if containerID := getContainerID(); len(containerID) > 0 {
		buffer = append(buffer, "|c:"...)
		buffer = append(buffer, containerID...)
	}
	return buffer
}

func appendTimestamp(buffer []byte, timestamp int64) []byte {
	if timestamp > noTimestamp {
		buffer = append(buffer, "|T"...)
		buffer = strconv.AppendInt(buffer, timestamp, 10)
	}
	return buffer
}

func appendExternalEnv(buffer []byte, originDetection bool) []byte {
	if externalEnv := getExternalEnv(); externalEnv != "" && originDetection {
		buffer = append(buffer, "|e:"...)
		buffer = append(buffer, externalEnv...)
	}
	return buffer
}

func appendTagCardinality(buffer []byte, cardinality Cardinality) []byte {
	cardString := cardinality.String()
	if cardString != "" {
		buffer = append(buffer, "|card:"...)
		buffer = append(buffer, cardString...)
	}
	return buffer
}
