package server

import (
	"fmt"
	"regexp"
	"strconv"
	"strings"

	"git.mchus.pro/mchus/logpile/internal/models"
)

type slotFirmwareInfo struct {
	Model    string
	Version  string
	Category string
}

var (
	psuFirmwareRe = regexp.MustCompile(`(?i)^PSU\s*([0-9A-Za-z_-]+)\s*(?:\(([^)]+)\))?$`)
	nicFirmwareRe = regexp.MustCompile(`(?i)^NIC\s+([^()]+?)\s*(?:\(([^)]+)\))?$`)
	gpuFirmwareRe = regexp.MustCompile(`(?i)^GPU\s+([^()]+?)\s*(?:\(([^)]+)\))?$`)
	nvsFirmwareRe = regexp.MustCompile(`(?i)^NVSwitch\s+([^()]+?)\s*(?:\(([^)]+)\))?$`)
)

func BuildHardwareDevices(hw *models.HardwareConfig) []models.HardwareDevice {
	if hw == nil {
		return nil
	}

	all := make([]models.HardwareDevice, 0, 1+len(hw.CPUs)+len(hw.Memory)+len(hw.Storage)+len(hw.PCIeDevices)+len(hw.GPUs)+len(hw.NetworkAdapters)+len(hw.PowerSupply))
	fwBySlot := buildFirmwareBySlot(hw.Firmware)
	nextID := 0
	add := func(d models.HardwareDevice) {
		d.ID = fmt.Sprintf("%s:%d", d.Kind, nextID)
		nextID++
		all = append(all, d)
	}

	add(models.HardwareDevice{
		Kind:         models.DeviceKindBoard,
		Source:       "board",
		Slot:         "board",
		Model:        strings.TrimSpace(hw.BoardInfo.ProductName),
		PartNumber:   strings.TrimSpace(hw.BoardInfo.PartNumber),
		Manufacturer: strings.TrimSpace(hw.BoardInfo.Manufacturer),
		SerialNumber: strings.TrimSpace(hw.BoardInfo.SerialNumber),
		Details: map[string]any{
			"description": strings.TrimSpace(hw.BoardInfo.Description),
			"version":     strings.TrimSpace(hw.BoardInfo.Version),
			"uuid":        strings.TrimSpace(hw.BoardInfo.UUID),
		},
	})

	for _, cpu := range hw.CPUs {
		add(models.HardwareDevice{
			Kind:             models.DeviceKindCPU,
			Source:           "cpus",
			Slot:             fmt.Sprintf("CPU%d", cpu.Socket),
			Model:            cpu.Model,
			SerialNumber:     cpu.SerialNumber,
			Cores:            cpu.Cores,
			Threads:          cpu.Threads,
			FrequencyMHz:     cpu.FrequencyMHz,
			MaxFreqMHz:       cpu.MaxFreqMHz,
			Status:           cpu.Status,
			StatusCheckedAt:  cpu.StatusCheckedAt,
			StatusChangedAt:  cpu.StatusChangedAt,
			StatusAtCollect:  cpu.StatusAtCollect,
			StatusHistory:    cpu.StatusHistory,
			ErrorDescription: cpu.ErrorDescription,
			Details: map[string]any{
				"description": cpu.Description,
				"socket":      cpu.Socket,
				"l1_cache_kb": cpu.L1CacheKB,
				"l2_cache_kb": cpu.L2CacheKB,
				"l3_cache_kb": cpu.L3CacheKB,
				"tdp_w":       cpu.TDP,
				"ppin":        cpu.PPIN,
			},
		})
	}

	for _, mem := range hw.Memory {
		if !mem.IsInstalledInventory() {
			continue
		}
		present := mem.Present
		add(models.HardwareDevice{
			Kind:             models.DeviceKindMemory,
			Source:           "memory",
			Slot:             mem.Slot,
			Location:         mem.Location,
			Manufacturer:     mem.Manufacturer,
			SerialNumber:     mem.SerialNumber,
			PartNumber:       mem.PartNumber,
			Type:             mem.Type,
			Present:          &present,
			SizeMB:           mem.SizeMB,
			Status:           mem.Status,
			StatusCheckedAt:  mem.StatusCheckedAt,
			StatusChangedAt:  mem.StatusChangedAt,
			StatusAtCollect:  mem.StatusAtCollect,
			StatusHistory:    mem.StatusHistory,
			ErrorDescription: mem.ErrorDescription,
			Details: map[string]any{
				"description":       mem.Description,
				"technology":        mem.Technology,
				"max_speed_mhz":     mem.MaxSpeedMHz,
				"current_speed_mhz": mem.CurrentSpeedMHz,
				"ranks":             mem.Ranks,
			},
		})
	}

	for _, stor := range hw.Storage {
		if !stor.Present {
			continue
		}
		present := stor.Present
		add(models.HardwareDevice{
			Kind:             models.DeviceKindStorage,
			Source:           "storage",
			Slot:             stor.Slot,
			Location:         stor.Location,
			Model:            stor.Model,
			Manufacturer:     stor.Manufacturer,
			SerialNumber:     stor.SerialNumber,
			Firmware:         stor.Firmware,
			Type:             stor.Type,
			Interface:        stor.Interface,
			Present:          &present,
			SizeGB:           stor.SizeGB,
			Status:           stor.Status,
			StatusCheckedAt:  stor.StatusCheckedAt,
			StatusChangedAt:  stor.StatusChangedAt,
			StatusAtCollect:  stor.StatusAtCollect,
			StatusHistory:    stor.StatusHistory,
			ErrorDescription: stor.ErrorDescription,
			Details: map[string]any{
				"description":  stor.Description,
				"backplane_id": stor.BackplaneID,
			},
		})
	}

	for _, p := range hw.PCIeDevices {
		if isEmptyPCIeDevice(p) {
			continue
		}
		slotKey := normalizeSlotKey(p.Slot)
		fwInfo := fwBySlot[slotKey]
		model := strings.TrimSpace(p.PartNumber)
		if model == "" {
			model = strings.TrimSpace(p.DeviceClass)
		}
		if model == "" {
			model = strings.TrimSpace(p.Description)
		}
		if model == "" && fwInfo.Model != "" {
			model = fwInfo.Model
		}
		add(models.HardwareDevice{
			Kind:             models.DeviceKindPCIe,
			Source:           "pcie_devices",
			Slot:             p.Slot,
			BDF:              p.BDF,
			DeviceClass:      p.DeviceClass,
			VendorID:         p.VendorID,
			DeviceID:         p.DeviceID,
			Model:            model,
			PartNumber:       p.PartNumber,
			Manufacturer:     p.Manufacturer,
			SerialNumber:     p.SerialNumber,
			Firmware:         fwInfo.Version,
			MACAddresses:     p.MACAddresses,
			LinkWidth:        p.LinkWidth,
			LinkSpeed:        p.LinkSpeed,
			MaxLinkWidth:     p.MaxLinkWidth,
			MaxLinkSpeed:     p.MaxLinkSpeed,
			Status:           p.Status,
			StatusCheckedAt:  p.StatusCheckedAt,
			StatusChangedAt:  p.StatusChangedAt,
			StatusAtCollect:  p.StatusAtCollect,
			StatusHistory:    p.StatusHistory,
			ErrorDescription: p.ErrorDescription,
			Details: map[string]any{
				"description": p.Description,
				"fw_category": fwInfo.Category,
			},
		})
	}

	for _, gpu := range hw.GPUs {
		add(models.HardwareDevice{
			Kind:             models.DeviceKindGPU,
			Source:           "gpus",
			Slot:             gpu.Slot,
			Location:         gpu.Location,
			BDF:              gpu.BDF,
			DeviceClass:      "DisplayController",
			VendorID:         gpu.VendorID,
			DeviceID:         gpu.DeviceID,
			Model:            gpu.Model,
			PartNumber:       gpu.PartNumber,
			Manufacturer:     gpu.Manufacturer,
			SerialNumber:     gpu.SerialNumber,
			Firmware:         gpu.Firmware,
			LinkWidth:        gpu.CurrentLinkWidth,
			LinkSpeed:        gpu.CurrentLinkSpeed,
			MaxLinkWidth:     gpu.MaxLinkWidth,
			MaxLinkSpeed:     gpu.MaxLinkSpeed,
			Status:           gpu.Status,
			StatusCheckedAt:  gpu.StatusCheckedAt,
			StatusChangedAt:  gpu.StatusChangedAt,
			StatusAtCollect:  gpu.StatusAtCollect,
			StatusHistory:    gpu.StatusHistory,
			ErrorDescription: gpu.ErrorDescription,
			Details: map[string]any{
				"description":     gpu.Description,
				"uuid":            gpu.UUID,
				"video_bios":      gpu.VideoBIOS,
				"irq":             gpu.IRQ,
				"bus_type":        gpu.BusType,
				"dma_size":        gpu.DMASize,
				"dma_mask":        gpu.DMAMask,
				"device_minor":    gpu.DeviceMinor,
				"temperature":     gpu.Temperature,
				"mem_temperature": gpu.MemTemperature,
				"power":           gpu.Power,
				"max_power":       gpu.MaxPower,
				"clock_speed":     gpu.ClockSpeed,
			},
		})
	}

	for _, nic := range hw.NetworkAdapters {
		if !nic.Present {
			continue
		}
		present := nic.Present
		add(models.HardwareDevice{
			Kind:             models.DeviceKindNetwork,
			Source:           "network_adapters",
			Slot:             nic.Slot,
			Location:         nic.Location,
			BDF:              nic.BDF,
			DeviceClass:      "NetworkController",
			VendorID:         nic.VendorID,
			DeviceID:         nic.DeviceID,
			Model:            nic.Model,
			PartNumber:       nic.PartNumber,
			Manufacturer:     nic.Vendor,
			SerialNumber:     nic.SerialNumber,
			Firmware:         nic.Firmware,
			PortCount:        nic.PortCount,
			PortType:         nic.PortType,
			MACAddresses:     nic.MACAddresses,
			LinkWidth:        nic.LinkWidth,
			LinkSpeed:        nic.LinkSpeed,
			MaxLinkWidth:     nic.MaxLinkWidth,
			MaxLinkSpeed:     nic.MaxLinkSpeed,
			NUMANode:         nic.NUMANode,
			Present:          &present,
			Status:           nic.Status,
			StatusCheckedAt:  nic.StatusCheckedAt,
			StatusChangedAt:  nic.StatusChangedAt,
			StatusAtCollect:  nic.StatusAtCollect,
			StatusHistory:    nic.StatusHistory,
			ErrorDescription: nic.ErrorDescription,
			Details: map[string]any{
				"description": nic.Description,
			},
		})
	}

	for _, psu := range hw.PowerSupply {
		if !psu.Present {
			continue
		}
		present := psu.Present
		add(models.HardwareDevice{
			Kind:             models.DeviceKindPSU,
			Source:           "power_supplies",
			Slot:             psu.Slot,
			Model:            psu.Model,
			PartNumber:       psu.PartNumber,
			Manufacturer:     psu.Vendor,
			SerialNumber:     psu.SerialNumber,
			Firmware:         psu.Firmware,
			Present:          &present,
			WattageW:         psu.WattageW,
			InputType:        psu.InputType,
			InputPowerW:      psu.InputPowerW,
			OutputPowerW:     psu.OutputPowerW,
			InputVoltage:     psu.InputVoltage,
			TemperatureC:     psu.TemperatureC,
			Status:           psu.Status,
			StatusCheckedAt:  psu.StatusCheckedAt,
			StatusChangedAt:  psu.StatusChangedAt,
			StatusAtCollect:  psu.StatusAtCollect,
			StatusHistory:    psu.StatusHistory,
			ErrorDescription: psu.ErrorDescription,
			Details: map[string]any{
				"description":    psu.Description,
				"output_voltage": psu.OutputVoltage,
			},
		})
	}

	return annotateDuplicateSerials(dedupeDevices(all))
}

