package inventory

import (
	"fmt"
	"runtime"
	"time"
	"unsafe"

	"golang.org/x/sys/windows"
	"golang.org/x/sys/windows/registry"

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

var (
	kernel32           = windows.NewLazySystemDLL("kernel32.dll")
	procGetSystemTimes = kernel32.NewProc("GetSystemTimes")
	// x/sys/windows exposes neither of these two, so they are bound by hand.
	procGlobalMemoryStatusEx = kernel32.NewProc("GlobalMemoryStatusEx")
)

func collect(r *pb.InventoryReport, includeStatic bool) {
	r.CPU.UsagePct = cpuUsage()
	// Windows has no load average. Left at zero; the UI already treats it as
	// optional because it is omitempty on the wire.

	m := memoryStatus()
	if m.TotalPhys > m.AvailPhys {
		r.Memory.UsedBytes = m.TotalPhys - m.AvailPhys
	}
	// TotalPageFile is the commit limit — physical memory plus the pagefile —
	// so the pagefile alone is the difference.
	swapTotal := sub(m.TotalPageFile, m.TotalPhys)
	swapUsed := sub(sub(m.TotalPageFile, m.AvailPageFile), sub(m.TotalPhys, m.AvailPhys))
	if swapUsed > swapTotal {
		swapUsed = swapTotal
	}
	r.SwapUsed = swapUsed

	if includeStatic {
		r.Memory.TotalBytes = m.TotalPhys
		r.SwapTotal = swapTotal
		r.CPU.Model, r.CPU.Cores = cpuStatic()
		r.Kernel = kernel()
		r.Partitions = partitions()
	}
}

func sub(a, b uint64) uint64 {
	if a > b {
		return a - b
	}
	return 0
}

type memoryStatusEx struct {
	Length               uint32
	MemoryLoad           uint32
	TotalPhys            uint64
	AvailPhys            uint64
	TotalPageFile        uint64
	AvailPageFile        uint64
	TotalVirtual         uint64
	AvailVirtual         uint64
	AvailExtendedVirtual uint64
}

func memoryStatus() memoryStatusEx {
	var m memoryStatusEx
	m.Length = uint32(unsafe.Sizeof(m))
	r, _, _ := procGlobalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&m)))
	if r == 0 {
		return memoryStatusEx{}
	}
	return m
}

func systemTimes() (idle, total uint64, ok bool) {
	var idleFT, kernelFT, userFT windows.Filetime
	r, _, _ := procGetSystemTimes.Call(
		uintptr(unsafe.Pointer(&idleFT)),
		uintptr(unsafe.Pointer(&kernelFT)),
		uintptr(unsafe.Pointer(&userFT)),
	)
	if r == 0 {
		return 0, 0, false
	}
	ft := func(f windows.Filetime) uint64 {
		return uint64(f.HighDateTime)<<32 | uint64(f.LowDateTime)
	}
	// Kernel time already includes idle time, so kernel+user is the whole.
	return ft(idleFT), ft(kernelFT) + ft(userFT), true
}

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

func cpuStatic() (model string, cores int) {
	cores = runtime.NumCPU()
	k, err := registry.OpenKey(registry.LOCAL_MACHINE,
		`HARDWARE\DESCRIPTION\System\CentralProcessor\0`, registry.QUERY_VALUE)
	if err != nil {
		return
	}
	defer k.Close()
	if s, _, err := k.GetStringValue("ProcessorNameString"); err == nil {
		model = s
	}
	return
}

func kernel() string {
	v := windows.RtlGetVersion()
	return fmt.Sprintf("%d.%d.%d", v.MajorVersion, v.MinorVersion, v.BuildNumber)
}

func partitions() []pb.PartitionReport {
	buf := make([]uint16, 256)
	n, err := windows.GetLogicalDriveStrings(uint32(len(buf)), &buf[0])
	if err != nil || n == 0 {
		return nil
	}

	var out []pb.PartitionReport
	for _, root := range splitNullStrings(buf[:n]) {
		rootPtr, err := windows.UTF16PtrFromString(root)
		if err != nil {
			continue
		}
		// Fixed disks only: network shares can hang, and removable drives
		// would appear and vanish between snapshots.
		if windows.GetDriveType(rootPtr) != windows.DRIVE_FIXED {
			continue
		}

		var free, total, totalFree uint64
		if err := windows.GetDiskFreeSpaceEx(rootPtr, &free, &total, &totalFree); err != nil {
			continue
		}

		fsBuf := make([]uint16, 32)
		var fstype string
		if err := windows.GetVolumeInformation(rootPtr, nil, 0, nil, nil, nil, &fsBuf[0], uint32(len(fsBuf))); err == nil {
			fstype = windows.UTF16ToString(fsBuf)
		}

		out = append(out, pb.PartitionReport{
			Device:     root,
			Mountpoint: root,
			Fstype:     fstype,
			TotalBytes: total,
			UsedBytes:  total - totalFree,
		})
	}
	return out
}

// splitNullStrings splits the NUL-separated, double-NUL-terminated block that
// GetLogicalDriveStrings writes.
func splitNullStrings(b []uint16) []string {
	var out []string
	start := 0
	for i, c := range b {
		if c != 0 {
			continue
		}
		if i > start {
			out = append(out, windows.UTF16ToString(b[start:i]))
		}
		start = i + 1
	}
	return out
}
