package collector

import (
	"bee/audit/internal/schema"
	"fmt"
	"log/slog"
	"os/exec"
	"strconv"
	"strings"
)

func collectPCIe() []schema.HardwarePCIeDevice {
	out, err := exec.Command("lspci", "-vmm", "-D").Output()
	if err != nil {
		slog.Warn("pcie: lspci failed", "err", err)
		return nil
	}
	devs := parseLspci(string(out))
	slog.Info("pcie: collected", "count", len(devs))
	return devs
}

func parseLspci(output string) []schema.HardwarePCIeDevice {
	// lspci -vmm -D outputs blank-line separated records, each field is "Key:\tValue"
	var devs []schema.HardwarePCIeDevice
	for _, block := range strings.Split(output, "\n\n") {
		block = strings.TrimSpace(block)
		if block == "" {
			continue
		}
		fields := map[string]string{}
		for _, line := range strings.Split(block, "\n") {
			idx := strings.Index(line, ":\t")
			if idx < 0 {
				continue
			}
			key := strings.TrimSpace(line[:idx])
			val := strings.TrimSpace(line[idx+2:])
			fields[key] = val
		}
		if !shouldIncludePCIeDevice(fields["Class"], fields["Vendor"], fields["Device"]) {
			continue
		}
		dev := parseLspciDevice(fields)
		devs = append(devs, dev)
	}
	return devs
}

func shouldIncludePCIeDevice(class, vendor, device string) bool {
	c := strings.ToLower(strings.TrimSpace(class))
	v := strings.ToLower(strings.TrimSpace(vendor))
	d := strings.ToLower(strings.TrimSpace(device))
	if c == "" {
		return true
	}

	// Keep inventory focused on useful replaceable components, not chipset/virtual noise.
	excluded := []string{
		"host bridge",
		"isa bridge",
		"pci bridge",
		"co-processor",
		"performance counter",
		"performance counters",
		"ram memory",
		"system peripheral",
		"communication controller",
		"signal processing controller",
		"usb controller",
		"smbus",
		"audio device",
		"serial bus controller",
		"unassigned class",
		"non-essential instrumentation",
	}
	for _, bad := range excluded {
		if strings.Contains(c, bad) {
			return false
		}
	}

	// Exclude BMC/management virtual VGA adapters — these are firmware video chips,
	// not real GPUs, and pollute the GPU inventory (e.g. iBMC, iDRAC, iLO VGA).
	if strings.Contains(c, "vga") || strings.Contains(c, "display") || strings.Contains(c, "3d") {
		bmcPatterns := []string{
			"management system chip",
			"management controller",
			"ibmc",
			"idrac",
			"ilo vga",
			"aspeed",
			"matrox",
		}
		for _, bad := range bmcPatterns {
			if strings.Contains(d, bad) {
				return false
			}
		}
	}

	if strings.Contains(v, "advanced micro devices") || strings.Contains(v, "[amd]") {
		internalAMDPatterns := []string{
			"dummy function",
			"reserved spp",
			"ptdma",
			"cryptographic coprocessor pspcpp",
			"pspcpp",
		}
		for _, bad := range internalAMDPatterns {
			if strings.Contains(d, bad) {
				return false
			}
		}
	}
	return true
}

func parseLspciDevice(fields map[string]string) schema.HardwarePCIeDevice {
	dev := schema.HardwarePCIeDevice{}
	present := true
	dev.Present = &present
	status := statusOK
	dev.Status = &status

	// Slot is the BDF: "0000:00:02.0"
	if bdf := fields["Slot"]; bdf != "" {
		dev.Slot = &bdf
		dev.BDF = &bdf
		// parse vendor_id and device_id from sysfs
		vendorID, deviceID := readPCIIDs(bdf)
		if vendorID != 0 {
			dev.VendorID = &vendorID
		}
		if deviceID != 0 {
			dev.DeviceID = &deviceID
		}
		if numaNode, ok := readPCINumaNode(bdf); ok {
			dev.NUMANode = &numaNode
		} else if numaNode, ok := parsePCINumaNode(fields["NUMANode"]); ok {
			dev.NUMANode = &numaNode
		}
		if width, ok := readPCIIntAttribute(bdf, "current_link_width"); ok {
			dev.LinkWidth = &width
		}
		if width, ok := readPCIIntAttribute(bdf, "max_link_width"); ok {
			dev.MaxLinkWidth = &width
		}
		if speed, ok := readPCIStringAttribute(bdf, "current_link_speed"); ok {
			linkSpeed := normalizePCILinkSpeed(speed)
			if linkSpeed != "" {
				dev.LinkSpeed = &linkSpeed
			}
		}
		if speed, ok := readPCIStringAttribute(bdf, "max_link_speed"); ok {
			linkSpeed := normalizePCILinkSpeed(speed)
			if linkSpeed != "" {
				dev.MaxLinkSpeed = &linkSpeed
			}
		}
	}

	if v := fields["Class"]; v != "" {
		class := mapPCIeDeviceClass(v)
		dev.DeviceClass = &class
	}
	if v := fields["Vendor"]; v != "" {
		dev.Manufacturer = &v
	}
	if v := fields["Device"]; v != "" {
		dev.Model = &v
	}

	// SVendor/SDevice available but not in schema — skip

	// Warn if PCIe link is running below its maximum negotiated speed.
	applyPCIeLinkSpeedWarning(&dev)

	return dev
}

