package collector

import (
	"encoding/json"
	"fmt"
	"log"
	"sort"
	"strings"
	"time"

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

// ReplayRedfishFromRawPayloads rebuilds AnalysisResult from saved Redfish raw payloads.
// It expects rawPayloads["redfish_tree"] to contain a map[path]document snapshot.
func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (*models.AnalysisResult, error) {
	if len(rawPayloads) == 0 {
		return nil, fmt.Errorf("missing raw_payloads")
	}
	treeAny, ok := rawPayloads["redfish_tree"]
	if !ok {
		return nil, fmt.Errorf("raw_payloads.redfish_tree is missing")
	}
	tree, ok := treeAny.(map[string]interface{})
	if !ok || len(tree) == 0 {
		return nil, fmt.Errorf("raw_payloads.redfish_tree has invalid format")
	}

	r := redfishSnapshotReader{tree: tree}
	if emit != nil {
		emit(Progress{Status: "running", Progress: 10, Message: "Redfish snapshot: replay service root..."})
	}
	if _, err := r.getJSON("/redfish/v1"); err != nil {
		log.Printf("redfish replay: service root /redfish/v1 missing from snapshot, continuing with defaults: %v", err)
	}

	systemPaths := r.discoverMemberPaths("/redfish/v1/Systems", "/redfish/v1/Systems/1")
	chassisPaths := r.discoverMemberPaths("/redfish/v1/Chassis", "/redfish/v1/Chassis/1")
	managerPaths := r.discoverMemberPaths("/redfish/v1/Managers", "/redfish/v1/Managers/1")
	primarySystem := firstPathOrDefault(systemPaths, "/redfish/v1/Systems/1")
	primaryChassis := firstPathOrDefault(chassisPaths, "/redfish/v1/Chassis/1")
	primaryManager := firstPathOrDefault(managerPaths, "/redfish/v1/Managers/1")

	if emit != nil {
		emit(Progress{Status: "running", Progress: 30, Message: "Redfish snapshot: replay system..."})
	}
	systemDoc, err := r.getJSON(primarySystem)
	if err != nil {
		return nil, fmt.Errorf("system info: %w", err)
	}
	chassisDoc, _ := r.getJSON(primaryChassis)
	biosDoc, _ := r.getJSON(joinPath(primarySystem, "/Bios"))
	secureBootDoc, _ := r.getJSON(joinPath(primarySystem, "/SecureBoot"))
	systemFRUDoc, _ := r.getJSON(joinPath(primarySystem, "/Oem/Public/FRU"))
	chassisFRUDoc, _ := r.getJSON(joinPath(primaryChassis, "/Oem/Public/FRU"))
	fruDoc := systemFRUDoc
	if len(fruDoc) == 0 {
		fruDoc = chassisFRUDoc
	}
	boardFallbackDocs := r.collectBoardFallbackDocs(systemPaths, chassisPaths)

	if emit != nil {
		emit(Progress{Status: "running", Progress: 55, Message: "Redfish snapshot: replay CPU/RAM/Storage..."})
	}
	processors := r.collectProcessors(primarySystem)
	memory := r.collectMemory(primarySystem)
	storageDevices := r.collectStorage(primarySystem)
	storageVolumes := r.collectStorageVolumes(primarySystem)

	if emit != nil {
		emit(Progress{Status: "running", Progress: 80, Message: "Redfish snapshot: replay network/BMC..."})
	}
	psus := r.collectPSUs(chassisPaths)
	pcieDevices := r.collectPCIeDevices(systemPaths, chassisPaths)
	gpus := r.collectGPUs(systemPaths, chassisPaths)
	gpus = r.collectGPUsFromProcessors(systemPaths, chassisPaths, gpus)
	nics := r.collectNICs(chassisPaths)
	r.enrichNICsFromNetworkInterfaces(&nics, systemPaths)
	thresholdSensors := r.collectThresholdSensors(chassisPaths)
	thermalSensors := r.collectThermalSensors(chassisPaths)
	powerSensors := r.collectPowerSensors(chassisPaths)
	discreteEvents := r.collectDiscreteSensorEvents(chassisPaths)
	healthEvents := r.collectHealthSummaryEvents(chassisPaths)
	driveFetchWarningEvents := buildDriveFetchWarningEvents(rawPayloads)
	managerDoc, _ := r.getJSON(primaryManager)
	networkProtocolDoc, _ := r.getJSON(joinPath(primaryManager, "/NetworkProtocol"))
	firmware := parseFirmware(systemDoc, biosDoc, managerDoc, secureBootDoc, networkProtocolDoc)
	firmware = dedupeFirmwareInfo(append(firmware, r.collectFirmwareInventory()...))
	boardInfo := parseBoardInfoWithFallback(systemDoc, chassisDoc, fruDoc)
	applyBoardInfoFallbackFromDocs(&boardInfo, boardFallbackDocs)
	boardInfo.BMCMACAddress = r.collectBMCMAC(managerPaths)
	assemblyFRU := r.collectAssemblyFRU(chassisPaths)
	collectedAt, sourceTimezone := inferRedfishCollectionTime(managerDoc, rawPayloads)

	result := &models.AnalysisResult{
		CollectedAt:    collectedAt,
		SourceTimezone: sourceTimezone,
		Events:         append(append(append(make([]models.Event, 0, len(discreteEvents)+len(healthEvents)+len(driveFetchWarningEvents)+1), healthEvents...), discreteEvents...), driveFetchWarningEvents...),
		FRU:            assemblyFRU,
		Sensors:        dedupeSensorReadings(append(append(thresholdSensors, thermalSensors...), powerSensors...)),
		RawPayloads:    cloneRawPayloads(rawPayloads),
		Hardware: &models.HardwareConfig{
			BoardInfo:       boardInfo,
			CPUs:            processors,
			Memory:          memory,
			Storage:         storageDevices,
			Volumes:         storageVolumes,
			PCIeDevices:     pcieDevices,
			GPUs:            gpus,
			PowerSupply:     psus,
			NetworkAdapters: nics,
			Firmware:        firmware,
		},
	}
	if strings.TrimSpace(sourceTimezone) != "" {
		if result.RawPayloads == nil {
			result.RawPayloads = map[string]any{}
		}
		result.RawPayloads["source_timezone"] = sourceTimezone
	}
	appendMissingServerModelWarning(result, systemDoc, joinPath(primarySystem, "/Oem/Public/FRU"), joinPath(primaryChassis, "/Oem/Public/FRU"))
	return result, nil
}

func inferRedfishCollectionTime(managerDoc map[string]interface{}, rawPayloads map[string]any) (time.Time, string) {
	dateTime := strings.TrimSpace(asString(managerDoc["DateTime"]))
	offset := strings.TrimSpace(asString(managerDoc["DateTimeLocalOffset"]))
	if dateTime != "" {
		if ts, err := time.Parse(time.RFC3339Nano, dateTime); err == nil {
			if offset == "" {
				offset = ts.Format("-07:00")
			}
			return ts.UTC(), offset
		}
		if ts, err := time.Parse(time.RFC3339, dateTime); err == nil {
			if offset == "" {
				offset = ts.Format("-07:00")
			}
			return ts.UTC(), offset
		}
	}
	if offset == "" && len(rawPayloads) > 0 {
		if tz, ok := rawPayloads["source_timezone"].(string); ok {
			offset = strings.TrimSpace(tz)
		}
	}
	return time.Time{}, offset
}

func appendMissingServerModelWarning(result *models.AnalysisResult, systemDoc map[string]interface{}, systemFRUPath, chassisFRUPath string) {
	if result == nil || result.Hardware == nil {
		return
	}
	if strings.TrimSpace(result.Hardware.BoardInfo.ProductName) != "" {
		return
	}

	reasons := make([]string, 0, 3)
	systemModelRaw := strings.TrimSpace(asString(systemDoc["Model"]))
	if systemModelRaw != "" && normalizeRedfishIdentityField(systemModelRaw) == "" {
		reasons = append(reasons, fmt.Sprintf("system model is placeholder: %q", systemModelRaw))
	}

	errs := redfishFetchErrorsFromRawPayloads(result.RawPayloads)
	if msg := errs[normalizeRedfishPath(systemFRUPath)]; strings.TrimSpace(msg) != "" {
		reasons = append(reasons, fmt.Sprintf("%s unavailable: %s", systemFRUPath, msg))
	}
	if msg := errs[normalizeRedfishPath(chassisFRUPath)]; strings.TrimSpace(msg) != "" {
		reasons = append(reasons, fmt.Sprintf("%s unavailable: %s", chassisFRUPath, msg))
	}
	if len(reasons) == 0 {
		reasons = append(reasons, "no non-placeholder ProductName/Model found in collected Redfish documents")
	}

	result.Events = append(result.Events, models.Event{
		Timestamp:   time.Now(),
		Source:      "Redfish",
		EventType:   "Collection Warning",
		Severity:    models.SeverityWarning,
		Description: "Server model is missing in collected Redfish data",
		RawData:     strings.Join(reasons, "; "),
	})
}

func redfishFetchErrorsFromRawPayloads(rawPayloads map[string]any) map[string]string {
	out := make(map[string]string)
	if len(rawPayloads) == 0 {
		return out
	}
	raw, ok := rawPayloads["redfish_fetch_errors"]
	if !ok {
		return out
	}
	switch list := raw.(type) {
	case []map[string]interface{}:
		return redfishFetchErrorListToMap(list)
	case []interface{}:
		normalized := make([]map[string]interface{}, 0, len(list))
		for _, item := range list {
			m, ok := item.(map[string]interface{})
			if !ok {
				continue
			}
			normalized = append(normalized, m)
		}
		return redfishFetchErrorListToMap(normalized)
	default:
		return out
	}
}

func buildDriveFetchWarningEvents(rawPayloads map[string]any) []models.Event {
	errs := redfishFetchErrorsFromRawPayloads(rawPayloads)
	if len(errs) == 0 {
		return nil
	}

	paths := make([]string, 0, len(errs))
	timeoutCount := 0
	for path, msg := range errs {
		normalizedPath := normalizeRedfishPath(path)
		if !strings.Contains(strings.ToLower(normalizedPath), "/drives/") {
			continue
		}
		paths = append(paths, normalizedPath)
		low := strings.ToLower(msg)
		if strings.Contains(low, "timeout") || strings.Contains(low, "deadline exceeded") {
			timeoutCount++
		}
	}
	if len(paths) == 0 {
		return nil
	}

	sort.Strings(paths)
	preview := paths
	const maxPreview = 8
	if len(preview) > maxPreview {
		preview = preview[:maxPreview]
	}
	rawData := strings.Join(preview, ", ")
	if len(paths) > len(preview) {
		rawData = fmt.Sprintf("%s (+%d more)", rawData, len(paths)-len(preview))
	}
	if timeoutCount > 0 {
		rawData = fmt.Sprintf("timeouts=%d; paths=%s", timeoutCount, rawData)
	}

	return []models.Event{
		{
			Timestamp:   time.Now(),
			Source:      "Redfish",
			EventType:   "Collection Warning",
			Severity:    models.SeverityWarning,
			Description: fmt.Sprintf("%d drive documents were unavailable; storage details may be incomplete", len(paths)),
			RawData:     rawData,
		},
	}
}

func (r redfishSnapshotReader) collectFirmwareInventory() []models.FirmwareInfo {
	docs, err := r.getCollectionMembers("/redfish/v1/UpdateService/FirmwareInventory")
	if err != nil || len(docs) == 0 {
		return nil
	}
	out := make([]models.FirmwareInfo, 0, len(docs))
	for _, doc := range docs {
		version := firstNonEmpty(
			asString(doc["Version"]),
			asString(doc["FirmwareVersion"]),
			asString(doc["SoftwareVersion"]),
		)
		if strings.TrimSpace(version) == "" {
			continue
		}
		name := firmwareInventoryDeviceName(doc)
		name = strings.TrimSpace(name)
		if name == "" {
			continue
		}
		// BMCImageN entries are redundant backup slot labels; skip them.
		if strings.HasPrefix(strings.ToLower(name), "bmcimage") {
			continue
		}
		out = append(out, models.FirmwareInfo{DeviceName: name, Version: version})
	}
	return out
}

func firmwareInventoryDeviceName(doc map[string]interface{}) string {
	name := strings.TrimSpace(asString(doc["DeviceName"]))
	if name != "" {
		return name
	}

	id := strings.TrimSpace(asString(doc["Id"]))
	rawName := strings.TrimSpace(asString(doc["Name"]))
	if strings.EqualFold(rawName, "Software Inventory") || strings.EqualFold(rawName, "Firmware Inventory") {
		if id != "" {
			return id
		}
	}
	if rawName != "" {
		return rawName
	}
	return id
}

func dedupeFirmwareInfo(items []models.FirmwareInfo) []models.FirmwareInfo {
	seen := make(map[string]struct{}, len(items))
	out := make([]models.FirmwareInfo, 0, len(items))
	for _, fw := range items {
		name := strings.TrimSpace(fw.DeviceName)
		ver := strings.TrimSpace(fw.Version)
		if name == "" || ver == "" {
			continue
		}
		key := strings.ToLower(name + "|" + ver)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, models.FirmwareInfo{DeviceName: name, Version: ver})
	}
	return out
}

