//go:build linux

package system

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"syscall"
)

// GetInfo reads system memory, disk, CPU, load, and temperature info.
// hddPath is the mount path for external HDD; if empty, HDD info is skipped.
// cpuCollector provides the latest CPU usage sample; may be nil.
func GetInfo(hddPath string, cpuCollector *CPUCollector) SystemInfo {
	info := SystemInfo{}

	// --- Memory from /proc/meminfo ---
	readMemInfo(&info)

	// --- Root filesystem disk usage ---
	readDiskUsage("/", &info.DiskTotalGB, &info.DiskUsedGB, &info.DiskAvailGB, &info.DiskPercent)

	// --- HDD disk usage (if configured) ---
	if hddPath != "" {
		info.HDDConfigured = true
		readDiskUsage(hddPath, &info.HDDTotalGB, &info.HDDUsedGB, &info.HDDAvailGB, &info.HDDPercent)
	}

	// --- Load average ---
	readLoadAvg(&info)

	// --- Temperature ---
	readTemperature(&info)

	// --- CPU from collector ---
	if cpuCollector != nil {
		info.CPUPercent = cpuCollector.CPUPercent()
	}

	return info
}

// GetTotalMemoryMB reads total system memory from /proc/meminfo.
func GetTotalMemoryMB() (int, error) {
	info := SystemInfo{}
	readMemInfo(&info)
	if info.TotalMemMB == 0 {
		return 0, fmt.Errorf("could not read MemTotal from /proc/meminfo")
	}
	return int(info.TotalMemMB), nil
}

func readMemInfo(info *SystemInfo) {
	f, err := os.Open("/proc/meminfo")
	if err != nil {
		return
	}
	defer f.Close()

	var totalKB, availKB uint64
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Text()
		switch {
		case strings.HasPrefix(line, "MemTotal:"):
			totalKB = parseMemLine(line)
		case strings.HasPrefix(line, "MemAvailable:"):
			availKB = parseMemLine(line)
		}
		if totalKB > 0 && availKB > 0 {
			break
		}
	}

	if totalKB > 0 {
		info.TotalMemMB = totalKB / 1024
		info.AvailMemMB = availKB / 1024
		info.UsedMemMB = info.TotalMemMB - info.AvailMemMB
		info.MemPercent = float64(info.UsedMemMB) / float64(info.TotalMemMB) * 100
	}
}

// parseMemLine extracts the kB value from a /proc/meminfo line like "MemTotal:       16384000 kB"
func parseMemLine(line string) uint64 {
	parts := strings.SplitN(line, ":", 2)
	if len(parts) < 2 {
		return 0
	}
	valStr := strings.TrimSpace(parts[1])
	valStr = strings.TrimSuffix(valStr, " kB")
	valStr = strings.TrimSpace(valStr)

	var val uint64
	for _, c := range valStr {
		if c >= '0' && c <= '9' {
			val = val*10 + uint64(c-'0')
		}
	}
	return val
}

func readDiskUsage(path string, totalGB, usedGB, availGB *float64, percent *float64) {
	var stat syscall.Statfs_t
	if err := syscall.Statfs(path, &stat); err != nil {
		return
	}

	bsize := uint64(stat.Bsize)
	total := stat.Blocks * bsize
	avail := stat.Bavail * bsize
	used := total - (stat.Bfree * bsize)

	const gb = 1024 * 1024 * 1024
	*totalGB = float64(total) / gb
	*usedGB = float64(used) / gb
	*availGB = float64(avail) / gb
	if total > 0 {
		*percent = float64(used) / float64(total) * 100
	}
}

// readLoadAvg reads 1/5/15 minute load averages from /proc/loadavg.
func readLoadAvg(info *SystemInfo) {
	data, err := os.ReadFile("/proc/loadavg")
	if err != nil {
		return
	}
	fmt.Sscanf(string(data), "%f %f %f", &info.LoadAvg1, &info.LoadAvg5, &info.LoadAvg15)
}

// readTemperature reads CPU/SoC temperature from thermal zones.
// Tries /host/sys first (Docker mount), then /sys (native).
func readTemperature(info *SystemInfo) {
	prefixes := []string{"/host/sys", "/sys"}

	for _, prefix := range prefixes {
		if readThermalZones(prefix, info) {
			return
		}
	}

	// Fallback: try hwmon
	for _, prefix := range prefixes {
		if readHwmon(prefix, info) {
			return
		}
	}
}

func readThermalZones(sysPrefix string, info *SystemInfo) bool {
	pattern := filepath.Join(sysPrefix, "class", "thermal", "thermal_zone*", "temp")
	matches, err := filepath.Glob(pattern)
	if err != nil || len(matches) == 0 {
		return false
	}

	sort.Strings(matches)

	var maxTemp float64
	var maxSource string

	for _, tempPath := range matches {
		data, err := os.ReadFile(tempPath)
		if err != nil {
			continue
		}

		var milliDeg int64
		if _, err := fmt.Sscanf(strings.TrimSpace(string(data)), "%d", &milliDeg); err != nil {
			continue
		}

		temp := float64(milliDeg) / 1000.0

		// Read the type file for the label
		zoneDir := filepath.Dir(tempPath)
		typePath := filepath.Join(zoneDir, "type")
		typeData, err := os.ReadFile(typePath)
		source := strings.TrimSpace(string(typeData))
		if err != nil || source == "" {
			source = filepath.Base(zoneDir)
		}

		if temp > maxTemp {
			maxTemp = temp
			maxSource = source
		}
	}

	if maxTemp > 0 {
		info.TemperatureCelsius = maxTemp
		info.TemperatureSource = maxSource
		return true
	}
	return false
}

func readHwmon(sysPrefix string, info *SystemInfo) bool {
	pattern := filepath.Join(sysPrefix, "class", "hwmon", "hwmon*", "temp1_input")
	matches, err := filepath.Glob(pattern)
	if err != nil || len(matches) == 0 {
		return false
	}

	var maxTemp float64
	var maxSource string

	for _, tempPath := range matches {
		data, err := os.ReadFile(tempPath)
		if err != nil {
			continue
		}

		var milliDeg int64
		if _, err := fmt.Sscanf(strings.TrimSpace(string(data)), "%d", &milliDeg); err != nil {
			continue
		}

		temp := float64(milliDeg) / 1000.0
		source := filepath.Base(filepath.Dir(tempPath))

		if temp > maxTemp {
			maxTemp = temp
			maxSource = source
		}
	}

	if maxTemp > 0 {
		info.TemperatureCelsius = maxTemp
		info.TemperatureSource = maxSource
		return true
	}
	return false
}
