// Package dell provides parser for Dell TSR archives.
package dell

import (
	"archive/zip"
	"bytes"
	"encoding/json"
	"encoding/xml"
	"fmt"
	"io"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"

	"git.mchus.pro/mchus/logpile/internal/models"
	"git.mchus.pro/mchus/logpile/internal/parser"
	"git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids"
)

const parserVersion = "3.0"

func init() {
	parser.Register(&Parser{})
}

// Parser implements VendorParser for Dell TSR archives.
type Parser struct{}

func (p *Parser) Name() string    { return "Dell TSR Parser" }
func (p *Parser) Vendor() string  { return "dell" }
func (p *Parser) Version() string { return parserVersion }

func (p *Parser) Detect(files []parser.ExtractedFile) int {
	confidence := 0
	expanded := expandNestedZipFiles(files)
	for _, f := range expanded {
		path := strings.ToLower(strings.TrimSpace(f.Path))
		switch {
		case strings.HasSuffix(path, ".pl.zip"):
			confidence += 40
		case strings.HasSuffix(path, "tsr/metadata.json") || strings.HasSuffix(path, "/metadata.json"):
			confidence += 25
		case strings.HasSuffix(path, "sysinfo_dcim_view.xml"):
			confidence += 30
		case strings.HasSuffix(path, "sysinfo_dcim_softwareidentity.xml"):
			confidence += 20
		case strings.HasSuffix(path, "curr_lclog.xml"):
			confidence += 10
		case path == "signature":
			confidence += 5
		}
		if confidence >= 100 {
			return 100
		}
	}
	return confidence
}

func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, error) {
	expanded := expandNestedZipFiles(files)

	result := &models.AnalysisResult{
		Events:  make([]models.Event, 0),
		FRU:     make([]models.FRUInfo, 0),
		Sensors: make([]models.SensorReading, 0),
		Hardware: &models.HardwareConfig{
			Firmware:        make([]models.FirmwareInfo, 0),
			CPUs:            make([]models.CPU, 0),
			Memory:          make([]models.MemoryDIMM, 0),
			Storage:         make([]models.Storage, 0),
			Volumes:         make([]models.StorageVolume, 0),
			PCIeDevices:     make([]models.PCIeDevice, 0),
			GPUs:            make([]models.GPU, 0),
			NetworkAdapters: make([]models.NetworkAdapter, 0),
			NetworkCards:    make([]models.NIC, 0),
			PowerSupply:     make([]models.PSU, 0),
		},
	}

	if f := findBySuffix(expanded, "tsr/metadata.json", "/metadata.json"); f != nil {
		parseMetadataJSON(f.Content, result)
	}
	if f := findBySuffix(expanded, "sysinfo_dcim_view.xml"); f != nil {
		parseDCIMViewXML(f.Content, result)
	}
	if f := findBySuffix(expanded, "sysinfo_dcim_softwareidentity.xml"); f != nil {
		parseSoftwareIdentityXML(f.Content, result)
	}
	if f := findBySuffix(expanded, "sysinfo_cim_sensor.xml"); f != nil {
		parseCIMSensorXML(f.Content, result)
	}
	if f := findBySuffix(expanded, "curr_lclog.xml"); f != nil {
		result.Events = append(result.Events, parseLCEventsXML(f.Content)...)
	}

	result.Hardware.Storage = dedupeStorage(result.Hardware.Storage)
	result.Hardware.Volumes = dedupeVolumes(result.Hardware.Volumes)
	result.Hardware.PowerSupply = dedupePSU(result.Hardware.PowerSupply)
	result.Hardware.NetworkAdapters = dedupeNetworkAdapters(result.Hardware.NetworkAdapters)
	result.Hardware.NetworkCards = nicCardsFromAdapters(result.Hardware.NetworkAdapters)
	result.Hardware.GPUs = dedupeGPU(result.Hardware.GPUs)
	result.Hardware.PCIeDevices = removePCIeOverlappingWithGPUs(dedupePCIe(result.Hardware.PCIeDevices), result.Hardware.GPUs)
	result.Hardware.CPUs = dedupeCPU(result.Hardware.CPUs)
	result.Hardware.Memory = dedupeDIMM(result.Hardware.Memory)
	result.Hardware.Firmware = dedupeFirmware(result.Hardware.Firmware)
	result.Sensors = dedupeSensors(result.Sensors)

	return result, nil
}

type metadataPayload struct {
	Make               string `json:"Make"`
	Model              string `json:"Model"`
	ServiceTag         string `json:"ServiceTag"`
	FirmwareVersion    string `json:"FirmwareVersion"`
	CollectionDateTime string `json:"CollectionDateTime"`
}

func parseMetadataJSON(content []byte, result *models.AnalysisResult) {
	var m metadataPayload
	if err := json.Unmarshal(content, &m); err != nil {
		return
	}

	if result.Hardware == nil {
		return
	}

	setIfEmpty(&result.Hardware.BoardInfo.Manufacturer, strings.TrimSpace(m.Make))
	setIfEmpty(&result.Hardware.BoardInfo.ProductName, strings.TrimSpace(m.Model))
	setIfEmpty(&result.Hardware.BoardInfo.SerialNumber, strings.TrimSpace(m.ServiceTag))

	fw := strings.TrimSpace(m.FirmwareVersion)
	if fw != "" {
		result.Hardware.Firmware = append(result.Hardware.Firmware, models.FirmwareInfo{
			DeviceName: "iDRAC",
			Version:    fw,
		})
	}

	if t := parseDellTSRTime(m.CollectionDateTime); !t.IsZero() {
		result.CollectedAt = t.UTC()
	}
}

type cimDoc struct {
	XMLName xml.Name `xml:"CIM"`
	Message struct {
		SimpleReq struct {
			Named []cimNamedInstance `xml:"VALUE.NAMEDINSTANCE"`
		} `xml:"SIMPLEREQ"`
	} `xml:"MESSAGE"`
}

type cimNamedInstance struct {
	Instance cimInstance `xml:"INSTANCE"`
}

type cimInstance struct {
	ClassName string        `xml:"CLASSNAME,attr"`
	Props     []cimProperty `xml:",any"`
}

type cimProperty struct {
	XMLName      xml.Name
	Name         string   `xml:"NAME,attr"`
	Value        string   `xml:"VALUE"`
	DisplayValue string   `xml:"DisplayValue"`
	ArrayValues  []string `xml:"VALUE.ARRAY>VALUE"`
	ArrayDisplay []string `xml:"VALUE.ARRAY>DisplayValue"`
}

