// SPDX-License-Identifier: BSD-3-Clause
//go:build windows

package cpu

import (
	"context"
	"errors"
	"fmt"
	"math/bits"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"
	"unsafe"

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

	"github.com/shirou/gopsutil/v4/internal/common"
)

var (
	procGetNativeSystemInfo              = common.Modkernel32.NewProc("GetNativeSystemInfo")
	procGetLogicalProcessorInformationEx = common.Modkernel32.NewProc("GetLogicalProcessorInformationEx")
	procGetSystemFirmwareTable           = common.Modkernel32.NewProc("GetSystemFirmwareTable")
	procCallNtPowerInformation           = common.ModPowrProf.NewProc("CallNtPowerInformation")
	procGetActiveProcessorGroupCount     = common.Modkernel32.NewProc("GetActiveProcessorGroupCount")
)

type win32_Processor struct { //nolint:revive //FIXME
	Family                    uint16
	Manufacturer              string
	Name                      string
	NumberOfLogicalProcessors uint32
	NumberOfCores             uint32
	ProcessorID               *string
	Stepping                  *string
	MaxClockSpeed             uint32
}

// SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION
// defined in windows api doc with the following
// https://docs.microsoft.com/en-us/windows/desktop/api/winternl/nf-winternl-ntquerysysteminformation#system_processor_performance_information
// additional fields documented here
// https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/sysinfo/processor_performance.htm
type win32_SystemProcessorPerformanceInformation struct { //nolint:revive //FIXME
	IdleTime       int64  // idle time in 100ns (this is not a filetime).
	KernelTime     int64  // kernel time in 100ns.  kernel time includes idle time. (this is not a filetime).
	UserTime       int64  // usertime in 100ns (this is not a filetime).
	DpcTime        int64  // dpc time in 100ns (this is not a filetime).
	InterruptTime  int64  // interrupt time in 100ns
	InterruptCount uint64 // ULONG needs to be uint64
}

// https://learn.microsoft.com/en-us/windows/win32/power/processor-power-information-str
type processorPowerInformation struct {
	number           uint32 // http://download.microsoft.com/download/a/d/f/adf1347d-08dc-41a4-9084-623b1194d4b2/MoreThan64proc.docx
	maxMhz           uint32
	currentMhz       uint32
	mhzLimit         uint32
	maxIdleState     uint32
	currentIdleState uint32
}

const (
	ClocksPerSec = 10000000.0

	// systemProcessorPerformanceInformationClass information class to query with NTQuerySystemInformation
	// https://processhacker.sourceforge.io/doc/ntexapi_8h.html#ad5d815b48e8f4da1ef2eb7a2f18a54e0
	win32_SystemProcessorPerformanceInformationClass = 8 //nolint:revive //FIXME

	// size of systemProcessorPerformanceInfoSize in memory
	win32_SystemProcessorPerformanceInfoSize = uint32(unsafe.Sizeof(win32_SystemProcessorPerformanceInformation{})) //nolint:revive //FIXME

	firmwareTableProviderSignatureRSMB = 0x52534d42 // "RSMB"  https://gitlab.winehq.org/dreamer/wine/-/blame/wine-7.0-rc6/dlls/ntdll/unix/system.c#L230
	smBiosHeaderSize                   = 8          // SMBIOS header size
	smbiosEndOfTable                   = 127        // Minimum length for processor structure
	smbiosTypeProcessor                = 4          // SMBIOS Type 4: Processor Information
	smbiosProcessorMinLength           = 0x18       // Minimum length for processor structure

	centralProcessorRegistryKey = `HARDWARE\DESCRIPTION\System\CentralProcessor`
)

type relationship uint32

// https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getlogicalprocessorinformationex
const (
	relationProcessorCore    = relationship(0)
	relationProcessorPackage = relationship(3)
)

const (
	kAffinitySize = unsafe.Sizeof(int(0))
	// https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/interrupt-affinity-and-priority
	maxLogicalProcessorsPerGroup = uint32(unsafe.Sizeof(kAffinitySize * 8))
	// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ne-wdm-power_information_level
	processorInformation = 11
)

// Times returns times stat per cpu and combined for all CPUs
func Times(percpu bool) ([]TimesStat, error) {
	return TimesWithContext(context.Background(), percpu)
}

