//go:build linux

package intel_powerstat

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"

	"github.com/influxdata/telegraf"
)

const (
	intelRaplPath                = "/sys/devices/virtual/powercap/intel-rapl"
	intelRaplSocketPartialPath   = "%s/intel-rapl:%s"
	energyUjPartialPath          = "%s/energy_uj"
	maxEnergyRangeUjPartialPath  = "%s/max_energy_range_uj"
	maxPowerUwPartialPath        = "%s/constraint_0_max_power_uw"
	intelRaplDramPartialPath     = "%s/intel-rapl:%s/%s"
	intelRaplDramNamePartialPath = "%s/name"
)

// raplService is responsible for interactions with RAPL.
type raplService interface {
	initializeRaplData()
	getRaplData() map[string]*raplData
	retrieveAndCalculateData(socketID string) error
	getConstraintMaxPowerWatts(socketID string) (float64, error)
}

type raplServiceImpl struct {
	log         telegraf.Logger
	data        map[string]*raplData
	dramFolders map[string]string
	fs          fileService
	logOnce     map[string]error
}

// initializeRaplData looks for RAPL folders and initializes data map with fetched information.
func (r *raplServiceImpl) initializeRaplData() {
	r.prepareData()
	r.findDramFolders()
}

func (r *raplServiceImpl) getRaplData() map[string]*raplData {
	return r.data
}

func (r *raplServiceImpl) retrieveAndCalculateData(socketID string) error {
	socketRaplPath := fmt.Sprintf(intelRaplSocketPartialPath, intelRaplPath, socketID)
	socketEnergyUjPath := fmt.Sprintf(energyUjPartialPath, socketRaplPath)
	err := checkFile(socketEnergyUjPath)
	if err != nil {
		return err
	}
	socketEnergyUjFile, err := os.Open(socketEnergyUjPath)
	if err != nil {
		return fmt.Errorf("error opening socket energy_uj file on path %q: %w", socketEnergyUjPath, err)
	}
	defer socketEnergyUjFile.Close()

	dramRaplPath := fmt.Sprintf(intelRaplDramPartialPath, intelRaplPath, socketID, r.dramFolders[socketID])
	dramEnergyUjPath := fmt.Sprintf(energyUjPartialPath, dramRaplPath)
	err = checkFile(dramEnergyUjPath)
	if err != nil {
		return err
	}
	dramEnergyUjFile, err := os.Open(dramEnergyUjPath)
	if err != nil {
		return fmt.Errorf("error opening dram energy_uj file on path %q: %w", dramEnergyUjPath, err)
	}
	defer dramEnergyUjFile.Close()

	socketMaxEnergyUjPath := fmt.Sprintf(maxEnergyRangeUjPartialPath, socketRaplPath)
	err = checkFile(socketMaxEnergyUjPath)
	if err != nil {
		return err
	}
	socketMaxEnergyUjFile, err := os.Open(socketMaxEnergyUjPath)
	if err != nil {
		return fmt.Errorf("error opening socket max_energy_range_uj file on path %q: %w", socketMaxEnergyUjPath, err)
	}
	defer socketMaxEnergyUjFile.Close()

	dramMaxEnergyUjPath := fmt.Sprintf(maxEnergyRangeUjPartialPath, dramRaplPath)
	err = checkFile(dramMaxEnergyUjPath)
	if err != nil {
		return err
	}
	dramMaxEnergyUjFile, err := os.Open(dramMaxEnergyUjPath)
	if err != nil {
		return fmt.Errorf("error opening dram max_energy_range_uj file on path %q: %w", dramMaxEnergyUjPath, err)
	}
	defer dramMaxEnergyUjFile.Close()

	return r.calculateData(socketID, socketEnergyUjFile, dramEnergyUjFile, socketMaxEnergyUjFile, dramMaxEnergyUjFile)
}

func (r *raplServiceImpl) getConstraintMaxPowerWatts(socketID string) (float64, error) {
	socketRaplPath := fmt.Sprintf(intelRaplSocketPartialPath, intelRaplPath, socketID)
	socketMaxPowerPath := fmt.Sprintf(maxPowerUwPartialPath, socketRaplPath)
	err := checkFile(socketMaxPowerPath)
	if err != nil {
		return 0, err
	}
	socketMaxPowerFile, err := os.Open(socketMaxPowerPath)
	if err != nil {
		return 0, fmt.Errorf("error opening constraint_0_max_power_uw file on path %q: %w", socketMaxPowerPath, err)
	}
	defer socketMaxPowerFile.Close()

	socketMaxPower, _, err := r.fs.readFileToFloat64(socketMaxPowerFile)
	return convertMicroWattToWatt(socketMaxPower), err
}

func (r *raplServiceImpl) prepareData() {
	intelRaplPrefix := "intel-rapl:"
	intelRapl := fmt.Sprintf("%s%s", intelRaplPrefix, "[0-9]*")
	raplPattern := fmt.Sprintf("%s/%s", intelRaplPath, intelRapl)

	raplPaths, err := r.fs.getStringsMatchingPatternOnPath(raplPattern)
	if err != nil {
		r.log.Errorf("error while preparing RAPL data: %v", err)
		r.data = make(map[string]*raplData)
		return
	}
	if len(raplPaths) == 0 {
		r.log.Debugf("RAPL data wasn't found using pattern: %s", raplPattern)
		r.data = make(map[string]*raplData)
		return
	}

	// If RAPL exists initialize data map (if it wasn't initialized before).
	if len(r.data) == 0 {
		for _, raplPath := range raplPaths {
			socketID := strings.TrimPrefix(filepath.Base(raplPath), intelRaplPrefix)
			r.data[socketID] = &raplData{
				socketCurrentEnergy: 0,
				dramCurrentEnergy:   0,
				socketEnergy:        0,
				dramEnergy:          0,
				readDate:            0,
			}
		}
	}
}