func isEmptyPCIeDevice(p models.PCIeDevice) bool {
	if isNumericSlot(strings.TrimSpace(p.Slot)) &&
		strings.TrimSpace(p.BDF) == "" &&
		p.VendorID == 0 &&
		p.DeviceID == 0 &&
		normalizedSerial(p.SerialNumber) == "" &&
		!hasMeaningfulText(p.PartNumber) &&
		!hasMeaningfulText(p.Manufacturer) &&
		!hasMeaningfulText(p.Description) &&
		len(p.MACAddresses) == 0 &&
		p.LinkWidth == 0 &&
		p.MaxLinkWidth == 0 {
		return true
	}

	if strings.TrimSpace(p.BDF) != "" {
		return false
	}
	if p.VendorID != 0 || p.DeviceID != 0 {
		return false
	}
	if normalizedSerial(p.SerialNumber) != "" {
		return false
	}
	if hasMeaningfulText(p.PartNumber) {
		return false
	}
	if hasMeaningfulText(p.Manufacturer) {
		return false
	}
	if hasMeaningfulText(p.Description) {
		return false
	}
	if strings.TrimSpace(p.DeviceClass) != "" {
		class := strings.ToLower(strings.TrimSpace(p.DeviceClass))
		if class != "unknown" && class != "other" && class != "pcie device" {
			return false
		}
	}
	return true
}

