//go:generate ../../../tools/readme_config_includer/generator
//go:build linux

package linux_cpu

import (
	_ "embed"
	"errors"
	"fmt"
	"io"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/internal/choice"
	"github.com/influxdata/telegraf/plugins/inputs"
)

//go:embed sample.conf
var sampleConfig string

const (
	defaultHostSys = "/sys"
	cpufreq        = "cpufreq"
	thermal        = "thermal"
)

type LinuxCPU struct {
	PathSysfs string          `toml:"host_sys"`
	Metrics   []string        `toml:"metrics"`
	Log       telegraf.Logger `toml:"-"`
	cpus      []cpu
}

type cpu struct {
	id    string
	path  string
	props map[string]string
}

type prop struct {
	name     string
	path     string
	optional bool
}

func (g *LinuxCPU) SampleConfig() string {
	return sampleConfig
}

func (g *LinuxCPU) Init() error {
	if g.PathSysfs == "" {
		g.PathSysfs = defaultHostSys
	}

	if len(g.Metrics) == 0 {
		// The user has not enabled any of the metrics
		return errors.New("no metrics selected")
	}

	cpus, err := g.discoverCpus()
	if err != nil {
		return err
	} else if len(cpus) == 0 {
		// Although the user has specified metrics to collect, `discoverCpus` failed to find the required metrics
		return errors.New("no CPUs detected to track")
	}
	g.cpus = cpus

	return nil
}

func (g *LinuxCPU) Gather(acc telegraf.Accumulator) error {
	for _, cpu := range g.cpus {
		fields := make(map[string]interface{})
		tags := map[string]string{"cpu": cpu.id}

		failed := false
		for name, propPath := range cpu.props {
			v, err := readUintFromFile(propPath)
			if err != nil {
				acc.AddError(err)
				failed = true
				break
			}

			fields[name] = v
		}

		if !failed {
			acc.AddFields("linux_cpu", fields, tags)
		}
	}

	return nil
}

func (g *LinuxCPU) discoverCpus() ([]cpu, error) {
	var cpus []cpu

	glob := path.Join(g.PathSysfs, "devices/system/cpu/cpu[0-9]*")
	cpuDirs, err := filepath.Glob(glob)
	if err != nil {
		return nil, err
	}

	if len(cpuDirs) == 0 {
		return nil, fmt.Errorf("no CPUs detected at: %s", glob)
	}

	for _, dir := range cpuDirs {
		_, cpuName := filepath.Split(dir)
		cpuNum := strings.TrimPrefix(cpuName, "cpu")

		cpu := cpu{
			id:    cpuNum,
			path:  dir,
			props: make(map[string]string),
		}

		var props []prop

		if choice.Contains(cpufreq, g.Metrics) {
			props = append(props,
				prop{name: "scaling_cur_freq", path: "cpufreq/scaling_cur_freq", optional: false},
				prop{name: "scaling_min_freq", path: "cpufreq/scaling_min_freq", optional: false},
				prop{name: "scaling_max_freq", path: "cpufreq/scaling_max_freq", optional: false},
				prop{name: "cpuinfo_cur_freq", path: "cpufreq/cpuinfo_cur_freq", optional: true},
				prop{name: "cpuinfo_min_freq", path: "cpufreq/cpuinfo_min_freq", optional: true},
				prop{name: "cpuinfo_max_freq", path: "cpufreq/cpuinfo_max_freq", optional: true},
			)
		}

		if choice.Contains(thermal, g.Metrics) {
			props = append(
				props,
				prop{name: "throttle_count", path: "thermal_throttle/core_throttle_count", optional: false},
				prop{name: "throttle_max_time", path: "thermal_throttle/core_throttle_max_time_ms", optional: false},
				prop{name: "throttle_total_time", path: "thermal_throttle/core_throttle_total_time_ms", optional: false},
			)
		}

		var failed = false
		for _, prop := range props {
			propPath := filepath.Join(dir, prop.path)
			err := validatePath(propPath)
			if err != nil {
				if prop.optional {
					continue
				}

				g.Log.Warnf("Failed to load property %s: %v", propPath, err)
				failed = true
				break
			}

			cpu.props[prop.name] = propPath
		}

		if len(cpu.props) == 0 {
			g.Log.Warnf("No properties enabled/loaded for CPU %s", cpuNum)
			failed = true
		}

		if !failed {
			cpus = append(cpus, cpu)
		}
	}
	return cpus, nil
}

func validatePath(propPath string) error {
	f, err := os.Open(propPath)
	if os.IsNotExist(err) {
		return fmt.Errorf("file with CPU property does not exist: %q", propPath)
	}
	if err != nil {
		return fmt.Errorf("cannot get system information for CPU property %q: %w", propPath, err)
	}

	_ = f.Close() // File is not written to, closing should be safe
	return nil
}

func readUintFromFile(propPath string) (uint64, error) {
	f, err := os.Open(propPath)
	if err != nil {
		return 0, err
	}
	defer f.Close()

	buffer := make([]byte, 22)

	n, err := f.Read(buffer)
	if err != nil && !errors.Is(err, io.EOF) {
		return 0, fmt.Errorf("error on reading file: %w", err)
	} else if n == 0 {
		return 0, errors.New("error on reading file: file is empty")
	}

	return strconv.ParseUint(string(buffer[:n-1]), 10, 64)
}

func init() {
	inputs.Add("linux_cpu", func() telegraf.Input {
		return &LinuxCPU{
			Metrics: []string{"cpufreq"},
		}
	})
}