func (r redfishSnapshotReader) collectThresholdSensors(chassisPaths []string) []models.SensorReading {
	out := make([]models.SensorReading, 0)
	seen := make(map[string]struct{})
	for _, chassisPath := range chassisPaths {
		thresholdPath := joinPath(chassisPath, "/ThresholdSensors")
		docs, _ := r.getCollectionMembers(thresholdPath)
		if len(docs) == 0 {
			if thresholdDoc, err := r.getJSON(thresholdPath); err == nil {
				docs = append(docs, redfishInlineSensors(thresholdDoc)...)
			}
		}
		for _, doc := range docs {
			sensor, ok := parseThresholdSensor(doc)
			if !ok {
				continue
			}
			key := strings.ToLower(strings.TrimSpace(sensor.Name))
			if key == "" {
				key = strings.ToLower(strings.TrimSpace(sensor.Type) + "|" + strings.TrimSpace(sensor.RawValue))
			}
			if _, exists := seen[key]; exists {
				continue
			}
			seen[key] = struct{}{}
			out = append(out, sensor)
		}
	}
	return out
}

func parseThresholdSensor(doc map[string]interface{}) (models.SensorReading, bool) {
	if len(doc) == 0 {
		return models.SensorReading{}, false
	}
	name := firstNonEmpty(asString(doc["Name"]), asString(doc["Id"]))
	status := mapStatus(doc["Status"])
	if status == "" {
		status = firstNonEmpty(asString(doc["Health"]), asString(doc["State"]))
	}
	reading := 0.0
	unit := ""
	rawValue := ""
	switch {
	case asString(doc["ReadingCelsius"]) != "":
		reading = asFloat(doc["ReadingCelsius"])
		unit = "C"
		rawValue = asString(doc["ReadingCelsius"])
	case asString(doc["ReadingVolts"]) != "":
		reading = asFloat(doc["ReadingVolts"])
		unit = "V"
		rawValue = asString(doc["ReadingVolts"])
	case asString(doc["ReadingAmps"]) != "":
		reading = asFloat(doc["ReadingAmps"])
		unit = "A"
		rawValue = asString(doc["ReadingAmps"])
	case asString(doc["ReadingWatts"]) != "":
		reading = asFloat(doc["ReadingWatts"])
		unit = "W"
		rawValue = asString(doc["ReadingWatts"])
	case asString(doc["Reading"]) != "":
		reading = asFloat(doc["Reading"])
		unit = asString(doc["ReadingUnits"])
		rawValue = asString(doc["Reading"])
	}

	if name == "" && rawValue == "" && status == "" {
		return models.SensorReading{}, false
	}
	return models.SensorReading{
		Name:     firstNonEmpty(name, "threshold-sensor"),
		Type:     firstNonEmpty(asString(doc["ReadingType"]), asString(doc["SensorType"]), "threshold"),
		Value:    reading,
		Unit:     unit,
		RawValue: rawValue,
		Status:   status,
	}, true
}

