//go:build darwin

package usb

import (
	"encoding/json"
	"fmt"
	"os/exec"
	"strconv"
	"strings"
)

// Enumerate lists USB devices on macOS via system_profiler.
//
// This is enough to see and report what is attached, which is what the
// diagnostics need. It is not enough to share anything: that requires opening
// devices through IOKit, which has no equivalent here — see the platform
// table in the README.
//
// Going through the command keeps the client cgo-free and therefore
// cross-compilable from any machine.
func Enumerate() ([]Device, error) {
	if external := ExternalDevices(); len(external) > 0 {
		// Devices handed in from outside are usable; report them first.
		return external, nil
	}

	out, err := exec.Command("system_profiler", "-json", "SPUSBDataType").Output()
	if err != nil {
		return nil, fmt.Errorf("running system_profiler: %w", err)
	}

	var report struct {
		Items []spUSBItem `json:"SPUSBDataType"`
	}
	if err := json.Unmarshal(out, &report); err != nil {
		return nil, fmt.Errorf("parsing system_profiler output: %w", err)
	}

	var devices []Device
	for _, item := range report.Items {
		collectItem(&devices, item)
	}

	return devices, nil
}

type spUSBItem struct {
	Name         string      `json:"_name"`
	VendorID     string      `json:"vendor_id"`
	ProductID    string      `json:"product_id"`
	Speed        string      `json:"device_speed"`
	Manufacturer string      `json:"manufacturer"`
	SerialNumber string      `json:"serial_num"`
	LocationID   string      `json:"location_id"`
	Items        []spUSBItem `json:"_items"`
}

func collectItem(devices *[]Device, item spUSBItem) {
	if item.VendorID != "" {
		dev := Device{
			BusID:        locationToBusID(item.LocationID),
			VendorID:     parseHexID(item.VendorID),
			ProductID:    parseHexID(item.ProductID),
			Speed:        parseSpeedName(item.Speed),
			Manufacturer: item.Manufacturer,
			Product:      item.Name,
			Serial:       item.SerialNumber,
		}
		*devices = append(*devices, dev)
	}

	for _, child := range item.Items {
		collectItem(devices, child)
	}
}

// parseHexID turns "0x046d  (Logitech Inc.)" into 0x046d.
func parseHexID(id string) uint16 {
	id = strings.TrimSpace(id)
	if i := strings.Index(id, " "); i > 0 {
		id = id[:i]
	}
	id = strings.TrimPrefix(id, "0x")

	v, err := strconv.ParseUint(id, 16, 16)
	if err != nil {
		return 0
	}
	return uint16(v)
}

// locationToBusID derives an identifier from the location ID, which encodes
// the device's position in the port tree.
func locationToBusID(locationID string) string {
	locationID = strings.TrimSpace(locationID)
	if i := strings.Index(locationID, " "); i > 0 {
		locationID = locationID[:i]
	}
	return strings.TrimPrefix(locationID, "0x")
}

// parseSpeedName maps system_profiler's wording onto USB/IP speed codes.
func parseSpeedName(speed string) uint32 {
	switch {
	case strings.Contains(speed, "low_speed"):
		return 1
	case strings.Contains(speed, "full_speed"):
		return 2
	case strings.Contains(speed, "high_speed"):
		return 3
	case strings.Contains(speed, "super_speed_plus"):
		return 6
	case strings.Contains(speed, "super_speed"):
		return 5
	default:
		return 0
	}
}
