package json

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

	"github.com/blues/jsonata-go"

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

type Serializer struct {
	TimestampUnits      config.Duration `toml:"json_timestamp_units"`
	TimestampFormat     string          `toml:"json_timestamp_format"`
	Transformation      string          `toml:"json_transformation"`
	NestedFieldsInclude []string        `toml:"json_nested_fields_include"`
	NestedFieldsExclude []string        `toml:"json_nested_fields_exclude"`

	nestedfields filter.Filter
}

func (s *Serializer) Init() error {
	// Default precision is 1s
	if s.TimestampUnits <= 0 {
		s.TimestampUnits = config.Duration(time.Second)
	}

	// Search for the power of ten less than the duration
	d := time.Nanosecond
	t := time.Duration(s.TimestampUnits)
	for {
		if d*10 > t {
			t = d
			break
		}
		d = d * 10
	}
	s.TimestampUnits = config.Duration(t)

	if len(s.NestedFieldsInclude) > 0 || len(s.NestedFieldsExclude) > 0 {
		f, err := filter.NewIncludeExcludeFilter(s.NestedFieldsInclude, s.NestedFieldsExclude)
		if err != nil {
			return err
		}
		s.nestedfields = f
	}

	return nil
}

func (s *Serializer) Serialize(metric telegraf.Metric) ([]byte, error) {
	var obj interface{}
	obj = s.createObject(metric)

	if s.Transformation != "" {
		var err error
		if obj, err = s.transform(obj); err != nil {
			if errors.Is(err, jsonata.ErrUndefined) {
				return nil, fmt.Errorf("%w (maybe configured for batch mode?)", err)
			}
			return nil, err
		}
	}

	serialized, err := json.Marshal(obj)
	if err != nil {
		return []byte{}, err
	}
	serialized = append(serialized, '\n')

	return serialized, nil
}

func (s *Serializer) SerializeBatch(metrics []telegraf.Metric) ([]byte, error) {
	objects := make([]interface{}, 0, len(metrics))
	for _, metric := range metrics {
		m := s.createObject(metric)
		objects = append(objects, m)
	}

	var obj interface{}
	obj = map[string]interface{}{
		"metrics": objects,
	}

	if s.Transformation != "" {
		var err error
		if obj, err = s.transform(obj); err != nil {
			if errors.Is(err, jsonata.ErrUndefined) {
				return nil, fmt.Errorf("%w (maybe configured for non-batch mode?)", err)
			}
			return nil, err
		}
	}

	serialized, err := json.Marshal(obj)
	if err != nil {
		return []byte{}, err
	}
	serialized = append(serialized, '\n')

	return serialized, nil
}

func (s *Serializer) createObject(metric telegraf.Metric) map[string]interface{} {
	m := make(map[string]interface{}, 4)

	tags := make(map[string]string, len(metric.TagList()))
	for _, tag := range metric.TagList() {
		tags[tag.Key] = tag.Value
	}
	m["tags"] = tags

	fields := make(map[string]interface{}, len(metric.FieldList()))
	for _, field := range metric.FieldList() {
		val := field.Value
		switch fv := field.Value.(type) {
		case float64:
			// JSON does not support these special values
			if math.IsNaN(fv) || math.IsInf(fv, 0) {
				continue
			}
		case string:
			// Check for nested fields if any
			if s.nestedfields != nil && s.nestedfields.Match(field.Key) {
				bv := []byte(fv)
				if json.Valid(bv) {
					var nested interface{}
					if err := json.Unmarshal(bv, &nested); err == nil {
						val = nested
					}
				}
			}
		}
		fields[field.Key] = val
	}
	m["fields"] = fields

	m["name"] = metric.Name()
	if s.TimestampFormat == "" {
		m["timestamp"] = metric.Time().UnixNano() / int64(s.TimestampUnits)
	} else {
		m["timestamp"] = metric.Time().UTC().Format(s.TimestampFormat)
	}
	return m
}

func (s *Serializer) transform(obj interface{}) (interface{}, error) {
	transformation, err := jsonata.Compile(s.Transformation)
	if err != nil {
		return nil, err
	}

	return transformation.Eval(obj)
}

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

// InitFromConfig is a compatibility function to construct the parser the old way
func (s *Serializer) InitFromConfig(cfg *serializers.Config) error {
	s.TimestampUnits = config.Duration(cfg.TimestampUnits)
	s.TimestampFormat = cfg.TimestampFormat
	s.Transformation = cfg.Transformation
	s.NestedFieldsInclude = cfg.JSONNestedFieldInclude
	s.NestedFieldsExclude = cfg.JSONNestedFieldExclude

	return nil
}
