//go:build linux

package diag

import (
	"fmt"
	"os"
	"strings"

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

func isPrivileged() bool { return os.Geteuid() == 0 }

func collectPlatform(r *Report) {
	r.System.KernelVersion = kernelVersion()

	checkUsbdevfs(r)
	checkVHCI(r)
	collectLinuxDevices(r)
	assessLinuxCapabilities(r)
}

func kernelVersion() string {
	var uname unix.Utsname
	if err := unix.Uname(&uname); err != nil {
		return ""
	}
	return fmt.Sprintf("%s %s",
		nullTerminated(uname.Sysname[:]), nullTerminated(uname.Release[:]))
}

func nullTerminated(b []byte) string {
	if i := strings.IndexByte(string(b), 0); i >= 0 {
		return string(b[:i])
	}
	return string(b)
}

// checkUsbdevfs verifies that device nodes exist and are usable.
//
// Being able to list devices through sysfs proves nothing: sharing needs to
// open the node under /dev/bus/usb, and that is where permissions bite.
func checkUsbdevfs(r *Report) {
	if _, err := os.Stat("/dev/bus/usb"); err != nil {
		detail := "/dev/bus/usb is missing"
		fix := "check that usbcore is loaded and devtmpfs is mounted"
		if r.System.Container {
			detail += " — this is a container, so it was probably not passed through"
			fix = "add   - /dev/bus/usb:/dev/bus/usb   to the container's volumes, and run it privileged"
		}
		r.addCheck("usbdevfs device nodes", false, detail, fix)
		return
	}

	if _, err := os.Stat("/sys/bus/usb/devices"); err != nil {
		r.addCheck("usbdevfs device nodes", false,
			"/sys/bus/usb is not mounted, so devices cannot be enumerated",
			"mount sysfs, or in a container add   - /sys/bus/usb:/sys/bus/usb")
		return
	}

	r.addCheck("usbdevfs device nodes", true, "/dev/bus/usb and /sys/bus/usb are present", "")

	if !isPrivileged() {
		r.addCheck("privileges", false,
			"not running as root — devices can be listed but not claimed",
			"run the client with sudo, or install it as a system service")
	} else {
		r.addCheck("privileges", true, "running as root", "")
	}
}

// checkVHCI verifies the kernel module needed to receive remote devices.
func checkVHCI(r *Report) {
	if _, err := os.Stat("/sys/devices/platform/vhci_hcd.0"); err == nil {
		r.addCheck("vhci-hcd module", true, "loaded — remote devices can be attached", "")
		return
	}

	detail := "not loaded — remote devices cannot be attached"
	if r.System.Container {
		detail += " (a container cannot load modules; this must happen on the host)"
	}

	r.addCheck("vhci-hcd module", false, detail,
		"sudo modprobe vhci-hcd    (persist with: echo vhci-hcd | sudo tee /etc/modules-load.d/vhci-hcd.conf)")
}

func collectLinuxDevices(r *Report) {
	devices, err := usb.Enumerate()
	if err != nil {
		r.note("device enumeration failed: %v", err)
		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),
		}

		if len(dev.Interfaces) > 0 {
			info.Driver = dev.Interfaces[0].Driver
		}

		// Sharing needs write access to the node, so test exactly that.
		if err := unix.Access(dev.DevPath, unix.R_OK|unix.W_OK); err != nil {
			info.Shareable = false
			info.Blocker = fmt.Sprintf("no write access to %s (%v)", dev.DevPath, err)
		} else {
			info.Shareable = true
		}

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

		// An empty endpoint map means the raw descriptors could not be read,
		// which is what makes transfer types guesswork later.
		if len(dev.Endpoints) == 0 {
			r.note("no endpoint descriptors for %s — could not read %s; "+
				"transfer types will be guessed from the request interval",
				dev.BusID, dev.DevPath)
		}

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

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

	switch {
	case shareable > 0:
		r.Sharing = Capability{Available: true, Mechanism: "usbdevfs"}
	case len(r.Devices) > 0:
		r.Sharing = Capability{
			Available: false,
			Reason:    "devices found, but none can be opened (permissions)",
			Mechanism: "usbdevfs",
		}
	default:
		r.Sharing = Capability{
			Available: false,
			Reason:    "no USB devices found",
			Mechanism: "usbdevfs",
		}
	}

	if _, err := os.Stat("/sys/devices/platform/vhci_hcd.0"); err == nil {
		r.Using = Capability{Available: true, Mechanism: "vhci-hcd"}
	} else {
		r.Using = Capability{
			Available: false,
			Reason:    "vhci-hcd is not loaded",
			Mechanism: "vhci-hcd",
		}
	}
}
