//go:build linux

package intel_powerstat

import (
	"bufio"
	"encoding/binary"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"
)

// fileService is responsible for handling operations on files.
type fileService interface {
	getCPUInfoStats() (map[string]*cpuInfo, error)
	getStringsMatchingPatternOnPath(path string) ([]string, error)
	readFile(path string) ([]byte, error)
	readFileToFloat64(reader io.Reader) (float64, int64, error)
	readFileAtOffsetToUint64(reader io.ReaderAt, offset int64) (uint64, error)
}

type fileServiceImpl struct {
}

// getCPUInfoStats retrieves basic information about CPU from /proc/cpuinfo.
func (fs *fileServiceImpl) getCPUInfoStats() (map[string]*cpuInfo, error) {
	path := "/proc/cpuinfo"
	cpuInfoFile, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("error while reading %q: %w", path, err)
	}
	defer cpuInfoFile.Close()

	scanner := bufio.NewScanner(cpuInfoFile)

	processorRegexp := regexp.MustCompile(`^processor\t+:\s([0-9]+)\n*$`)
	physicalIDRegexp := regexp.MustCompile(`^physical id\t+:\s([0-9]+)\n*$`)
	coreIDRegexp := regexp.MustCompile(`^core id\t+:\s([0-9]+)\n*$`)
	vendorIDRegexp := regexp.MustCompile(`^vendor_id\t+:\s([a-zA-Z]+)\n*$`)
	cpuFamilyRegexp := regexp.MustCompile(`^cpu\sfamily\t+:\s([0-9]+)\n*$`)
	modelRegexp := regexp.MustCompile(`^model\t+:\s([0-9]+)\n*$`)
	flagsRegexp := regexp.MustCompile(`^flags\t+:\s(.+)\n*$`)

	stats := make(map[string]*cpuInfo)
	currentInfo := &cpuInfo{}

	for scanner.Scan() {
		line := scanner.Text()

		processorRes := processorRegexp.FindStringSubmatch(line)
		if len(processorRes) > 1 {
			currentInfo = &cpuInfo{
				cpuID: processorRes[1],
			}
		}

		vendorIDRes := vendorIDRegexp.FindStringSubmatch(line)
		if len(vendorIDRes) > 1 {
			currentInfo.vendorID = vendorIDRes[1]
		}

		physicalIDRes := physicalIDRegexp.FindStringSubmatch(line)
		if len(physicalIDRes) > 1 {
			currentInfo.physicalID = physicalIDRes[1]
		}

		coreIDRes := coreIDRegexp.FindStringSubmatch(line)
		if len(coreIDRes) > 1 {
			currentInfo.coreID = coreIDRes[1]
		}

		cpuFamilyRes := cpuFamilyRegexp.FindStringSubmatch(line)
		if len(cpuFamilyRes) > 1 {
			currentInfo.cpuFamily = cpuFamilyRes[1]
		}

		modelRes := modelRegexp.FindStringSubmatch(line)
		if len(modelRes) > 1 {
			currentInfo.model = modelRes[1]
		}

		flagsRes := flagsRegexp.FindStringSubmatch(line)
		if len(flagsRes) > 1 {
			currentInfo.flags = flagsRes[1]

			// Flags is the last value we have to acquire, so currentInfo is added to map.
			stats[currentInfo.cpuID] = currentInfo
		}
	}

	return stats, nil
}

// getStringsMatchingPatternOnPath looks for filenames and directory names on path matching given regexp.
// It ignores file system errors such as I/O errors reading directories. The only possible returned error
// is ErrBadPattern, when pattern is malformed.
func (fs *fileServiceImpl) getStringsMatchingPatternOnPath(path string) ([]string, error) {
	return filepath.Glob(path)
}

// readFile reads file on path and return string content.
func (fs *fileServiceImpl) readFile(path string) ([]byte, error) {
	out, err := os.ReadFile(path)
	if err != nil {
		return make([]byte, 0), err
	}
	return out, nil
}

// readFileToFloat64 reads file on path and tries to parse content to float64.
func (fs *fileServiceImpl) readFileToFloat64(reader io.Reader) (float64, int64, error) {
	read, err := io.ReadAll(reader)
	if err != nil {
		return 0, 0, err
	}

	readDate := time.Now().UnixNano()

	// Remove new line character
	trimmedString := strings.TrimRight(string(read), "\n")
	// Parse result to float64
	parsedValue, err := strconv.ParseFloat(trimmedString, 64)
	if err != nil {
		return 0, 0, fmt.Errorf("error parsing string to float for %s", trimmedString)
	}

	return parsedValue, readDate, nil
}

// readFileAtOffsetToUint64 reads 8 bytes from passed file at given offset.
func (fs *fileServiceImpl) readFileAtOffsetToUint64(reader io.ReaderAt, offset int64) (uint64, error) {
	buffer := make([]byte, 8)

	if offset == 0 {
		return 0, fmt.Errorf("file offset %d should not be 0", offset)
	}

	_, err := reader.ReadAt(buffer, offset)
	if err != nil {
		return 0, fmt.Errorf("error on reading file at offset %d: %w", offset, err)
	}

	return binary.LittleEndian.Uint64(buffer), nil
}

func newFileService() *fileServiceImpl {
	return &fileServiceImpl{}
}

func checkFile(path string) error {
	if path == "" {
		return fmt.Errorf("empty path given")
	}

	lInfo, err := os.Lstat(path)
	if err != nil {
		if os.IsNotExist(err) {
			return fmt.Errorf("file %q doesn't exist", path)
		}
		return fmt.Errorf("cannot obtain file info of %q: %w", path, err)
	}
	mode := lInfo.Mode()
	if mode&os.ModeSymlink != 0 {
		return fmt.Errorf("file %q is a symlink", path)
	}
	return nil
}
