package graphite

import (
	"bytes"
	"fmt"
	"math"
	"regexp"
	"sort"
	"strconv"
	"strings"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/filter"
	"github.com/influxdata/telegraf/plugins/serializers"
)

// defaultTemplate is the default template used for graphite serialization.
const defaultTemplate = "host.tags.measurement.field"

var (
	compatibleAllowedCharsName  = regexp.MustCompile(`[^ "-:\<>-\]_a-~\p{L}]`) //nolint:gocritic  // valid range for use-case
	compatibleAllowedCharsValue = regexp.MustCompile(`[^ -:<-~\p{L}]`)         //nolint:gocritic  // valid range for use-case
	compatibleLeadingTildeDrop  = regexp.MustCompile(`^[~]*(.*)`)
	hyphenChars                 = strings.NewReplacer(
		"/", "-",
		"@", "-",
		"*", "-",
	)
	dropChars = strings.NewReplacer(
		`\`, "",
		"..", ".",
	)

	fieldDeleter = strings.NewReplacer(".FIELDNAME", "", "FIELDNAME.", "")
)

type Serializer struct {
	Prefix          string   `toml:"prefix"`
	Template        string   `toml:"template"`
	StrictRegex     string   `toml:"graphite_strict_sanitize_regex"`
	TagSupport      bool     `toml:"graphite_tag_support"`
	TagSanitizeMode string   `toml:"graphite_tag_sanitize_mode"`
	Separator       string   `toml:"graphite_separator"`
	Templates       []string `toml:"templates"`

	tmplts             []*template
	strictAllowedChars *regexp.Regexp
}

type template struct {
	filter filter.Filter
	value  string
}

func (s *Serializer) Init() error {
	graphiteTemplates, defaultTemplate, err := initTemplates(s.Templates)
	if err != nil {
		return err
	}
	s.tmplts = graphiteTemplates

	if defaultTemplate != "" {
		s.Template = defaultTemplate
	}

	if s.TagSanitizeMode == "" {
		s.TagSanitizeMode = "strict"
	}

	if s.Separator == "" {
		s.Separator = "."
	}

	if s.StrictRegex == "" {
		s.strictAllowedChars = regexp.MustCompile(`[^a-zA-Z0-9-:._=\p{L}]`)
	} else {
		var err error
		s.strictAllowedChars, err = regexp.Compile(s.StrictRegex)
		if err != nil {
			return fmt.Errorf("invalid regex provided %q: %w", s.StrictRegex, err)
		}
	}

	return nil
}

func (s *Serializer) Serialize(metric telegraf.Metric) ([]byte, error) {
	var out []byte

	// Convert UnixNano to Unix timestamps
	timestamp := metric.Time().UnixNano() / 1000000000

	switch s.TagSupport {
	case true:
		for fieldName, value := range metric.Fields() {
			fieldValue := formatValue(value)
			if fieldValue == "" {
				continue
			}
			bucket := s.serializeBucketNameWithTags(metric.Name(), metric.Tags(), s.Prefix, s.Separator, fieldName, s.TagSanitizeMode)
			metricString := fmt.Sprintf("%s %s %d\n",
				// insert "field" section of template
				bucket,
				// bucket,
				fieldValue,
				timestamp)
			point := []byte(metricString)
			out = append(out, point...)
		}
	default:
		template := s.Template
		for _, graphiteTemplate := range s.tmplts {
			if graphiteTemplate.filter.Match(metric.Name()) {
				template = graphiteTemplate.value
				break
			}
		}

		bucket := SerializeBucketName(metric.Name(), metric.Tags(), template, s.Prefix)
		if bucket == "" {
			return out, nil
		}

		for fieldName, value := range metric.Fields() {
			fieldValue := formatValue(value)
			if fieldValue == "" {
				continue
			}
			metricString := fmt.Sprintf("%s %s %d\n",
				// insert "field" section of template
				s.strictSanitize(InsertField(bucket, fieldName)),
				fieldValue,
				timestamp)
			point := []byte(metricString)
			out = append(out, point...)
		}
	}
	return out, nil
}

func (s *Serializer) SerializeBatch(metrics []telegraf.Metric) ([]byte, error) {
	var batch bytes.Buffer
	for _, m := range metrics {
		buf, err := s.Serialize(m)
		if err != nil {
			return nil, err
		}
		batch.Write(buf)
	}
	return batch.Bytes(), nil
}

// SerializeBucketName will take the given measurement name and tags and
// produce a graphite bucket. It will use the Serializer.Template
// to generate this, or DefaultTemplate.
//
// NOTE: SerializeBucketName replaces the "field" portion of the template with
// FIELDNAME. It is up to the user to replace this. This is so that
// SerializeBucketName can be called just once per measurement, rather than
// once per field. See Serializer.InsertField() function.
func SerializeBucketName(measurement string, tags map[string]string, template, prefix string) string {
	if template == "" {
		template = defaultTemplate
	}
	tagsCopy := make(map[string]string)
	for k, v := range tags {
		tagsCopy[k] = v
	}

	var out []string
	templateParts := strings.Split(template, ".")
	for _, templatePart := range templateParts {
		switch templatePart {
		case "measurement":
			out = append(out, measurement)
		case "tags":
			// we will replace this later
			out = append(out, "TAGS")
		case "field":
			// user of SerializeBucketName needs to replace this
			out = append(out, "FIELDNAME")
		default:
			// This is a tag being applied
			if tagvalue, ok := tagsCopy[templatePart]; ok {
				out = append(out, strings.ReplaceAll(tagvalue, ".", "_"))
				delete(tagsCopy, templatePart)
			}
		}
	}

	// insert remaining tags into output name
	for i, templatePart := range out {
		if templatePart == "TAGS" {
			out[i] = buildTags(tagsCopy)
			break
		}
	}

	if len(out) == 0 {
		return ""
	}

	if prefix == "" {
		return strings.Join(out, ".")
	}
	return prefix + "." + strings.Join(out, ".")
}

// InsertField takes the bucket string from SerializeBucketName and replaces the FIELDNAME portion.
// If fieldName == "value", it will simply delete the FIELDNAME portion.
func InsertField(bucket, fieldName string) string {
	// if the field name is "value", then dont use it
	if fieldName == "value" {
		return fieldDeleter.Replace(bucket)
	}
	return strings.Replace(bucket, "FIELDNAME", fieldName, 1)
}

func formatValue(value interface{}) string {
	switch v := value.(type) {
	case string:
		return ""
	case bool:
		if v {
			return "1"
		}
		return "0"
	case uint64:
		return strconv.FormatUint(v, 10)
	case int64:
		return strconv.FormatInt(v, 10)
	case float64:
		if math.IsNaN(v) {
			return ""
		}

		if math.IsInf(v, 0) {
			return ""
		}
		return strconv.FormatFloat(v, 'f', -1, 64)
	}

	return ""
}

func initTemplates(templates []string) ([]*template, string, error) {
	defaultTemplate := ""
	graphiteTemplates := make([]*template, 0, len(templates))
	for i, t := range templates {
		parts := strings.Fields(t)

		if len(parts) == 0 {
			return nil, "", fmt.Errorf("missing template at position: %d", i)
		}
		if len(parts) == 1 {
			if parts[0] == "" {
				return nil, "", fmt.Errorf("missing template at position: %d", i)
			}

			// Override default template
			defaultTemplate = t
			continue
		}

		if len(parts) > 2 {
			return nil, "", fmt.Errorf("invalid template format: %q", t)
		}

		tFilter, err := filter.Compile([]string{parts[0]})

		if err != nil {
			return nil, "", err
		}

		graphiteTemplates = append(graphiteTemplates, &template{
			filter: tFilter,
			value:  parts[1],
		})
	}

	return graphiteTemplates, defaultTemplate, nil
}

// serializeBucketNameWithTags will take the given measurement name and tags and
// produce a graphite bucket. It will use the Graphite11Serializer.
// http://graphite.readthedocs.io/en/latest/tags.html
func (s *Serializer) serializeBucketNameWithTags(measurement string, tags map[string]string, prefix, separator, field, tagSanitizeMode string) string {
	var out string
	var tagsCopy []string
	for k, v := range tags {
		if k == "name" {
			k = "_name"
		}
		if tagSanitizeMode == "compatible" {
			tagsCopy = append(tagsCopy, compatibleSanitize(k, v))
		} else {
			tagsCopy = append(tagsCopy, s.strictSanitize(k+"="+v))
		}
	}
	sort.Strings(tagsCopy)

	if prefix != "" {
		out = prefix + separator
	}

	out += measurement

	if field != "value" {
		out += separator + field
	}

	out = s.strictSanitize(out)

	if len(tagsCopy) > 0 {
		out += ";" + strings.Join(tagsCopy, ";")
	}

	return out
}

func buildTags(tags map[string]string) string {
	keys := make([]string, 0, len(tags))
	for k := range tags {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	var b strings.Builder
	for i, k := range keys {
		tagValue := strings.ReplaceAll(tags[k], ".", "_")
		if i > 0 {
			b.WriteByte('.')
		}
		b.WriteString(tagValue)
	}
	return b.String()
}

func (s *Serializer) strictSanitize(value string) string {
	// Apply special hyphenation rules to preserve backwards compatibility
	value = hyphenChars.Replace(value)
	// Apply rule to drop some chars to preserve backwards compatibility
	value = dropChars.Replace(value)
	// Replace any remaining illegal chars
	return s.strictAllowedChars.ReplaceAllLiteralString(value, "_")
}

func compatibleSanitize(name, value string) string {
	name = compatibleAllowedCharsName.ReplaceAllLiteralString(name, "_")
	value = compatibleAllowedCharsValue.ReplaceAllLiteralString(value, "_")
	value = compatibleLeadingTildeDrop.FindStringSubmatch(value)[1]
	return name + "=" + value
}

func init() {
	serializers.Add("graphite",
		func() telegraf.Serializer {
			return &Serializer{}
		},
	)
}