func (r redfishSnapshotReader) collectDiscreteSensorEvents(chassisPaths []string) []models.Event {
	out := make([]models.Event, 0)
	for _, chassisPath := range chassisPaths {
		discretePath := joinPath(chassisPath, "/DiscreteSensors")
		docs, _ := r.getCollectionMembers(discretePath)
		if len(docs) == 0 {
			if discreteDoc, err := r.getJSON(discretePath); err == nil {
				docs = append(docs, redfishInlineSensors(discreteDoc)...)
			}
		}
		for _, doc := range docs {
			ev, ok := parseDiscreteSensorEvent(doc)
			if !ok {
				continue
			}
			out = append(out, ev)
		}
	}
	return out
}

func parseDiscreteSensorEvent(doc map[string]interface{}) (models.Event, bool) {
	name := firstNonEmpty(asString(doc["Name"]), asString(doc["Id"]))
	status := mapStatus(doc["Status"])
	if status == "" {
		status = firstNonEmpty(asString(doc["Health"]), asString(doc["State"]))
	}
	if name == "" || status == "" {
		return models.Event{}, false
	}
	normalized := strings.ToLower(strings.TrimSpace(status))
	if normalized == "ok" || normalized == "enabled" || normalized == "normal" || normalized == "present" {
		return models.Event{}, false
	}
	return models.Event{
		Timestamp:   time.Now(),
		Source:      "Redfish",
		SensorName:  name,
		EventType:   "Discrete Sensor Status",
		Severity:    models.SeverityWarning,
		Description: fmt.Sprintf("%s reports %s", name, status),
		RawData:     firstNonEmpty(asString(doc["Description"]), status),
	}, true
}

func (r redfishSnapshotReader) collectThermalSensors(chassisPaths []string) []models.SensorReading {
	out := make([]models.SensorReading, 0)
	for _, chassisPath := range chassisPaths {
		doc, err := r.getJSON(joinPath(chassisPath, "/Thermal"))
		if err != nil || len(doc) == 0 {
			continue
		}
		for _, fanDoc := range redfishArrayObjects(doc["Fans"]) {
			out = append(out, parseThermalFanSensor(fanDoc))
		}
		for _, tempDoc := range redfishArrayObjects(doc["Temperatures"]) {
			out = append(out, parseThermalTemperatureSensor(tempDoc))
		}
	}
	return out
}

func (r redfishSnapshotReader) collectPowerSensors(chassisPaths []string) []models.SensorReading {
	out := make([]models.SensorReading, 0)
	for _, chassisPath := range chassisPaths {
		doc, err := r.getJSON(joinPath(chassisPath, "/Power"))
		if err != nil || len(doc) == 0 {
			continue
		}
		out = append(out, parsePowerOemPublicSensors(doc)...)
		for _, controlDoc := range redfishArrayObjects(doc["PowerControl"]) {
			if sensor, ok := parsePowerControlSensor(controlDoc); ok {
				out = append(out, sensor)
			}
		}
		for _, psuDoc := range redfishArrayObjects(doc["PowerSupplies"]) {
			out = append(out, parsePowerSupplySensors(psuDoc)...)
		}
	}
	return out
}

func parseThermalFanSensor(doc map[string]interface{}) models.SensorReading {
	name := firstNonEmpty(asString(doc["Name"]), asString(doc["MemberId"]), "Fan")
	unit := firstNonEmpty(asString(doc["ReadingUnits"]), "RPM")
	value := asFloat(doc["Reading"])
	raw := firstNonEmpty(asString(doc["Reading"]), asString(doc["Name"]))
	status := firstNonEmpty(mapStatus(doc["Status"]), asString(doc["State"]), asString(doc["Health"]), "unknown")
	return models.SensorReading{
		Name:     name,
		Type:     "fan_speed",
		Value:    value,
		Unit:     unit,
		RawValue: raw,
		Status:   status,
	}
}

func parseThermalTemperatureSensor(doc map[string]interface{}) models.SensorReading {
	name := firstNonEmpty(asString(doc["Name"]), asString(doc["MemberId"]), "Temperature")
	reading := asFloat(doc["ReadingCelsius"])
	raw := asString(doc["ReadingCelsius"])
	if raw == "" {
		reading = asFloat(doc["Reading"])
		raw = asString(doc["Reading"])
	}
	status := firstNonEmpty(mapStatus(doc["Status"]), asString(doc["State"]), asString(doc["Health"]), "unknown")
	return models.SensorReading{
		Name:     name,
		Type:     "temperature",
		Value:    reading,
		Unit:     "C",
		RawValue: raw,
		Status:   status,
	}
}

func parsePowerOemPublicSensors(doc map[string]interface{}) []models.SensorReading {
	oem, ok := doc["Oem"].(map[string]interface{})
	if !ok {
		return nil
	}
	public, ok := oem["Public"].(map[string]interface{})
	if !ok {
		return nil
	}
	var out []models.SensorReading
	add := func(name, key string) {
		raw := asString(public[key])
		if strings.TrimSpace(raw) == "" {
			return
		}
		out = append(out, models.SensorReading{
			Name:     name,
			Type:     "power",
			Value:    asFloat(public[key]),
			Unit:     "W",
			RawValue: raw,
			Status:   "OK",
		})
	}
	add("Total_Power", "TotalPower")
	add("CPU_Power", "CurrentCPUPowerWatts")
	add("Memory_Power", "CurrentMemoryPowerWatts")
	add("Fan_Power", "CurrentFANPowerWatts")
	return out
}

func parsePowerControlSensor(doc map[string]interface{}) (models.SensorReading, bool) {
	raw := asString(doc["PowerConsumedWatts"])
	if strings.TrimSpace(raw) == "" {
		return models.SensorReading{}, false
	}
	name := firstNonEmpty(asString(doc["Name"]), asString(doc["MemberId"]), "PowerControl")
	status := firstNonEmpty(mapStatus(doc["Status"]), asString(doc["State"]), asString(doc["Health"]), "unknown")
	return models.SensorReading{
		Name:     name + "_Consumed",
		Type:     "power",
		Value:    asFloat(doc["PowerConsumedWatts"]),
		Unit:     "W",
		RawValue: raw,
		Status:   status,
	}, true
}

func parsePowerSupplySensors(doc map[string]interface{}) []models.SensorReading {
	name := firstNonEmpty(asString(doc["Name"]), "PSU")
	status := firstNonEmpty(mapStatus(doc["Status"]), asString(doc["State"]), asString(doc["Health"]), "unknown")
	var out []models.SensorReading
	add := func(suffix, key, unit string) {
		raw := asString(doc[key])
		if strings.TrimSpace(raw) == "" {
			return
		}
		out = append(out, models.SensorReading{
			Name:     fmt.Sprintf("%s_%s", name, suffix),
			Type:     strings.ToLower(suffix),
			Value:    asFloat(doc[key]),
			Unit:     unit,
			RawValue: raw,
			Status:   status,
		})
	}
	add("InputPower", "PowerInputWatts", "W")
	add("OutputPower", "LastPowerOutputWatts", "W")
	add("InputVoltage", "LineInputVoltage", "V")
	return out
}

