//go:build linux && amd64

package intel_powerstat

import (
	"errors"
	"fmt"
	"math"
	"strconv"

	ptel "github.com/intel/powertelemetry"

	"github.com/influxdata/telegraf"
)

// cpuMetricType is an enum type to identify core metrics.
type cpuMetricType int

// cpuMetricType enum defines supported core metrics.
const (
	// metric relying on cpuFreq
	cpuFrequency cpuMetricType = iota

	// metric relying on msr
	cpuTemperature

	// metrics relying on msr with storage
	cpuC0StateResidency
	cpuC1StateResidency
	cpuC3StateResidency
	cpuC6StateResidency
	cpuC7StateResidency
	cpuBusyCycles // alias of cpuC0StateResidency
	cpuBusyFrequency

	// metrics relying on perf
	cpuC0SubstateC01Percent
	cpuC0SubstateC02Percent
	cpuC0SubstateC0WaitPercent
)

// Helper method to return a string representation of a core metric.
func (m cpuMetricType) String() string {
	switch m {
	case cpuFrequency:
		return "cpu_frequency"
	case cpuTemperature:
		return "cpu_temperature"
	case cpuBusyFrequency:
		return "cpu_busy_frequency"
	case cpuC0StateResidency:
		return "cpu_c0_state_residency"
	case cpuC1StateResidency:
		return "cpu_c1_state_residency"
	case cpuC3StateResidency:
		return "cpu_c3_state_residency"
	case cpuC6StateResidency:
		return "cpu_c6_state_residency"
	case cpuC7StateResidency:
		return "cpu_c7_state_residency"
	case cpuBusyCycles:
		return "cpu_busy_cycles"
	case cpuC0SubstateC01Percent:
		return "cpu_c0_substate_c01"
	case cpuC0SubstateC02Percent:
		return "cpu_c0_substate_c02"
	case cpuC0SubstateC0WaitPercent:
		return "cpu_c0_substate_c0_wait"
	}
	return ""
}

// UnmarshalText parses the cpu metric from the TOML config file
func (m *cpuMetricType) UnmarshalText(data []byte) (err error) {
	parsedMetric, err := cpuMetricTypeFromString(string(data))
	if err != nil {
		return err
	}
	*m = parsedMetric
	return nil
}

func cpuMetricTypeFromString(metric string) (cpuMetricType, error) {
	switch metric {
	case "cpu_frequency":
		return cpuFrequency, nil
	case "cpu_temperature":
		return cpuTemperature, nil
	case "cpu_busy_frequency":
		return cpuBusyFrequency, nil
	case "cpu_c0_state_residency":
		return cpuC0StateResidency, nil
	case "cpu_c1_state_residency":
		return cpuC1StateResidency, nil
	case "cpu_c3_state_residency":
		return cpuC3StateResidency, nil
	case "cpu_c6_state_residency":
		return cpuC6StateResidency, nil
	case "cpu_c7_state_residency":
		return cpuC7StateResidency, nil
	case "cpu_busy_cycles":
		return cpuBusyCycles, nil
	case "cpu_c0_substate_c01":
		return cpuC0SubstateC01Percent, nil
	case "cpu_c0_substate_c02":
		return cpuC0SubstateC02Percent, nil
	case "cpu_c0_substate_c0_wait":
		return cpuC0SubstateC0WaitPercent, nil
	}

	return -1, fmt.Errorf("invalid cpu metric specified: %q", metric)
}

// packageMetricType is an enum type to identify package metrics.
type packageMetricType int

// packageMetricType enum defines supported package metrics.
const (
	// metrics relying on rapl
	packageCurrentPowerConsumption packageMetricType = iota
	packageCurrentDramPowerConsumption
	packageThermalDesignPower

	// metrics relying on msr
	packageCPUBaseFrequency

	// hybrid metric relying on uncoreFreq as a primary mechanism and on msr as fallback mechanism.
	packageUncoreFrequency

	// metrics relying on msr
	packageTurboLimit
)

// Helper method to return a string representation of a package metric.
func (m packageMetricType) String() string {
	switch m {
	case packageCurrentPowerConsumption:
		return "current_power_consumption"
	case packageCurrentDramPowerConsumption:
		return "current_dram_power_consumption"
	case packageThermalDesignPower:
		return "thermal_design_power"
	case packageCPUBaseFrequency:
		return "cpu_base_frequency"
	case packageUncoreFrequency:
		return "uncore_frequency"
	case packageTurboLimit:
		return "max_turbo_frequency"
	}
	return ""
}

// UnmarshalText parses the package metric from the TOML config file
func (m *packageMetricType) UnmarshalText(data []byte) (err error) {
	parsedMetric, err := packageMetricTypeFromString(string(data))
	if err != nil {
		return err
	}
	*m = parsedMetric
	return nil
}