func parseDCIMViewXML(content []byte, result *models.AnalysisResult) {
	var doc cimDoc
	if err := xml.Unmarshal(content, &doc); err != nil {
		return
	}

	for _, ni := range doc.Message.SimpleReq.Named {
		inst := ni.Instance
		if strings.TrimSpace(inst.ClassName) == "" {
			continue
		}

		props := propertyMap(inst.Props)
		switch inst.ClassName {
		case "DCIM_SystemView":
			parseSystemView(props, result)
		case "DCIM_CPUView":
			parseCPUView(props, result)
		case "DCIM_MemoryView":
			parseMemoryView(props, result)
		case "DCIM_PhysicalDiskView":
			parsePhysicalDiskView(props, result)
		case "DCIM_VirtualDiskView":
			parseVirtualDiskView(props, result)
		case "DCIM_PowerSupplyView":
			parsePowerSupplyView(props, result)
		case "DCIM_PCIDeviceView":
			parsePCIeDeviceView(props, result)
		case "DCIM_NICView", "DCIM_InfiniBandView":
			parseNICView(props, result)
		case "DCIM_VideoView":
			parseVideoView(props, result)
		case "DCIM_ControllerView":
			parseControllerView(props, result)
		case "DCIM_ControllerBatteryView":
			parseControllerBatteryView(props, result)
		case "DCIM_EnclosureView":
			parseEnclosureView(props, result)
		case "DCIM_iDRACCardView":
			parseIDRACCardView(props, result)
		case "DCIM_FanView":
			parseFanView(props, result)
		}
	}
}

func parseCIMSensorXML(content []byte, result *models.AnalysisResult) {
	var doc cimDoc
	if err := xml.Unmarshal(content, &doc); err != nil {
		return
	}
	for _, ni := range doc.Message.SimpleReq.Named {
		inst := ni.Instance
		props := propertyMap(inst.Props)
		switch inst.ClassName {
		case "DCIM_GPUSensor":
			parseGPUCIMSensor(props, result)
		default:
			parseGenericCIMSensor(props, result)
		}
	}
}

func parseSoftwareIdentityXML(content []byte, result *models.AnalysisResult) {
	var doc cimDoc
	if err := xml.Unmarshal(content, &doc); err != nil {
		return
	}
	for _, ni := range doc.Message.SimpleReq.Named {
		props := propertyMap(ni.Instance.Props)
		version := firstNonEmpty(props["versionstring"], props["revisionstring"])
		if strings.TrimSpace(version) == "" {
			continue
		}
		name := nicMACInModelRE.ReplaceAllString(firstNonEmpty(props["elementname"], props["fqdd"], props["instanceid"]), "")
		if strings.TrimSpace(name) == "" {
			continue
		}
		result.Hardware.Firmware = append(result.Hardware.Firmware, models.FirmwareInfo{
			DeviceName:  strings.TrimSpace(name),
			Version:     strings.TrimSpace(version),
			Description: strings.TrimSpace(firstNonEmpty(props["fqdd"], props["componenttype"])),
		})
	}
}

type lcEvents struct {
	Events []lcEvent `xml:"Event"`
}

type lcEvent struct {
	AgentID   string `xml:"AgentID,attr"`
	Category  string `xml:"Category,attr"`
	Severity  string `xml:"Severity,attr"`
	Timestamp string `xml:"Timestamp,attr"`
	Message   string `xml:"Message"`
	MessageID string `xml:"MessageID"`
	FQDD      string `xml:"FQDD"`
}

func parseLCEventsXML(content []byte) []models.Event {
	var doc lcEvents
	if err := xml.Unmarshal(content, &doc); err != nil {
		return nil
	}
	out := make([]models.Event, 0, len(doc.Events))
	for _, e := range doc.Events {
		ts := parseDellEventTime(e.Timestamp)
		desc := strings.TrimSpace(e.Message)
		if desc == "" {
			continue
		}
		out = append(out, models.Event{
			ID:          strings.TrimSpace(e.MessageID),
			Timestamp:   ts,
			Source:      firstNonEmpty(strings.TrimSpace(e.AgentID), "iDRAC"),
			SensorName:  strings.TrimSpace(e.FQDD),
			EventType:   firstNonEmpty(strings.TrimSpace(e.Category), strings.TrimSpace(e.MessageID)),
			Severity:    mapDellSeverity(e.Severity),
			Description: desc,
		})
	}
	return out
}

func parseSystemView(props map[string]string, result *models.AnalysisResult) {
	board := &result.Hardware.BoardInfo
	setIfEmpty(&board.Manufacturer, props["manufacturer"])
	setIfEmpty(&board.ProductName, props["model"])
	setIfEmpty(&board.SerialNumber, props["servicetag"])
	setIfEmpty(&board.PartNumber, props["boardpartnumber"])
	setIfEmpty(&board.Version, props["biosversionstring"])
	setIfEmpty(&board.UUID, props["uuid"])
	if strings.TrimSpace(board.Description) == "" {
		board.Description = strings.TrimSpace(props["systemgeneration"])
	}

	addFirmware(result, "BIOS", props["biosversionstring"], "system bios")
	addFirmware(result, "Lifecycle Controller", props["lifecyclecontrollerversion"], "idrac lifecycle")
	addFirmware(result, "CPLD", props["cpldversion"], "system cpld")
}

func parseCPUView(props map[string]string, result *models.AnalysisResult) {
	model := strings.TrimSpace(props["model"])
	if model == "" {
		return
	}
	cpu := models.CPU{
		Socket:       parseSocketFromFQDD(firstNonEmpty(props["fqdd"], props["instanceid"])),
		Model:        model,
		Description:  strings.TrimSpace(props["manufacturer"]),
		Cores:        parseIntLoose(firstNonEmpty(props["numberofenabledcores"], props["numberofprocessorcores"])),
		Threads:      parseIntLoose(props["numberofenabledthreads"]),
		FrequencyMHz: parseIntLoose(props["currentclockspeed"]),
		MaxFreqMHz:   parseIntLoose(props["maxclockspeed"]),
		PPIN:         strings.TrimSpace(props["ppin"]),
		Status:       normalizeStatus(props["primarystatus"]),
	}
	result.Hardware.CPUs = append(result.Hardware.CPUs, cpu)
}

func parseMemoryView(props map[string]string, result *models.AnalysisResult) {
	slot := strings.TrimSpace(firstNonEmpty(props["devicedescription"], props["fqdd"], props["instanceid"]))
	if slot == "" {
		return
	}
	status := normalizeStatus(props["primarystatus"])
	dimm := models.MemoryDIMM{
		Slot:            slot,
		Location:        strings.TrimSpace(firstNonEmpty(props["fqdd"], slot)),
		Present:         status != "absent",
		SizeMB:          parseIntLoose(props["size"]),
		Type:            firstNonEmpty(props["memorytypeextended"], props["memorytype"], "DIMM"),
		Technology:      strings.TrimSpace(props["memorytechnology"]),
		MaxSpeedMHz:     parseIntLoose(props["speed"]),
		CurrentSpeedMHz: parseIntLoose(props["currentoperatingspeed"]),
		Manufacturer:    strings.TrimSpace(props["manufacturer"]),
		SerialNumber:    strings.TrimSpace(props["serialnumber"]),
		PartNumber:      strings.TrimSpace(props["partnumber"]),
		Status:          status,
	}
	result.Hardware.Memory = append(result.Hardware.Memory, dimm)
}