func redfishArrayObjects(v any) []map[string]interface{} {
	list, ok := v.([]interface{})
	if !ok || len(list) == 0 {
		return nil
	}
	out := make([]map[string]interface{}, 0, len(list))
	for _, item := range list {
		m, ok := item.(map[string]interface{})
		if !ok {
			continue
		}
		out = append(out, m)
	}
	return out
}

func redfishInlineSensors(doc map[string]interface{}) []map[string]interface{} {
	return redfishArrayObjects(doc["Sensors"])
}

func dedupeSensorReadings(items []models.SensorReading) []models.SensorReading {
	if len(items) <= 1 {
		return items
	}
	out := make([]models.SensorReading, 0, len(items))
	seen := make(map[string]struct{}, len(items))
	for _, s := range items {
		key := strings.ToLower(strings.TrimSpace(s.Name) + "|" + strings.TrimSpace(s.Type))
		if strings.TrimSpace(key) == "|" {
			key = strings.ToLower(strings.TrimSpace(s.RawValue))
		}
		if strings.TrimSpace(key) == "" {
			continue
		}
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, s)
	}
	return out
}

func (r redfishSnapshotReader) collectHealthSummaryEvents(chassisPaths []string) []models.Event {
	out := make([]models.Event, 0)
	for _, chassisPath := range chassisPaths {
		doc, err := r.getJSON(joinPath(chassisPath, "/HealthSummary"))
		if err != nil || len(doc) == 0 {
			continue
		}
		health := firstNonEmpty(
			mapStatus(doc["Status"]),
			asString(doc["Health"]),
			asString(doc["HealthRollup"]),
			findFirstNormalizedStringByKeys(doc, "Health", "HealthRollup", "OverallHealth"),
		)
		if health == "" {
			continue
		}
		if strings.EqualFold(health, "OK") || strings.EqualFold(health, "Normal") {
			continue
		}
		raw, _ := json.Marshal(doc)
		out = append(out, models.Event{
			Timestamp:   time.Now(),
			Source:      "Redfish",
			EventType:   "Health Summary",
			Severity:    models.SeverityWarning,
			Description: fmt.Sprintf("Chassis health summary reports %s", health),
			RawData:     string(raw),
		})
	}
	return out
}

func (r redfishSnapshotReader) enrichNICsFromNetworkInterfaces(nics *[]models.NetworkAdapter, systemPaths []string) {
	if nics == nil {
		return
	}
	bySlot := make(map[string]int, len(*nics))
	for i, nic := range *nics {
		bySlot[strings.ToLower(strings.TrimSpace(nic.Slot))] = i
	}

	for _, systemPath := range systemPaths {
		ifaces, err := r.getCollectionMembers(joinPath(systemPath, "/NetworkInterfaces"))
		if err != nil || len(ifaces) == 0 {
			continue
		}
		for _, iface := range ifaces {
			slot := firstNonEmpty(asString(iface["Id"]), asString(iface["Name"]))
			if strings.TrimSpace(slot) == "" {
				continue
			}
			idx, ok := bySlot[strings.ToLower(strings.TrimSpace(slot))]
			if !ok {
				*nics = append(*nics, models.NetworkAdapter{
					Slot:    slot,
					Present: true,
					Model:   firstNonEmpty(asString(iface["Model"]), asString(iface["Name"])),
					Status:  mapStatus(iface["Status"]),
				})
				idx = len(*nics) - 1
				bySlot[strings.ToLower(strings.TrimSpace(slot))] = idx
			}

			portsPath := redfishLinkedPath(iface, "NetworkPorts")
			if portsPath == "" {
				continue
			}
			portDocs, err := r.getCollectionMembers(portsPath)
			if err != nil || len(portDocs) == 0 {
				continue
			}
			macs := append([]string{}, (*nics)[idx].MACAddresses...)
			for _, p := range portDocs {
				macs = append(macs, collectNetworkPortMACs(p)...)
			}
			(*nics)[idx].MACAddresses = dedupeStrings(macs)
			if sanitizeNetworkPortCount((*nics)[idx].PortCount) == 0 {
				(*nics)[idx].PortCount = len(portDocs)
			}
		}
	}
}

func collectNetworkPortMACs(doc map[string]interface{}) []string {
	if len(doc) == 0 {
		return nil
	}
	out := make([]string, 0, 4)
	if list, ok := doc["AssociatedNetworkAddresses"].([]interface{}); ok {
		for _, item := range list {
			if s := strings.TrimSpace(asString(item)); s != "" {
				out = append(out, s)
			}
		}
	}
	for _, key := range []string{"MACAddress", "PermanentMACAddress", "CurrentMACAddress"} {
		if s := strings.TrimSpace(asString(doc[key])); s != "" {
			out = append(out, s)
		}
	}
	return out
}

func dedupeStrings(items []string) []string {
	seen := make(map[string]struct{}, len(items))
	out := make([]string, 0, len(items))
	for _, item := range items {
		s := strings.TrimSpace(item)
		if s == "" {
			continue
		}
		key := strings.ToLower(s)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, s)
	}
	return out
}

func (r redfishSnapshotReader) collectBoardFallbackDocs(systemPaths, chassisPaths []string) []map[string]interface{} {
	out := make([]map[string]interface{}, 0)
	for _, chassisPath := range chassisPaths {
		for _, suffix := range []string{"/Boards", "/Backplanes"} {
			path := joinPath(chassisPath, suffix)
			if docs, err := r.getCollectionMembers(path); err == nil && len(docs) > 0 {
				out = append(out, docs...)
				continue
			}
			if doc, err := r.getJSON(path); err == nil && len(doc) > 0 {
				out = append(out, doc)
			}
		}
	}
	for _, path := range append(append([]string{}, systemPaths...), chassisPaths...) {
		for _, suffix := range []string{"/Oem/Public", "/Oem/Public/ThermalConfig", "/ThermalConfig"} {
			docPath := joinPath(path, suffix)
			if doc, err := r.getJSON(docPath); err == nil && len(doc) > 0 {
				out = append(out, doc)
			}
		}
	}
	return out
}

func applyBoardInfoFallbackFromDocs(board *models.BoardInfo, docs []map[string]interface{}) {
	if board == nil || len(docs) == 0 {
		return
	}
	for _, doc := range docs {
		candidate := parseBoardInfoFromFRUDoc(doc)
		if !isLikelyServerProductName(candidate.ProductName) {
			continue
		}
		if board.Manufacturer == "" {
			board.Manufacturer = candidate.Manufacturer
		}
		if board.ProductName == "" {
			board.ProductName = candidate.ProductName
		}
		if board.SerialNumber == "" {
			board.SerialNumber = candidate.SerialNumber
		}
		if board.PartNumber == "" {
			board.PartNumber = candidate.PartNumber
		}
		if board.Manufacturer != "" && board.ProductName != "" && board.SerialNumber != "" && board.PartNumber != "" {
			return
		}
	}
}

func isLikelyServerProductName(v string) bool {
	v = strings.TrimSpace(v)
	if v == "" {
		return false
	}
	n := strings.ToUpper(v)
	if strings.Contains(n, "NULL") {
		return false
	}
	componentTokens := []string{
		"DIMM", "DDR", "NVME", "SSD", "HDD", "GPU", "NIC", "RAID",
		"PSU", "FAN", "BACKPLANE", "FRU",
	}
	for _, token := range componentTokens {
		if strings.Contains(n, strings.ToUpper(token)) {
			return false
		}
	}
	return true
}

