package collector

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"time"
	"unsafe"

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

// SystemData ist das Haupt-Daten-Struct das an das Backend gesendet wird
type SystemData struct {
	Windows *WindowsData `json:"windows,omitempty"`
}

type WindowsData struct {
	Hostname   string      `json:"hostname"`
	OSVersion  string      `json:"os_version"`
	OSBuild    string      `json:"os_build"`
	UptimeSecs int64       `json:"uptime_seconds"`
	CPU        CPUInfo     `json:"cpu"`
	Memory     MemoryInfo  `json:"memory"`
	Disks      []DiskInfo  `json:"disks"`
	Services   []SvcInfo   `json:"services,omitempty"`
	Domain     string      `json:"domain,omitempty"`
	LastUpdate time.Time   `json:"last_update"`
}

type CPUInfo struct {
	Model        string  `json:"model"`
	Cores        int     `json:"cores"`
	LogicalCores int     `json:"logical_cores"`
	LoadPercent  float64 `json:"load_percent"`
}

type MemoryInfo struct {
	TotalBytes     uint64  `json:"total_bytes"`
	AvailableBytes uint64  `json:"available_bytes"`
	UsedPercent    float64 `json:"used_percent"`
}

type DiskInfo struct {
	Drive       string  `json:"drive"`
	Label       string  `json:"label,omitempty"`
	TotalBytes  uint64  `json:"total_bytes"`
	FreeBytes   uint64  `json:"free_bytes"`
	UsedPercent float64 `json:"used_percent"`
	Filesystem  string  `json:"filesystem,omitempty"`
}

type SvcInfo struct {
	Name        string `json:"name"`
	DisplayName string `json:"display_name"`
	Status      string `json:"status"`
	StartType   string `json:"start_type"`
}

// MEMORYSTATUSEX fuer GlobalMemoryStatusEx
type memoryStatusEx struct {
	dwLength                uint32
	dwMemoryLoad            uint32
	ullTotalPhys            uint64
	ullAvailPhys            uint64
	ullTotalPageFile        uint64
	ullAvailPageFile        uint64
	ullTotalVirtual         uint64
	ullAvailVirtual         uint64
	ullAvailExtendedVirtual uint64
}

var (
	kernel32                = windows.NewLazySystemDLL("kernel32.dll")
	procGlobalMemoryStatusEx = kernel32.NewProc("GlobalMemoryStatusEx")
	procGetTickCount64       = kernel32.NewProc("GetTickCount64")
	procGetSystemTimes       = kernel32.NewProc("GetSystemTimes")
)

// Collect sammelt alle Systemdaten
func Collect() (*SystemData, error) {
	wd := &WindowsData{
		LastUpdate: time.Now(),
	}

	// Hostname
	if h, err := os.Hostname(); err == nil {
		wd.Hostname = h
	}

	// OS-Version
	wd.OSVersion, wd.OSBuild = getOSVersion()

	// Uptime
	wd.UptimeSecs = getUptime()

	// CPU
	wd.CPU = getCPUInfo()

	// Memory
	wd.Memory = getMemoryInfo()

	// Disks
	wd.Disks = getDiskInfo()

	// Domain
	wd.Domain = getDomain()

	return &SystemData{Windows: wd}, nil
}