func parsePhysicalDiskView(props map[string]string, result *models.AnalysisResult) {
	slot := firstNonEmpty(props["slot"], extractDiskSlotFromFQDD(props["fqdd"]), props["fqdd"])
	model := strings.TrimSpace(props["model"])
	if strings.TrimSpace(slot) == "" && model == "" {
		return
	}
	st := models.Storage{
		Slot:         strings.TrimSpace(slot),
		Type:         inferStorageType(props["mediatype"], props["busprotocol"]),
		Model:        model,
		SizeGB:       parseBytesToGB(props["sizeinbytes"]),
		SerialNumber: strings.TrimSpace(props["serialnumber"]),
		Manufacturer: strings.TrimSpace(props["manufacturer"]),
		Firmware:     strings.TrimSpace(firstNonEmpty(props["revision"], props["firmwareversion"])),
		Interface:    strings.TrimSpace(props["busprotocol"]),
		Present:      true,
		Location:     strings.TrimSpace(props["devicedescription"]),
		Status:       normalizeStatus(firstNonEmpty(props["raidstatus"], props["primarystatus"])),
	}
	if v := strings.TrimSpace(props["remainingratedwriteendurance"]); v != "" {
		n := parseIntLoose(v)
		st.RemainingEndurancePct = &n
	}
	result.Hardware.Storage = append(result.Hardware.Storage, st)
}

func parseVirtualDiskView(props map[string]string, result *models.AnalysisResult) {
	id := strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"]))
	if id == "" {
		return
	}
	capacityBytes := parseInt64Loose(props["sizeinbytes"])
	sizeGB := 0
	if capacityBytes > 0 {
		sizeGB = int(capacityBytes / 1_000_000_000)
	}
	v := models.StorageVolume{
		ID:            id,
		Name:          strings.TrimSpace(firstNonEmpty(props["name"], id)),
		Controller:    extractControllerFromVolumeFQDD(id),
		RAIDLevel:     normalizeRAIDLevel(firstNonEmpty(props["raidtypes"], props["raidstatus"])),
		SizeGB:        sizeGB,
		CapacityBytes: capacityBytes,
		Status:        normalizeStatus(firstNonEmpty(props["raidstatus"], props["primarystatus"])),
	}
	result.Hardware.Volumes = append(result.Hardware.Volumes, v)
}

func parsePowerSupplyView(props map[string]string, result *models.AnalysisResult) {
	fqdd := strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"]))
	model := strings.TrimSpace(props["model"])
	if fqdd == "" && model == "" {
		return
	}
	slot := normalizePSUSlot(fqdd)
	status := normalizeStatus(props["primarystatus"])
	psu := models.PSU{
		Slot:         slot,
		Present:      status != "absent",
		Model:        model,
		Description:  strings.TrimSpace(props["devicedescription"]),
		Vendor:       strings.TrimSpace(props["manufacturer"]),
		WattageW:     parseIntLoose(firstNonEmpty(props["totaloutputpower"], props["effectivecapacity"])),
		SerialNumber: strings.TrimSpace(props["serialnumber"]),
		PartNumber:   strings.TrimSpace(props["partnumber"]),
		Firmware:     strings.TrimSpace(props["firmwareversion"]),
		Status:       status,
		InputVoltage: parseFloatLoose(props["inputvoltage"]),
		InputType:    strings.TrimSpace(props["type"]),
	}
	result.Hardware.PowerSupply = append(result.Hardware.PowerSupply, psu)
}

// pcieFQDDNoisePrefix lists FQDD prefixes that represent internal chipset/CPU
// components or devices already captured with richer data elsewhere:
//   - HostBridge/P2PBridge/ISABridge/SMBus: AMD EPYC internal fabric, not PCIe slots
//   - AHCI.Embedded: AMD FCH SATA, not a slot device
//   - Video.Embedded: BMC/iDRAC Matrox graphics chip, not user-visible
//   - NIC.Embedded: already parsed from DCIM_NICView with model and MAC addresses
var pcieFQDDNoisePrefix = []string{
	"HostBridge.Embedded.",
	"P2PBridge.Embedded.",
	"ISABridge.Embedded.",
	"SMBus.Embedded.",
	"AHCI.Embedded.",
	"Video.Embedded.",
	// All NIC FQDD classes are parsed from DCIM_NICView / DCIM_InfiniBandView into
	// NetworkAdapters with model, MAC, firmware, and VendorID/DeviceID.  The
	// DCIM_PCIDeviceView duplicate carries only DataBusWidth ("Unknown", "16x or x16")
	// and no useful extra data, so suppress it here.
	"NIC.",
	"InfiniBand.",
}

func parsePCIeDeviceView(props map[string]string, result *models.AnalysisResult) {
	// "description" is the chip/device model (e.g. "MT28908 Family [ConnectX-6]"); prefer
	// it over "devicedescription" which is the location string ("InfiniBand in Slot 1 Port 1").
	desc := strings.TrimSpace(firstNonEmpty(props["description"], props["devicedescription"]))
	fqdd := strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"]))
	if desc == "" && fqdd == "" {
		return
	}
	for _, prefix := range pcieFQDDNoisePrefix {
		if strings.HasPrefix(fqdd, prefix) {
			return
		}
	}
	vendorID := parseHexOrDec(firstNonEmpty(props["pcivendorid"], props["vendorid"]))
	deviceID := parseHexOrDec(firstNonEmpty(props["pcideviceid"], props["deviceid"]))
	manufacturer := strings.TrimSpace(props["manufacturer"])

	// General rule: if chip model not found in logs but PCI IDs are known, resolve from pci.ids
	if desc == "" && vendorID != 0 && deviceID != 0 {
		desc = pciids.DeviceName(vendorID, deviceID)
	}
	if manufacturer == "" && vendorID != 0 {
		manufacturer = pciids.VendorName(vendorID)
	}

	p := models.PCIeDevice{
		Slot:         fqdd,
		Description:  desc,
		VendorID:     vendorID,
		DeviceID:     deviceID,
		BDF:          formatBDF(props["busnumber"], props["devicenumber"], props["functionnumber"]),
		Manufacturer: manufacturer,
		NUMANode:     parseIntLoose(props["cpuaffinity"]),
		Status:       normalizeStatus(props["primarystatus"]),
	}
	result.Hardware.PCIeDevices = append(result.Hardware.PCIeDevices, p)
}

