package inventory

import (
	"bufio"
	"os"
	"strconv"
	"strings"
	"syscall"
	"time"

	"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/grpc/pb"
)

func collect(r *pb.InventoryReport, includeStatic bool) {
	r.CPU.UsagePct = cpuUsage()
	r.CPU.Load1 = load1()
	memTotal, memAvail, swapTotal, swapFree := meminfo()
	if memTotal > memAvail {
		r.Memory.UsedBytes = memTotal - memAvail
	}
	if swapTotal > swapFree {
		r.SwapUsed = swapTotal - swapFree
	}
	if includeStatic {
		r.Memory.TotalBytes = memTotal
		r.SwapTotal = swapTotal
		r.CPU.Model, r.CPU.Cores = cpuStatic()
		r.Kernel = kernel()
		r.Partitions = partitions()
	}
}

func readProc(path string) string { b, _ := os.ReadFile(path); return string(b) }

func cpuSample() (idle, total uint64) {
	f, err := os.Open("/proc/stat")
	if err != nil {
		return
	}
	defer f.Close()
	sc := bufio.NewScanner(f)
	if sc.Scan() {
		fields := strings.Fields(sc.Text())
		for i, v := range fields[1:] {
			n, _ := strconv.ParseUint(v, 10, 64)
			total += n
			if i == 3 {
				idle = n
			}
		}
	}
	return
}

func cpuUsage() float64 {
	i1, t1 := cpuSample()
	time.Sleep(100 * time.Millisecond)
	i2, t2 := cpuSample()
	dt := float64(t2 - t1)
	if dt <= 0 {
		return 0
	}
	return (1 - float64(i2-i1)/dt) * 100
}

func load1() float64 {
	fields := strings.Fields(readProc("/proc/loadavg"))
	if len(fields) > 0 {
		v, _ := strconv.ParseFloat(fields[0], 64)
		return v
	}
	return 0
}

func meminfo() (total, avail, swapTotal, swapFree uint64) {
	f, err := os.Open("/proc/meminfo")
	if err != nil {
		return
	}
	defer f.Close()
	sc := bufio.NewScanner(f)
	for sc.Scan() {
		fields := strings.Fields(sc.Text())
		if len(fields) < 2 {
			continue
		}
		kb, _ := strconv.ParseUint(fields[1], 10, 64)
		b := kb * 1024
		switch strings.TrimSuffix(fields[0], ":") {
		case "MemTotal":
			total = b
		case "MemAvailable":
			avail = b
		case "SwapTotal":
			swapTotal = b
		case "SwapFree":
			swapFree = b
		}
	}
	return
}

func cpuStatic() (model string, cores int) {
	f, err := os.Open("/proc/cpuinfo")
	if err != nil {
		return
	}
	defer f.Close()
	sc := bufio.NewScanner(f)
	for sc.Scan() {
		line := sc.Text()
		if strings.HasPrefix(line, "processor") {
			cores++
		} else if strings.HasPrefix(line, "model name") && model == "" {
			if i := strings.Index(line, ":"); i >= 0 {
				model = strings.TrimSpace(line[i+1:])
			}
		}
	}
	return
}

func kernel() string {
	return strings.TrimSpace(readProc("/proc/sys/kernel/osrelease"))
}

func partitions() []pb.PartitionReport {
	allowed := map[string]bool{"ext4": true, "xfs": true, "btrfs": true, "zfs": true, "vfat": true, "ntfs": true, "ext3": true}
	f, err := os.Open("/proc/mounts")
	if err != nil {
		return nil
	}
	defer f.Close()
	var out []pb.PartitionReport
	seen := map[string]bool{}
	sc := bufio.NewScanner(f)
	for sc.Scan() {
		fields := strings.Fields(sc.Text())
		if len(fields) < 3 || !allowed[fields[2]] || seen[fields[1]] {
			continue
		}
		seen[fields[1]] = true
		var st syscall.Statfs_t
		if syscall.Statfs(fields[1], &st) != nil {
			continue
		}
		bsize := uint64(st.Bsize)
		total := st.Blocks * bsize
		// Bfree, not Bavail: the difference is the root-reserved 5% on ext4,
		// which is not used space. df counts it the same way.
		used := (st.Blocks - st.Bfree) * bsize
		out = append(out, pb.PartitionReport{
			Device: fields[0], Mountpoint: fields[1], Fstype: fields[2],
			TotalBytes: total, UsedBytes: used,
		})
	}
	return out
}