type redfishSnapshotReader struct {
	tree map[string]interface{}
}

func (r redfishSnapshotReader) getJSON(requestPath string) (map[string]interface{}, error) {
	p := normalizeRedfishPath(requestPath)
	if doc, ok := r.tree[p]; ok {
		if m, ok := doc.(map[string]interface{}); ok {
			return m, nil
		}
	}
	if p != "/" {
		if doc, ok := r.tree[stringsTrimTrailingSlash(p)]; ok {
			if m, ok := doc.(map[string]interface{}); ok {
				return m, nil
			}
		}
		if doc, ok := r.tree[p+"/"]; ok {
			if m, ok := doc.(map[string]interface{}); ok {
				return m, nil
			}
		}
	}
	return nil, fmt.Errorf("snapshot path not found: %s", requestPath)
}

func (r redfishSnapshotReader) getCollectionMembers(collectionPath string) ([]map[string]interface{}, error) {
	collection, err := r.getJSON(collectionPath)
	if err != nil {
		return r.fallbackCollectionMembers(collectionPath, err)
	}
	memberPaths := redfishCollectionMemberRefs(collection)
	if len(memberPaths) == 0 {
		return r.fallbackCollectionMembers(collectionPath, nil)
	}
	out := make([]map[string]interface{}, 0, len(memberPaths))
	for _, memberPath := range memberPaths {
		doc, err := r.getJSON(memberPath)
		if err != nil {
			continue
		}
		if strings.TrimSpace(asString(doc["@odata.id"])) == "" {
			doc["@odata.id"] = normalizeRedfishPath(memberPath)
		}
		out = append(out, doc)
	}
	if len(out) == 0 {
		return r.fallbackCollectionMembers(collectionPath, nil)
	}
	return out, nil
}

func (r redfishSnapshotReader) fallbackCollectionMembers(collectionPath string, originalErr error) ([]map[string]interface{}, error) {
	prefix := strings.TrimSuffix(normalizeRedfishPath(collectionPath), "/") + "/"
	if prefix == "/" {
		if originalErr != nil {
			return nil, originalErr
		}
		return []map[string]interface{}{}, nil
	}
	paths := make([]string, 0)
	for key := range r.tree {
		p := normalizeRedfishPath(key)
		if !strings.HasPrefix(p, prefix) {
			continue
		}
		rest := strings.TrimPrefix(p, prefix)
		if rest == "" || strings.Contains(rest, "/") {
			continue
		}
		paths = append(paths, p)
	}
	if len(paths) == 0 {
		if originalErr != nil {
			return nil, originalErr
		}
		return []map[string]interface{}{}, nil
	}
	sort.Strings(paths)
	out := make([]map[string]interface{}, 0, len(paths))
	for _, p := range paths {
		doc, err := r.getJSON(p)
		if err != nil {
			continue
		}
		if strings.TrimSpace(asString(doc["@odata.id"])) == "" {
			doc["@odata.id"] = normalizeRedfishPath(p)
		}
		out = append(out, doc)
	}
	return out, nil
}

func cloneRawPayloads(src map[string]any) map[string]any {
	if len(src) == 0 {
		return nil
	}
	dst := make(map[string]any, len(src))
	for k, v := range src {
		dst[k] = v
	}
	return dst
}

func (r redfishSnapshotReader) discoverMemberPaths(collectionPath, fallbackPath string) []string {
	collection, err := r.getJSON(collectionPath)
	if err == nil {
		if refs, ok := collection["Members"].([]interface{}); ok && len(refs) > 0 {
			paths := make([]string, 0, len(refs))
			for _, refAny := range refs {
				ref, ok := refAny.(map[string]interface{})
				if !ok {
					continue
				}
				memberPath := asString(ref["@odata.id"])
				if memberPath != "" {
					paths = append(paths, memberPath)
				}
			}
			if len(paths) > 0 {
				return paths
			}
		}
	}
	if fallbackPath != "" {
		return []string{fallbackPath}
	}
	return nil
}

func (r redfishSnapshotReader) getLinkedPCIeFunctions(doc map[string]interface{}) []map[string]interface{} {
	if links, ok := doc["Links"].(map[string]interface{}); ok {
		if refs, ok := links["PCIeFunctions"].([]interface{}); ok && len(refs) > 0 {
			out := make([]map[string]interface{}, 0, len(refs))
			for _, refAny := range refs {
				ref, ok := refAny.(map[string]interface{})
				if !ok {
					continue
				}
				memberPath := asString(ref["@odata.id"])
				if memberPath == "" {
					continue
				}
				memberDoc, err := r.getJSON(memberPath)
				if err != nil {
					continue
				}
				out = append(out, memberDoc)
			}
			return out
		}
	}
	if pcieFunctions, ok := doc["PCIeFunctions"].(map[string]interface{}); ok {
		if collectionPath := asString(pcieFunctions["@odata.id"]); collectionPath != "" {
			memberDocs, err := r.getCollectionMembers(collectionPath)
			if err == nil {
				return memberDocs
			}
		}
	}
	return nil
}

func (r redfishSnapshotReader) getLinkedSupplementalDocs(doc map[string]interface{}, keys ...string) []map[string]interface{} {
	if len(doc) == 0 || len(keys) == 0 {
		return nil
	}
	var out []map[string]interface{}
	seen := make(map[string]struct{})
	for _, key := range keys {
		path := normalizeRedfishPath(redfishLinkedPath(doc, key))
		if path == "" {
			continue
		}
		if _, ok := seen[path]; ok {
			continue
		}
		supplementalDoc, err := r.getJSON(path)
		if err != nil || len(supplementalDoc) == 0 {
			continue
		}
		seen[path] = struct{}{}
		out = append(out, supplementalDoc)
	}
	return out
}

func (r redfishSnapshotReader) collectProcessors(systemPath string) []models.CPU {
	memberDocs, err := r.getCollectionMembers(joinPath(systemPath, "/Processors"))
	if err != nil || len(memberDocs) == 0 {
		return nil
	}
	out := make([]models.CPU, 0, len(memberDocs))
	socketIdx := 0
	for _, doc := range memberDocs {
		if pt := strings.TrimSpace(asString(doc["ProcessorType"])); pt != "" &&
			!strings.EqualFold(pt, "CPU") && !strings.EqualFold(pt, "General") {
			continue
		}
		cpu := parseCPUs([]map[string]interface{}{doc})[0]
		if cpu.Socket == 0 && socketIdx > 0 && strings.TrimSpace(asString(doc["Socket"])) == "" {
			cpu.Socket = socketIdx
			if cpu.Details == nil {
				cpu.Details = map[string]any{}
			}
			cpu.Details["socket"] = cpu.Socket
		}
		supplementalDocs := r.getLinkedSupplementalDocs(doc, "ProcessorMetrics", "EnvironmentMetrics", "Metrics")
		if len(supplementalDocs) > 0 {
			cpu.Details = mergeGenericDetails(cpu.Details, redfishCPUDetailsAcrossDocs(doc, supplementalDocs...))
		}
		out = append(out, cpu)
		socketIdx++
	}
	return out
}

func (r redfishSnapshotReader) collectMemory(systemPath string) []models.MemoryDIMM {
	memberDocs, err := r.getCollectionMembers(joinPath(systemPath, "/Memory"))
	if err != nil || len(memberDocs) == 0 {
		return nil
	}
	out := make([]models.MemoryDIMM, 0, len(memberDocs))
	for _, doc := range memberDocs {
		dimm := parseMemory([]map[string]interface{}{doc})[0]
		supplementalDocs := r.getLinkedSupplementalDocs(doc, "MemoryMetrics", "EnvironmentMetrics", "Metrics")
		if len(supplementalDocs) > 0 {
			dimm.Details = mergeGenericDetails(dimm.Details, redfishMemoryDetailsAcrossDocs(doc, supplementalDocs...))
		}
		out = append(out, dimm)
	}
	return out
}

