package regex

import (
	"errors"
	"fmt"
	"regexp"

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

type converterType int

const (
	convertTags = iota
	convertFields
	convertTagRename
	convertFieldRename
	convertMetricRename
)

func (ct converterType) String() string {
	switch ct {
	case convertTags:
		return "tags"
	case convertFields:
		return "fields"
	case convertTagRename:
		return "tag_rename"
	case convertFieldRename:
		return "field_rename"
	case convertMetricRename:
		return "metric_rename"
	}
	return fmt.Sprintf("unknown %d", int(ct))
}

type converter struct {
	Key         string `toml:"key"`
	Pattern     string `toml:"pattern"`
	Replacement string `toml:"replacement"`
	ResultKey   string `toml:"result_key"`
	Append      bool   `toml:"append"`

	filter filter.Filter
	re     *regexp.Regexp
	groups []string
	apply  func(m telegraf.Metric)
}

func (c *converter) setup(ct converterType, log telegraf.Logger) error {
	// Compile the pattern
	re, err := regexp.Compile(c.Pattern)
	if err != nil {
		return err
	}
	c.re = re

	switch ct {
	case convertTags, convertFields:
		if c.Key == "" {
			return errors.New("key required")
		}
		f, err := filter.Compile([]string{c.Key})
		if err != nil {
			return err
		}
		c.filter = f

		// Check for named groups
		if c.ResultKey == "" && c.Replacement == "" {
			groups := c.re.SubexpNames()
			allNamed := len(groups) > 1
			for _, g := range groups[1:] {
				if g == "" {
					allNamed = false
					break
				}
			}
			if allNamed {
				log.Infof("%s: Using named-group mode...", ct)
				c.groups = groups[1:]
			} else {
				msg := "Neither 'result_key' nor 'replacement' given with unnamed or mixed groups;"
				msg += " using explicit, empty replacement!"
				log.Warnf("%s: %s", ct, msg)
			}
		} else {
			log.Infof("%s: Using explicit mode...", ct)
		}
	case convertTagRename, convertFieldRename:
		switch c.ResultKey {
		case "":
			c.ResultKey = "keep"
		case "overwrite", "keep":
			// Do nothing as those are valid choices
		default:
			return fmt.Errorf("invalid metrics result_key %q", c.ResultKey)
		}
	}

	// Select the application function
	switch ct {
	case convertTags:
		c.apply = c.applyTags
	case convertFields:
		c.apply = c.applyFields
	case convertTagRename:
		c.apply = c.applyTagRename
	case convertFieldRename:
		c.apply = c.applyFieldRename
	case convertMetricRename:
		c.apply = c.applyMetricRename
	}

	return nil
}

func (c *converter) applyTags(m telegraf.Metric) {
	for _, tag := range m.TagList() {
		if !c.filter.Match(tag.Key) || !c.re.MatchString(tag.Value) {
			continue
		}

		// Handle named groups
		if len(c.groups) > 0 {
			matches := c.re.FindStringSubmatch(tag.Value)
			for i, match := range matches[1:] {
				if match == "" {
					continue
				}
				name := c.groups[i]
				if c.Append {
					if v, ok := m.GetTag(name); ok {
						match = v + match
					}
				}
				m.AddTag(name, match)
			}
			continue
		}

		// Handle explicit replacements
		newKey := tag.Key
		if c.ResultKey != "" {
			newKey = c.ResultKey
		}

		newValue := c.re.ReplaceAllString(tag.Value, c.Replacement)
		if c.Append {
			if v, ok := m.GetTag(newKey); ok {
				newValue = v + newValue
			}
		}
		m.AddTag(newKey, newValue)
	}
}

func (c *converter) applyFields(m telegraf.Metric) {
	for _, field := range m.FieldList() {
		if !c.filter.Match(field.Key) {
			continue
		}

		value, ok := field.Value.(string)
		if !ok || !c.re.MatchString(value) {
			continue
		}

		// Handle named groups
		if len(c.groups) > 0 {
			matches := c.re.FindStringSubmatch(value)
			for i, match := range matches[1:] {
				if match == "" {
					continue
				}
				name := c.groups[i]
				if c.Append {
					if v, ok := m.GetTag(name); ok {
						match = v + match
					}
				}
				m.AddField(name, match)
			}
			continue
		}

		// Handle explicit replacements
		newKey := field.Key
		if c.ResultKey != "" {
			newKey = c.ResultKey
		}

		newValue := c.re.ReplaceAllString(value, c.Replacement)
		m.AddField(newKey, newValue)
	}
}

func (c *converter) applyTagRename(m telegraf.Metric) {
	replacements := make(map[string]string)
	for _, tag := range m.TagList() {
		name := tag.Key
		if c.re.MatchString(name) {
			newName := c.re.ReplaceAllString(name, c.Replacement)

			if !m.HasTag(newName) {
				// There is no colliding tag, we can just change the name.
				tag.Key = newName
				continue
			}

			if c.ResultKey == "overwrite" {
				// We got a colliding tag, remember the replacement and do it later
				replacements[name] = newName
			}
		}
	}
	// We needed to postpone the replacement as we cannot modify the tag-list
	// while iterating it as this will result in invalid memory dereference panic.
	for oldName, newName := range replacements {
		value, ok := m.GetTag(oldName)
		if !ok {
			// Just in case the tag got removed in the meantime
			continue
		}
		m.AddTag(newName, value)
		m.RemoveTag(oldName)
	}
}

func (c *converter) applyFieldRename(m telegraf.Metric) {
	replacements := make(map[string]string)
	for _, field := range m.FieldList() {
		name := field.Key
		if c.re.MatchString(name) {
			newName := c.re.ReplaceAllString(name, c.Replacement)

			if !m.HasField(newName) {
				// There is no colliding field, we can just change the name.
				field.Key = newName
				continue
			}

			if c.ResultKey == "overwrite" {
				// We got a colliding field, remember the replacement and do it later
				replacements[name] = newName
			}
		}
	}
	// We needed to postpone the replacement as we cannot modify the field-list
	// while iterating it as this will result in invalid memory dereference panic.
	for oldName, newName := range replacements {
		value, ok := m.GetField(oldName)
		if !ok {
			// Just in case the field got removed in the meantime
			continue
		}
		m.AddField(newName, value)
		m.RemoveField(oldName)
	}
}

func (c *converter) applyMetricRename(m telegraf.Metric) {
	value := m.Name()
	if c.re.MatchString(value) {
		newValue := c.re.ReplaceAllString(value, c.Replacement)
		m.SetName(newValue)
	}
}
