//go:build windows

package diag

import (
	"fmt"
	"os/exec"
	"strings"

	"github.com/duffy/usb-server/internal/usb"
	"golang.org/x/sys/windows"
	"golang.org/x/sys/windows/registry"
)

const usbShareServiceName = "usbshare"

func isPrivileged() bool {
	// An elevated process has the administrators group enabled in its token.
	var sid *windows.SID
	err := windows.AllocateAndInitializeSid(
		&windows.SECURITY_NT_AUTHORITY,
		2,
		windows.SECURITY_BUILTIN_DOMAIN_RID,
		windows.DOMAIN_ALIAS_RID_ADMINS,
		0, 0, 0, 0, 0, 0,
		&sid,
	)
	if err != nil {
		return false
	}
	defer windows.FreeSid(sid)

	token := windows.Token(0) // the process token
	member, err := token.IsMember(sid)
	return err == nil && member
}

func collectPlatform(r *Report) {
	r.System.OSVersion = windowsVersion()

	checkTestSigning(r)
	checkDriverService(r)
	checkDriverInterface(r)
	collectWindowsDevices(r)
	assessWindowsCapabilities(r)
}

// windowsVersion reads the build information from the registry, which does
// not lie about the version the way GetVersionEx does for unmanifested
// processes.
func windowsVersion() string {
	key, err := registry.OpenKey(registry.LOCAL_MACHINE,
		`SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
	if err != nil {
		return ""
	}
	defer key.Close()

	productName, _, _ := key.GetStringValue("ProductName")
	displayVersion, _, _ := key.GetStringValue("DisplayVersion")
	build, _, _ := key.GetStringValue("CurrentBuildNumber")
	ubr, _, _ := key.GetIntegerValue("UBR")

	parts := []string{productName}
	if displayVersion != "" {
		parts = append(parts, displayVersion)
	}
	if build != "" {
		if ubr > 0 {
			parts = append(parts, fmt.Sprintf("build %s.%d", build, ubr))
		} else {
			parts = append(parts, "build "+build)
		}
	}
	return strings.Join(parts, " ")
}

// checkTestSigning reports whether unsigned drivers may load.
//
// This is the single most common reason a freshly built driver does nothing:
// it is installed, the INF looks fine, and Windows silently refuses to load
// it because it is not signed by Microsoft.
func checkTestSigning(r *Report) {
	out, err := exec.Command("bcdedit", "/enum", "{current}").Output()
	if err != nil {
		r.addCheck("test signing", false,
			"could not read the boot configuration: "+err.Error(),
			"run this from an elevated command prompt")
		return
	}

	text := strings.ToLower(string(out))
	testSigning := strings.Contains(text, "testsigning") && strings.Contains(text, "yes")

	if testSigning {
		r.addCheck("test signing", true, "enabled — unsigned drivers may load", "")
	} else {
		r.addCheck("test signing", false,
			"disabled — Windows will refuse to load an unsigned driver, usually without any visible error",
			"bcdedit /set testsigning on   (then reboot; only do this on a test machine)")
	}
}

// checkDriverService reports whether the filter driver is registered and
// running.
func checkDriverService(r *Report) {
	manager, err := windows.OpenSCManager(nil, nil, windows.SC_MANAGER_CONNECT)
	if err != nil {
		r.addCheck("usbshare driver service", false,
			"could not open the service manager: "+err.Error(), "")
		return
	}
	defer windows.CloseServiceHandle(manager)

	namePtr, _ := windows.UTF16PtrFromString(usbShareServiceName)
	service, err := windows.OpenService(manager, namePtr, windows.SERVICE_QUERY_STATUS)
	if err != nil {
		r.addCheck("usbshare driver service", false,
			"not registered — the driver has not been installed",
			"right-click driver/windows/usbshare.inf and choose Install, then attach it "+
				"to a device in Device Manager")
		return
	}
	defer windows.CloseServiceHandle(service)

	var status windows.SERVICE_STATUS
	if err := windows.QueryServiceStatus(service, &status); err != nil {
		r.addCheck("usbshare driver service", false,
			"registered, but its status could not be read: "+err.Error(), "")
		return
	}

	switch status.CurrentState {
	case windows.SERVICE_RUNNING:
		r.addCheck("usbshare driver service", true, "registered and running", "")
	case windows.SERVICE_STOPPED:
		// A filter driver only starts when it is attached to a device, so
		// stopped is expected until then rather than an error in itself.
		r.addCheck("usbshare driver service", false,
			"registered but not running — normal until the filter is attached to a device",
			"attach the filter to a device in Device Manager, then replug it")
	default:
		r.addCheck("usbshare driver service", false,
			fmt.Sprintf("registered, service state %d", status.CurrentState), "")
	}
}

// checkDriverInterface reports whether any device exposes the filter's
// interface, which is what user mode actually needs.
func checkDriverInterface(r *Report) {
	devices, err := usb.Enumerate()
	if err != nil {
		r.addCheck("usbshare device interface", false,
			"no device exposes the interface: "+err.Error(),
			"the driver must be attached to a specific device, not just installed")
		return
	}

	if len(devices) == 0 {
		r.addCheck("usbshare device interface", false,
			"the driver is present but no device is attached to it",
			"in Device Manager, update the driver for the device you want to share")
		return
	}

	r.addCheck("usbshare device interface", true,
		fmt.Sprintf("%d device(s) reachable through the filter", len(devices)), "")
}

func collectWindowsDevices(r *Report) {
	devices, err := usb.Enumerate()
	if err != nil {
		r.note("device enumeration failed: %v", err)
		collectWindowsDevicesFallback(r)
		return
	}

	for _, dev := range devices {
		info := DeviceInfo{
			BusID:     dev.BusID,
			VendorID:  fmt.Sprintf("%04x", dev.VendorID),
			ProductID: fmt.Sprintf("%04x", dev.ProductID),
			Name:      dev.DisplayName(),
			Class:     fmt.Sprintf("%02x", dev.DeviceClass),
			Speed:     speedName(dev.Speed),
			Shareable: true,
		}

		for _, ep := range dev.Endpoints {
			info.Endpoints = append(info.Endpoints, endpointInfo(ep))
		}

		r.Devices = append(r.Devices, info)
	}

	// Everything the filter cannot see is still worth listing: it explains
	// why an expected device is absent.
	collectWindowsDevicesFallback(r)
}

// collectWindowsDevicesFallback lists all USB devices via PowerShell, whether
// or not the filter is attached.
//
// Shelling out is deliberate: reproducing this through SetupAPI would be a
// few hundred lines of syscall code for something that only ever runs when a
// human is already reading the output.
func collectWindowsDevicesFallback(r *Report) {
	cmd := exec.Command("powershell", "-NoProfile", "-Command",
		`Get-PnpDevice -Class USB -ErrorAction SilentlyContinue | `+
			`Select-Object -Property InstanceId,FriendlyName,Status,Service | `+
			`ForEach-Object { "$($_.InstanceId)|$($_.FriendlyName)|$($_.Status)|$($_.Service)" }`)

	out, err := cmd.Output()
	if err != nil {
		r.note("could not list USB devices via PowerShell: %v", err)
		return
	}

	seen := make(map[string]bool)
	for _, d := range r.Devices {
		seen[strings.ToLower(d.VendorID+":"+d.ProductID)] = true
	}

	for _, line := range strings.Split(string(out), "\n") {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}

		parts := strings.Split(line, "|")
		if len(parts) < 4 {
			continue
		}

		instanceID, friendly, status, service := parts[0], parts[1], parts[2], parts[3]
		vid, pid := parseVidPid(instanceID)
		if vid == "" {
			continue
		}
		if seen[strings.ToLower(vid+":"+pid)] {
			continue // already listed through the filter
		}

		blocker := "usbshare filter not attached"
		if !strings.EqualFold(status, "OK") {
			blocker = "device status: " + status
		}

		r.Devices = append(r.Devices, DeviceInfo{
			VendorID:  vid,
			ProductID: pid,
			Name:      friendly,
			Driver:    service,
			Shareable: false,
			Blocker:   blocker,
		})
	}
}

// parseVidPid pulls the IDs out of an instance ID such as
// USB\VID_046D&PID_C52B\5&1a2b3c4d&0&2.
func parseVidPid(instanceID string) (vid, pid string) {
	upper := strings.ToUpper(instanceID)

	if i := strings.Index(upper, "VID_"); i >= 0 && len(upper) >= i+8 {
		vid = strings.ToLower(upper[i+4 : i+8])
	}
	if i := strings.Index(upper, "PID_"); i >= 0 && len(upper) >= i+8 {
		pid = strings.ToLower(upper[i+4 : i+8])
	}
	return vid, pid
}

func assessWindowsCapabilities(r *Report) {
	shareable := 0
	for _, d := range r.Devices {
		if d.Shareable {
			shareable++
		}
	}

	if shareable > 0 {
		r.Sharing = Capability{
			Available: true,
			Mechanism: "usbshare filter driver",
		}
	} else {
		r.Sharing = Capability{
			Available: false,
			Reason:    "no device is attached to the usbshare filter driver",
			Mechanism: "usbshare filter driver",
		}
	}

	// The use side needs usbip-win2's VHCI driver, which is a separate
	// product with its own installer.
	if _, err := exec.LookPath("usbip"); err == nil {
		r.Using = Capability{Available: true, Mechanism: "usbip-win2 VHCI"}
	} else {
		r.Using = Capability{
			Available: false,
			Reason:    "usbip.exe not found",
			Mechanism: "usbip-win2 VHCI",
		}
		r.addCheck("usbip-win2", false,
			"not installed — receiving remote devices needs its VHCI driver",
			"install from https://github.com/vadimgrn/usbip-win2/releases")
	}
}
