//go:generate ../../../tools/readme_config_includer/generator
package scale

import (
	_ "embed"
	"errors"
	"fmt"
	"strings"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/filter"
	"github.com/influxdata/telegraf/internal"
	"github.com/influxdata/telegraf/plugins/processors"
)

//go:embed sample.conf
var sampleConfig string

type Scale struct {
	Scalings []scaling       `toml:"scaling"`
	Log      telegraf.Logger `toml:"-"`
}

type scaling struct {
	InMin  *float64 `toml:"input_minimum"`
	InMax  *float64 `toml:"input_maximum"`
	OutMin *float64 `toml:"output_minimum"`
	OutMax *float64 `toml:"output_maximum"`
	Factor *float64 `toml:"factor"`
	Offset *float64 `toml:"offset"`
	Fields []string `toml:"fields"`

	fieldFilter filter.Filter
	scale       float64
	shiftIn     float64
	shiftOut    float64
}

func (*Scale) SampleConfig() string {
	return sampleConfig
}

func (s *Scale) Init() error {
	if s.Scalings == nil {
		return errors.New("no valid scaling defined")
	}

	allFields := make(map[string]bool)
	for i := range s.Scalings {
		for _, field := range s.Scalings[i].Fields {
			// only generate a warning for the first duplicate field filter
			if warn, ok := allFields[field]; ok && warn {
				s.Log.Warnf("Filter field %q used twice in scalings", field)
				allFields[field] = false
			} else {
				allFields[field] = true
			}
		}

		if err := s.Scalings[i].init(); err != nil {
			return fmt.Errorf("scaling %d: %w", i+1, err)
		}
	}
	return nil
}

func (s *Scale) Apply(in ...telegraf.Metric) []telegraf.Metric {
	for _, metric := range in {
		s.scaleValues(metric)
	}
	return in
}

// handle the scaling process
func (s *Scale) scaleValues(metric telegraf.Metric) {
	fields := metric.FieldList()

	for _, scaling := range s.Scalings {
		for _, field := range fields {
			if !scaling.fieldFilter.Match(field.Key) {
				continue
			}

			v, err := internal.ToFloat64(field.Value)
			if err != nil {
				s.Log.Errorf("Error converting %q to float: %v", field.Key, err)
				continue
			}

			// scale the field values using the defined scaler
			field.Value = scaling.process(v)
		}
	}
}

func (s *scaling) init() error {
	s.scale, s.shiftOut, s.shiftIn = float64(1.0), float64(0.0), float64(0.0)
	allMinMaxSet := s.OutMax != nil && s.OutMin != nil && s.InMax != nil && s.InMin != nil
	anyMinMaxSet := s.OutMax != nil || s.OutMin != nil || s.InMax != nil || s.InMin != nil
	factorSet := s.Factor != nil || s.Offset != nil
	if anyMinMaxSet && factorSet {
		return fmt.Errorf("cannot use factor/offset and minimum/maximum at the same time for fields %s",
			strings.Join(s.Fields, ","))
	} else if anyMinMaxSet && !allMinMaxSet {
		return fmt.Errorf("all minimum and maximum values need to be set for fields %s", strings.Join(s.Fields, ","))
	} else if !anyMinMaxSet && !factorSet {
		return fmt.Errorf("no scaling defined for fields %s", strings.Join(s.Fields, ","))
	} else if allMinMaxSet {
		if *s.InMax == *s.InMin {
			return fmt.Errorf("input minimum and maximum are equal for fields %s", strings.Join(s.Fields, ","))
		}

		if *s.OutMax == *s.OutMin {
			return fmt.Errorf("output minimum and maximum are equal for fields %s", strings.Join(s.Fields, ","))
		}

		s.scale = (*s.OutMax - *s.OutMin) / (*s.InMax - *s.InMin)
		s.shiftOut = *s.OutMin
		s.shiftIn = *s.InMin
	} else {
		if s.Factor != nil {
			s.scale = *s.Factor
		}
		if s.Offset != nil {
			s.shiftOut = *s.Offset
		}
	}

	scalingFilter, err := filter.Compile(s.Fields)
	if err != nil {
		return fmt.Errorf("could not compile fields filter: %w", err)
	}
	s.fieldFilter = scalingFilter

	return nil
}

// scale a float according to the input and output range
func (s *scaling) process(value float64) float64 {
	return s.scale*(value-s.shiftIn) + s.shiftOut
}

func init() {
	processors.Add("scale", func() telegraf.Processor {
		return &Scale{}
	})
}