// readPCIIDs reads vendor and device IDs from sysfs for a given BDF.
func readPCIIDs(bdf string) (vendorID, deviceID int) {
	base := "/sys/bus/pci/devices/" + bdf
	if v, err := readHexFile(base + "/vendor"); err == nil {
		vendorID = v
	}
	if v, err := readHexFile(base + "/device"); err == nil {
		deviceID = v
	}
	return
}

func readHexFile(path string) (int, error) {
	out, err := exec.Command("cat", path).Output()
	if err != nil {
		return 0, err
	}
	s := strings.TrimSpace(strings.TrimPrefix(string(out), "0x"))
	n, err := strconv.ParseInt(s, 16, 64)
	return int(n), err
}

func readPCINumaNode(bdf string) (int, bool) {
	value, ok := readPCIIntAttribute(bdf, "numa_node")
	if !ok || value < 0 {
		return 0, false
	}
	return value, true
}

func parsePCINumaNode(raw string) (int, bool) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return 0, false
	}
	value, err := strconv.Atoi(raw)
	if err != nil || value < 0 {
		return 0, false
	}
	return value, true
}

func readPCIIntAttribute(bdf, attribute string) (int, bool) {
	out, err := exec.Command("cat", "/sys/bus/pci/devices/"+bdf+"/"+attribute).Output()
	if err != nil {
		return 0, false
	}
	value, err := strconv.Atoi(strings.TrimSpace(string(out)))
	if err != nil || value < 0 {
		return 0, false
	}
	return value, true
}

func readPCIStringAttribute(bdf, attribute string) (string, bool) {
	out, err := exec.Command("cat", "/sys/bus/pci/devices/"+bdf+"/"+attribute).Output()
	if err != nil {
		return "", false
	}
	value := strings.TrimSpace(string(out))
	if value == "" {
		return "", false
	}
	return value, true
}

// applyPCIeLinkSpeedWarning sets the device status to Warning if the current PCIe link
// speed is below the maximum negotiated speed supported by both ends.
func applyPCIeLinkSpeedWarning(dev *schema.HardwarePCIeDevice) {
	if dev.LinkSpeed == nil || dev.MaxLinkSpeed == nil {
		return
	}
	if pcieLinkSpeedRank(*dev.LinkSpeed) < pcieLinkSpeedRank(*dev.MaxLinkSpeed) {
		warn := statusWarning
		dev.Status = &warn
		desc := fmt.Sprintf("PCIe link speed degraded: running at %s, capable of %s", *dev.LinkSpeed, *dev.MaxLinkSpeed)
		dev.ErrorDescription = &desc
	}
}

// pcieLinkSpeedRank returns a numeric rank for a normalized Gen string (e.g. "Gen4" → 4).
// Returns 0 for unrecognised values so comparisons fail safe.
func pcieLinkSpeedRank(gen string) int {
	switch gen {
	case "Gen1":
		return 1
	case "Gen2":
		return 2
	case "Gen3":
		return 3
	case "Gen4":
		return 4
	case "Gen5":
		return 5
	case "Gen6":
		return 6
	default:
		return 0
	}
}

func normalizePCILinkSpeed(raw string) string {
	raw = strings.TrimSpace(strings.ToLower(raw))
	switch {
	case strings.Contains(raw, "2.5"):
		return "Gen1"
	case strings.Contains(raw, "5.0"):
		return "Gen2"
	case strings.Contains(raw, "8.0"):
		return "Gen3"
	case strings.Contains(raw, "16.0"):
		return "Gen4"
	case strings.Contains(raw, "32.0"):
		return "Gen5"
	case strings.Contains(raw, "64.0"):
		return "Gen6"
	default:
		return ""
	}
}
