//go:build linux

package usb

import (
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
)

const sysfsUSBDevices = "/sys/bus/usb/devices"

// Enumerate lists all USB devices by reading sysfs, plus any device that was
// registered from outside the process (see RegisterExternalDevice).
func Enumerate() ([]Device, error) {
	entries, err := os.ReadDir(sysfsUSBDevices)
	if err != nil {
		// On Android sysfs is not readable by an app, but devices handed in
		// through the bridge still work. Only report a failure when there is
		// nothing at all to go on.
		if external := ExternalDevices(); len(external) > 0 {
			return external, nil
		}
		return nil, fmt.Errorf("reading sysfs: %w", err)
	}

	var devices []Device
	for _, entry := range entries {
		name := entry.Name()

		// Skip interfaces (contain ":") and "usb*" root hubs
		if strings.Contains(name, ":") || strings.HasPrefix(name, "usb") {
			continue
		}

		// Must be a device path like "1-1", "1-1.4", "2-3", etc.
		if !isDevicePath(name) {
			continue
		}

		dev, err := readDevice(name)
		if err != nil {
			continue // skip devices we can't read
		}

		// Skip hubs
		if dev.IsHub() {
			continue
		}

		devices = append(devices, *dev)
	}

	return mergeExternal(devices), nil
}

func isDevicePath(name string) bool {
	// Device paths look like "1-1", "1-1.4", "2-3.1.2"
	// First char is a digit (bus number)
	if len(name) < 3 {
		return false
	}
	if name[0] < '1' || name[0] > '9' {
		return false
	}
	if name[1] != '-' {
		return false
	}
	return true
}

func readDevice(busID string) (*Device, error) {
	sysPath := filepath.Join(sysfsUSBDevices, busID)

	dev := &Device{
		BusID:   busID,
		SysPath: sysPath,
	}

	// Read basic attributes
	dev.BusNum = readUint32(sysPath, "busnum")
	dev.DevNum = readUint32(sysPath, "devnum")
	dev.Speed = parseSpeed(readString(sysPath, "speed"))
	dev.VendorID = readHex16(sysPath, "idVendor")
	dev.ProductID = readHex16(sysPath, "idProduct")
	dev.BcdDevice = readHex16(sysPath, "bcdDevice")
	dev.DeviceClass = readHex8(sysPath, "bDeviceClass")
	dev.DeviceSubClass = readHex8(sysPath, "bDeviceSubClass")
	dev.DeviceProtocol = readHex8(sysPath, "bDeviceProtocol")
	dev.ConfigValue = uint8(readUint32(sysPath, "bConfigurationValue"))
	dev.NumConfigs = uint8(readUint32(sysPath, "bNumConfigurations"))

	// Read string descriptors
	dev.Manufacturer = readString(sysPath, "manufacturer")
	dev.Product = readString(sysPath, "product")
	dev.Serial = readString(sysPath, "serial")

	// Compute dev path
	dev.DevPath = fmt.Sprintf("/dev/bus/usb/%03d/%03d", dev.BusNum, dev.DevNum)

	// Read interfaces from sysfs. This gives us the bound kernel driver per
	// interface, which the raw descriptors don't contain.
	dev.Interfaces = readInterfaces(sysPath, busID)

	// Overlay the raw descriptors from the usbdevfs file. Only these expose
	// interface alternate settings and correct endpoint attributes; sysfs
	// shows just the active alternate setting. Without the non-zero alternate
	// settings the endpoint type map is wrong for webcams and audio devices.
	applyRawDescriptors(dev)

	return dev, nil
}