func parseNICView(props map[string]string, result *models.AnalysisResult) {
	fqdd := strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"]))
	model := nicMACInModelRE.ReplaceAllString(strings.TrimSpace(firstNonEmpty(props["productname"], props["devicedescription"])), "")
	if fqdd == "" && model == "" {
		return
	}
	mac := strings.TrimSpace(firstNonEmpty(props["currentmacaddress"], props["permanentmacaddress"]))
	vendorID := parseHexOrDec(firstNonEmpty(props["pcivendorid"], props["vendorid"]))
	deviceID := parseHexOrDec(firstNonEmpty(props["pcideviceid"], props["deviceid"]))
	vendor := strings.TrimSpace(firstNonEmpty(props["vendorname"], props["manufacturer"]))

	// Prefer pci.ids chip model over generic ProductName when PCI IDs are available.
	// Dell TSR often reports a marketing name (e.g. "Mellanox Network Adapter") while
	// pci.ids has the precise chip identifier (e.g. "MT28908 Family [ConnectX-6]").
	if vendorID != 0 && deviceID != 0 {
		if chipModel := pciids.DeviceName(vendorID, deviceID); chipModel != "" {
			model = chipModel
		}
		if vendor == "" {
			vendor = pciids.VendorName(vendorID)
		}
	}

	n := models.NetworkAdapter{
		Slot:         fqdd,
		Location:     strings.TrimSpace(firstNonEmpty(props["devicedescription"], fqdd)),
		Present:      true,
		Model:        model,
		Description:  strings.TrimSpace(props["protocol"]),
		Vendor:       vendor,
		VendorID:     vendorID,
		DeviceID:     deviceID,
		SerialNumber: strings.TrimSpace(props["serialnumber"]),
		PartNumber:   strings.TrimSpace(props["partnumber"]),
		Firmware: strings.TrimSpace(firstNonEmpty(
			props["familyversion"],
			props["efiversion"],
			props["landriverversion"],
			props["controllerbiosversion"],
		)),
		PortCount: inferPortCountFromFQDD(fqdd),
		NUMANode:  parseIntLoose(props["cpuaffinity"]),
		Status:    normalizeStatus(props["primarystatus"]),
	}
	if mac != "" {
		n.MACAddresses = []string{mac}
		if n.PortCount == 0 {
			n.PortCount = 1
		}
	}
	result.Hardware.NetworkAdapters = append(result.Hardware.NetworkAdapters, n)
}

func parseVideoView(props map[string]string, result *models.AnalysisResult) {
	fqdd := strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"]))
	model := strings.TrimSpace(firstNonEmpty(props["marketingname"], props["description"], props["devicedescription"]))
	serial := strings.TrimSpace(props["serialnumber"])
	if fqdd == "" && model == "" && serial == "" {
		return
	}

	gpu := models.GPU{
		Slot:         fqdd,
		Location:     strings.TrimSpace(firstNonEmpty(props["devicedescription"], fqdd)),
		Model:        model,
		Description:  strings.TrimSpace(props["description"]),
		Manufacturer: strings.TrimSpace(props["manufacturer"]),
		VendorID:     parseHexOrDec(firstNonEmpty(props["pcivendorid"], props["pcisubvendorid"])),
		DeviceID:     parseHexOrDec(firstNonEmpty(props["pcideviceid"], props["pcisubdeviceid"])),
		BDF:          formatBDF(props["busnumber"], props["devicenumber"], props["functionnumber"]),
		UUID:         strings.TrimSpace(props["gpuguid"]),
		SerialNumber: serial,
		PartNumber:   strings.TrimSpace(firstNonEmpty(props["gpupartnumber"], props["boardpartnumber"])),
		Firmware:     strings.TrimSpace(props["firmwareversion"]),
		Status:       normalizeStatus(firstNonEmpty(props["gpuhealth"], props["gpustate"], props["primarystatus"])),
	}
	result.Hardware.GPUs = append(result.Hardware.GPUs, gpu)
}

func parseControllerView(props map[string]string, result *models.AnalysisResult) {
	fqdd := strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"]))
	name := strings.TrimSpace(firstNonEmpty(props["productname"], props["devicedescription"]))
	if fqdd == "" && name == "" {
		return
	}

	result.Hardware.PCIeDevices = append(result.Hardware.PCIeDevices, models.PCIeDevice{
		Slot:         fqdd,
		Description:  name,
		VendorID:     parseHexOrDec(firstNonEmpty(props["pcivendorid"], props["pcisubvendorid"])),
		DeviceID:     parseHexOrDec(firstNonEmpty(props["pcideviceid"], props["pcisubdeviceid"])),
		BDF:          formatBDF(props["bus"], props["device"], props["function"]),
		DeviceClass:  "storage-controller",
		Manufacturer: strings.TrimSpace(firstNonEmpty(props["devicecardmanufacturer"], props["manufacturer"])),
		PartNumber:   strings.TrimSpace(firstNonEmpty(props["ppid"], props["boardpartnumber"])),
		NUMANode:     parseIntLoose(props["cpuaffinity"]),
		Status:       normalizeStatus(props["primarystatus"]),
	})

	addFirmware(result, firstNonEmpty(name, fqdd), props["controllerfirmwareversion"], firstNonEmpty(fqdd, "storage controller"))
}

func parseControllerBatteryView(props map[string]string, result *models.AnalysisResult) {
	name := strings.TrimSpace(firstNonEmpty(props["devicedescription"], props["fqdd"], props["instanceid"]))
	if name == "" {
		return
	}
	result.Sensors = append(result.Sensors, models.SensorReading{
		Name:     name,
		Type:     "battery",
		RawValue: strings.TrimSpace(firstNonEmpty(props["raidstate"], props["primarystatus"])),
		Status:   normalizeStatus(firstNonEmpty(props["primarystatus"], props["raidstate"])),
	})
}

func parseEnclosureView(props map[string]string, result *models.AnalysisResult) {
	desc := strings.TrimSpace(firstNonEmpty(props["devicedescription"], props["productname"]))
	if desc == "" {
		return
	}
	result.FRU = append(result.FRU, models.FRUInfo{
		DeviceID:     strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"])),
		Description:  desc,
		ProductName:  strings.TrimSpace(props["productname"]),
		SerialNumber: strings.TrimSpace(props["servicetag"]),
		AssetTag:     strings.TrimSpace(props["assettag"]),
		Version:      strings.TrimSpace(props["version"]),
	})
}

func parseIDRACCardView(props map[string]string, result *models.AnalysisResult) {
	addFirmware(result, "iDRAC", props["firmwareversion"], "idrac card")

	desc := strings.TrimSpace(firstNonEmpty(props["devicedescription"], "iDRAC"))
	result.FRU = append(result.FRU, models.FRUInfo{
		DeviceID:     strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"])),
		Description:  desc,
		Manufacturer: strings.TrimSpace(result.Hardware.BoardInfo.Manufacturer),
		ProductName:  strings.TrimSpace(props["model"]),
		SerialNumber: strings.TrimSpace(props["guid"]),
		Version:      strings.TrimSpace(props["firmwareversion"]),
	})
}

func parseFanView(props map[string]string, result *models.AnalysisResult) {
	name := strings.TrimSpace(firstNonEmpty(props["devicedescription"], props["fqdd"], props["instanceid"]))
	if name == "" {
		return
	}
	reading := parseScaledFloat(props["currentreading"], props["unitmodifier"])
	result.Sensors = append(result.Sensors, models.SensorReading{
		Name:     name,
		Type:     "fan_speed",
		Value:    reading,
		Unit:     "RPM",
		RawValue: strings.TrimSpace(props["currentreading"]),
		Status:   normalizeStatus(props["primarystatus"]),
	})
}