func TimesWithContext(_ context.Context, percpu bool) ([]TimesStat, error) {
	if percpu {
		return perCPUTimes()
	}

	var ret []TimesStat
	var lpIdleTime common.FILETIME
	var lpKernelTime common.FILETIME
	var lpUserTime common.FILETIME
	// GetSystemTimes returns 0 for error, in which case we check err,
	// see https://pkg.go.dev/golang.org/x/sys/windows#LazyProc.Call
	r, _, err := common.ProcGetSystemTimes.Call(
		uintptr(unsafe.Pointer(&lpIdleTime)),
		uintptr(unsafe.Pointer(&lpKernelTime)),
		uintptr(unsafe.Pointer(&lpUserTime)))
	if r == 0 {
		return nil, err
	}

	LOT := float64(0.0000001)
	HIT := (LOT * 4294967296.0)
	idle := ((HIT * float64(lpIdleTime.DwHighDateTime)) + (LOT * float64(lpIdleTime.DwLowDateTime)))
	user := ((HIT * float64(lpUserTime.DwHighDateTime)) + (LOT * float64(lpUserTime.DwLowDateTime)))
	kernel := ((HIT * float64(lpKernelTime.DwHighDateTime)) + (LOT * float64(lpKernelTime.DwLowDateTime)))
	system := (kernel - idle)

	ret = append(ret, TimesStat{
		CPU:    "cpu-total",
		Idle:   float64(idle),
		User:   float64(user),
		System: float64(system),
	})
	return ret, nil
}

func Info() ([]InfoStat, error) {
	return InfoWithContext(context.Background())
}

// this function iterates over each set bit in the package affinity mask, each bit represent a logical processor in a group (assuming you are iteriang over a package mask)
// the function is used also to compute the global logical processor number
// https://learn.microsoft.com/en-us/windows/win32/procthread/processor-groups
// see https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-group_affinity
// and https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-processor_relationship
// and https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-system_logical_processor_information_ex
func forEachSetBit64(mask uint64, fn func(bit int)) {
	m := mask
	for m != 0 {
		b := bits.TrailingZeros64(m)
		fn(b)
		m &= m - 1
	}
}

func getProcessorPowerInformation(ctx context.Context) ([]processorPowerInformation, error) {
	numLP, countErr := CountsWithContext(ctx, true)
	if countErr != nil {
		return nil, fmt.Errorf("failed to get logical processor count: %w", countErr)
	}
	if numLP <= 0 {
		return nil, fmt.Errorf("invalid logical processor count: %d", numLP)
	}

	ppiSize := uintptr(numLP) * unsafe.Sizeof(processorPowerInformation{})
	buf := make([]byte, ppiSize)
	ppi, _, err := procCallNtPowerInformation.Call(
		uintptr(processorInformation),
		0,
		0,
		uintptr(unsafe.Pointer(&buf[0])),
		uintptr(ppiSize),
	)
	if ppi != 0 {
		return nil, fmt.Errorf("CallNtPowerInformation failed with code %d: %w", ppi, err)
	}
	ppis := unsafe.Slice((*processorPowerInformation)(unsafe.Pointer(&buf[0])), numLP)
	return ppis, nil
}

func InfoWithContext(ctx context.Context) ([]InfoStat, error) {
	var ret []InfoStat
	processorPackages, err := getSystemLogicalProcessorInformationEx(relationProcessorPackage)
	if err != nil {
		return ret, fmt.Errorf("failed to get processor package information: %w", err)
	}

	ppis, powerInformationErr := getProcessorPowerInformation(ctx)
	if powerInformationErr != nil {
		return ret, fmt.Errorf("failed to get processor power information: %w", powerInformationErr)
	}

	family, processorId, smBIOSErr := getSMBIOSProcessorInfo()
	if smBIOSErr != nil {
		return ret, smBIOSErr
	}

	for i, pkg := range processorPackages {
		logicalCount := 0
		maxMhz := 0
		model := ""
		vendorId := ""
		// iterate over each set bit in the package affinity mask
		for _, ga := range pkg.processor.groupMask {
			g := int(ga.group)
			forEachSetBit64(uint64(ga.mask), func(bit int) {
				// the global logical processor label
				globalLpl := g*int(maxLogicalProcessorsPerGroup) + bit
				if globalLpl >= 0 && globalLpl < len(ppis) {
					logicalCount++
					m := int(ppis[globalLpl].maxMhz)
					if m > maxMhz {
						maxMhz = m
					}
				}

				registryKeyPath := filepath.Join(centralProcessorRegistryKey, strconv.Itoa(globalLpl))
				key, err := registry.OpenKey(registry.LOCAL_MACHINE, registryKeyPath, registry.QUERY_VALUE|registry.READ)
				if err == nil {
					model = getRegistryStringValueIfUnset(key, "ProcessorNameString", model)
					vendorId = getRegistryStringValueIfUnset(key, "VendorIdentifier", vendorId)
					_ = key.Close()
				}
			})
		}
		ret = append(ret, InfoStat{
			CPU:        int32(i),
			Family:     strconv.FormatUint(uint64(family), 10),
			VendorID:   vendorId,
			ModelName:  model,
			Cores:      int32(logicalCount),
			PhysicalID: processorId,
			Mhz:        float64(maxMhz),
			Flags:      []string{},
		})
	}

	return ret, nil
}

