package dropwizard

import (
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/tidwall/gjson"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/internal/templating"
	"github.com/influxdata/telegraf/metric"
	"github.com/influxdata/telegraf/plugins/parsers"
	"github.com/influxdata/telegraf/plugins/parsers/influx"
)

// Parser parses json inputs containing dropwizard metrics,
// either top-level or embedded inside a json field.
// This parser is using gjson for retrieving paths within the json file.
type Parser struct {
	MetricRegistryPath string            `toml:"dropwizard_metric_registry_path"`
	TimePath           string            `toml:"dropwizard_time_path"`
	TimeFormat         string            `toml:"dropwizard_time_format"`
	TagsPath           string            `toml:"dropwizard_tags_path"`
	TagPathsMap        map[string]string `toml:"dropwizard_tag_paths_map"`
	Separator          string            `toml:"separator"`
	Templates          []string          `toml:"templates"`
	DefaultTags        map[string]string `toml:"-"`
	Log                telegraf.Logger   `toml:"-"`

	templateEngine *templating.Engine
	seriesParser   *influx.Parser
	timeFunc       func() time.Time
}

func (p *Parser) Init() error {
	parser := &influx.Parser{
		Type: "series",
	}
	err := parser.Init()
	if err != nil {
		return err
	}
	p.seriesParser = parser

	if len(p.Templates) != 0 {
		defaultTemplate, err := templating.NewDefaultTemplateWithPattern("measurement*")
		if err != nil {
			return err
		}

		templateEngine, err := templating.NewEngine(p.Separator, defaultTemplate, p.Templates)
		if err != nil {
			return err
		}
		p.templateEngine = templateEngine
	}

	if p.timeFunc == nil {
		p.timeFunc = time.Now
	}

	return nil
}

func (p *Parser) SetTimeFunc(fn func() time.Time) {
	p.timeFunc = fn
}

// Parse parses the input bytes to an array of metrics
func (p *Parser) Parse(buf []byte) ([]telegraf.Metric, error) {
	metrics := make([]telegraf.Metric, 0)

	metricTime, err := p.parseTime(buf)
	if err != nil {
		return nil, err
	}
	dwr, err := p.unmarshalMetrics(buf)
	if err != nil {
		return nil, err
	}

	metrics, err = p.readDWMetrics("counter", dwr["counters"], metrics, metricTime)
	if err != nil {
		return nil, err
	}
	metrics, err = p.readDWMetrics("meter", dwr["meters"], metrics, metricTime)
	if err != nil {
		return nil, err
	}
	metrics, err = p.readDWMetrics("gauge", dwr["gauges"], metrics, metricTime)
	if err != nil {
		return nil, err
	}
	metrics, err = p.readDWMetrics("histogram", dwr["histograms"], metrics, metricTime)
	if err != nil {
		return nil, err
	}
	metrics, err = p.readDWMetrics("timer", dwr["timers"], metrics, metricTime)
	if err != nil {
		return nil, err
	}

	jsonTags := p.readTags(buf)

	// fill json tags first
	for k, v := range jsonTags {
		for _, m := range metrics {
			// only set the tag if it doesn't already exist:
			if !m.HasTag(k) {
				m.AddTag(k, v)
			}
		}
	}
	// fill default tags last
	for k, v := range p.DefaultTags {
		for _, m := range metrics {
			// only set the default tag if it doesn't already exist:
			if !m.HasTag(k) {
				m.AddTag(k, v)
			}
		}
	}

	return metrics, nil
}

// ParseLine is not supported by the dropwizard format
func (*Parser) ParseLine(string) (telegraf.Metric, error) {
	return nil, errors.New("parsing line is not supported by the dropwizard format")
}

// SetDefaultTags sets the default tags
func (p *Parser) SetDefaultTags(tags map[string]string) {
	p.DefaultTags = tags
}

