package workerload

import (
	"errors"
	"os"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"time"
)

type SystemSampler struct {
	mu sync.Mutex

	root          string
	lastCPUUsage  int64
	lastCPUTime   time.Time
	lastThrottled int64
}

func NewSystemSampler() *SystemSampler {
	return &SystemSampler{root: "/sys/fs/cgroup"}
}

func NewSystemSamplerAt(root string) *SystemSampler {
	return &SystemSampler{root: strings.TrimRight(root, "/")}
}

func (s *SystemSampler) Sample(databaseConnections, databaseMax int32) ResourceSample {
	s.mu.Lock()
	defer s.mu.Unlock()
	now := time.Now()
	memoryCurrent, memoryLimit := s.memory()
	cpuUsage, throttledCount := s.cpuStat()
	cpuLimit := s.cpuLimit()
	cpuUtilization := 0.0
	if s.lastCPUUsage > 0 && cpuUsage >= s.lastCPUUsage && !s.lastCPUTime.IsZero() {
		elapsed := now.Sub(s.lastCPUTime).Seconds()
		if elapsed > 0 {
			cpuUtilization = float64(cpuUsage-s.lastCPUUsage) / 1_000_000 / elapsed / cpuLimit
		}
	}
	throttled := s.lastThrottled > 0 && throttledCount > s.lastThrottled
	s.lastCPUUsage = cpuUsage
	s.lastThrottled = throttledCount
	s.lastCPUTime = now
	return ResourceSample{
		MemoryCurrentBytes: memoryCurrent,
		MemoryLimitBytes:   memoryLimit,
		CPUUtilization:     bounded(cpuUtilization),
		CPUThrottled:       throttled,
		DBConnections:      databaseConnections,
		DBMaxConnections:   databaseMax,
		SampledAt:          now,
	}
}

func (s *SystemSampler) memory() (int64, int64) {
	current, currentErr := readIntFile(s.root + "/memory.current")
	limit, limitErr := readLimitFile(s.root + "/memory.max")
	if currentErr == nil && limitErr == nil {
		return current, limit
	}
	current, _ = readIntFile(s.root + "/memory/memory.usage_in_bytes")
	limit, _ = readLimitFile(s.root + "/memory/memory.limit_in_bytes")
	return current, limit
}

func (s *SystemSampler) cpuStat() (usageUsec, throttled int64) {
	data, err := os.ReadFile(s.root + "/cpu.stat")
	if err != nil {
		return 0, 0
	}
	for _, line := range strings.Split(string(data), "\n") {
		fields := strings.Fields(line)
		if len(fields) != 2 {
			continue
		}
		value, parseErr := strconv.ParseInt(fields[1], 10, 64)
		if parseErr != nil {
			continue
		}
		switch fields[0] {
		case "usage_usec":
			usageUsec = value
		case "nr_throttled":
			throttled = value
		}
	}
	return usageUsec, throttled
}

func (s *SystemSampler) cpuLimit() float64 {
	data, err := os.ReadFile(s.root + "/cpu.max")
	if err == nil {
		fields := strings.Fields(string(data))
		if len(fields) == 2 && fields[0] != "max" {
			quota, quotaErr := strconv.ParseFloat(fields[0], 64)
			period, periodErr := strconv.ParseFloat(fields[1], 64)
			if quotaErr == nil && periodErr == nil && quota > 0 && period > 0 {
				return max(quota/period, .001)
			}
		}
	}
	return max(float64(runtime.GOMAXPROCS(0)), 1)
}

func readIntFile(path string) (int64, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return 0, err
	}
	return strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
}

func readLimitFile(path string) (int64, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return 0, err
	}
	value := strings.TrimSpace(string(data))
	if value == "" || value == "max" {
		return 0, errors.New("cgroup limit is unlimited")
	}
	limit, err := strconv.ParseInt(value, 10, 64)
	if err != nil || limit <= 0 || limit > 1<<60 {
		return 0, errors.New("cgroup limit is not finite")
	}
	return limit, nil
}