// perCPUTimes returns times stat per cpu, per core and overall for all CPUs
func perCPUTimes() ([]TimesStat, error) {
	var ret []TimesStat
	stats, err := perfInfo()
	if err != nil {
		return nil, err
	}
	for core, v := range stats {
		c := TimesStat{
			CPU:    fmt.Sprintf("cpu%d", core),
			User:   float64(v.UserTime) / ClocksPerSec,
			System: float64(v.KernelTime-v.IdleTime) / ClocksPerSec,
			Idle:   float64(v.IdleTime) / ClocksPerSec,
			Irq:    float64(v.InterruptTime) / ClocksPerSec,
		}
		ret = append(ret, c)
	}
	return ret, nil
}

// makes call to Windows API function to retrieve performance information for each core
func perfInfo() ([]win32_SystemProcessorPerformanceInformation, error) {
	// On hosts with more than 64 logical CPUs Windows splits CPUs into Processor Groups
	// (up to 64 logical CPUs per group). The non-Ex NtQuerySystemInformation only returns
	// data for the calling thread's group, so whenever the Ex variant is available we
	// iterate every active group and concatenate the results. See issue #887.
	if common.ProcNtQuerySystemInformationEx.Find() == nil {
		return perfInfoAllGroups()
	}
	return perfInfoSingleGroup()
}

// perfInfoSingleGroup queries SystemProcessorPerformanceInformation via the non-Ex
// NtQuerySystemInformation call. This is the legacy fallback for environments where
// NtQuerySystemInformationEx cannot be resolved; it only returns data for the calling
// thread's processor group.
func perfInfoSingleGroup() ([]win32_SystemProcessorPerformanceInformation, error) {
	// Make maxResults large for safety.
	// We can't invoke the api call with a results array that's too small.
	// If we have more than 2056 cores on a single host, then it's probably the future.
	maxBuffer := 2056
	// buffer for results from the windows proc
	resultBuffer := make([]win32_SystemProcessorPerformanceInformation, maxBuffer)
	// size of the buffer in memory
	bufferSize := uintptr(win32_SystemProcessorPerformanceInfoSize) * uintptr(maxBuffer)
	// size of the returned response
	var retSize uint32

	// Invoke windows api proc.
	// The returned err from the windows dll proc will always be non-nil even when successful.
	// See https://godoc.org/golang.org/x/sys/windows#LazyProc.Call for more information
	retCode, _, err := common.ProcNtQuerySystemInformation.Call(
		win32_SystemProcessorPerformanceInformationClass, // System Information Class -> SystemProcessorPerformanceInformation
		uintptr(unsafe.Pointer(&resultBuffer[0])),        // pointer to first element in result buffer
		bufferSize,                        // size of the buffer in memory
		uintptr(unsafe.Pointer(&retSize)), // pointer to the size of the returned results the windows proc will set this
	)

	// check return code for errors
	if retCode != 0 {
		return nil, fmt.Errorf("call to NtQuerySystemInformation returned 0x%x: %w", retCode, err)
	}

	// calculate the number of returned elements based on the returned size
	numReturnedElements := retSize / win32_SystemProcessorPerformanceInfoSize

	// trim results to the number of returned elements
	return resultBuffer[:numReturnedElements], nil
}