func (p *Parser) readTags(buf []byte) map[string]string {
	if p.TagsPath != "" {
		var tagsBytes []byte
		tagsResult := gjson.GetBytes(buf, p.TagsPath)
		if tagsResult.Index > 0 {
			tagsBytes = buf[tagsResult.Index : tagsResult.Index+len(tagsResult.Raw)]
		} else {
			tagsBytes = []byte(tagsResult.Raw)
		}
		var tags map[string]string
		err := json.Unmarshal(tagsBytes, &tags)
		if err != nil {
			p.Log.Warnf("Failed to parse tags from JSON path %q: %s", p.TagsPath, err.Error())
		} else if len(tags) > 0 {
			return tags
		}
	}

	tags := make(map[string]string)
	for tagKey, jsonPath := range p.TagPathsMap {
		tags[tagKey] = gjson.GetBytes(buf, jsonPath).String()
	}
	return tags
}

func (p *Parser) parseTime(buf []byte) (time.Time, error) {
	if p.TimePath != "" {
		timeFormat := p.TimeFormat
		if timeFormat == "" {
			timeFormat = time.RFC3339
		}
		timeString := gjson.GetBytes(buf, p.TimePath).String()
		if timeString == "" {
			return time.Time{}, fmt.Errorf("time not found in JSON path %s", p.TimePath)
		}
		t, err := time.Parse(timeFormat, timeString)
		if err != nil {
			return time.Time{}, fmt.Errorf("time %s cannot be parsed with format %s, %w", timeString, timeFormat, err)
		}
		return t.UTC(), nil
	}
	return p.timeFunc(), nil
}

func (p *Parser) unmarshalMetrics(buf []byte) (map[string]interface{}, error) {
	var registryBytes []byte
	if p.MetricRegistryPath != "" {
		regResult := gjson.GetBytes(buf, p.MetricRegistryPath)
		if regResult.Index > 0 {
			registryBytes = buf[regResult.Index : regResult.Index+len(regResult.Raw)]
		} else {
			registryBytes = []byte(regResult.Raw)
		}
		if len(registryBytes) == 0 {
			err := fmt.Errorf("metric registry not found in JSON path %s", p.MetricRegistryPath)
			return nil, err
		}
	} else {
		registryBytes = buf
	}
	var jsonOut map[string]interface{}
	err := json.Unmarshal(registryBytes, &jsonOut)
	if err != nil {
		err = fmt.Errorf("unable to parse dropwizard metric registry from JSON document, %w", err)
		return nil, err
	}
	return jsonOut, nil
}

func (p *Parser) readDWMetrics(metricType string, dwms interface{}, metrics []telegraf.Metric, tm time.Time) ([]telegraf.Metric, error) {
	if dwmsTyped, ok := dwms.(map[string]interface{}); ok {
		for dwmName, dwmFields := range dwmsTyped {
			measurementName := dwmName
			tags := make(map[string]string)
			fieldPrefix := ""
			if p.templateEngine != nil {
				var err error
				measurementName, tags, fieldPrefix, err = p.templateEngine.Apply(dwmName)
				if err != nil {
					return nil, fmt.Errorf("failed to apply template for type %s: %w", metricType, err)
				}
				if len(fieldPrefix) > 0 {
					fieldPrefix = fmt.Sprintf("%s%s", fieldPrefix, p.Separator)
				}
			}

			parsed, err := p.seriesParser.Parse([]byte(measurementName))
			var m telegraf.Metric
			if err != nil || len(parsed) != 1 {
				m = metric.New(measurementName, make(map[string]string), make(map[string]interface{}), tm)
			} else {
				m = parsed[0]
				m.SetTime(tm)
			}

			m.AddTag("metric_type", metricType)
			for k, v := range tags {
				m.AddTag(k, v)
			}

			if fields, ok := dwmFields.(map[string]interface{}); ok {
				for k, v := range fields {
					switch v := v.(type) {
					case float64, string, bool:
						m.AddField(fieldPrefix+k, v)
					default:
						// ignore
					}
				}
			}

			metrics = append(metrics, m)
		}
	}

	return metrics, nil
}

func init() {
	parsers.Add("dropwizard",
		func(string) telegraf.Parser {
			return &Parser{}
		})
}