func (r *raplServiceImpl) findDramFolders() {
	intelRaplPrefix := "intel-rapl:"
	intelRaplDram := fmt.Sprintf("%s%s", intelRaplPrefix, "[0-9]*[0-9]*")
	// Clean existing map
	r.dramFolders = make(map[string]string)

	for socketID := range r.data {
		path := fmt.Sprintf(intelRaplSocketPartialPath, intelRaplPath, socketID)
		raplFoldersPattern := fmt.Sprintf("%s/%s", path, intelRaplDram)
		pathsToRaplFolders, err := r.fs.getStringsMatchingPatternOnPath(raplFoldersPattern)
		if err != nil {
			r.log.Errorf("error during lookup for rapl dram: %v", err)
			continue
		}
		if len(pathsToRaplFolders) == 0 {
			r.log.Debugf("RAPL folders weren't found using pattern: %s", raplFoldersPattern)
			continue
		}

		raplFolders := make([]string, 0)
		for _, folderPath := range pathsToRaplFolders {
			raplFolders = append(raplFolders, filepath.Base(folderPath))
		}

		r.findDramFolder(raplFolders, socketID)
	}
}

func (r *raplServiceImpl) findDramFolder(raplFolders []string, socketID string) {
	if r.logOnce == nil {
		r.logOnce = make(map[string]error)
	}

	for _, raplFolder := range raplFolders {
		potentialDramPath := fmt.Sprintf(intelRaplDramPartialPath, intelRaplPath, socketID, raplFolder)
		nameFilePath := fmt.Sprintf(intelRaplDramNamePartialPath, potentialDramPath)
		read, err := r.fs.readFile(nameFilePath)
		if err != nil {
			if val := r.logOnce[nameFilePath]; val == nil || val.Error() != err.Error() {
				r.log.Errorf("error reading file on path %q: %v", nameFilePath, err)
				r.logOnce[nameFilePath] = err
			}
			continue
		}
		r.logOnce[nameFilePath] = nil
		// Remove new line character
		trimmedString := strings.TrimRight(string(read), "\n")
		if trimmedString == "dram" {
			// There should be only one DRAM folder per socket
			r.dramFolders[socketID] = raplFolder
			return
		}
	}
}

func (r *raplServiceImpl) calculateData(socketID string, socketEnergyUjFile io.Reader, dramEnergyUjFile io.Reader,
	socketMaxEnergyUjFile io.Reader, dramMaxEnergyUjFile io.Reader,
) error {
	newSocketEnergy, _, err := r.readEnergyInJoules(socketEnergyUjFile)
	if err != nil {
		return err
	}

	newDramEnergy, readDate, err := r.readEnergyInJoules(dramEnergyUjFile)
	if err != nil {
		return err
	}

	interval := convertNanoSecondsToSeconds(readDate - r.data[socketID].readDate)
	r.data[socketID].readDate = readDate
	if interval == 0 {
		return fmt.Errorf("interval between last two Telegraf cycles is 0")
	}

	if newSocketEnergy >= r.data[socketID].socketEnergy {
		r.data[socketID].socketCurrentEnergy = (newSocketEnergy - r.data[socketID].socketEnergy) / interval
	} else {
		socketMaxEnergy, _, err := r.readEnergyInJoules(socketMaxEnergyUjFile)
		if err != nil {
			return err
		}
		// When socket energy_uj counter reaches maximum value defined in max_energy_range_uj file it
		// starts counting from 0.
		r.data[socketID].socketCurrentEnergy = (socketMaxEnergy - r.data[socketID].socketEnergy + newSocketEnergy) / interval
	}

	if newDramEnergy >= r.data[socketID].dramEnergy {
		r.data[socketID].dramCurrentEnergy = (newDramEnergy - r.data[socketID].dramEnergy) / interval
	} else {
		dramMaxEnergy, _, err := r.readEnergyInJoules(dramMaxEnergyUjFile)
		if err != nil {
			return err
		}
		// When dram energy_uj counter reaches maximum value defined in max_energy_range_uj file it
		// starts counting from 0.
		r.data[socketID].dramCurrentEnergy = (dramMaxEnergy - r.data[socketID].dramEnergy + newDramEnergy) / interval
	}
	r.data[socketID].socketEnergy = newSocketEnergy
	r.data[socketID].dramEnergy = newDramEnergy

	return nil
}

func (r *raplServiceImpl) readEnergyInJoules(reader io.Reader) (float64, int64, error) {
	currentEnergy, readDate, err := r.fs.readFileToFloat64(reader)
	return convertMicroJoulesToJoules(currentEnergy), readDate, err
}

func newRaplServiceWithFs(logger telegraf.Logger, fs fileService) *raplServiceImpl {
	return &raplServiceImpl{
		log:         logger,
		data:        make(map[string]*raplData),
		dramFolders: make(map[string]string),
		fs:          fs,
	}
}