func parseGPUCIMSensor(props map[string]string, result *models.AnalysisResult) {
	deviceID := strings.TrimSpace(props["deviceid"])
	primaryTemp := parseScaledFloat(props["primarygputemperature"], "-1")
	memTemp := parseScaledFloat(props["memorytemperature"], "-1")
	power := parseIntLoose(props["powerconsumption"])

	if primaryTemp > 0 {
		result.Sensors = append(result.Sensors, models.SensorReading{
			Name:     firstNonEmpty(deviceID, "GPU"),
			Type:     "temperature",
			Value:    primaryTemp,
			Unit:     "C",
			RawValue: strings.TrimSpace(props["primarygputemperature"]),
			Status:   normalizeCIMHealthStatus(props["thermalalertstatus"]),
		})
	}
	if memTemp > 0 {
		result.Sensors = append(result.Sensors, models.SensorReading{
			Name:     firstNonEmpty(deviceID, "GPU") + " memory",
			Type:     "temperature",
			Value:    memTemp,
			Unit:     "C",
			RawValue: strings.TrimSpace(props["memorytemperature"]),
			Status:   normalizeCIMHealthStatus(props["thermalalertstatus"]),
		})
	}

	for i := range result.Hardware.GPUs {
		g := &result.Hardware.GPUs[i]
		if deviceID != "" && !strings.EqualFold(strings.TrimSpace(g.Slot), deviceID) {
			continue
		}
		if g.Temperature == 0 && primaryTemp > 0 {
			g.Temperature = int(primaryTemp)
		}
		if g.MemTemperature == 0 && memTemp > 0 {
			g.MemTemperature = int(memTemp)
		}
		if g.Power == 0 && power > 0 {
			g.Power = power
		}
	}
}

func parseGenericCIMSensor(props map[string]string, result *models.AnalysisResult) {
	name := strings.TrimSpace(firstNonEmpty(props["elementname"], props["devicedescription"], props["deviceid"]))
	if name == "" {
		return
	}
	currentRaw := strings.TrimSpace(props["currentreading"])
	if currentRaw == "" {
		return
	}

	value := parseScaledFloat(currentRaw, props["unitmodifier"])
	unit, sensorType := mapCIMSensorUnitAndType(props["baseunits"], props["sensortype"], name)
	status := normalizeCIMHealthStatus(firstNonEmpty(props["primarystatus"], props["healthstate"]))
	result.Sensors = append(result.Sensors, models.SensorReading{
		Name:     name,
		Type:     sensorType,
		Value:    value,
		Unit:     unit,
		RawValue: currentRaw,
		Status:   status,
	})
}

func propertyMap(props []cimProperty) map[string]string {
	out := make(map[string]string, len(props))
	for _, p := range props {
		name := normalizeKey(p.Name)
		if name == "" {
			continue
		}
		value := strings.TrimSpace(p.DisplayValue)
		if value == "" {
			value = strings.TrimSpace(p.Value)
		}
		if value == "" && len(p.ArrayDisplay) > 0 {
			value = strings.Join(nonEmptyStrings(p.ArrayDisplay), ", ")
		}
		if value == "" && len(p.ArrayValues) > 0 {
			value = strings.Join(nonEmptyStrings(p.ArrayValues), ", ")
		}
		out[name] = strings.TrimSpace(value)
	}
	return out
}

func findBySuffix(files []parser.ExtractedFile, suffixes ...string) *parser.ExtractedFile {
	for i := range files {
		path := strings.ToLower(strings.TrimSpace(files[i].Path))
		for _, suffix := range suffixes {
			if strings.HasSuffix(path, strings.ToLower(suffix)) {
				return &files[i]
			}
		}
	}
	return nil
}

func expandNestedZipFiles(files []parser.ExtractedFile) []parser.ExtractedFile {
	out := make([]parser.ExtractedFile, 0, len(files)+32)
	out = append(out, files...)

	for _, f := range files {
		if !strings.HasSuffix(strings.ToLower(strings.TrimSpace(f.Path)), ".zip") {
			continue
		}
		zr, err := zip.NewReader(bytes.NewReader(f.Content), int64(len(f.Content)))
		if err != nil {
			continue
		}
		for _, zf := range zr.File {
			if zf.FileInfo().IsDir() {
				continue
			}
			if zf.FileInfo().Size() > 10*1024*1024 {
				continue
			}
			rc, err := zf.Open()
			if err != nil {
				continue
			}
			content, err := io.ReadAll(rc)
			rc.Close()
			if err != nil {
				continue
			}
			out = append(out, parser.ExtractedFile{
				Path:    zf.Name,
				Content: content,
				ModTime: zf.Modified,
			})
		}
	}
	return out
}

func addFirmware(result *models.AnalysisResult, name, version, desc string) {
	name = strings.TrimSpace(name)
	version = strings.TrimSpace(version)
	if name == "" || version == "" {
		return
	}
	result.Hardware.Firmware = append(result.Hardware.Firmware, models.FirmwareInfo{
		DeviceName:  name,
		Version:     version,
		Description: strings.TrimSpace(desc),
	})
}

func parseDellTSRTime(v string) time.Time {
	v = strings.TrimSpace(v)
	if v == "" {
		return time.Time{}
	}
	layouts := []string{
		"2006-01-02 15:04:05.000-0700",
		"2006-01-02 15:04:05-0700",
	}
	for _, layout := range layouts {
		if t, err := time.Parse(layout, v); err == nil {
			return t
		}
	}
	return time.Time{}
}

func parseDellEventTime(v string) time.Time {
	v = strings.TrimSpace(v)
	if v == "" {
		return time.Time{}
	}
	layouts := []string{
		"2006-01-02T15:04:05-0700",
		time.RFC3339,
	}
	for _, layout := range layouts {
		if t, err := time.Parse(layout, v); err == nil {
			return t
		}
	}
	return time.Time{}
}

func mapDellSeverity(v string) models.Severity {
	switch strings.ToLower(strings.TrimSpace(v)) {
	case "critical":
		return models.SeverityCritical
	case "warning":
		return models.SeverityWarning
	default:
		return models.SeverityInfo
	}
}

func normalizeStatus(v string) string {
	s := strings.ToLower(strings.TrimSpace(v))
	switch {
	case s == "", s == "unknown", s == "not applicable":
		return ""
	case strings.Contains(s, "ok"), strings.Contains(s, "online"), strings.Contains(s, "enabled"), strings.Contains(s, "presence"):
		return "ok"
	case strings.Contains(s, "warn"), strings.Contains(s, "degrad"):
		return "warning"
	case strings.Contains(s, "critical"), strings.Contains(s, "fail"), strings.Contains(s, "error"):
		return "critical"
	case strings.Contains(s, "absent"), strings.Contains(s, "missing"):
		return "absent"
	default:
		return s
	}
}

func inferStorageType(mediaType, protocol string) string {
	s := strings.ToLower(strings.TrimSpace(mediaType + " " + protocol))
	switch {
	case strings.Contains(s, "nvme"):
		return "nvme"
	case strings.Contains(s, "solid state"), strings.Contains(s, "ssd"):
		return "ssd"
	case strings.Contains(s, "hard disk"), strings.Contains(s, "hdd"), strings.Contains(s, "sata"), strings.Contains(s, "sas"):
		return "disk"
	default:
		return "disk"
	}
}

func normalizeRAIDLevel(v string) string {
	s := strings.TrimSpace(v)
	if s == "" {
		return ""
	}
	u := strings.ToUpper(s)
	if strings.HasPrefix(u, "RAID") {
		return u
	}
	if strings.HasPrefix(strings.ToLower(s), "raid") {
		return strings.ToUpper(s)
	}
	if strings.Contains(u, "RAID") {
		return strings.ToUpper(s)
	}
	return s
}

