//go:build windows

package ctrld

import (
	"context"
	"strconv"
	"strings"
	"syscall"
	"unsafe"

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

var (
	wtsapi32                         = windows.NewLazySystemDLL("wtsapi32.dll")
	procWTSGetActiveConsoleSessionId = wtsapi32.NewProc("WTSGetActiveConsoleSessionId")
	procWTSQuerySessionInformation   = wtsapi32.NewProc("WTSQuerySessionInformationW")
	procWTSFreeMemory                = wtsapi32.NewProc("WTSFreeMemory")
)

const (
	WTSUserName = 5
)

// DiscoverMainUser attempts to find the primary user on Windows systems.
// This is designed to work reliably under RMM deployments where traditional
// environment variables and session detection may not be available.
//
// Priority chain (deterministic, lowest RID wins among candidates):
// 1. Active console session user via WTSGetActiveConsoleSessionId
// 2. Registry ProfileList scan for Administrators group members
// 3. Fallback to lowest RID from ProfileList
func DiscoverMainUser(ctx context.Context) string {
	logger := LoggerFromCtx(ctx).Debug()

	// Method 1: Check active console session
	logger.Msg("attempting to discover user via active console session")
	if user := getActiveConsoleUser(ctx); user != "" {
		logger.Str("method", "console").Str("user", user).Msg("found user via active console session")
		return user
	}

	// Method 2: Scan registry for admin users
	logger.Msg("attempting to discover user via registry with admin preference")
	if user := getRegistryUserWithAdminPreference(ctx); user != "" {
		logger.Str("method", "registry+admin").Str("user", user).Msg("found admin user via registry")
		return user
	}

	// Method 3: Fallback to lowest RID from registry
	logger.Msg("attempting to discover user via registry lowest RID")
	if user := getLowestRegistryUser(ctx); user != "" {
		logger.Str("method", "registry").Str("user", user).Msg("found user via registry")
		return user
	}

	logger.Msg("all user discovery methods failed")
	return "unknown"
}

// getActiveConsoleUser gets the username of the active console session
func getActiveConsoleUser(ctx context.Context) string {
	// Guard against missing WTS procedures (e.g., Windows Server Core).
	if err := procWTSGetActiveConsoleSessionId.Find(); err != nil {
		LoggerFromCtx(ctx).Debug().Err(err).Msg("WTSGetActiveConsoleSessionId not available, skipping console session check")
		return ""
	}
	sessionId, _, _ := procWTSGetActiveConsoleSessionId.Call()
	if sessionId == 0xFFFFFFFF { // Invalid session
		LoggerFromCtx(ctx).Debug().Msg("no active console session found")
		return ""
	}

	var buffer uintptr
	var bytesReturned uint32

	if err := procWTSQuerySessionInformation.Find(); err != nil {
		LoggerFromCtx(ctx).Debug().Err(err).Msg("WTSQuerySessionInformationW not available")
		return ""
	}
	ret, _, _ := procWTSQuerySessionInformation.Call(
		0, // WTS_CURRENT_SERVER_HANDLE
		sessionId,
		uintptr(WTSUserName),
		uintptr(unsafe.Pointer(&buffer)),
		uintptr(unsafe.Pointer(&bytesReturned)),
	)

	if ret == 0 {
		LoggerFromCtx(ctx).Debug().Msg("failed to query session information")
		return ""
	}
	defer procWTSFreeMemory.Call(buffer)

	// Convert buffer to string
	username := windows.UTF16PtrToString((*uint16)(unsafe.Pointer(buffer)))
	if username == "" {
		return ""
	}

	return username
}

// getRegistryUserWithAdminPreference scans registry profiles and prefers admin users
func getRegistryUserWithAdminPreference(ctx context.Context) string {
	profiles := getRegistryProfiles(ctx)
	if len(profiles) == 0 {
		return ""
	}

	var adminProfiles []registryProfile
	var regularProfiles []registryProfile

	// Separate admin and regular users
	for _, profile := range profiles {
		if isUserInAdministratorsGroup(profile.username) {
			adminProfiles = append(adminProfiles, profile)
		} else {
			regularProfiles = append(regularProfiles, profile)
		}
	}

	// Prefer admin users, then regular users
	candidates := adminProfiles
	if len(candidates) == 0 {
		candidates = regularProfiles
	}

	if len(candidates) == 0 {
		return ""
	}

	// Return user with lowest RID (deterministic choice)
	lowestRID := candidates[0].rid
	result := candidates[0].username

	for _, candidate := range candidates[1:] {
		if candidate.rid < lowestRID {
			lowestRID = candidate.rid
			result = candidate.username
		}
	}

	return result
}

// getLowestRegistryUser returns the user with lowest RID from registry
func getLowestRegistryUser(ctx context.Context) string {
	profiles := getRegistryProfiles(ctx)
	if len(profiles) == 0 {
		return ""
	}

	// Return user with lowest RID (deterministic choice)
	lowestRID := profiles[0].rid
	result := profiles[0].username

	for _, profile := range profiles[1:] {
		if profile.rid < lowestRID {
			lowestRID = profile.rid
			result = profile.username
		}
	}

	return result
}

type registryProfile struct {
	username string
	rid      uint32
	sid      string
}

// getRegistryProfiles scans the registry ProfileList for user profiles
func getRegistryProfiles(ctx context.Context) []registryProfile {
	key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList`, registry.ENUMERATE_SUB_KEYS)
	if err != nil {
		LoggerFromCtx(ctx).Debug().Err(err).Msg("failed to open ProfileList registry key")
		return nil
	}
	defer key.Close()

	subkeys, err := key.ReadSubKeyNames(-1)
	if err != nil {
		LoggerFromCtx(ctx).Debug().Err(err).Msg("failed to read ProfileList subkeys")
		return nil
	}

	var profiles []registryProfile

	for _, subkey := range subkeys {
		// Only process SIDs that start with S-1-5-21 (domain/local user accounts)
		if !strings.HasPrefix(subkey, "S-1-5-21-") {
			continue
		}

		profileKey, err := registry.OpenKey(key, subkey, registry.QUERY_VALUE)
		if err != nil {
			continue
		}

		profileImagePath, _, err := profileKey.GetStringValue("ProfileImagePath")
		profileKey.Close()
		if err != nil {
			continue
		}

		// Extract username from profile path (e.g., C:\Users\username)
		pathParts := strings.Split(profileImagePath, `\`)
		if len(pathParts) == 0 {
			continue
		}
		username := pathParts[len(pathParts)-1]

		// Extract RID from SID (last component after final hyphen)
		sidParts := strings.Split(subkey, "-")
		if len(sidParts) == 0 {
			continue
		}
		ridStr := sidParts[len(sidParts)-1]
		rid, err := strconv.ParseUint(ridStr, 10, 32)
		if err != nil {
			continue
		}

		// Only consider regular users (RID >= 1000, excludes built-in accounts).
		// rid == 500 is the default Administrator account (DOMAIN_USER_RID_ADMIN).
		// See: https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids
		if rid == 500 || rid >= 1000 {
			profiles = append(profiles, registryProfile{
				username: username,
				rid:      uint32(rid),
				sid:      subkey,
			})
		}
	}

	return profiles
}

// isUserInAdministratorsGroup checks if a user is in the Administrators group
func isUserInAdministratorsGroup(username string) bool {
	// Open the user account
	usernamePtr, err := syscall.UTF16PtrFromString(username)
	if err != nil {
		return false
	}

	var userSID *windows.SID
	var domain *uint16
	var userSIDSize, domainSize uint32
	var use uint32

	// First call to get buffer sizes
	err = windows.LookupAccountName(nil, usernamePtr, userSID, &userSIDSize, domain, &domainSize, &use)
	if err != nil && err != windows.ERROR_INSUFFICIENT_BUFFER {
		return false
	}

	// Allocate buffers and make actual call
	userSID = (*windows.SID)(unsafe.Pointer(&make([]byte, userSIDSize)[0]))
	domain = (*uint16)(unsafe.Pointer(&make([]uint16, domainSize)[0]))

	err = windows.LookupAccountName(nil, usernamePtr, userSID, &userSIDSize, domain, &domainSize, &use)
	if err != nil {
		return false
	}

	// Check if user is member of Administrators group (S-1-5-32-544)
	adminSID, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
	if err != nil {
		return false
	}

	// Open user token (this is a simplified check)
	var token windows.Token
	err = windows.OpenProcessToken(windows.CurrentProcess(), windows.TOKEN_QUERY, &token)
	if err != nil {
		return false
	}
	defer token.Close()

	// Check group membership
	member, err := token.IsMember(adminSID)
	if err != nil {
		return false
	}

	return member
}