func isNumericSlot(slot string) bool {
	if slot == "" {
		return false
	}
	for _, r := range slot {
		if r < '0' || r > '9' {
			return false
		}
	}
	return true
}

func hasMeaningfulText(v string) bool {
	s := strings.ToLower(strings.TrimSpace(v))
	if s == "" {
		return false
	}
	switch s {
	case "-", "n/a", "na", "none", "null", "unknown":
		return false
	default:
		return true
	}
}

func dedupeDevices(items []models.HardwareDevice) []models.HardwareDevice {
	if len(items) < 2 {
		return items
	}
	parent := make([]int, len(items))
	for i := range parent {
		parent[i] = i
	}
	find := func(x int) int {
		for parent[x] != x {
			parent[x] = parent[parent[x]]
			x = parent[x]
		}
		return x
	}
	union := func(a, b int) {
		ra := find(a)
		rb := find(b)
		if ra != rb {
			parent[rb] = ra
		}
	}

	for i := 0; i < len(items); i++ {
		for j := i + 1; j < len(items); j++ {
			if shouldMergeDevices(items[i], items[j]) {
				union(i, j)
			}
		}
	}

	groups := make(map[int][]int, len(items))
	order := make([]int, 0, len(items))
	for i := range items {
		root := find(i)
		if _, ok := groups[root]; !ok {
			order = append(order, root)
		}
		groups[root] = append(groups[root], i)
	}

	out := make([]models.HardwareDevice, 0, len(order))
	for _, root := range order {
		indices := groups[root]
		bestIdx := indices[0]
		bestScore := qualityScore(items[bestIdx])
		for _, idx := range indices[1:] {
			if s := qualityScore(items[idx]); s > bestScore {
				bestIdx = idx
				bestScore = s
			}
		}
		merged := items[bestIdx]
		for _, idx := range indices {
			if idx == bestIdx {
				continue
			}
			merged = mergeDevices(merged, items[idx])
		}
		out = append(out, merged)
	}

	for i := range out {
		out[i].ID = out[i].Kind + ":" + strconv.Itoa(i)
	}
	return out
}