func parseSocketFromFQDD(v string) int {
	re := regexp.MustCompile(`(?i)socket\.(\d+)`)
	m := re.FindStringSubmatch(strings.TrimSpace(v))
	if len(m) != 2 {
		return 0
	}
	return parseIntLoose(m[1])
}

func extractDiskSlotFromFQDD(v string) string {
	re := regexp.MustCompile(`(?i)disk\.bay\.(\d+)`)
	m := re.FindStringSubmatch(strings.TrimSpace(v))
	if len(m) == 2 {
		return m[1]
	}
	return ""
}

func extractControllerFromVolumeFQDD(v string) string {
	parts := strings.Split(strings.TrimSpace(v), ":")
	if len(parts) < 2 {
		return ""
	}
	return strings.TrimSpace(parts[1])
}

func normalizePSUSlot(v string) string {
	re := regexp.MustCompile(`(?i)psu\.slot\.(\d+)`)
	m := re.FindStringSubmatch(strings.TrimSpace(v))
	if len(m) == 2 {
		return "PSU" + m[1]
	}
	return strings.TrimSpace(v)
}

func inferPortCountFromFQDD(v string) int {
	re := regexp.MustCompile(`(?i)-(\d+)-`)
	m := re.FindStringSubmatch(v)
	if len(m) == 2 {
		return parseIntLoose(m[1])
	}
	return 0
}

func formatBDF(bus, dev, fn string) string {
	b := parseIntLoose(bus)
	d := parseIntLoose(dev)
	f := parseIntLoose(fn)
	if b == 0 && d == 0 && f == 0 {
		return ""
	}
	return fmt.Sprintf("%02x:%02x.%x", b, d, f)
}

func parseBytesToGB(v string) int {
	n := parseInt64Loose(v)
	if n <= 0 {
		return 0
	}
	return int(n / 1_000_000_000)
}

var firstNumberRE = regexp.MustCompile(`[-+]?[0-9]+`)
var firstFloatRE = regexp.MustCompile(`[-+]?[0-9]+(?:\.[0-9]+)?`)
var nicMACInModelRE = regexp.MustCompile(`\s+-\s+([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$`)

func parseIntLoose(v string) int {
	v = strings.TrimSpace(v)
	if v == "" {
		return 0
	}
	if i := parseHexOrDec(v); i != 0 {
		return i
	}
	num := firstNumberRE.FindString(v)
	if num == "" {
		return 0
	}
	n, _ := strconv.Atoi(num)
	return n
}

func parseInt64Loose(v string) int64 {
	v = strings.TrimSpace(v)
	if v == "" {
		return 0
	}
	num := firstNumberRE.FindString(v)
	if num == "" {
		return 0
	}
	n, _ := strconv.ParseInt(num, 10, 64)
	return n
}

func parseFloatLoose(v string) float64 {
	v = strings.TrimSpace(v)
	if v == "" {
		return 0
	}
	num := firstFloatRE.FindString(v)
	if num == "" {
		return 0
	}
	f, _ := strconv.ParseFloat(num, 64)
	return f
}

func parseScaledFloat(valueRaw, unitModRaw string) float64 {
	base := parseFloatLoose(valueRaw)
	if base == 0 {
		return 0
	}
	mod := parseIntLoose(unitModRaw)
	if mod == 0 {
		return base
	}
	scale := 1.0
	if mod > 0 {
		for i := 0; i < mod; i++ {
			scale *= 10
		}
		return base * scale
	}
	for i := 0; i < -mod; i++ {
		scale *= 10
	}
	return base / scale
}

func parseHexOrDec(v string) int {
	v = strings.TrimSpace(strings.ToLower(v))
	if v == "" {
		return 0
	}
	if strings.HasPrefix(v, "0x") {
		n, _ := strconv.ParseInt(strings.TrimPrefix(v, "0x"), 16, 32)
		return int(n)
	}
	if n, err := strconv.ParseInt(v, 10, 32); err == nil {
		return int(n)
	}
	if n, err := strconv.ParseInt(v, 16, 32); err == nil {
		return int(n)
	}
	return 0
}

func normalizeKey(s string) string {
	s = strings.ToLower(strings.TrimSpace(s))
	var b strings.Builder
	b.Grow(len(s))
	for _, r := range s {
		if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
			b.WriteRune(r)
		}
	}
	return b.String()
}

func mapCIMSensorUnitAndType(baseUnits, sensorTypeRaw, name string) (string, string) {
	switch parseIntLoose(baseUnits) {
	case 2:
		return "C", "temperature"
	case 5:
		return "V", "voltage"
	case 6:
		return "A", "current"
	case 7:
		return "W", "power"
	case 19:
		return "RPM", "fan_speed"
	case 65:
		return "%", "utilization"
	}

	lower := strings.ToLower(name)
	switch {
	case strings.Contains(lower, "temp"):
		return "C", "temperature"
	case strings.Contains(lower, "fan"):
		return "RPM", "fan_speed"
	case strings.Contains(lower, "volt"):
		return "V", "voltage"
	case strings.Contains(lower, "current"):
		return "A", "current"
	case strings.Contains(lower, "power"), strings.Contains(lower, "pwr"):
		return "W", "power"
	}

	switch parseIntLoose(sensorTypeRaw) {
	case 2:
		return "C", "temperature"
	case 3:
		return "V", "voltage"
	case 5:
		return "RPM", "fan_speed"
	case 13:
		return "", "power"
	}
	return "", "sensor"
}

func normalizeCIMHealthStatus(v string) string {
	switch parseIntLoose(v) {
	case 1, 2, 5, 15:
		return "ok"
	case 3, 4, 10:
		return "warning"
	case 6, 7, 8, 9, 11, 12, 13, 14, 16, 17, 18:
		return "critical"
	default:
		return normalizeStatus(v)
	}
}

func firstNonEmpty(values ...string) string {
	for _, v := range values {
		v = strings.TrimSpace(v)
		if v != "" {
			return v
		}
	}
	return ""
}

func setIfEmpty(dst *string, value string) {
	if strings.TrimSpace(*dst) != "" {
		return
	}
	*dst = strings.TrimSpace(value)
}

func nonEmptyStrings(values []string) []string {
	out := make([]string, 0, len(values))
	for _, v := range values {
		v = strings.TrimSpace(v)
		if v != "" {
			out = append(out, v)
		}
	}
	return out
}

func dedupeStorage(items []models.Storage) []models.Storage {
	out := make([]models.Storage, 0, len(items))
	indexByKey := make(map[string]int)
	for _, item := range items {
		key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.SerialNumber, item.Slot, item.Model)))
		if key == "" {
			continue
		}
		if idx, ok := indexByKey[key]; ok {
			mergeStorage(&out[idx], item)
			continue
		}
		indexByKey[key] = len(out)
		out = append(out, item)
	}
	return out
}