func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storage {
	var out []models.Storage
	storageMembers, _ := r.getCollectionMembers(joinPath(systemPath, "/Storage"))
	for _, member := range storageMembers {
		if driveCollection, ok := member["Drives"].(map[string]interface{}); ok {
			if driveCollectionPath := asString(driveCollection["@odata.id"]); driveCollectionPath != "" {
				driveDocs, err := r.getCollectionMembers(driveCollectionPath)
				if err == nil {
					for _, driveDoc := range driveDocs {
						if !isVirtualStorageDrive(driveDoc) {
							supplementalDocs := r.getLinkedSupplementalDocs(driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
							out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
						}
					}
					if len(driveDocs) == 0 {
						for _, driveDoc := range r.probeDirectDiskBayChildren(driveCollectionPath) {
							supplementalDocs := r.getLinkedSupplementalDocs(driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
							out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
						}
					}
				}
				continue
			}
		}
		if drives, ok := member["Drives"].([]interface{}); ok {
			for _, driveAny := range drives {
				driveRef, ok := driveAny.(map[string]interface{})
				if !ok {
					continue
				}
				odata := asString(driveRef["@odata.id"])
				if odata == "" {
					continue
				}
				driveDoc, err := r.getJSON(odata)
				if err != nil {
					continue
				}
				if !isVirtualStorageDrive(driveDoc) {
					supplementalDocs := r.getLinkedSupplementalDocs(driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
					out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
				}
			}
			continue
		}
		if looksLikeDrive(member) {
			if isVirtualStorageDrive(member) {
				continue
			}
			supplementalDocs := r.getLinkedSupplementalDocs(member, "DriveMetrics", "EnvironmentMetrics", "Metrics")
			out = append(out, parseDriveWithSupplementalDocs(member, supplementalDocs...))
		}

		for _, enclosurePath := range redfishLinkRefs(member, "Links", "Enclosures") {
			driveDocs, err := r.getCollectionMembers(joinPath(enclosurePath, "/Drives"))
			if err == nil {
				for _, driveDoc := range driveDocs {
					if looksLikeDrive(driveDoc) && !isVirtualStorageDrive(driveDoc) {
						supplementalDocs := r.getLinkedSupplementalDocs(driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
						out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
					}
				}
				if len(driveDocs) == 0 {
					for _, driveDoc := range r.probeDirectDiskBayChildren(joinPath(enclosurePath, "/Drives")) {
						if isVirtualStorageDrive(driveDoc) {
							continue
						}
						out = append(out, parseDrive(driveDoc))
					}
				}
			}
		}
	}

	for _, driveDoc := range r.collectKnownStorageMembers(systemPath, []string{
		"/Storage/IntelVROC/Drives",
		"/Storage/IntelVROC/Controllers/1/Drives",
	}) {
		if looksLikeDrive(driveDoc) && !isVirtualStorageDrive(driveDoc) {
			supplementalDocs := r.getLinkedSupplementalDocs(driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
			out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
		}
	}

	simpleStorageMembers, _ := r.getCollectionMembers(joinPath(systemPath, "/SimpleStorage"))
	for _, member := range simpleStorageMembers {
		devices, ok := member["Devices"].([]interface{})
		if !ok {
			continue
		}
		for _, devAny := range devices {
			devDoc, ok := devAny.(map[string]interface{})
			if !ok || !looksLikeDrive(devDoc) || isVirtualStorageDrive(devDoc) {
				continue
			}
			out = append(out, parseDrive(devDoc))
		}
	}

	chassisPaths := r.discoverMemberPaths("/redfish/v1/Chassis", "/redfish/v1/Chassis/1")
	for _, chassisPath := range chassisPaths {
		driveDocs, err := r.getCollectionMembers(joinPath(chassisPath, "/Drives"))
		if err != nil {
			continue
		}
		for _, driveDoc := range driveDocs {
			if !looksLikeDrive(driveDoc) || isVirtualStorageDrive(driveDoc) {
				continue
			}
			out = append(out, parseDrive(driveDoc))
		}
	}
	for _, chassisPath := range chassisPaths {
		if !isSupermicroNVMeBackplanePath(chassisPath) {
			continue
		}
		for _, driveDoc := range r.probeSupermicroNVMeDiskBays(chassisPath) {
			if !looksLikeDrive(driveDoc) || isVirtualStorageDrive(driveDoc) {
				continue
			}
			out = append(out, parseDrive(driveDoc))
		}
	}
	return dedupeStorage(out)
}

func (r redfishSnapshotReader) collectStorageVolumes(systemPath string) []models.StorageVolume {
	var out []models.StorageVolume
	storageMembers, _ := r.getCollectionMembers(joinPath(systemPath, "/Storage"))
	for _, member := range storageMembers {
		controller := firstNonEmpty(asString(member["Id"]), asString(member["Name"]))
		volumeCollectionPath := redfishLinkedPath(member, "Volumes")
		if volumeCollectionPath == "" {
			continue
		}
		volumeDocs, err := r.getCollectionMembers(volumeCollectionPath)
		if err != nil {
			continue
		}
		for _, volDoc := range volumeDocs {
			if looksLikeVolume(volDoc) {
				out = append(out, parseStorageVolume(volDoc, controller))
			}
		}
	}
	for _, volDoc := range r.collectKnownStorageMembers(systemPath, []string{
		"/Storage/IntelVROC/Volumes",
		"/Storage/HA-RAID/Volumes",
		"/Storage/MRVL.HA-RAID/Volumes",
	}) {
		if looksLikeVolume(volDoc) {
			out = append(out, parseStorageVolume(volDoc, storageControllerFromPath(asString(volDoc["@odata.id"]))))
		}
	}
	return dedupeStorageVolumes(out)
}

func (r redfishSnapshotReader) collectKnownStorageMembers(systemPath string, relativeCollections []string) []map[string]interface{} {
	var out []map[string]interface{}
	for _, rel := range relativeCollections {
		docs, err := r.getCollectionMembers(joinPath(systemPath, rel))
		if err != nil || len(docs) == 0 {
			continue
		}
		out = append(out, docs...)
	}
	return out
}

func (r redfishSnapshotReader) probeSupermicroNVMeDiskBays(backplanePath string) []map[string]interface{} {
	return r.probeDirectDiskBayChildren(joinPath(backplanePath, "/Drives"))
}

func (r redfishSnapshotReader) probeDirectDiskBayChildren(drivesCollectionPath string) []map[string]interface{} {
	var out []map[string]interface{}
	for _, path := range directDiskBayCandidates(drivesCollectionPath) {
		doc, err := r.getJSON(path)
		if err != nil || !looksLikeDrive(doc) {
			continue
		}
		out = append(out, doc)
	}
	return out
}

func (r redfishSnapshotReader) collectNICs(chassisPaths []string) []models.NetworkAdapter {
	var nics []models.NetworkAdapter
	for _, chassisPath := range chassisPaths {
		adapterDocs, err := r.getCollectionMembers(joinPath(chassisPath, "/NetworkAdapters"))
		if err != nil {
			continue
		}
		for _, doc := range adapterDocs {
			nic := parseNIC(doc)
			for _, pciePath := range networkAdapterPCIeDevicePaths(doc) {
				pcieDoc, err := r.getJSON(pciePath)
				if err != nil {
					continue
				}
				functionDocs := r.getLinkedPCIeFunctions(pcieDoc)
				supplementalDocs := r.getLinkedSupplementalDocs(pcieDoc, "EnvironmentMetrics", "Metrics")
				for _, fn := range functionDocs {
					supplementalDocs = append(supplementalDocs, r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")...)
				}
				enrichNICFromPCIe(&nic, pcieDoc, functionDocs, supplementalDocs)
			}
			// Collect MACs from NetworkDeviceFunctions when not found via PCIe path.
			if len(nic.MACAddresses) == 0 {
				r.enrichNICMACsFromNetworkDeviceFunctions(&nic, doc)
			}
			nics = append(nics, nic)
		}
	}
	return dedupeNetworkAdapters(nics)
}

func (r redfishSnapshotReader) collectPSUs(chassisPaths []string) []models.PSU {
	var out []models.PSU
	seen := make(map[string]int)
	idx := 1
	for _, chassisPath := range chassisPaths {
		if memberDocs, err := r.getCollectionMembers(joinPath(chassisPath, "/PowerSubsystem/PowerSupplies")); err == nil && len(memberDocs) > 0 {
			for _, doc := range memberDocs {
				supplementalDocs := r.getLinkedSupplementalDocs(doc, "EnvironmentMetrics", "Metrics")
				idx = appendPSU(&out, seen, parsePSUWithSupplementalDocs(doc, idx, supplementalDocs...), idx)
			}
			continue
		}
		if powerDoc, err := r.getJSON(joinPath(chassisPath, "/Power")); err == nil {
			if members, ok := powerDoc["PowerSupplies"].([]interface{}); ok && len(members) > 0 {
				for _, item := range members {
					doc, ok := item.(map[string]interface{})
					if !ok {
						continue
					}
					supplementalDocs := r.getLinkedSupplementalDocs(doc, "EnvironmentMetrics", "Metrics")
					idx = appendPSU(&out, seen, parsePSUWithSupplementalDocs(doc, idx, supplementalDocs...), idx)
				}
			}
		}
	}
	return out
}

func (r redfishSnapshotReader) collectGPUs(systemPaths, chassisPaths []string) []models.GPU {
	collections := make([]string, 0, len(systemPaths)*3+len(chassisPaths)*2)
	for _, systemPath := range systemPaths {
		collections = append(collections, joinPath(systemPath, "/PCIeDevices"))
		collections = append(collections, joinPath(systemPath, "/Accelerators"))
		collections = append(collections, joinPath(systemPath, "/GraphicsControllers"))
	}
	for _, chassisPath := range chassisPaths {
		collections = append(collections, joinPath(chassisPath, "/PCIeDevices"))
		collections = append(collections, joinPath(chassisPath, "/Accelerators"))
	}
	var out []models.GPU
	seen := make(map[string]struct{})
	idx := 1
	for _, collectionPath := range collections {
		memberDocs, err := r.getCollectionMembers(collectionPath)
		if err != nil || len(memberDocs) == 0 {
			continue
		}
		for _, doc := range memberDocs {
			functionDocs := r.getLinkedPCIeFunctions(doc)
			if !looksLikeGPU(doc, functionDocs) {
				continue
			}
			supplementalDocs := r.getLinkedSupplementalDocs(doc, "EnvironmentMetrics", "Metrics")
			for _, fn := range functionDocs {
				supplementalDocs = append(supplementalDocs, r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")...)
			}
			gpu := parseGPUWithSupplementalDocs(doc, functionDocs, supplementalDocs, idx)
			idx++
			if shouldSkipGenericGPUDuplicate(out, gpu) {
				continue
			}
			key := gpuDocDedupKey(doc, gpu)
			if key == "" {
				continue
			}
			if _, ok := seen[key]; ok {
				continue
			}
			seen[key] = struct{}{}
			out = append(out, gpu)
		}
	}
	return dropModelOnlyGPUPlaceholders(out)
}

func (r redfishSnapshotReader) collectPCIeDevices(systemPaths, chassisPaths []string) []models.PCIeDevice {
	collections := make([]string, 0, len(systemPaths)+len(chassisPaths))
	for _, systemPath := range systemPaths {
		collections = append(collections, joinPath(systemPath, "/PCIeDevices"))
	}
	for _, chassisPath := range chassisPaths {
		collections = append(collections, joinPath(chassisPath, "/PCIeDevices"))
	}
	var out []models.PCIeDevice
	for _, collectionPath := range collections {
		memberDocs, err := r.getCollectionMembers(collectionPath)
		if err != nil || len(memberDocs) == 0 {
			continue
		}
		for _, doc := range memberDocs {
			functionDocs := r.getLinkedPCIeFunctions(doc)
			if looksLikeGPU(doc, functionDocs) {
				continue
			}
			supplementalDocs := r.getLinkedSupplementalDocs(doc, "EnvironmentMetrics", "Metrics")
			supplementalDocs = append(supplementalDocs, r.getChassisScopedPCIeSupplementalDocs(doc)...)
			for _, fn := range functionDocs {
				supplementalDocs = append(supplementalDocs, r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")...)
			}
			dev := parsePCIeDeviceWithSupplementalDocs(doc, functionDocs, supplementalDocs)
			if isUnidentifiablePCIeDevice(dev) {
				continue
			}
			out = append(out, dev)
		}
	}
	for _, systemPath := range systemPaths {
		functionDocs, err := r.getCollectionMembers(joinPath(systemPath, "/PCIeFunctions"))
		if err != nil || len(functionDocs) == 0 {
			continue
		}
		for idx, fn := range functionDocs {
			supplementalDocs := r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")
			dev := parsePCIeFunctionWithSupplementalDocs(fn, supplementalDocs, idx+1)
			out = append(out, dev)
		}
	}
	return dedupePCIeDevices(out)
}

func (r redfishSnapshotReader) getChassisScopedPCIeSupplementalDocs(doc map[string]interface{}) []map[string]interface{} {
	if !looksLikeNVSwitchPCIeDoc(doc) {
		return nil
	}
	docPath := normalizeRedfishPath(asString(doc["@odata.id"]))
	chassisPath := chassisPathForPCIeDoc(docPath)
	if chassisPath == "" {
		return nil
	}
	out := make([]map[string]interface{}, 0, 4)
	for _, path := range []string{
		joinPath(chassisPath, "/EnvironmentMetrics"),
		joinPath(chassisPath, "/ThermalSubsystem/ThermalMetrics"),
	} {
		supplementalDoc, err := r.getJSON(path)
		if err != nil || len(supplementalDoc) == 0 {
			continue
		}
		out = append(out, supplementalDoc)
	}
	return out
}

func stringsTrimTrailingSlash(s string) string {
	for len(s) > 1 && s[len(s)-1] == '/' {
		s = s[:len(s)-1]
	}
	return s
}

// collectBMCMAC returns the MAC address of the first active BMC management
// interface found in Managers/*/EthernetInterfaces. Returns empty string if
// no MAC is available.
func (r redfishSnapshotReader) collectBMCMAC(managerPaths []string) string {
	for _, managerPath := range managerPaths {
		members, err := r.getCollectionMembers(joinPath(managerPath, "/EthernetInterfaces"))
		if err != nil || len(members) == 0 {
			continue
		}
		for _, doc := range members {
			mac := strings.TrimSpace(firstNonEmpty(
				asString(doc["PermanentMACAddress"]),
				asString(doc["MACAddress"]),
			))
			if mac == "" || strings.EqualFold(mac, "00:00:00:00:00:00") {
				continue
			}
			return strings.ToUpper(mac)
		}
	}
	return ""
}

// collectAssemblyFRU reads Chassis/*/Assembly documents and returns FRU entries
// for subcomponents (backplanes, PSUs, DIMMs, etc.) that carry meaningful
// serial or part numbers. Entries already present in dedicated collections
// (PSUs, DIMMs) are included here as well so that all FRU data is available
// in one place; deduplication by serial is performed.
func (r redfishSnapshotReader) collectAssemblyFRU(chassisPaths []string) []models.FRUInfo {
	seen := make(map[string]struct{})
	var out []models.FRUInfo

	add := func(fru models.FRUInfo) {
		key := strings.ToUpper(strings.TrimSpace(fru.SerialNumber))
		if key == "" {
			key = strings.ToUpper(strings.TrimSpace(fru.Description + "|" + fru.PartNumber))
		}
		if key == "" || key == "|" {
			return
		}
		if _, ok := seen[key]; ok {
			return
		}
		seen[key] = struct{}{}
		out = append(out, fru)
	}

	for _, chassisPath := range chassisPaths {
		doc, err := r.getJSON(joinPath(chassisPath, "/Assembly"))
		if err != nil || len(doc) == 0 {
			continue
		}
		assemblies, _ := doc["Assemblies"].([]interface{})
		for _, aAny := range assemblies {
			a, ok := aAny.(map[string]interface{})
			if !ok {
				continue
			}
			name := strings.TrimSpace(firstNonEmpty(asString(a["Name"]), asString(a["Description"])))
			model := strings.TrimSpace(asString(a["Model"]))
			partNumber := strings.TrimSpace(asString(a["PartNumber"]))
			serial := extractAssemblySerial(a)

			if serial == "" && partNumber == "" {
				continue
			}
			add(models.FRUInfo{
				Description:  name,
				ProductName:  model,
				SerialNumber: serial,
				PartNumber:   partNumber,
			})
		}
	}
	return out
}

// extractAssemblySerial tries to find a serial number in an Assembly entry.
// Standard Redfish Assembly has no top-level SerialNumber; vendors put it in Oem.
func extractAssemblySerial(a map[string]interface{}) string {
	// Some implementations expose it at top level.
	if s := strings.TrimSpace(asString(a["SerialNumber"])); s != "" {
		return s
	}
	// Dig into Oem for vendor-specific structures (e.g. Huawei COMMONb).
	oem, _ := a["Oem"].(map[string]interface{})
	for _, v := range oem {
		subtree, ok := v.(map[string]interface{})
		if !ok {
			continue
		}
		for _, v2 := range subtree {
			node, ok := v2.(map[string]interface{})
			if !ok {
				continue
			}
			if s := strings.TrimSpace(asString(node["SerialNumber"])); s != "" {
				return s
			}
		}
	}
	return ""
}

// enrichNICMACsFromNetworkDeviceFunctions reads the NetworkDeviceFunctions
// collection linked from a NetworkAdapter document and populates the NIC's
// MACAddresses from each function's Ethernet.PermanentMACAddress / MACAddress.
// Called when PCIe-path enrichment does not produce any MACs.
func (r redfishSnapshotReader) enrichNICMACsFromNetworkDeviceFunctions(nic *models.NetworkAdapter, adapterDoc map[string]interface{}) {
	ndfCol, ok := adapterDoc["NetworkDeviceFunctions"].(map[string]interface{})
	if !ok {
		return
	}
	colPath := asString(ndfCol["@odata.id"])
	if colPath == "" {
		return
	}
	funcDocs, err := r.getCollectionMembers(colPath)
	if err != nil || len(funcDocs) == 0 {
		return
	}
	for _, fn := range funcDocs {
		eth, _ := fn["Ethernet"].(map[string]interface{})
		if eth == nil {
			continue
		}
		mac := strings.TrimSpace(firstNonEmpty(
			asString(eth["PermanentMACAddress"]),
			asString(eth["MACAddress"]),
		))
		if mac == "" {
			continue
		}
		nic.MACAddresses = dedupeStrings(append(nic.MACAddresses, strings.ToUpper(mac)))
	}
	if len(funcDocs) > 0 && nic.PortCount == 0 {
		nic.PortCount = sanitizeNetworkPortCount(len(funcDocs))
	}
}

// collectGPUsFromProcessors finds GPUs that some BMCs (e.g. MSI) expose as
// Processor entries with ProcessorType=GPU rather than as PCIe devices.
// It supplements the existing gpus slice (already found via PCIe path),
// skipping entries already present by UUID or SerialNumber.
// Serial numbers are looked up from Chassis members named after each GPU Id.
func (r redfishSnapshotReader) collectGPUsFromProcessors(systemPaths, chassisPaths []string, existing []models.GPU) []models.GPU {
	// Build a lookup: chassis member ID → chassis doc (for serial numbers).
	chassisByID := make(map[string]map[string]interface{})
	for _, cp := range chassisPaths {
		doc, err := r.getJSON(cp)
		if err != nil || len(doc) == 0 {
			continue
		}
		id := strings.TrimSpace(asString(doc["Id"]))
		if id != "" {
			chassisByID[strings.ToUpper(id)] = doc
		}
	}

	// Build dedup sets from existing GPUs.
	seenUUID := make(map[string]struct{})
	seenSerial := make(map[string]struct{})
	for _, g := range existing {
		if u := strings.ToUpper(strings.TrimSpace(g.UUID)); u != "" {
			seenUUID[u] = struct{}{}
		}
		if s := strings.ToUpper(strings.TrimSpace(g.SerialNumber)); s != "" {
			seenSerial[s] = struct{}{}
		}
	}

	out := append([]models.GPU{}, existing...)
	idx := len(existing) + 1

	for _, systemPath := range systemPaths {
		procDocs, err := r.getCollectionMembers(joinPath(systemPath, "/Processors"))
		if err != nil {
			continue
		}
		for _, doc := range procDocs {
			if !strings.EqualFold(strings.TrimSpace(asString(doc["ProcessorType"])), "GPU") {
				continue
			}

			// Resolve serial: prefer the processor doc itself (e.g. Supermicro
			// HGX_Baseboard_0/Processors/GPU_SXM_N carries SerialNumber directly),
			// then fall back to a matching chassis doc keyed by processor Id
			// (e.g. MSI: Chassis/GPU_SXM_1/SerialNumber).
			gpuID := strings.TrimSpace(asString(doc["Id"]))
			serial := findFirstNormalizedStringByKeys(doc, "SerialNumber")
			if chassisDoc, ok := chassisByID[strings.ToUpper(gpuID)]; ok {
				if cs := strings.TrimSpace(asString(chassisDoc["SerialNumber"])); cs != "" {
					serial = cs
				}
			}

			uuid := strings.TrimSpace(asString(doc["UUID"]))
			uuidKey := strings.ToUpper(uuid)
			serialKey := strings.ToUpper(serial)

			if uuidKey != "" {
				if _, dup := seenUUID[uuidKey]; dup {
					continue
				}
				seenUUID[uuidKey] = struct{}{}
			}
			if serialKey != "" {
				if _, dup := seenSerial[serialKey]; dup {
					continue
				}
				seenSerial[serialKey] = struct{}{}
			}

			slotLabel := firstNonEmpty(
				redfishLocationLabel(doc["Location"]),
				redfishLocationLabel(doc["PhysicalLocation"]),
			)
			if slotLabel == "" && gpuID != "" {
				slotLabel = gpuID
			}
			if slotLabel == "" {
				slotLabel = fmt.Sprintf("GPU%d", idx)
			}

			out = append(out, models.GPU{
				Slot:         slotLabel,
				Model:        firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
				Manufacturer: asString(doc["Manufacturer"]),
				PartNumber:   asString(doc["PartNumber"]),
				SerialNumber: serial,
				UUID:         uuid,
				Status:       mapStatus(doc["Status"]),
			})
			idx++
		}
	}
	return out
}