func shouldMergeDevices(a, b models.HardwareDevice) bool {
	aSN := strings.ToLower(normalizedSerial(a.SerialNumber))
	bSN := strings.ToLower(normalizedSerial(b.SerialNumber))
	aBDF := strings.ToLower(strings.TrimSpace(a.BDF))
	bBDF := strings.ToLower(strings.TrimSpace(b.BDF))
	aSlot := normalizeSlot(a.Slot)
	bSlot := normalizeSlot(b.Slot)

	// Memory DIMMs can legitimately share serial number in some dumps.
	// Never merge DIMMs with different slots.
	if a.Kind == models.DeviceKindMemory && b.Kind == models.DeviceKindMemory {
		if aSlot != "" && bSlot != "" && aSlot != bSlot {
			return false
		}
	}

	// Hard conflicts.
	if aSN != "" && bSN != "" && aSN == bSN {
		if a.Kind == models.DeviceKindMemory && b.Kind == models.DeviceKindMemory {
			return aSlot != "" && bSlot != "" && aSlot == bSlot
		}
		return true
	}
	if aSN != "" && bSN != "" && aSN != bSN {
		return false
	}
	if aBDF != "" && bBDF != "" && aBDF != bBDF {
		return false
	}

	// Strong identities.
	if aBDF != "" && aBDF == bBDF {
		return true
	}

	// If both have no strong IDs, be conservative.
	if aSN == "" && bSN == "" && aBDF == "" && bBDF == "" {
		if hasMACOverlap(a.MACAddresses, b.MACAddresses) {
			return true
		}
		if aSlot != "" && aSlot == bSlot {
			return true
		}
		return false
	}

	score := 0
	if samePCIID(a, b) {
		score += 4
	}
	if sameModel(a, b) {
		score += 3
	}
	if sameManufacturer(a, b) {
		score += 2
	}
	if aSlot != "" && aSlot == bSlot {
		score += 2
	}
	if hasMACOverlap(a.MACAddresses, b.MACAddresses) {
		score += 2
	}
	if sameKindFamily(a.Kind, b.Kind) {
		score++
	}
	if samePCIID(a, b) && ((aBDF != "" && bBDF == "") || (aBDF == "" && bBDF != "")) {
		score += 2
	}

	return score >= 7
}