func mergeStorage(dst *models.Storage, src models.Storage) {
	setIfEmpty(&dst.Slot, src.Slot)
	setIfEmpty(&dst.Type, src.Type)
	setIfEmpty(&dst.Model, src.Model)
	if dst.SizeGB == 0 {
		dst.SizeGB = src.SizeGB
	}
	setIfEmpty(&dst.SerialNumber, src.SerialNumber)
	setIfEmpty(&dst.Manufacturer, src.Manufacturer)
	setIfEmpty(&dst.Firmware, src.Firmware)
	setIfEmpty(&dst.Interface, src.Interface)
	if src.Present {
		dst.Present = true
	}
	setIfEmpty(&dst.Location, src.Location)
	setIfEmpty(&dst.Status, src.Status)
	dst.Details = mergeDellDetails(dst.Details, src.Details)
}

func dedupeVolumes(items []models.StorageVolume) []models.StorageVolume {
	out := make([]models.StorageVolume, 0, len(items))
	seen := make(map[string]int)
	for _, item := range items {
		key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.ID, item.Name)))
		if key == "" {
			continue
		}
		if idx, ok := seen[key]; ok {
			mergeVolume(&out[idx], item)
			continue
		}
		seen[key] = len(out)
		out = append(out, item)
	}
	return out
}

func mergeVolume(dst *models.StorageVolume, src models.StorageVolume) {
	setIfEmpty(&dst.ID, src.ID)
	setIfEmpty(&dst.Name, src.Name)
	setIfEmpty(&dst.Controller, src.Controller)
	setIfEmpty(&dst.RAIDLevel, src.RAIDLevel)
	if dst.SizeGB == 0 {
		dst.SizeGB = src.SizeGB
	}
	if dst.CapacityBytes == 0 {
		dst.CapacityBytes = src.CapacityBytes
	}
	setIfEmpty(&dst.Status, src.Status)
}

func dedupePSU(items []models.PSU) []models.PSU {
	out := make([]models.PSU, 0, len(items))
	seen := make(map[string]int)
	for _, item := range items {
		key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.SerialNumber, item.Slot, item.Model)))
		if key == "" {
			continue
		}
		if idx, ok := seen[key]; ok {
			mergePSU(&out[idx], item)
			continue
		}
		seen[key] = len(out)
		out = append(out, item)
	}
	return out
}

func mergePSU(dst *models.PSU, src models.PSU) {
	setIfEmpty(&dst.Slot, src.Slot)
	if src.Present {
		dst.Present = true
	}
	setIfEmpty(&dst.Model, src.Model)
	setIfEmpty(&dst.Description, src.Description)
	setIfEmpty(&dst.Vendor, src.Vendor)
	if dst.WattageW == 0 {
		dst.WattageW = src.WattageW
	}
	setIfEmpty(&dst.SerialNumber, src.SerialNumber)
	setIfEmpty(&dst.PartNumber, src.PartNumber)
	setIfEmpty(&dst.Firmware, src.Firmware)
	setIfEmpty(&dst.Status, src.Status)
	if dst.InputVoltage == 0 {
		dst.InputVoltage = src.InputVoltage
	}
	setIfEmpty(&dst.InputType, src.InputType)
	dst.Details = mergeDellDetails(dst.Details, src.Details)
}

func mergeDellDetails(primary, secondary map[string]any) map[string]any {
	if len(secondary) == 0 {
		return primary
	}
	if primary == nil {
		primary = make(map[string]any, len(secondary))
	}
	for key, value := range secondary {
		if _, ok := primary[key]; !ok {
			primary[key] = value
		}
	}
	return primary
}

func dedupeNetworkAdapters(items []models.NetworkAdapter) []models.NetworkAdapter {
	out := make([]models.NetworkAdapter, 0, len(items))
	seen := make(map[string]int)
	for _, item := range items {
		key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.SerialNumber, strings.Join(item.MACAddresses, ","), item.Slot)))
		if key == "" {
			continue
		}
		if idx, ok := seen[key]; ok {
			mergeNetworkAdapter(&out[idx], item)
			continue
		}
		seen[key] = len(out)
		out = append(out, item)
	}
	return out
}

func mergeNetworkAdapter(dst *models.NetworkAdapter, src models.NetworkAdapter) {
	setIfEmpty(&dst.Slot, src.Slot)
	setIfEmpty(&dst.Location, src.Location)
	if src.Present {
		dst.Present = true
	}
	setIfEmpty(&dst.Model, src.Model)
	setIfEmpty(&dst.Description, src.Description)
	setIfEmpty(&dst.Vendor, src.Vendor)
	if dst.VendorID == 0 {
		dst.VendorID = src.VendorID
	}
	if dst.DeviceID == 0 {
		dst.DeviceID = src.DeviceID
	}
	setIfEmpty(&dst.SerialNumber, src.SerialNumber)
	setIfEmpty(&dst.PartNumber, src.PartNumber)
	setIfEmpty(&dst.Firmware, src.Firmware)
	if dst.PortCount == 0 {
		dst.PortCount = src.PortCount
	}
	setIfEmpty(&dst.Status, src.Status)
	for _, mac := range src.MACAddresses {
		mac = strings.TrimSpace(mac)
		if mac == "" {
			continue
		}
		if !containsStringFold(dst.MACAddresses, mac) {
			dst.MACAddresses = append(dst.MACAddresses, mac)
		}
	}
}

func nicCardsFromAdapters(items []models.NetworkAdapter) []models.NIC {
	out := make([]models.NIC, 0, len(items))
	for _, item := range items {
		out = append(out, models.NIC{
			Name:         firstNonEmpty(item.Slot, item.Location, "NIC"),
			Model:        item.Model,
			Description:  item.Description,
			MACAddress:   firstNonEmpty(item.MACAddresses...),
			SerialNumber: item.SerialNumber,
		})
	}
	return out
}

// removePCIeOverlappingWithGPUs drops PCIe entries that duplicate a GPU already
// captured from DCIM_VideoView. Dell TSR lists GPUs in both DCIM_VideoView and
// DCIM_PCIDeviceView; the VideoView record is authoritative (has serial, firmware,
// temperature) so the PCIe duplicate must be removed.
func removePCIeOverlappingWithGPUs(pcie []models.PCIeDevice, gpus []models.GPU) []models.PCIeDevice {
	if len(gpus) == 0 {
		return pcie
	}
	gpuSlots := make(map[string]struct{}, len(gpus))
	gpuBDFs := make(map[string]struct{}, len(gpus))
	for _, g := range gpus {
		if s := strings.ToLower(strings.TrimSpace(g.Slot)); s != "" {
			gpuSlots[s] = struct{}{}
		}
		if b := strings.ToLower(strings.TrimSpace(g.BDF)); b != "" {
			gpuBDFs[b] = struct{}{}
		}
	}
	out := make([]models.PCIeDevice, 0, len(pcie))
	for _, p := range pcie {
		slot := strings.ToLower(strings.TrimSpace(p.Slot))
		bdf := strings.ToLower(strings.TrimSpace(p.BDF))
		if _, ok := gpuSlots[slot]; ok && slot != "" {
			continue
		}
		if _, ok := gpuBDFs[bdf]; ok && bdf != "" {
			continue
		}
		out = append(out, p)
	}
	return out
}