// perfInfoAllGroups queries SystemProcessorPerformanceInformation for every active
// processor group via NtQuerySystemInformationEx and concatenates the results. The
// group index is passed as the InputBuffer per the Ex calling convention documented at
// https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/sysinfo/queryex.htm
func perfInfoAllGroups() ([]win32_SystemProcessorPerformanceInformation, error) {
	// GetActiveProcessorGroupCount returns 0 only on failure; propagate the error
	// rather than silently defaulting to a single group and returning partial data.
	r, _, callErr := procGetActiveProcessorGroupCount.Call()
	if r == 0 {
		return nil, fmt.Errorf("GetActiveProcessorGroupCount returned 0: %w", callErr)
	}
	groupCount := uint16(r)

	var result []win32_SystemProcessorPerformanceInformation
	for g := uint16(0); g < groupCount; g++ {
		numLP := windows.GetActiveProcessorCount(g)
		if numLP == 0 {
			return nil, fmt.Errorf("GetActiveProcessorCount returned 0 for processor group %d", g)
		}
		// buffer sized exactly for this group's logical CPU count
		buf := make([]win32_SystemProcessorPerformanceInformation, numLP)
		bufSize := uintptr(win32_SystemProcessorPerformanceInfoSize) * uintptr(numLP)
		var retSize uint32
		// InputBuffer is a USHORT (2 bytes) holding the target processor group index.
		group := g
		retCode, _, err := common.ProcNtQuerySystemInformationEx.Call(
			win32_SystemProcessorPerformanceInformationClass, // System Information Class -> SystemProcessorPerformanceInformation
			uintptr(unsafe.Pointer(&group)),                  // InputBuffer: pointer to USHORT group index
			unsafe.Sizeof(group),                             // InputBufferLength: sizeof(USHORT) = 2
			uintptr(unsafe.Pointer(&buf[0])),                 // pointer to first element in result buffer
			bufSize,                                          // size of the buffer in memory
			uintptr(unsafe.Pointer(&retSize)),                // pointer to the size of the returned results the windows proc will set this
		)
		if retCode != 0 {
			return nil, fmt.Errorf("call to NtQuerySystemInformationEx(group=%d) returned 0x%x: %w", g, retCode, err)
		}
		// Guard against a retSize that is not a whole number of entries or exceeds
		// the allocated buffer (e.g. CPU hot-add racing with GetActiveProcessorCount).
		if retSize%win32_SystemProcessorPerformanceInfoSize != 0 || uintptr(retSize) > bufSize {
			return nil, fmt.Errorf("NtQuerySystemInformationEx(group=%d) returned unexpected retSize=%d (bufSize=%d)", g, retSize, bufSize)
		}
		n := retSize / win32_SystemProcessorPerformanceInfoSize
		result = append(result, buf[:n]...)
	}
	return result, nil
}

// SystemInfo is an equivalent representation of SYSTEM_INFO in the Windows API.
// https://msdn.microsoft.com/en-us/library/ms724958%28VS.85%29.aspx?f=255&MSPPError=-2147217396
// https://github.com/elastic/go-windows/blob/bb1581babc04d5cb29a2bfa7a9ac6781c730c8dd/kernel32.go#L43
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
}

type groupAffinity struct {
	mask     uintptr // https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/interrupt-affinity-and-priority#about-kaffinity
	group    uint16
	reserved [3]uint16
}

// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-processor_relationship
type processorRelationship struct {
	flags          byte
	efficientClass byte
	reserved       [20]byte
	groupCount     uint16
	groupMask      [1]groupAffinity
}

// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-system_logical_processor_information_ex
type systemLogicalProcessorInformationEx struct {
	relationship uint32
	size         uint32
	processor    processorRelationship
}