func mergeDevices(primary, secondary models.HardwareDevice) models.HardwareDevice {
	fillString := func(dst *string, src string) {
		if strings.TrimSpace(*dst) == "" && strings.TrimSpace(src) != "" {
			*dst = src
		}
	}
	fillInt := func(dst *int, src int) {
		if *dst == 0 && src != 0 {
			*dst = src
		}
	}
	fillFloat := func(dst *float64, src float64) {
		if *dst == 0 && src != 0 {
			*dst = src
		}
	}

	fillString(&primary.ID, secondary.ID)
	fillString(&primary.Kind, secondary.Kind)
	fillString(&primary.Source, secondary.Source)
	fillString(&primary.Slot, secondary.Slot)
	fillString(&primary.Location, secondary.Location)
	fillString(&primary.BDF, secondary.BDF)
	fillString(&primary.DeviceClass, secondary.DeviceClass)
	fillInt(&primary.VendorID, secondary.VendorID)
	fillInt(&primary.DeviceID, secondary.DeviceID)
	fillString(&primary.Model, secondary.Model)
	fillString(&primary.PartNumber, secondary.PartNumber)
	fillString(&primary.Manufacturer, secondary.Manufacturer)
	fillString(&primary.SerialNumber, secondary.SerialNumber)
	fillString(&primary.Firmware, secondary.Firmware)
	fillString(&primary.Type, secondary.Type)
	fillString(&primary.Interface, secondary.Interface)
	if primary.Present == nil && secondary.Present != nil {
		primary.Present = secondary.Present
	}
	fillInt(&primary.SizeMB, secondary.SizeMB)
	fillInt(&primary.SizeGB, secondary.SizeGB)
	fillInt(&primary.Cores, secondary.Cores)
	fillInt(&primary.Threads, secondary.Threads)
	fillInt(&primary.FrequencyMHz, secondary.FrequencyMHz)
	fillInt(&primary.MaxFreqMHz, secondary.MaxFreqMHz)
	fillInt(&primary.PortCount, secondary.PortCount)
	fillString(&primary.PortType, secondary.PortType)
	if len(primary.MACAddresses) == 0 && len(secondary.MACAddresses) > 0 {
		primary.MACAddresses = secondary.MACAddresses
	}
	fillInt(&primary.LinkWidth, secondary.LinkWidth)
	fillString(&primary.LinkSpeed, secondary.LinkSpeed)
	fillInt(&primary.MaxLinkWidth, secondary.MaxLinkWidth)
	fillString(&primary.MaxLinkSpeed, secondary.MaxLinkSpeed)
	fillInt(&primary.WattageW, secondary.WattageW)
	fillString(&primary.InputType, secondary.InputType)
	fillInt(&primary.InputPowerW, secondary.InputPowerW)
	fillInt(&primary.OutputPowerW, secondary.OutputPowerW)
	fillFloat(&primary.InputVoltage, secondary.InputVoltage)
	fillInt(&primary.TemperatureC, secondary.TemperatureC)
	fillString(&primary.Status, secondary.Status)
	if primary.StatusCheckedAt == nil && secondary.StatusCheckedAt != nil {
		primary.StatusCheckedAt = secondary.StatusCheckedAt
	}
	if primary.StatusChangedAt == nil && secondary.StatusChangedAt != nil {
		primary.StatusChangedAt = secondary.StatusChangedAt
	}
	if primary.StatusAtCollect == nil && secondary.StatusAtCollect != nil {
		primary.StatusAtCollect = secondary.StatusAtCollect
	}
	if len(primary.StatusHistory) == 0 && len(secondary.StatusHistory) > 0 {
		primary.StatusHistory = secondary.StatusHistory
	}
	fillString(&primary.ErrorDescription, secondary.ErrorDescription)
	if primary.Details == nil && secondary.Details != nil {
		primary.Details = secondary.Details
	}
	return primary
}