func dedupePCIe(items []models.PCIeDevice) []models.PCIeDevice {
	out := make([]models.PCIeDevice, 0, len(items))
	seen := make(map[string]int)
	for _, item := range items {
		key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.BDF, item.Slot, item.Description)))
		if key == "" {
			continue
		}
		if idx, ok := seen[key]; ok {
			mergePCIe(&out[idx], item)
			continue
		}
		seen[key] = len(out)
		out = append(out, item)
	}
	return out
}

func mergePCIe(dst *models.PCIeDevice, src models.PCIeDevice) {
	setIfEmpty(&dst.Slot, src.Slot)
	setIfEmpty(&dst.Description, src.Description)
	if dst.VendorID == 0 {
		dst.VendorID = src.VendorID
	}
	if dst.DeviceID == 0 {
		dst.DeviceID = src.DeviceID
	}
	setIfEmpty(&dst.BDF, src.BDF)
	setIfEmpty(&dst.DeviceClass, src.DeviceClass)
	setIfEmpty(&dst.Manufacturer, src.Manufacturer)
	setIfEmpty(&dst.Status, src.Status)
}

func dedupeCPU(items []models.CPU) []models.CPU {
	out := make([]models.CPU, 0, len(items))
	seen := make(map[string]int)
	for _, item := range items {
		key := strings.ToLower(strings.TrimSpace(firstNonEmpty(fmt.Sprintf("%d", item.Socket), item.PPIN, item.Model)))
		if key == "" {
			continue
		}
		if idx, ok := seen[key]; ok {
			mergeCPU(&out[idx], item)
			continue
		}
		seen[key] = len(out)
		out = append(out, item)
	}
	return out
}

func dedupeGPU(items []models.GPU) []models.GPU {
	out := make([]models.GPU, 0, len(items))
	seen := make(map[string]int)
	for _, item := range items {
		key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.SerialNumber, item.UUID, item.BDF, item.Slot, item.Model)))
		if key == "" {
			continue
		}
		if idx, ok := seen[key]; ok {
			mergeGPU(&out[idx], item)
			continue
		}
		seen[key] = len(out)
		out = append(out, item)
	}
	return out
}

func mergeGPU(dst *models.GPU, src models.GPU) {
	setIfEmpty(&dst.Slot, src.Slot)
	setIfEmpty(&dst.Location, src.Location)
	setIfEmpty(&dst.Model, src.Model)
	setIfEmpty(&dst.Description, src.Description)
	setIfEmpty(&dst.Manufacturer, src.Manufacturer)
	if dst.VendorID == 0 {
		dst.VendorID = src.VendorID
	}
	if dst.DeviceID == 0 {
		dst.DeviceID = src.DeviceID
	}
	setIfEmpty(&dst.BDF, src.BDF)
	setIfEmpty(&dst.UUID, src.UUID)
	setIfEmpty(&dst.SerialNumber, src.SerialNumber)
	setIfEmpty(&dst.PartNumber, src.PartNumber)
	setIfEmpty(&dst.Firmware, src.Firmware)
	setIfEmpty(&dst.Status, src.Status)
}

func mergeCPU(dst *models.CPU, src models.CPU) {
	if dst.Socket == 0 {
		dst.Socket = src.Socket
	}
	setIfEmpty(&dst.Model, src.Model)
	setIfEmpty(&dst.Description, src.Description)
	if dst.Cores == 0 {
		dst.Cores = src.Cores
	}
	if dst.Threads == 0 {
		dst.Threads = src.Threads
	}
	if dst.FrequencyMHz == 0 {
		dst.FrequencyMHz = src.FrequencyMHz
	}
	if dst.MaxFreqMHz == 0 {
		dst.MaxFreqMHz = src.MaxFreqMHz
	}
	setIfEmpty(&dst.PPIN, src.PPIN)
	setIfEmpty(&dst.Status, src.Status)
}

func dedupeDIMM(items []models.MemoryDIMM) []models.MemoryDIMM {
	out := make([]models.MemoryDIMM, 0, len(items))
	seen := make(map[string]int)
	for _, item := range items {
		key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.SerialNumber, item.Slot)))
		if key == "" {
			continue
		}
		if idx, ok := seen[key]; ok {
			mergeDIMM(&out[idx], item)
			continue
		}
		seen[key] = len(out)
		out = append(out, item)
	}
	return out
}

func mergeDIMM(dst *models.MemoryDIMM, src models.MemoryDIMM) {
	setIfEmpty(&dst.Slot, src.Slot)
	setIfEmpty(&dst.Location, src.Location)
	if src.Present {
		dst.Present = true
	}
	if dst.SizeMB == 0 {
		dst.SizeMB = src.SizeMB
	}
	setIfEmpty(&dst.Type, src.Type)
	setIfEmpty(&dst.Technology, src.Technology)
	if dst.MaxSpeedMHz == 0 {
		dst.MaxSpeedMHz = src.MaxSpeedMHz
	}
	if dst.CurrentSpeedMHz == 0 {
		dst.CurrentSpeedMHz = src.CurrentSpeedMHz
	}
	setIfEmpty(&dst.Manufacturer, src.Manufacturer)
	setIfEmpty(&dst.SerialNumber, src.SerialNumber)
	setIfEmpty(&dst.PartNumber, src.PartNumber)
	setIfEmpty(&dst.Status, src.Status)
}

func dedupeFirmware(items []models.FirmwareInfo) []models.FirmwareInfo {
	out := make([]models.FirmwareInfo, 0, len(items))
	seen := make(map[string]struct{})
	for _, item := range items {
		key := strings.ToLower(strings.TrimSpace(item.DeviceName + "|" + item.Version + "|" + item.Description))
		if key == "" || strings.TrimSpace(item.Version) == "" {
			continue
		}
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, item)
	}
	sort.SliceStable(out, func(i, j int) bool {
		return strings.ToLower(out[i].DeviceName) < strings.ToLower(out[j].DeviceName)
	})
	return out
}

func dedupeSensors(items []models.SensorReading) []models.SensorReading {
	out := make([]models.SensorReading, 0, len(items))
	seen := make(map[string]int)
	for _, item := range items {
		name := strings.TrimSpace(item.Name)
		if name == "" {
			continue
		}
		key := strings.ToLower(strings.TrimSpace(item.Type + "|" + name))
		if idx, ok := seen[key]; ok {
			mergeSensor(&out[idx], item)
			continue
		}
		seen[key] = len(out)
		out = append(out, item)
	}
	return out
}

func mergeSensor(dst *models.SensorReading, src models.SensorReading) {
	setIfEmpty(&dst.Name, src.Name)
	setIfEmpty(&dst.Type, src.Type)
	if dst.Value == 0 && src.Value != 0 {
		dst.Value = src.Value
	}
	setIfEmpty(&dst.Unit, src.Unit)
	setIfEmpty(&dst.RawValue, src.RawValue)
	setIfEmpty(&dst.Status, src.Status)
}

func containsStringFold(items []string, value string) bool {
	for _, item := range items {
		if strings.EqualFold(strings.TrimSpace(item), strings.TrimSpace(value)) {
			return true
		}
	}
	return false
}