func getOSVersion() (version, build string) {
	// Registry: HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion
	k, err := registry.OpenKey(registry.LOCAL_MACHINE,
		`SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
	if err != nil {
		return "Windows (unbekannt)", ""
	}
	defer k.Close()

	productName, _, _ := k.GetStringValue("ProductName")
	displayVersion, _, _ := k.GetStringValue("DisplayVersion")
	currentBuild, _, _ := k.GetStringValue("CurrentBuild")
	ubr, _, _ := k.GetIntegerValue("UBR")

	if displayVersion != "" {
		version = fmt.Sprintf("%s %s", productName, displayVersion)
	} else {
		version = productName
	}
	if ubr > 0 {
		build = fmt.Sprintf("%s.%d", currentBuild, ubr)
	} else {
		build = currentBuild
	}
	return
}

func getUptime() int64 {
	ms, _, _ := procGetTickCount64.Call()
	return int64(ms) / 1000
}

func getCPUInfo() CPUInfo {
	info := CPUInfo{}

	// Modell aus Registry
	k, err := registry.OpenKey(registry.LOCAL_MACHINE,
		`HARDWARE\DESCRIPTION\System\CentralProcessor\0`, registry.QUERY_VALUE)
	if err == nil {
		defer k.Close()
		info.Model, _, _ = k.GetStringValue("ProcessorNameString")
		info.Model = strings.TrimSpace(info.Model)
	}

	// Kern-Anzahl
	info.LogicalCores = getLogicalCoreCount()
	info.Cores = getPhysicalCoreCount()
	if info.Cores == 0 {
		info.Cores = info.LogicalCores
	}

	// CPU-Last via PowerShell (einmalig, nicht ideal aber zuverlässig)
	info.LoadPercent = getCPULoad()

	return info
}

type systemInfo struct {
	wProcessorArchitecture      uint16
	wReserved                   uint16
	dwPageSize                  uint32
	lpMinimumApplicationAddress uintptr
	lpMaximumApplicationAddress uintptr
	dwActiveProcessorMask       uintptr
	dwNumberOfProcessors        uint32
	dwProcessorType             uint32
	dwAllocationGranularity     uint32
	wProcessorLevel             uint16
	wProcessorRevision          uint16
}

func getLogicalCoreCount() int {
	var si systemInfo
	kernel32.NewProc("GetSystemInfo").Call(uintptr(unsafe.Pointer(&si)))
	return int(si.dwNumberOfProcessors)
}

func getPhysicalCoreCount() int {
	out, err := runPS("(Get-CimInstance Win32_Processor | Measure-Object -Property NumberOfCores -Sum).Sum")
	if err != nil {
		return 0
	}
	n, _ := strconv.Atoi(strings.TrimSpace(out))
	return n
}

func getCPULoad() float64 {
	// GetSystemTimes: idle, kernel (includes idle), user
	type filetime struct{ lo, hi uint32 }
	var idle, kernel, user filetime

	r, _, _ := procGetSystemTimes.Call(
		uintptr(unsafe.Pointer(&idle)),
		uintptr(unsafe.Pointer(&kernel)),
		uintptr(unsafe.Pointer(&user)),
	)
	if r == 0 {
		return 0
	}

	toUint64 := func(f filetime) uint64 { return uint64(f.hi)<<32 | uint64(f.lo) }

	t1idle := toUint64(idle)
	t1kernel := toUint64(kernel)
	t1user := toUint64(user)

	time.Sleep(500 * time.Millisecond)

	r, _, _ = procGetSystemTimes.Call(
		uintptr(unsafe.Pointer(&idle)),
		uintptr(unsafe.Pointer(&kernel)),
		uintptr(unsafe.Pointer(&user)),
	)
	if r == 0 {
		return 0
	}

	t2idle := toUint64(idle)
	t2kernel := toUint64(kernel)
	t2user := toUint64(user)

	idleDelta := t2idle - t1idle
	kernelDelta := t2kernel - t1kernel
	userDelta := t2user - t1user

	total := kernelDelta + userDelta
	if total == 0 {
		return 0
	}
	busy := total - idleDelta
	return float64(busy) / float64(total) * 100.0
}

func getMemoryInfo() MemoryInfo {
	var ms memoryStatusEx
	ms.dwLength = uint32(unsafe.Sizeof(ms))
	procGlobalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&ms)))

	used := ms.ullTotalPhys - ms.ullAvailPhys
	usedPct := 0.0
	if ms.ullTotalPhys > 0 {
		usedPct = float64(used) / float64(ms.ullTotalPhys) * 100.0
	}
	return MemoryInfo{
		TotalBytes:     ms.ullTotalPhys,
		AvailableBytes: ms.ullAvailPhys,
		UsedPercent:    usedPct,
	}
}

func getDiskInfo() []DiskInfo {
	var disks []DiskInfo

	// Alle logischen Laufwerke ermitteln
	buf := make([]uint16, 256)
	kernel32.NewProc("GetLogicalDriveStringsW").Call(
		uintptr(len(buf)),
		uintptr(unsafe.Pointer(&buf[0])),
	)

	// buf enthält null-separierte Strings ("C:\\\0D:\\\0\0")
	drives := windows.UTF16ToString(buf)
	for _, drive := range strings.Split(drives, "\x00") {
		drive = strings.TrimSpace(drive)
		if len(drive) < 2 {
			continue
		}

		// Nur lokale Festplatten (DRIVE_FIXED = 3)
		driveType := kernel32.NewProc("GetDriveTypeW")
		drivePath := windows.StringToUTF16Ptr(drive)
		t, _, _ := driveType.Call(uintptr(unsafe.Pointer(drivePath)))
		if t != 3 { // DRIVE_FIXED
			continue
		}

		// Speicherplatz
		var freeBytesToCaller, totalBytes, freeBytes uint64
		kernel32.NewProc("GetDiskFreeSpaceExW").Call(
			uintptr(unsafe.Pointer(drivePath)),
			uintptr(unsafe.Pointer(&freeBytesToCaller)),
			uintptr(unsafe.Pointer(&totalBytes)),
			uintptr(unsafe.Pointer(&freeBytes)),
		)

		if totalBytes == 0 {
			continue
		}

		usedPct := float64(totalBytes-freeBytes) / float64(totalBytes) * 100.0

		// Laufwerksbuchstabe ohne Backslash
		letter := strings.TrimSuffix(strings.TrimSuffix(drive, `\`), "/")

		disks = append(disks, DiskInfo{
			Drive:       letter,
			TotalBytes:  totalBytes,
			FreeBytes:   freeBytes,
			UsedPercent: usedPct,
		})
	}
	return disks
}

func getDomain() string {
	out, err := runPS("(Get-WmiObject Win32_ComputerSystem).Domain")
	if err != nil {
		return ""
	}
	d := strings.TrimSpace(out)
	if d == "WORKGROUP" {
		return ""
	}
	return d
}

// ToJSON serialisiert SystemData für den Heartbeat
func (s *SystemData) ToJSON() (json.RawMessage, error) {
	b, err := json.Marshal(s)
	return json.RawMessage(b), err
}

// runPS führt einen PowerShell-Befehl aus und gibt stdout zurück
func runPS(cmd string) (string, error) {
	out, err := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", cmd).Output()
	if err != nil {
		log.Printf("PowerShell-Fehler (%s): %v", cmd[:min(len(cmd), 40)], err)
		return "", err
	}
	return strings.TrimSpace(string(out)), nil
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}