func samePCIID(a, b models.HardwareDevice) bool {
	if (a.VendorID == 0 && a.DeviceID == 0) || (b.VendorID == 0 && b.DeviceID == 0) {
		return false
	}
	return a.VendorID == b.VendorID && a.DeviceID == b.DeviceID
}

func sameModel(a, b models.HardwareDevice) bool {
	am := normalizeText(coalesce(a.Model, a.PartNumber, a.DeviceClass))
	bm := normalizeText(coalesce(b.Model, b.PartNumber, b.DeviceClass))
	return am != "" && am == bm
}

func sameManufacturer(a, b models.HardwareDevice) bool {
	am := normalizeText(a.Manufacturer)
	bm := normalizeText(b.Manufacturer)
	return am != "" && am == bm
}

func hasMACOverlap(a, b []string) bool {
	if len(a) == 0 || len(b) == 0 {
		return false
	}
	set := make(map[string]struct{}, len(a))
	for _, mac := range a {
		key := normalizeText(mac)
		if key != "" {
			set[key] = struct{}{}
		}
	}
	for _, mac := range b {
		if _, ok := set[normalizeText(mac)]; ok {
			return true
		}
	}
	return false
}

func sameKindFamily(a, b string) bool {
	if a == b {
		return true
	}
	family := map[string]bool{
		models.DeviceKindPCIe:    true,
		models.DeviceKindGPU:     true,
		models.DeviceKindNetwork: true,
	}
	return family[a] && family[b]
}