func packageMetricTypeFromString(metric string) (packageMetricType, error) {
	switch metric {
	case "current_power_consumption":
		return packageCurrentPowerConsumption, nil
	case "current_dram_power_consumption":
		return packageCurrentDramPowerConsumption, nil
	case "thermal_design_power":
		return packageThermalDesignPower, nil
	case "cpu_base_frequency":
		return packageCPUBaseFrequency, nil
	case "uncore_frequency":
		return packageUncoreFrequency, nil
	case "max_turbo_frequency":
		return packageTurboLimit, nil
	}

	return -1, fmt.Errorf("invalid package metric specified: %q", metric)
}

// numeric is a type constraint definition.
type numeric interface {
	float64 | uint64
}

// metricInfoProvider provides measurement name, fields, and tags needed by the accumulator to add a metric.
type metricInfoProvider interface {
	// measurement returns a string with the name of measurement.
	measurement() string

	// fields returns a map of string keys with metric name and metric values.
	fields() (map[string]interface{}, error)

	// tags returns a map of string key and string value to add additional metric-specific information.
	tags() map[string]string

	// name returns the name of a metric.
	name() string
}

// addMetric takes a metricInfoProvider interface and adds metric information to an accumulator.
func addMetric(acc telegraf.Accumulator, m metricInfoProvider, logOnceMap map[string]struct{}) {
	fields, err := m.fields()
	if err == nil {
		acc.AddGauge(
			m.measurement(),
			fields,
			m.tags(),
		)
		return
	}

	// Always add to the accumulator errors not related to module not initialized.
	var moduleErr *ptel.ModuleNotInitializedError
	if !errors.As(err, &moduleErr) {
		acc.AddError(err)
		return
	}

	// Add only once module not initialized error related to module and metric name.
	logErrorOnce(
		acc,
		logOnceMap,
		fmt.Sprintf("%s_%s", moduleErr.Name, m.name()),
		fmt.Errorf("failed to get %q: %w", m.name(), moduleErr),
	)
}

// metricCommon has metric information common to different types.
type metricCommon struct {
	metric interface{}
	units  string
}

func (m *metricCommon) name() string {
	switch m.metric.(type) {
	case cpuMetricType:
		return m.metric.(cpuMetricType).String()
	case packageMetricType:
		return m.metric.(packageMetricType).String()
	default:
		return ""
	}
}

func (m *metricCommon) measurement() string {
	switch m.metric.(type) {
	case cpuMetricType:
		return "powerstat_core"
	case packageMetricType:
		return "powerstat_package"
	default:
		return ""
	}
}

// cpuMetric is a generic type that has the information to identify a CPU-related metric,
// as well as function to retrieve its value at any time. Implements metricAdder interface.
type cpuMetric[T numeric] struct {
	metricCommon

	cpuID     int
	coreID    int
	packageID int
	fetchFn   func(cpuID int) (T, error)
}

func (m *cpuMetric[T]) fields() (map[string]interface{}, error) {
	val, err := m.fetchFn(m.cpuID)
	if err != nil {
		return nil, fmt.Errorf("failed to get %q for CPU ID %v: %w", m.metric, m.cpuID, err)
	}

	return map[string]interface{}{
		fmt.Sprintf("%s_%s", m.metric, m.units): round(val),
	}, nil
}

func (m *cpuMetric[T]) tags() map[string]string {
	return map[string]string{
		"core_id":    strconv.Itoa(m.coreID),
		"cpu_id":     strconv.Itoa(m.cpuID),
		"package_id": strconv.Itoa(m.packageID),
	}
}

// packageMetric is a generic type that has the information to identify a package-related metric,
// as well as the function to retrieve its value at any time. Implements metricAdder interface.
type packageMetric[T numeric] struct {
	metricCommon

	packageID int
	fetchFn   func(packageID int) (T, error)
}

func (m *packageMetric[T]) fields() (map[string]interface{}, error) {
	val, err := m.fetchFn(m.packageID)
	if err != nil {
		return nil, fmt.Errorf("failed to get %q for package ID %v: %w", m.metric, m.packageID, err)
	}

	return map[string]interface{}{
		fmt.Sprintf("%s_%s", m.metric, m.units): round(val),
	}, nil
}

func (m *packageMetric[T]) tags() map[string]string {
	return map[string]string{
		"package_id": strconv.Itoa(m.packageID),
	}
}

// round returns the result of rounding the argument, only if it's a 64 bit floating-point type.
func round[T numeric](val T) T {
	if v, ok := any(val).(float64); ok {
		val = T(math.Round(v*100) / 100)
	}
	return val
}