// applyRawDescriptors reads the device's descriptor blob from its usbdevfs
// file and fills in Endpoints plus any interface data sysfs did not provide.
// Failure is not fatal: reading /dev/bus/usb requires permissions we may not
// have when merely listing devices, and the sysfs data alone is enough for
// that. Sharing a device opens the same file anyway and would fail earlier.
func applyRawDescriptors(dev *Device) {
	data, err := os.ReadFile(dev.DevPath)
	if err != nil {
		return
	}

	pd, err := ParseDescriptors(data)
	if err != nil {
		return
	}

	cfg := pd.FindConfig(dev.ConfigValue)
	if cfg == nil {
		// The device is unconfigured, or sysfs and the descriptors disagree.
		// Fall back to the first configuration.
		if len(pd.Configs) == 0 {
			return
		}
		cfg = &pd.Configs[0]
	}

	dev.Endpoints = cfg.AllEndpoints()

	// Merge: keep the driver names from sysfs, take everything else from the
	// descriptors (which are authoritative and include endpoint intervals).
	drivers := make(map[uint8]string, len(dev.Interfaces))
	for _, iface := range dev.Interfaces {
		drivers[iface.Number] = iface.Driver
	}

	ifaces := cfg.ActiveInterfaces()
	for i := range ifaces {
		ifaces[i].Driver = drivers[ifaces[i].Number]
	}
	if len(ifaces) > 0 {
		dev.Interfaces = ifaces
	}
}

func readInterfaces(sysPath, busID string) []Interface {
	entries, err := os.ReadDir(sysPath)
	if err != nil {
		return nil
	}

	var ifaces []Interface
	for _, entry := range entries {
		name := entry.Name()
		// Interface directories look like "1-1.4:1.0"
		if !strings.HasPrefix(name, busID+":") {
			continue
		}

		ifacePath := filepath.Join(sysPath, name)
		iface := Interface{
			Number:   readHex8(ifacePath, "bInterfaceNumber"),
			Class:    readHex8(ifacePath, "bInterfaceClass"),
			SubClass: readHex8(ifacePath, "bInterfaceSubClass"),
			Protocol: readHex8(ifacePath, "bInterfaceProtocol"),
		}

		// Read driver
		driverLink, err := os.Readlink(filepath.Join(ifacePath, "driver"))
		if err == nil {
			iface.Driver = filepath.Base(driverLink)
		}

		// Read endpoints
		iface.Endpoints = readEndpoints(ifacePath)

		ifaces = append(ifaces, iface)
	}

	return ifaces
}

func readEndpoints(ifacePath string) []Endpoint {
	entries, err := os.ReadDir(ifacePath)
	if err != nil {
		return nil
	}

	var eps []Endpoint
	for _, entry := range entries {
		name := entry.Name()
		if !strings.HasPrefix(name, "ep_") || name == "ep_00" {
			continue
		}

		epPath := filepath.Join(ifacePath, name)

		// Every numeric endpoint attribute in sysfs is hex, without a 0x
		// prefix — wMaxPacketSize "0040" means 64, not 40.
		eps = append(eps, Endpoint{
			Address:       readHex8(epPath, "bEndpointAddress"),
			TransferType:  readHex8(epPath, "bmAttributes") & 0x03,
			MaxPacketSize: readHex16(epPath, "wMaxPacketSize"),
			Interval:      readHex8(epPath, "bInterval"),
		})
	}

	return eps
}

func readString(dir, attr string) string {
	data, err := os.ReadFile(filepath.Join(dir, attr))
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(data))
}

func readUint32(dir, attr string) uint32 {
	s := readString(dir, attr)
	if s == "" {
		return 0
	}
	v, _ := strconv.ParseUint(s, 10, 32)
	return uint32(v)
}

func readHex16(dir, attr string) uint16 {
	s := readString(dir, attr)
	if s == "" {
		return 0
	}
	v, _ := strconv.ParseUint(s, 16, 16)
	return uint16(v)
}

func readHex8(dir, attr string) uint8 {
	s := readString(dir, attr)
	if s == "" {
		return 0
	}
	v, _ := strconv.ParseUint(s, 16, 8)
	return uint8(v)
}

func parseSpeed(s string) uint32 {
	switch s {
	case "1.5":
		return 1 // Low
	case "12":
		return 2 // Full
	case "480":
		return 3 // High
	case "5000":
		return 5 // Super
	case "10000":
		return 6 // Super+
	case "20000":
		return 6 // Super+ (USB 3.2 2x1)
	default:
		return 0 // Unknown
	}
}