func normalizeText(v string) string {
	s := strings.ToLower(strings.TrimSpace(v))
	s = strings.ReplaceAll(s, " ", "")
	s = strings.ReplaceAll(s, "_", "")
	s = strings.ReplaceAll(s, "-", "")
	return s
}

func normalizeSlot(slot string) string {
	return normalizeText(slot)
}

func qualityScore(d models.HardwareDevice) int {
	score := 0
	if normalizedSerial(d.SerialNumber) != "" {
		score += 6
	}
	if strings.TrimSpace(d.BDF) != "" {
		score += 4
	}
	if strings.TrimSpace(d.Model) != "" {
		score += 3
	}
	if strings.TrimSpace(d.Firmware) != "" {
		score += 2
	}
	if strings.TrimSpace(d.Status) != "" {
		score++
	}
	return score
}

func normalizedSerial(serial string) string {
	s := strings.TrimSpace(serial)
	if s == "" {
		return ""
	}
	switch strings.ToUpper(s) {
	case "N/A", "NA", "NONE", "NULL", "UNKNOWN", "-":
		return ""
	default:
		return s
	}
}

func buildFirmwareBySlot(firmware []models.FirmwareInfo) map[string]slotFirmwareInfo {
	out := make(map[string]slotFirmwareInfo)
	add := func(slot, model, version, category string) {
		key := normalizeSlotKey(slot)
		if key == "" || strings.TrimSpace(version) == "" {
			return
		}
		existing, ok := out[key]
		if ok && strings.TrimSpace(existing.Model) != "" {
			return
		}
		out[key] = slotFirmwareInfo{
			Model:    strings.TrimSpace(model),
			Version:  strings.TrimSpace(version),
			Category: category,
		}
	}

	for _, fw := range firmware {
		name := strings.TrimSpace(fw.DeviceName)
		if name == "" {
			continue
		}
		if m := psuFirmwareRe.FindStringSubmatch(name); len(m) == 3 {
			model := strings.TrimSpace(m[2])
			if model == "" {
				model = "PSU"
			}
			add(m[1], model, fw.Version, "psu")
			continue
		}
		if m := nicFirmwareRe.FindStringSubmatch(name); len(m) == 3 {
			model := strings.TrimSpace(m[2])
			if model == "" {
				model = "NIC"
			}
			add(m[1], model, fw.Version, "nic")
			continue
		}
		if m := gpuFirmwareRe.FindStringSubmatch(name); len(m) == 3 {
			model := strings.TrimSpace(m[2])
			if model == "" {
				model = "GPU"
			}
			add(m[1], model, fw.Version, "gpu")
			continue
		}
		if m := nvsFirmwareRe.FindStringSubmatch(name); len(m) == 3 {
			model := strings.TrimSpace(m[2])
			if model == "" {
				model = "NVSwitch"
			}
			add(m[1], model, fw.Version, "nvswitch")
			continue
		}
	}

	return out
}

func normalizeSlotKey(slot string) string {
	return strings.ToLower(strings.TrimSpace(slot))
}

func annotateDuplicateSerials(items []models.HardwareDevice) []models.HardwareDevice {
	if len(items) < 2 {
		return items
	}

	countByKindSerial := make(map[string]int)
	for _, d := range items {
		serial := normalizedSerial(d.SerialNumber)
		if serial == "" {
			continue
		}
		key := d.Kind + "|" + strings.ToLower(serial)
		countByKindSerial[key]++
	}

	seenByKindSerial := make(map[string]int)
	for i := range items {
		serial := normalizedSerial(items[i].SerialNumber)
		if serial == "" {
			continue
		}
		key := items[i].Kind + "|" + strings.ToLower(serial)
		if countByKindSerial[key] < 2 {
			continue
		}
		seenByKindSerial[key]++
		items[i].SerialNumber = serial + " (DUP#" + strconv.Itoa(seenByKindSerial[key]) + ")"
	}

	return items
}