// getSMBIOSProcessorInfo reads the SMBIOS Type 4 (Processor Information) structure and returns the Processor Family and ProcessorId fields.
// If not found, returns 0 and an empty string.
func getSMBIOSProcessorInfo() (family uint8, processorId string, err error) {
	// https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getsystemfirmwaretable
	size, _, err := procGetSystemFirmwareTable.Call(
		uintptr(firmwareTableProviderSignatureRSMB),
		0,
		0,
		0,
	)
	if size == 0 {
		return 0, "", fmt.Errorf("failed to get SMBIOS table size: %w", err)
	}
	buf := make([]byte, size)
	ret, _, err := procGetSystemFirmwareTable.Call(
		uintptr(firmwareTableProviderSignatureRSMB),
		0,
		uintptr(unsafe.Pointer(&buf[0])),
		uintptr(size),
	)
	if ret == 0 {
		return 0, "", fmt.Errorf("failed to read SMBIOS table: %w", err)
	}
	// https://wiki.osdev.org/System_Management_BIOS
	i := smBiosHeaderSize // skip SMBIOS header (first 8 bytes)
	maxIterations := len(buf) * 2
	iterations := 0
	for i < len(buf) && iterations < maxIterations {
		iterations++
		if i+4 > len(buf) {
			break
		}
		typ := buf[i]
		length := buf[i+1]
		if typ == smbiosEndOfTable {
			break
		}
		if typ == smbiosTypeProcessor && length >= smbiosProcessorMinLength && i+int(length) <= len(buf) {
			// Ensure we have enough bytes for procIdBytes
			if i+16 > len(buf) {
				break
			}
			// Get the processor family from byte at offset 6
			family = buf[i+6]
			// Extract processor ID bytes (8 bytes total) from offsets 8-15
			procIdBytes := buf[i+8 : i+16]
			// Convert first 4 bytes to 32-bit EAX register value (little endian)
			eax := uint32(procIdBytes[0]) | uint32(procIdBytes[1])<<8 | uint32(procIdBytes[2])<<16 | uint32(procIdBytes[3])<<24
			// Convert last 4 bytes to 32-bit EDX register value (little endian)
			edx := uint32(procIdBytes[4]) | uint32(procIdBytes[5])<<8 | uint32(procIdBytes[6])<<16 | uint32(procIdBytes[7])<<24
			// Format processor ID as 16 character hex string (EDX+EAX)
			procId := fmt.Sprintf("%08X%08X", edx, eax)
			return family, procId, nil
		}
		// skip to next structure
		j := i + int(length)
		innerIterations := 0
		maxInner := len(buf) // failsafe for inner loop
		for j+1 < len(buf) && innerIterations < maxInner {
			innerIterations++
			if buf[j] == 0 && buf[j+1] == 0 {
				j += 2
				break
			}
			j++
		}
		if innerIterations >= maxInner {
			break // malformed buffer, avoid infinite loop
		}
		i = j
	}
	return 0, "", fmt.Errorf("SMBIOS processor information not found: %w", syscall.ERROR_NOT_FOUND)
}

func getSystemLogicalProcessorInformationEx(relationship relationship) ([]systemLogicalProcessorInformationEx, error) {
	var length uint32
	// First call to determine the required buffer size
	_, _, err := procGetLogicalProcessorInformationEx.Call(uintptr(relationship), 0, uintptr(unsafe.Pointer(&length)))
	if err != nil && !errors.Is(err, windows.ERROR_INSUFFICIENT_BUFFER) {
		return nil, fmt.Errorf("failed to get buffer size: %w", err)
	}

	// Allocate the buffer
	buffer := make([]byte, length)

	// Second call to retrieve the processor information
	_, _, err = procGetLogicalProcessorInformationEx.Call(uintptr(relationship), uintptr(unsafe.Pointer(&buffer[0])), uintptr(unsafe.Pointer(&length)))
	if err != nil && !errors.Is(err, windows.NTE_OP_OK) {
		return nil, fmt.Errorf("failed to get logical processor information: %w", err)
	}

	// Convert the byte slice into a slice of systemLogicalProcessorInformationEx structs
	offset := uintptr(0)
	var infos []systemLogicalProcessorInformationEx
	for offset < uintptr(length) {
		info := (*systemLogicalProcessorInformationEx)(unsafe.Pointer(uintptr(unsafe.Pointer(&buffer[0])) + offset))
		infos = append(infos, *info)
		offset += uintptr(info.size)
	}

	return infos, nil
}

func getPhysicalCoreCount() (int, error) {
	infos, err := getSystemLogicalProcessorInformationEx(relationProcessorCore)
	return len(infos), err
}

func getRegistryStringValueIfUnset(key registry.Key, keyName, value string) string {
	if value != "" {
		return value
	}
	val, _, err := key.GetStringValue(keyName)
	if err == nil {
		return strings.TrimSpace(val)
	}
	return ""
}

func CountsWithContext(_ context.Context, logical bool) (int, error) {
	if logical {
		// Get logical processor count https://github.com/giampaolo/psutil/blob/d01a9eaa35a8aadf6c519839e987a49d8be2d891/psutil/_psutil_windows.c#L97
		ret := windows.GetActiveProcessorCount(windows.ALL_PROCESSOR_GROUPS)
		if ret != 0 {
			return int(ret), nil
		}

		var sInfo systemInfo
		_, _, err := procGetNativeSystemInfo.Call(uintptr(unsafe.Pointer(&sInfo)))
		if sInfo.dwNumberOfProcessors == 0 {
			return 0, err
		}
		return int(sInfo.dwNumberOfProcessors), nil
	}

	// Get physical core count https://github.com/giampaolo/psutil/blob/d01a9eaa35a8aadf6c519839e987a49d8be2d891/psutil/_psutil_windows.c#L499
	return getPhysicalCoreCount()
}
