package collector

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"path"
	"sort"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"

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

type RedfishConnector struct {
	timeout       time.Duration
	debug         bool
	debugSnapshot bool
}

type redfishPrefetchMetrics struct {
	Enabled    bool
	Candidates int
	Targets    int
	Docs       int
	Added      int
	Duration   time.Duration
	SkipReason string
}

type redfishPostProbeMetrics struct {
	NVMECandidates       int
	NVMESelected         int
	NVMEAdded            int
	CollectionCandidates int
	CollectionSelected   int
	SkippedExplicit      int
	Added                int
	Duration             time.Duration
}

type redfishRequestTelemetry struct {
	mu      sync.Mutex
	overall redfishPhaseTelemetry
	byPhase map[string]*redfishPhaseTelemetry
}

type redfishTelemetrySummary struct {
	Requests  int
	Errors    int
	ErrorRate float64
	Avg       time.Duration
	P95       time.Duration
}

type redfishTelemetryContextKey struct{}
type redfishTelemetryPhaseContextKey struct{}

type redfishPhaseTelemetry struct {
	requests  int
	errors    int
	durations []time.Duration
	lastAvg   time.Duration
	lastP95   time.Duration
}

func NewRedfishConnector() *RedfishConnector {
	debug := false
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_DEBUG")); v != "" && v != "0" && !strings.EqualFold(v, "false") {
		debug = true
	}
	debugSnapshot := false
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_SNAPSHOT_DEBUG")); v != "" && v != "0" && !strings.EqualFold(v, "false") {
		debugSnapshot = true
	}
	return &RedfishConnector{
		timeout:       10 * time.Second,
		debug:         debug,
		debugSnapshot: debugSnapshot || debug,
	}
}

func (c *RedfishConnector) Protocol() string {
	return "redfish"
}

func (c *RedfishConnector) Probe(ctx context.Context, req Request) (*ProbeResult, error) {
	baseURL, err := c.baseURL(req)
	if err != nil {
		return nil, err
	}
	client := c.httpClientWithTimeout(req, c.timeout)
	if _, err := c.getJSON(ctx, client, req, baseURL, "/redfish/v1"); err != nil {
		return nil, fmt.Errorf("redfish service root: %w", err)
	}
	systemPaths := c.discoverMemberPaths(ctx, client, req, baseURL, "/redfish/v1/Systems", "/redfish/v1/Systems/1")
	primarySystem := firstNonEmptyPath(systemPaths, "/redfish/v1/Systems/1")
	systemDoc, err := c.getJSON(ctx, client, req, baseURL, primarySystem)
	if err != nil {
		return nil, fmt.Errorf("redfish system: %w", err)
	}
	powerState := redfishSystemPowerState(systemDoc)
	return &ProbeResult{
		Reachable:      true,
		Protocol:       "redfish",
		HostPowerState: powerState,
		HostPoweredOn:  isRedfishHostPoweredOn(powerState),
		SystemPath:     primarySystem,
	}, nil
}

func (c *RedfishConnector) debugf(format string, args ...interface{}) {
	if !c.debug {
		return
	}
	log.Printf("redfish-debug: "+format, args...)
}

func (c *RedfishConnector) debugSnapshotf(format string, args ...interface{}) {
	if !c.debugSnapshot {
		return
	}
	log.Printf("redfish-snapshot-debug: "+format, args...)
}

func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit ProgressFn) (*models.AnalysisResult, error) {
	collectStart := time.Now()
	telemetry := newRedfishRequestTelemetry()
	ctx = context.WithValue(ctx, redfishTelemetryContextKey{}, telemetry)
	baseURL, err := c.baseURL(req)
	if err != nil {
		return nil, err
	}

	snapshotClient := c.httpClientWithTimeout(req, redfishSnapshotRequestTimeout())
	prefetchClient := c.httpClientWithTimeout(req, redfishPrefetchRequestTimeout())
	criticalClient := c.httpClientWithTimeout(req, redfishCriticalRequestTimeout())
	hintClient := c.httpClientWithTimeout(req, 4*time.Second)

	if emit != nil {
		emit(Progress{Status: "running", Progress: 10, Message: "Redfish: подключение к BMC..."})
	}
	discoveryCtx := withRedfishTelemetryPhase(ctx, "discovery")
	serviceRootDoc, err := c.getJSON(discoveryCtx, snapshotClient, req, baseURL, "/redfish/v1")
	if err != nil {
		return nil, fmt.Errorf("redfish service root: %w", err)
	}

	systemPaths := c.discoverMemberPaths(discoveryCtx, snapshotClient, req, baseURL, "/redfish/v1/Systems", "/redfish/v1/Systems/1")
	primarySystem := firstNonEmptyPath(systemPaths, "/redfish/v1/Systems/1")
	chassisPaths := c.discoverMemberPaths(discoveryCtx, snapshotClient, req, baseURL, "/redfish/v1/Chassis", "/redfish/v1/Chassis/1")
	managerPaths := c.discoverMemberPaths(discoveryCtx, snapshotClient, req, baseURL, "/redfish/v1/Managers", "/redfish/v1/Managers/1")
	primaryChassis := firstNonEmptyPath(chassisPaths, "/redfish/v1/Chassis/1")
	primaryManager := firstNonEmptyPath(managerPaths, "/redfish/v1/Managers/1")
	systemDoc, _ := c.getJSON(discoveryCtx, snapshotClient, req, baseURL, primarySystem)
	chassisDoc, _ := c.getJSON(discoveryCtx, snapshotClient, req, baseURL, primaryChassis)
	managerDoc, _ := c.getJSON(discoveryCtx, snapshotClient, req, baseURL, primaryManager)
	resourceHints := append(append([]string{}, systemPaths...), append(chassisPaths, managerPaths...)...)
	hintDocs := c.collectProfileHintDocs(discoveryCtx, hintClient, req, baseURL, primarySystem, primaryChassis)
	signals := redfishprofile.CollectSignals(serviceRootDoc, systemDoc, chassisDoc, managerDoc, resourceHints, hintDocs...)
	matchResult := redfishprofile.MatchProfiles(signals)
	acquisitionPlan := redfishprofile.BuildAcquisitionPlan(signals)
	telemetrySummary := telemetry.Snapshot()
	phaseTelemetry := telemetry.PhaseSnapshots()
	adaptedTuning, throttled := adaptRedfishAcquisitionTuning(acquisitionPlan.Tuning, telemetrySummary)
	acquisitionPlan.Tuning = adaptedTuning
	activeModules := make([]ModuleActivation, 0, len(matchResult.Scores))
	moduleScores := make([]ModuleScore, 0, len(matchResult.Scores))
	for _, score := range matchResult.Scores {
		moduleScores = append(moduleScores, ModuleScore{
			Name:     score.Name,
			Score:    score.Score,
			Active:   score.Active,
			Priority: score.Priority,
		})
		if score.Active {
			activeModules = append(activeModules, ModuleActivation{Name: score.Name, Score: score.Score})
		}
	}
	if emit != nil {
		emit(Progress{
			Status:        "running",
			Progress:      25,
			Message:       fmt.Sprintf("Redfish: профили mode=%s active=%s", acquisitionPlan.Mode, formatActiveModuleLog(activeModules)),
			ActiveModules: activeModules,
			ModuleScores:  moduleScores,
			DebugInfo: &CollectDebugInfo{
				AdaptiveThrottled: throttled,
				SnapshotWorkers:   acquisitionPlan.Tuning.SnapshotWorkers,
				PrefetchWorkers:   acquisitionPlan.Tuning.PrefetchWorkers,
				PrefetchEnabled:   acquisitionPlan.Tuning.PrefetchEnabled,
				PhaseTelemetry:    buildCollectPhaseTelemetry(phaseTelemetry),
			},
		})
		if throttled {
			emit(Progress{
				Status:        "running",
				Progress:      26,
				Message:       fmt.Sprintf("Redfish: adaptive throttling p95=%dms err=%.0f%% workers=%d prefetch=%d", telemetrySummary.P95.Milliseconds(), telemetrySummary.ErrorRate*100, acquisitionPlan.Tuning.SnapshotWorkers, acquisitionPlan.Tuning.PrefetchWorkers),
				ActiveModules: activeModules,
				ModuleScores:  moduleScores,
				DebugInfo: &CollectDebugInfo{
					AdaptiveThrottled: throttled,
					SnapshotWorkers:   acquisitionPlan.Tuning.SnapshotWorkers,
					PrefetchWorkers:   acquisitionPlan.Tuning.PrefetchWorkers,
					PrefetchEnabled:   acquisitionPlan.Tuning.PrefetchEnabled,
					PhaseTelemetry:    buildCollectPhaseTelemetry(phaseTelemetry),
				},
			})
		}
	}
	resolvedPlan := redfishprofile.ResolveAcquisitionPlan(matchResult, acquisitionPlan, redfishprofile.DiscoveredResources{
		SystemPaths:  systemPaths,
		ChassisPaths: chassisPaths,
		ManagerPaths: managerPaths,
	}, signals)
	acquisitionPlan = resolvedPlan.Plan
	seedPaths := resolvedPlan.SeedPaths
	criticalPaths := resolvedPlan.CriticalPaths
	if len(acquisitionPlan.Profiles) > 0 {
		log.Printf(
			"redfish-profile-plan: mode=%s profiles=%s notes=%s scores=%s req=%d err=%d p95=%dms avg=%dms throttled=%t",
			acquisitionPlan.Mode,
			strings.Join(acquisitionPlan.Profiles, ","),
			strings.Join(acquisitionPlan.Notes, "; "),
			formatModuleScoreLog(moduleScores),
			telemetrySummary.Requests,
			telemetrySummary.Errors,
			telemetrySummary.P95.Milliseconds(),
			telemetrySummary.Avg.Milliseconds(),
			throttled,
		)
	}
	if emit != nil {
		emit(Progress{
			Status:       "running",
			Progress:     30,
			Message:      "Redfish: чтение структуры Redfish...",
			CurrentPhase: "snapshot",
			ETASeconds:   acquisitionPlan.Tuning.ETABaseline.SnapshotSeconds,
		})
	}

	if emit != nil {
		emit(Progress{Status: "running", Progress: 55, Message: "Redfish: подготовка snapshot...", CurrentPhase: "snapshot", ETASeconds: acquisitionPlan.Tuning.ETABaseline.SnapshotSeconds})
		emit(Progress{Status: "running", Progress: 80, Message: "Redfish: подготовка расширенного snapshot...", CurrentPhase: "snapshot", ETASeconds: acquisitionPlan.Tuning.ETABaseline.SnapshotSeconds})
		emit(Progress{Status: "running", Progress: 90, Message: "Redfish: сбор расширенного snapshot...", CurrentPhase: "snapshot", ETASeconds: acquisitionPlan.Tuning.ETABaseline.SnapshotSeconds})
	}
	// collectCtx covers all data-fetching phases (snapshot, prefetch, plan-B).
	// Cancelling it via the skip signal aborts only the collection phases while
	// leaving the replay phase intact so results from already-fetched data are preserved.
	collectCtx, cancelCollect := context.WithCancel(ctx)
	defer cancelCollect()
	if req.SkipHungCh != nil {
		go func() {
			select {
			case <-req.SkipHungCh:
				if emit != nil {
					emit(Progress{
						Status:   "running",
						Progress: 97,
						Message:  "Redfish: пропуск зависших запросов, анализ уже собранных данных...",
					})
				}
				log.Printf("redfish: skip-hung triggered, cancelling collection phases")
				cancelCollect()
			case <-ctx.Done():
			}
		}()
	}

	c.debugSnapshotf("snapshot crawl start host=%s port=%d", req.Host, req.Port)
	rawTree, fetchErrors, postProbeMetrics, snapshotTimingSummary := c.collectRawRedfishTree(withRedfishTelemetryPhase(collectCtx, "snapshot"), snapshotClient, req, baseURL, seedPaths, acquisitionPlan.Tuning, emit)
	c.debugSnapshotf("snapshot crawl done docs=%d", len(rawTree))
	fetchErrMap := redfishFetchErrorListToMap(fetchErrors)

	prefetchedCritical, prefetchMetrics := c.prefetchCriticalRedfishDocs(withRedfishTelemetryPhase(collectCtx, "prefetch"), prefetchClient, req, baseURL, criticalPaths, rawTree, fetchErrMap, acquisitionPlan.Tuning, emit)
	for p, doc := range prefetchedCritical {
		if _, exists := rawTree[p]; exists {
			continue
		}
		rawTree[p] = doc
		prefetchMetrics.Added++
	}
	for p := range prefetchedCritical {
		delete(fetchErrMap, p)
	}
	log.Printf(
		"redfish-prefetch-metrics: enabled=%t candidates=%d targets=%d docs=%d added=%d dur=%s skip=%s",
		prefetchMetrics.Enabled,
		prefetchMetrics.Candidates,
		prefetchMetrics.Targets,
		prefetchMetrics.Docs,
		prefetchMetrics.Added,
		prefetchMetrics.Duration.Round(time.Millisecond),
		firstNonEmpty(prefetchMetrics.SkipReason, "-"),
	)
	if recoveredN := c.recoverCriticalRedfishDocsPlanB(withRedfishTelemetryPhase(collectCtx, "critical_plan_b"), criticalClient, req, baseURL, criticalPaths, rawTree, fetchErrMap, acquisitionPlan.Tuning, emit); recoveredN > 0 {
		c.debugSnapshotf("critical plan-b recovered docs=%d", recoveredN)
	}
	if recoveredN := c.recoverProfilePlanBDocs(withRedfishTelemetryPhase(collectCtx, "profile_plan_b"), criticalClient, req, baseURL, acquisitionPlan, rawTree, emit); recoveredN > 0 {
		c.debugSnapshotf("profile plan-b recovered docs=%d", recoveredN)
	}
	// Hide transient fetch errors for endpoints that were eventually recovered into rawTree.
	for p := range fetchErrMap {
		if _, ok := rawTree[p]; ok {
			delete(fetchErrMap, p)
		}
	}
	if emit != nil {
		emit(Progress{Status: "running", Progress: 99, Message: "Redfish: анализ raw snapshot..."})
	}
	// Collect hardware event logs separately (not part of tree-walk to avoid bloat).
	rawLogEntries := c.collectRedfishLogEntries(withRedfishTelemetryPhase(ctx, "log_entries"), snapshotClient, req, baseURL, systemPaths, managerPaths)
	var debugPayloads map[string]any
	if req.DebugPayloads {
		debugPayloads = c.collectDebugPayloads(ctx, snapshotClient, req, baseURL, systemPaths)
	}
	rawPayloads := map[string]any{
		"redfish_tree": rawTree,
		"redfish_profiles": map[string]any{
			"mode":           acquisitionPlan.Mode,
			"profiles":       acquisitionPlan.Profiles,
			"active_modules": activeModules,
			"module_scores":  moduleScores,
			"notes":          acquisitionPlan.Notes,
			"plan_b_paths":   acquisitionPlan.PlanBPaths,
			"scoped_paths": map[string]any{
				"system_seed_suffixes":      acquisitionPlan.ScopedPaths.SystemSeedSuffixes,
				"system_critical_suffixes":  acquisitionPlan.ScopedPaths.SystemCriticalSuffixes,
				"chassis_seed_suffixes":     acquisitionPlan.ScopedPaths.ChassisSeedSuffixes,
				"chassis_critical_suffixes": acquisitionPlan.ScopedPaths.ChassisCriticalSuffixes,
				"manager_seed_suffixes":     acquisitionPlan.ScopedPaths.ManagerSeedSuffixes,
				"manager_critical_suffixes": acquisitionPlan.ScopedPaths.ManagerCriticalSuffixes,
			},
			"tuning": map[string]any{
				"snapshot_max_documents":     acquisitionPlan.Tuning.SnapshotMaxDocuments,
				"snapshot_workers":           acquisitionPlan.Tuning.SnapshotWorkers,
				"snapshot_exclude_contains":  acquisitionPlan.Tuning.SnapshotExcludeContains,
				"prefetch_workers":       acquisitionPlan.Tuning.PrefetchWorkers,
				"prefetch_enabled":       boolPointerValue(acquisitionPlan.Tuning.PrefetchEnabled),
				"nvme_post_probe":        boolPointerValue(acquisitionPlan.Tuning.NVMePostProbeEnabled),
				"post_probe_policy": map[string]any{
					"direct_nvme_disk_bay": acquisitionPlan.Tuning.PostProbePolicy.EnableDirectNVMEDiskBayProbe,
					"numeric_collection":   acquisitionPlan.Tuning.PostProbePolicy.EnableNumericCollectionProbe,
					"sensor_collection":    acquisitionPlan.Tuning.PostProbePolicy.EnableSensorCollectionProbe,
				},
				"prefetch_policy": map[string]any{
					"include_suffixes": acquisitionPlan.Tuning.PrefetchPolicy.IncludeSuffixes,
					"exclude_contains": acquisitionPlan.Tuning.PrefetchPolicy.ExcludeContains,
				},
				"recovery_policy": map[string]any{
					"critical_collection_member_retry": acquisitionPlan.Tuning.RecoveryPolicy.EnableCriticalCollectionMemberRetry,
					"critical_slow_probe":              acquisitionPlan.Tuning.RecoveryPolicy.EnableCriticalSlowProbe,
					"profile_plan_b":                   acquisitionPlan.Tuning.RecoveryPolicy.EnableProfilePlanB,
				},
				"rate_policy": map[string]any{
					"target_p95_latency_ms":   acquisitionPlan.Tuning.RatePolicy.TargetP95LatencyMS,
					"throttle_p95_latency_ms": acquisitionPlan.Tuning.RatePolicy.ThrottleP95LatencyMS,
					"min_snapshot_workers":    acquisitionPlan.Tuning.RatePolicy.MinSnapshotWorkers,
					"min_prefetch_workers":    acquisitionPlan.Tuning.RatePolicy.MinPrefetchWorkers,
					"disable_prefetch_error":  acquisitionPlan.Tuning.RatePolicy.DisablePrefetchOnErrors,
				},
				"eta_baseline": map[string]any{
					"discovery_seconds":       acquisitionPlan.Tuning.ETABaseline.DiscoverySeconds,
					"snapshot_seconds":        acquisitionPlan.Tuning.ETABaseline.SnapshotSeconds,
					"prefetch_seconds":        acquisitionPlan.Tuning.ETABaseline.PrefetchSeconds,
					"critical_plan_b_seconds": acquisitionPlan.Tuning.ETABaseline.CriticalPlanBSeconds,
					"profile_plan_b_seconds":  acquisitionPlan.Tuning.ETABaseline.ProfilePlanBSeconds,
				},
			},
			"request_telemetry": map[string]any{
				"requests":   telemetry.Snapshot().Requests,
				"errors":     telemetry.Snapshot().Errors,
				"error_rate": telemetry.Snapshot().ErrorRate,
				"avg_ms":     telemetry.Snapshot().Avg.Milliseconds(),
				"p95_ms":     telemetry.Snapshot().P95.Milliseconds(),
				"phases":     redfishPhaseTelemetryPayload(phaseTelemetry),
			},
		},
		"redfish_telemetry": map[string]any{
			"requests":            telemetrySummary.Requests,
			"errors":              telemetrySummary.Errors,
			"error_rate":          telemetrySummary.ErrorRate,
			"avg_ms":              telemetrySummary.Avg.Milliseconds(),
			"p95_ms":              telemetrySummary.P95.Milliseconds(),
			"adaptive_throttled":  throttled,
			"snapshot_timing_top": snapshotTimingSummary,
			"snapshot_workers":    acquisitionPlan.Tuning.SnapshotWorkers,
			"prefetch_workers":    acquisitionPlan.Tuning.PrefetchWorkers,
			"prefetch_enabled":    boolPointerValue(acquisitionPlan.Tuning.PrefetchEnabled),
			"nvme_post_probe":     boolPointerValue(acquisitionPlan.Tuning.NVMePostProbeEnabled),
			"post_probe_policy": map[string]any{
				"direct_nvme_disk_bay": acquisitionPlan.Tuning.PostProbePolicy.EnableDirectNVMEDiskBayProbe,
				"numeric_collection":   acquisitionPlan.Tuning.PostProbePolicy.EnableNumericCollectionProbe,
				"sensor_collection":    acquisitionPlan.Tuning.PostProbePolicy.EnableSensorCollectionProbe,
			},
			"prefetch_policy": map[string]any{
				"include_suffixes": acquisitionPlan.Tuning.PrefetchPolicy.IncludeSuffixes,
				"exclude_contains": acquisitionPlan.Tuning.PrefetchPolicy.ExcludeContains,
			},
			"recovery_policy": map[string]any{
				"critical_collection_member_retry": acquisitionPlan.Tuning.RecoveryPolicy.EnableCriticalCollectionMemberRetry,
				"critical_slow_probe":              acquisitionPlan.Tuning.RecoveryPolicy.EnableCriticalSlowProbe,
				"profile_plan_b":                   acquisitionPlan.Tuning.RecoveryPolicy.EnableProfilePlanB,
			},
			"target_p95_latency_ms":   acquisitionPlan.Tuning.RatePolicy.TargetP95LatencyMS,
			"throttle_p95_latency_ms": acquisitionPlan.Tuning.RatePolicy.ThrottleP95LatencyMS,
			"eta_baseline": map[string]any{
				"discovery_seconds":       acquisitionPlan.Tuning.ETABaseline.DiscoverySeconds,
				"snapshot_seconds":        acquisitionPlan.Tuning.ETABaseline.SnapshotSeconds,
				"prefetch_seconds":        acquisitionPlan.Tuning.ETABaseline.PrefetchSeconds,
				"critical_plan_b_seconds": acquisitionPlan.Tuning.ETABaseline.CriticalPlanBSeconds,
				"profile_plan_b_seconds":  acquisitionPlan.Tuning.ETABaseline.ProfilePlanBSeconds,
			},
			"phases": redfishPhaseTelemetryPayload(phaseTelemetry),
		},
	}
	if len(fetchErrMap) > 0 {
		rawPayloads["redfish_fetch_errors"] = redfishFetchErrorMapToList(fetchErrMap)
	}
	if len(rawLogEntries) > 0 {
		rawPayloads["redfish_log_entries"] = rawLogEntries
	}
	if len(debugPayloads) > 0 {
		rawPayloads["redfish_debug_payloads"] = debugPayloads
	}
	// Unified tunnel: live collection and raw import go through the same analyzer over redfish_tree.
	result, err := ReplayRedfishFromRawPayloads(rawPayloads, nil)
	if err != nil {
		return nil, err
	}
	totalElapsed := time.Since(collectStart).Round(time.Second)
	if !result.InventoryLastModifiedAt.IsZero() {
		log.Printf("redfish-collect: inventory last modified at %s (age: %s)",
			result.InventoryLastModifiedAt.Format(time.RFC3339),
			time.Since(result.InventoryLastModifiedAt).Round(time.Minute),
		)
	}
	log.Printf(
		"redfish-postprobe-metrics: nvme_candidates=%d nvme_selected=%d nvme_added=%d candidates=%d selected=%d skipped_explicit=%d added=%d dur=%s",
		postProbeMetrics.NVMECandidates,
		postProbeMetrics.NVMESelected,
		postProbeMetrics.NVMEAdded,
		postProbeMetrics.CollectionCandidates,
		postProbeMetrics.CollectionSelected,
		postProbeMetrics.SkippedExplicit,
		postProbeMetrics.Added,
		postProbeMetrics.Duration.Round(time.Millisecond),
	)
	log.Printf(
		"redfish-telemetry: req=%d err=%d err_rate=%.2f avg=%dms p95=%dms throttled=%t snapshot_workers=%d prefetch_workers=%d timing_top=%s",
		telemetrySummary.Requests,
		telemetrySummary.Errors,
		telemetrySummary.ErrorRate,
		telemetrySummary.Avg.Milliseconds(),
		telemetrySummary.P95.Milliseconds(),
		throttled,
		acquisitionPlan.Tuning.SnapshotWorkers,
		acquisitionPlan.Tuning.PrefetchWorkers,
		firstNonEmpty(snapshotTimingSummary, "-"),
	)
	for _, line := range redfishPhaseTelemetryLogLines(phaseTelemetry) {
		log.Printf("redfish-telemetry-phase: %s", line)
	}
	log.Printf("redfish-collect: completed in %s (docs=%d, fetch_errors=%d)", totalElapsed, len(rawTree), len(fetchErrMap))
	if emit != nil {
		emit(Progress{
			Status:   "running",
			Progress: 99,
			Message:  fmt.Sprintf("Redfish telemetry: req=%d err=%d p95=%dms throttled=%t", telemetrySummary.Requests, telemetrySummary.Errors, telemetrySummary.P95.Milliseconds(), throttled),
			DebugInfo: &CollectDebugInfo{
				AdaptiveThrottled: throttled,
				SnapshotWorkers:   acquisitionPlan.Tuning.SnapshotWorkers,
				PrefetchWorkers:   acquisitionPlan.Tuning.PrefetchWorkers,
				PrefetchEnabled:   acquisitionPlan.Tuning.PrefetchEnabled,
				PhaseTelemetry:    buildCollectPhaseTelemetry(phaseTelemetry),
			},
		})
		emit(Progress{
			Status:   "running",
			Progress: 100,
			Message:  fmt.Sprintf("Redfish: сбор завершен за %s", totalElapsed),
		})
	}
	return result, nil
}

// collectDebugPayloads fetches vendor-specific diagnostic endpoints on a best-effort basis.
// Results are stored in rawPayloads["redfish_debug_payloads"] and exported with the bundle.
// Enabled only when Request.DebugPayloads is true.
func (c *RedfishConnector) collectDebugPayloads(ctx context.Context, client *http.Client, req Request, baseURL string, systemPaths []string) map[string]any {
	out := map[string]any{}
	for _, systemPath := range systemPaths {
		// AMI/MSI: inventory CRC groups — reveals which groups are supported by this BMC.
		if doc, err := c.getJSON(ctx, client, req, baseURL, joinPath(systemPath, "/Oem/Ami/Inventory/Crc")); err == nil {
			out[joinPath(systemPath, "/Oem/Ami/Inventory/Crc")] = doc
		}
	}
	return out
}

func firstNonEmptyPath(paths []string, fallback string) string {
	for _, p := range paths {
		if strings.TrimSpace(p) != "" {
			return p
		}
	}
	return fallback
}

func isRedfishHostPoweredOn(state string) bool {
	switch strings.ToLower(strings.TrimSpace(state)) {
	case "on", "poweringon":
		return true
	default:
		return false
	}
}

func redfishSystemPowerState(systemDoc map[string]interface{}) string {
	if len(systemDoc) == 0 {
		return ""
	}
	if state := strings.TrimSpace(asString(systemDoc["PowerState"])); state != "" {
		return state
	}
	if summary, ok := systemDoc["PowerSummary"].(map[string]interface{}); ok {
		return strings.TrimSpace(asString(summary["PowerState"]))
	}
	return ""
}

func (c *RedfishConnector) postJSON(ctx context.Context, client *http.Client, req Request, baseURL, resourcePath string, payload map[string]any) error {
	body, err := json.Marshal(payload)
	if err != nil {
		return err
	}
	target := strings.TrimSpace(resourcePath)
	if !strings.HasPrefix(strings.ToLower(target), "http://") && !strings.HasPrefix(strings.ToLower(target), "https://") {
		target = baseURL + normalizeRedfishPath(target)
	}
	u, err := url.Parse(target)
	if err != nil {
		return err
	}
	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), strings.NewReader(string(body)))
	if err != nil {
		return err
	}
	httpReq.Header.Set("Content-Type", "application/json")
	switch req.AuthType {
	case "password":
		httpReq.SetBasicAuth(req.Username, req.Password)
	case "token":
		httpReq.Header.Set("Authorization", "Bearer "+req.Token)
	}
	resp, err := client.Do(httpReq)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return fmt.Errorf("status %d from %s: %s", resp.StatusCode, resourcePath, strings.TrimSpace(string(respBody)))
	}
	return nil
}

func (c *RedfishConnector) prefetchCriticalRedfishDocs(
	ctx context.Context,
	client *http.Client,
	req Request,
	baseURL string,
	criticalPaths []string,
	rawTree map[string]interface{},
	fetchErrMap map[string]string,
	tuning redfishprofile.AcquisitionTuning,
	emit ProgressFn,
) (map[string]interface{}, redfishPrefetchMetrics) {
	metrics := redfishPrefetchMetrics{
		Enabled: redfishPrefetchEnabled(tuning),
	}
	if !metrics.Enabled || len(criticalPaths) == 0 {
		metrics.SkipReason = "disabled-or-empty"
		return nil, metrics
	}

	candidates := redfishPrefetchTargets(criticalPaths, tuning)
	metrics.Candidates = len(candidates)
	if len(candidates) == 0 {
		metrics.SkipReason = "no-candidates"
		return nil, metrics
	}
	targets := redfishAdaptivePrefetchTargets(candidates, rawTree, fetchErrMap)
	metrics.Targets = len(targets)
	if len(targets) == 0 {
		metrics.SkipReason = "not-needed"
		if emit != nil {
			emit(Progress{
				Status:   "running",
				Progress: 96,
				Message:  fmt.Sprintf("Redfish: prefetch пропущен (адаптивно, кандидатов=%d)", metrics.Candidates),
			})
		}
		return nil, metrics
	}
	if emit != nil {
		emit(Progress{
			Status:       "running",
			Progress:     96,
			Message:      fmt.Sprintf("Redfish: prefetch критичных endpoint (адаптивно %d/%d)...", len(targets), len(candidates)),
			CurrentPhase: "prefetch",
			ETASeconds:   int(estimateProgressETA(time.Now(), 0, len(targets), 2*time.Second).Seconds()),
		})
	}

	start := time.Now()
	out := make(map[string]interface{}, len(targets))
	seen := make(map[string]struct{}, len(targets))
	var mu sync.Mutex

	addDoc := func(path string, doc map[string]interface{}) {
		path = normalizeRedfishPath(path)
		if path == "" || len(doc) == 0 {
			return
		}
		mu.Lock()
		if _, exists := seen[path]; !exists {
			seen[path] = struct{}{}
			out[path] = doc
		}
		mu.Unlock()
	}

	workerN := redfishPrefetchWorkers(tuning)
	jobs := make(chan string, len(targets))
	var wg sync.WaitGroup
	for i := 0; i < workerN; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for p := range jobs {
				doc, err := c.getJSONWithRetry(ctx, client, req, baseURL, p, redfishPrefetchRetryAttempts(), redfishPrefetchRetryBackoff())
				if err != nil {
					continue
				}
				addDoc(p, doc)
				memberPaths := redfishCollectionMemberRefs(doc)
				if len(memberPaths) == 0 {
					continue
				}
				if maxMembers := redfishPrefetchMemberRecoveryMax(); maxMembers > 0 && len(memberPaths) > maxMembers {
					memberPaths = memberPaths[:maxMembers]
				}
				for _, memberPath := range memberPaths {
					memberPath = normalizeRedfishPath(memberPath)
					if memberPath == "" {
						continue
					}
					mu.Lock()
					_, exists := seen[memberPath]
					mu.Unlock()
					if exists {
						continue
					}
					memberDoc, err := c.getJSONWithRetry(ctx, client, req, baseURL, memberPath, redfishPrefetchMemberRetryAttempts(), redfishPrefetchRetryBackoff())
					if err != nil {
						continue
					}
					addDoc(memberPath, memberDoc)
				}
			}
		}()
	}

	for _, p := range targets {
		select {
		case jobs <- p:
		case <-ctx.Done():
			close(jobs)
			wg.Wait()
			metrics.Docs = len(out)
			metrics.Duration = time.Since(start)
			metrics.SkipReason = "ctx-cancelled"
			return out, metrics
		}
	}
	close(jobs)
	wg.Wait()
	metrics.Docs = len(out)
	metrics.Duration = time.Since(start)

	if emit != nil {
		emit(Progress{
			Status:       "running",
			Progress:     96,
			Message:      fmt.Sprintf("Redfish: prefetch завершен (адаптивно targets=%d, docs=%d)", len(targets), len(out)),
			CurrentPhase: "prefetch",
		})
	}
	return out, metrics
}

func redfishAdaptivePrefetchTargets(candidates []string, rawTree map[string]interface{}, fetchErrs map[string]string) []string {
	out := make([]string, 0, len(candidates))
	seen := make(map[string]struct{}, len(candidates))
	for _, p := range candidates {
		p = normalizeRedfishPath(p)
		if p == "" {
			continue
		}
		if _, exists := seen[p]; exists {
			continue
		}
		needsFetch := false
		docAny, inTree := rawTree[p]
		if !inTree {
			needsFetch = true
			if msg, hasErr := fetchErrs[p]; hasErr && !isRetryableRedfishFetchError(fmt.Errorf("%s", msg)) {
				needsFetch = false
			}
		} else if doc, ok := docAny.(map[string]interface{}); ok {
			needsFetch = redfishCollectionNeedsMemberRecovery(doc, rawTree, fetchErrs)
		}
		if !needsFetch {
			continue
		}
		seen[p] = struct{}{}
		out = append(out, p)
	}
	return out
}

func redfishCollectionNeedsMemberRecovery(collectionDoc map[string]interface{}, rawTree map[string]interface{}, fetchErrs map[string]string) bool {
	memberPaths := redfishCollectionMemberRefs(collectionDoc)
	if len(memberPaths) == 0 {
		return false
	}
	for _, memberPath := range memberPaths {
		memberPath = normalizeRedfishPath(memberPath)
		if memberPath == "" {
			continue
		}
		if _, exists := rawTree[memberPath]; exists {
			continue
		}
		if msg, hasErr := fetchErrs[memberPath]; hasErr && !isRetryableRedfishFetchError(fmt.Errorf("%s", msg)) {
			continue
		}
		return true
	}
	return false
}

func (c *RedfishConnector) httpClient(req Request) *http.Client {
	return c.httpClientWithTimeout(req, c.timeout)
}

func (c *RedfishConnector) httpClientWithTimeout(req Request, timeout time.Duration) *http.Client {
	transport := &http.Transport{}
	if req.TLSMode == "insecure" {
		transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec
	}
	return &http.Client{
		Transport: transport,
		Timeout:   timeout,
	}
}

func (c *RedfishConnector) baseURL(req Request) (string, error) {
	host := strings.TrimSpace(req.Host)
	if host == "" {
		return "", fmt.Errorf("empty host")
	}

	if strings.HasPrefix(host, "http://") || strings.HasPrefix(host, "https://") {
		u, err := url.Parse(host)
		if err != nil {
			return "", fmt.Errorf("invalid host URL: %w", err)
		}
		u.Path = ""
		u.RawQuery = ""
		u.Fragment = ""
		return strings.TrimRight(u.String(), "/"), nil
	}

	scheme := "https"
	if req.TLSMode == "insecure" && req.Port == 80 {
		scheme = "http"
	}
	return fmt.Sprintf("%s://%s:%d", scheme, host, req.Port), nil
}

func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Client, req Request, baseURL, systemPath string) []models.Storage {
	var out []models.Storage
	storageMembers, _ := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(systemPath, "/Storage"))
	for _, member := range storageMembers {
		// "Drives" can be embedded refs or a link to a collection.
		if driveCollection, ok := member["Drives"].(map[string]interface{}); ok {
			if driveCollectionPath := asString(driveCollection["@odata.id"]); driveCollectionPath != "" {
				driveDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, driveCollectionPath)
				if err == nil {
					for _, driveDoc := range driveDocs {
						if isVirtualStorageDrive(driveDoc) {
							continue
						}
						supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
						out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
					}
					if len(driveDocs) == 0 {
						for _, driveDoc := range c.probeDirectDiskBayChildren(ctx, client, req, baseURL, driveCollectionPath) {
							if isVirtualStorageDrive(driveDoc) {
								continue
							}
							supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, 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 := c.getJSON(ctx, client, req, baseURL, odata)
				if err != nil {
					continue
				}
				if isVirtualStorageDrive(driveDoc) {
					continue
				}
				supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
				out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
			}
			continue
		}

		// Some implementations return drive fields right in storage member object.
		if looksLikeDrive(member) {
			if isVirtualStorageDrive(member) {
				continue
			}
			supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, member, "DriveMetrics", "EnvironmentMetrics", "Metrics")
			out = append(out, parseDriveWithSupplementalDocs(member, supplementalDocs...))
		}

		// Supermicro/RAID implementations can expose physical disks under chassis enclosures
		// linked from Storage.Links.Enclosures, while Storage.Drives stays empty.
		for _, enclosurePath := range redfishLinkRefs(member, "Links", "Enclosures") {
			driveDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(enclosurePath, "/Drives"))
			if err == nil {
				for _, driveDoc := range driveDocs {
					if looksLikeDrive(driveDoc) && !isVirtualStorageDrive(driveDoc) {
						supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
						out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
					}
				}
				if len(driveDocs) == 0 {
					for _, driveDoc := range c.probeDirectDiskBayChildren(ctx, client, req, baseURL, joinPath(enclosurePath, "/Drives")) {
						if isVirtualStorageDrive(driveDoc) {
							continue
						}
						out = append(out, parseDrive(driveDoc))
					}
				}
			}
		}
	}

	// IntelVROC often exposes rich drive inventory via dedicated child collections.
	for _, driveDoc := range c.collectKnownStorageMembers(ctx, client, req, baseURL, systemPath, []string{
		"/Storage/IntelVROC/Drives",
		"/Storage/IntelVROC/Controllers/1/Drives",
	}) {
		if looksLikeDrive(driveDoc) && !isVirtualStorageDrive(driveDoc) {
			supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
			out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
		}
	}

	// Fallback for platforms that expose disks in SimpleStorage.
	simpleStorageMembers, _ := c.getCollectionMembers(ctx, client, req, baseURL, 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, parseDriveWithSupplementalDocs(devDoc))
		}
	}

	// Fallback for platforms exposing physical drives under Chassis.
	chassisPaths := c.discoverMemberPaths(ctx, client, req, baseURL, "/redfish/v1/Chassis", "/redfish/v1/Chassis/1")
	for _, chassisPath := range chassisPaths {
		driveDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(chassisPath, "/Drives"))
		if err != nil {
			continue
		}
		for _, driveDoc := range driveDocs {
			if !looksLikeDrive(driveDoc) || isVirtualStorageDrive(driveDoc) {
				continue
			}
			supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
			out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
		}
	}
	for _, chassisPath := range chassisPaths {
		if !isSupermicroNVMeBackplanePath(chassisPath) {
			continue
		}
		for _, driveDoc := range c.probeSupermicroNVMeDiskBays(ctx, client, req, baseURL, chassisPath) {
			if !looksLikeDrive(driveDoc) || isVirtualStorageDrive(driveDoc) {
				continue
			}
			supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
			out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
		}
	}

	out = dedupeStorage(out)
	return out
}

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

func (c *RedfishConnector) collectNICs(ctx context.Context, client *http.Client, req Request, baseURL string, chassisPaths []string) []models.NetworkAdapter {
	var nics []models.NetworkAdapter
	for _, chassisPath := range chassisPaths {
		adapterDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(chassisPath, "/NetworkAdapters"))
		if err != nil {
			continue
		}
		for _, doc := range adapterDocs {
			nic := parseNIC(doc)
			adapterFunctionDocs := c.getNetworkAdapterFunctionDocs(ctx, client, req, baseURL, doc)
			for _, pciePath := range networkAdapterPCIeDevicePaths(doc) {
				pcieDoc, err := c.getJSON(ctx, client, req, baseURL, pciePath)
				if err != nil {
					continue
				}
				functionDocs := c.getLinkedPCIeFunctions(ctx, client, req, baseURL, pcieDoc)
				for _, adapterFnDoc := range adapterFunctionDocs {
					functionDocs = append(functionDocs, c.getLinkedPCIeFunctions(ctx, client, req, baseURL, adapterFnDoc)...)
				}
				functionDocs = dedupeJSONDocsByPath(functionDocs)
				supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, pcieDoc, "EnvironmentMetrics", "Metrics")
				for _, fn := range functionDocs {
					supplementalDocs = append(supplementalDocs, c.getLinkedSupplementalDocs(ctx, client, req, baseURL, fn, "EnvironmentMetrics", "Metrics")...)
				}
				enrichNICFromPCIe(&nic, pcieDoc, functionDocs, supplementalDocs)
			}
			nics = append(nics, nic)
		}
	}
	return dedupeNetworkAdapters(nics)
}

func (c *RedfishConnector) collectPSUs(ctx context.Context, client *http.Client, req Request, baseURL string, chassisPaths []string) []models.PSU {
	var out []models.PSU
	seen := make(map[string]int)
	idx := 1
	for _, chassisPath := range chassisPaths {
		// Redfish 2022+/X14+ commonly uses PowerSubsystem as the primary source.
		if memberDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(chassisPath, "/PowerSubsystem/PowerSupplies")); err == nil && len(memberDocs) > 0 {
			for _, doc := range memberDocs {
				supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, doc, "EnvironmentMetrics", "Metrics")
				idx = appendPSU(&out, seen, parsePSUWithSupplementalDocs(doc, idx, supplementalDocs...), idx)
			}
			continue
		}

		// Legacy source: embedded array in Chassis/<id>/Power.
		if powerDoc, err := c.getJSON(ctx, client, req, baseURL, 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 := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, doc, "EnvironmentMetrics", "Metrics")
					idx = appendPSU(&out, seen, parsePSUWithSupplementalDocs(doc, idx, supplementalDocs...), idx)
				}
			}
		}
	}
	return out
}

func appendPSU(out *[]models.PSU, seen map[string]int, psu models.PSU, currentIdx int) int {
	nextIdx := currentIdx + 1
	keys := psuIdentityKeys(psu)
	if len(keys) == 0 {
		return nextIdx
	}
	for _, key := range keys {
		if idx, ok := seen[key]; ok {
			(*out)[idx] = mergePSUEntries((*out)[idx], psu)
			for _, mergedKey := range psuIdentityKeys((*out)[idx]) {
				seen[mergedKey] = idx
			}
			return nextIdx
		}
	}
	idx := len(*out)
	for _, key := range keys {
		seen[key] = idx
	}
	*out = append(*out, psu)
	return nextIdx
}

func (c *RedfishConnector) collectKnownStorageMembers(ctx context.Context, client *http.Client, req Request, baseURL, systemPath string, relativeCollections []string) []map[string]interface{} {
	var out []map[string]interface{}
	for _, rel := range relativeCollections {
		docs, err := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(systemPath, rel))
		if err != nil || len(docs) == 0 {
			continue
		}
		out = append(out, docs...)
	}
	return out
}

func redfishLinkedPath(doc map[string]interface{}, key string) string {
	if v, ok := doc[key].(map[string]interface{}); ok {
		return asString(v["@odata.id"])
	}
	return ""
}

func (c *RedfishConnector) getLinkedSupplementalDocs(
	ctx context.Context,
	client *http.Client,
	req Request,
	baseURL string,
	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 := c.getJSON(ctx, client, req, baseURL, path)
		if err != nil || len(supplementalDoc) == 0 {
			continue
		}
		seen[path] = struct{}{}
		out = append(out, supplementalDoc)
	}
	return out
}

func (c *RedfishConnector) collectGPUs(ctx context.Context, client *http.Client, req Request, baseURL string, 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 := c.getCollectionMembers(ctx, client, req, baseURL, collectionPath)
		if err != nil || len(memberDocs) == 0 {
			continue
		}

		for _, doc := range memberDocs {
			functionDocs := c.getLinkedPCIeFunctions(ctx, client, req, baseURL, doc)
			if !looksLikeGPU(doc, functionDocs) {
				continue
			}

			supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, doc, "EnvironmentMetrics", "Metrics")
			for _, fn := range functionDocs {
				supplementalDocs = append(supplementalDocs, c.getLinkedSupplementalDocs(ctx, client, req, baseURL, 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 (c *RedfishConnector) collectPCIeDevices(ctx context.Context, client *http.Client, req Request, baseURL string, 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 := c.getCollectionMembers(ctx, client, req, baseURL, collectionPath)
		if err != nil || len(memberDocs) == 0 {
			continue
		}

		for _, doc := range memberDocs {
			functionDocs := c.getLinkedPCIeFunctions(ctx, client, req, baseURL, doc)
			if looksLikeGPU(doc, functionDocs) {
				continue
			}
			supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, doc, "EnvironmentMetrics", "Metrics")
			supplementalDocs = append(supplementalDocs, c.getChassisScopedPCIeSupplementalDocs(ctx, client, req, baseURL, doc)...)
			for _, fn := range functionDocs {
				supplementalDocs = append(supplementalDocs, c.getLinkedSupplementalDocs(ctx, client, req, baseURL, fn, "EnvironmentMetrics", "Metrics")...)
			}
			dev := parsePCIeDeviceWithSupplementalDocs(doc, functionDocs, supplementalDocs)
			if isUnidentifiablePCIeDevice(dev) {
				continue
			}
			out = append(out, dev)
		}
	}

	// Fallback: some BMCs expose only PCIeFunctions collection without PCIeDevices.
	for _, systemPath := range systemPaths {
		functionDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(systemPath, "/PCIeFunctions"))
		if err != nil || len(functionDocs) == 0 {
			continue
		}
		for idx, fn := range functionDocs {
			supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, fn, "EnvironmentMetrics", "Metrics")
			dev := parsePCIeFunctionWithSupplementalDocs(fn, supplementalDocs, idx+1)
			out = append(out, dev)
		}
	}

	return dedupePCIeDevices(out)
}

func (c *RedfishConnector) getChassisScopedPCIeSupplementalDocs(ctx context.Context, client *http.Client, req Request, baseURL string, doc map[string]interface{}) []map[string]interface{} {
	docPath := normalizeRedfishPath(asString(doc["@odata.id"]))
	chassisPath := chassisPathForPCIeDoc(docPath)
	if chassisPath == "" {
		return nil
	}

	out := make([]map[string]interface{}, 0, 6)
	seen := make(map[string]struct{})
	add := func(path string) {
		path = normalizeRedfishPath(path)
		if path == "" {
			return
		}
		if _, ok := seen[path]; ok {
			return
		}
		supplementalDoc, err := c.getJSON(ctx, client, req, baseURL, path)
		if err != nil || len(supplementalDoc) == 0 {
			return
		}
		seen[path] = struct{}{}
		out = append(out, supplementalDoc)
	}

	if looksLikeNVSwitchPCIeDoc(doc) {
		add(joinPath(chassisPath, "/EnvironmentMetrics"))
		add(joinPath(chassisPath, "/ThermalSubsystem/ThermalMetrics"))
	}
	deviceDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(chassisPath, "/Devices"))
	if err == nil {
		for _, deviceDoc := range deviceDocs {
			if !redfishPCIeMatchesChassisDeviceDoc(doc, deviceDoc) {
				continue
			}
			add(asString(deviceDoc["@odata.id"]))
		}
	}
	return out
}

func (c *RedfishConnector) discoverMemberPaths(ctx context.Context, client *http.Client, req Request, baseURL, collectionPath, fallbackPath string) []string {
	collection, err := c.getJSON(ctx, client, req, baseURL, 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 (c *RedfishConnector) collectProfileHintDocs(ctx context.Context, client *http.Client, req Request, baseURL, systemPath, chassisPath string) []map[string]interface{} {
	paths := []string{
		"/redfish/v1/UpdateService/FirmwareInventory",
		joinPath(systemPath, "/NetworkInterfaces"),
		joinPath(chassisPath, "/Drives"),
		joinPath(chassisPath, "/NetworkAdapters"),
	}
	seen := make(map[string]struct{}, len(paths))
	docs := make([]map[string]interface{}, 0, len(paths))
	for _, path := range paths {
		path = normalizeRedfishPath(path)
		if path == "" {
			continue
		}
		if _, ok := seen[path]; ok {
			continue
		}
		seen[path] = struct{}{}
		doc, err := c.getJSON(ctx, client, req, baseURL, path)
		if err != nil {
			continue
		}
		docs = append(docs, doc)
	}
	return docs
}

func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *http.Client, req Request, baseURL string, seedPaths []string, tuning redfishprofile.AcquisitionTuning, emit ProgressFn) (map[string]interface{}, []map[string]interface{}, redfishPostProbeMetrics, string) {
	maxDocuments := redfishSnapshotMaxDocuments(tuning)
	workers := redfishSnapshotWorkers(tuning)
	const heartbeatInterval = 5 * time.Second
	crawlStart := time.Now()
	memoryClient := c.httpClientWithTimeout(req, redfishSnapshotMemoryRequestTimeout())
	memoryGate := make(chan struct{}, redfishSnapshotMemoryConcurrency())
	postProbeClient := c.httpClientWithTimeout(req, redfishSnapshotPostProbeRequestTimeout())
	branchLimiter := newRedfishSnapshotBranchLimiter(redfishSnapshotBranchConcurrency())
	branchRetryPause := redfishSnapshotBranchRequeueBackoff()
	timings := newRedfishPathTimingCollector(4)

	postProbeMetrics := redfishPostProbeMetrics{}
	out := make(map[string]interface{}, maxDocuments)
	fetchErrors := make(map[string]string)
	seen := make(map[string]struct{}, maxDocuments)
	rootCounts := make(map[string]int)
	var mu sync.Mutex
	var processed int32
	var lastPath atomic.Value

	// Workers enqueue newly discovered links into the same queue they consume.
	// The queue capacity must be at least the crawl cap to avoid producer/consumer
	// deadlock when several workers discover many links at once.
	jobs := make(chan string, maxDocuments)
	var wg sync.WaitGroup

	enqueue := func(path string) {
		path = normalizeRedfishPath(path)
		if !shouldCrawlPath(path) {
			return
		}
		for _, pattern := range tuning.SnapshotExcludeContains {
			if pattern != "" && strings.Contains(path, pattern) {
				return
			}
		}
		mu.Lock()
		if len(seen) >= maxDocuments {
			mu.Unlock()
			return
		}
		if _, ok := seen[path]; ok {
			mu.Unlock()
			return
		}
		seen[path] = struct{}{}
		wg.Add(1)
		mu.Unlock()
		jobs <- path
	}

	enqueue("/redfish/v1")
	for _, seed := range seedPaths {
		enqueue(seed)
	}
	c.debugSnapshotf("snapshot queue initialized workers=%d max_documents=%d", workers, maxDocuments)
	stopHeartbeat := make(chan struct{})
	if emit != nil {
		go func() {
			ticker := time.NewTicker(heartbeatInterval)
			defer ticker.Stop()
			for {
				select {
				case <-ticker.C:
					n := atomic.LoadInt32(&processed)
					mu.Lock()
					countsCopy := make(map[string]int, len(rootCounts))
					for k, v := range rootCounts {
						countsCopy[k] = v
					}
					seenN := len(seen)
					outN := len(out)
					mu.Unlock()
					roots := topRoots(countsCopy, 2)
					last := "/redfish/v1"
					if v := lastPath.Load(); v != nil {
						if s, ok := v.(string); ok && s != "" {
							last = s
						}
					}
					eta := formatETA(estimateSnapshotETA(crawlStart, int(n), seenN, len(jobs), workers, client.Timeout))
					emit(Progress{
						Status:       "running",
						Progress:     92 + int(minInt32(n/200, 6)),
						Message:      fmt.Sprintf("Redfish snapshot: heartbeat документов=%d (ok=%d, seen=%d), ETA≈%s, корни=%s, последний=%s", n, outN, seenN, eta, strings.Join(roots, ", "), compactProgressPath(last)),
						CurrentPhase: "snapshot",
						ETASeconds:   int(estimateSnapshotETA(crawlStart, int(n), seenN, len(jobs), workers, client.Timeout).Seconds()),
					})
				case <-stopHeartbeat:
					return
				case <-ctx.Done():
					return
				}
			}
		}()
	}
	for i := 0; i < workers; i++ {
		go func(workerID int) {
			for current := range jobs {
				if !branchLimiter.tryAcquire(current) {
					select {
					case jobs <- current:
						c.debugSnapshotf("worker=%d requeue branch-busy path=%s branch=%s queue_len=%d", workerID, current, redfishSnapshotBranchKey(current), len(jobs))
						select {
						case <-time.After(branchRetryPause):
						case <-ctx.Done():
						}
						continue
					default:
					}
					if !branchLimiter.waitAcquire(ctx, current, branchRetryPause) {
						n := atomic.AddInt32(&processed, 1)
						mu.Lock()
						if _, ok := fetchErrors[current]; !ok && ctx.Err() != nil {
							fetchErrors[current] = ctx.Err().Error()
						}
						mu.Unlock()
						if emit != nil && ctx.Err() != nil {
							emit(Progress{
								Status:   "running",
								Progress: 92 + int(minInt32(n/200, 6)),
								Message:  fmt.Sprintf("Redfish snapshot: ошибка на %s", compactProgressPath(current)),
							})
						}
						wg.Done()
						continue
					}
				}

				lastPath.Store(current)
				c.debugSnapshotf("worker=%d fetch start path=%s queue_len=%d", workerID, current, len(jobs))
				fetchStart := time.Now()
				doc, err := func() (map[string]interface{}, error) {
					defer branchLimiter.release(current)
					if !isRedfishMemoryMemberPath(current) {
						return c.getJSON(ctx, client, req, baseURL, current)
					}
					select {
					case memoryGate <- struct{}{}:
					case <-ctx.Done():
						return nil, ctx.Err()
					}
					defer func() { <-memoryGate }()
					return c.getJSONWithRetry(
						ctx,
						memoryClient,
						req,
						baseURL,
						current,
						redfishSnapshotMemoryRetryAttempts(),
						redfishSnapshotMemoryRetryBackoff(),
					)
				}()
				timings.Observe(current, time.Since(fetchStart), err != nil)
				if err == nil {
					mu.Lock()
					out[current] = doc
					rootCounts[redfishTopRoot(current)]++
					mu.Unlock()

					for _, ref := range extractODataIDs(doc) {
						enqueue(ref)
					}
				}
				n := atomic.AddInt32(&processed, 1)
				if err != nil {
					mu.Lock()
					if _, ok := fetchErrors[current]; !ok {
						fetchErrors[current] = err.Error()
					}
					mu.Unlock()
					c.debugSnapshotf("worker=%d fetch error path=%s err=%v", workerID, current, err)
					if emit != nil && shouldReportSnapshotFetchError(err) {
						emit(Progress{
							Status:   "running",
							Progress: 92 + int(minInt32(n/200, 6)),
							Message:  fmt.Sprintf("Redfish snapshot: ошибка на %s", compactProgressPath(current)),
						})
					}
				}
				if emit != nil && n%40 == 0 {
					mu.Lock()
					countsCopy := make(map[string]int, len(rootCounts))
					for k, v := range rootCounts {
						countsCopy[k] = v
					}
					seenN := len(seen)
					mu.Unlock()
					roots := topRoots(countsCopy, 2)
					last := current
					if v := lastPath.Load(); v != nil {
						if s, ok := v.(string); ok && s != "" {
							last = s
						}
					}
					eta := formatETA(estimateSnapshotETA(crawlStart, int(n), seenN, len(jobs), workers, client.Timeout))
					emit(Progress{
						Status:       "running",
						Progress:     92 + int(minInt32(n/200, 6)),
						Message:      fmt.Sprintf("Redfish snapshot: документов=%d, ETA≈%s, корни=%s, последний=%s", n, eta, strings.Join(roots, ", "), compactProgressPath(last)),
						CurrentPhase: "snapshot",
						ETASeconds:   int(estimateSnapshotETA(crawlStart, int(n), seenN, len(jobs), workers, client.Timeout).Seconds()),
					})
				}
				if n%20 == 0 || err != nil {
					mu.Lock()
					seenN := len(seen)
					outN := len(out)
					mu.Unlock()
					c.debugSnapshotf("snapshot progress processed=%d stored=%d seen=%d queue_len=%d", n, outN, seenN, len(jobs))
				}

				wg.Done()
			}
		}(i + 1)
	}

	wg.Wait()
	close(stopHeartbeat)
	close(jobs)

	// Profile-owned policy for direct NVMe Disk.Bay probing.
	postProbeTotalStart := time.Now()
	driveCollections := make([]string, 0)
	if tuning.PostProbePolicy.EnableDirectNVMEDiskBayProbe {
		for path, docAny := range out {
			normalized := normalizeRedfishPath(path)
			if !strings.HasSuffix(normalized, "/Drives") {
				continue
			}
			postProbeMetrics.NVMECandidates++
			doc, _ := docAny.(map[string]interface{})
			if !shouldAdaptiveNVMeProbe(doc) {
				continue
			}
			// Skip chassis types that cannot contain NVMe storage (e.g. GPU modules,
			// RoT components, NVSwitch zones on HGX systems) to avoid probing hundreds
			// of Disk.Bay.N URLs against chassis that will never have drives.
			chassisPath := strings.TrimSuffix(normalized, "/Drives")
			if chassisDocAny, ok := out[chassisPath]; ok {
				if chassisDoc, ok := chassisDocAny.(map[string]interface{}); ok {
					if !chassisTypeCanHaveNVMe(asString(chassisDoc["ChassisType"])) {
						continue
					}
				}
			}
			driveCollections = append(driveCollections, normalized)
		}
	}
	sort.Strings(driveCollections)
	postProbeMetrics.NVMESelected = len(driveCollections)
	nvmeProbeStart := time.Now()
	nvmePostProbeEnabled := redfishNVMePostProbeEnabled(tuning)
	for i, path := range driveCollections {
		if !nvmePostProbeEnabled {
			break
		}
		if emit != nil && len(driveCollections) > 0 && (i == 0 || i%4 == 0 || i == len(driveCollections)-1) {
			emit(Progress{
				Status:       "running",
				Progress:     97,
				Message:      fmt.Sprintf("Redfish snapshot: post-probe NVMe (%d/%d, ETA≈%s), коллекция=%s", i+1, len(driveCollections), formatETA(estimateProgressETA(nvmeProbeStart, i, len(driveCollections), 2*time.Second)), compactProgressPath(path)),
				CurrentPhase: "snapshot_postprobe_nvme",
				ETASeconds:   int(estimateProgressETA(nvmeProbeStart, i, len(driveCollections), 2*time.Second).Seconds()),
			})
		}
		for _, bayPath := range directDiskBayCandidates(path) {
			doc, err := c.getJSON(ctx, client, req, baseURL, bayPath)
			if err != nil {
				continue
			}
			if !looksLikeDrive(doc) {
				continue
			}
			normalizedBayPath := normalizeRedfishPath(bayPath)
			if _, exists := out[normalizedBayPath]; exists {
				continue
			}
			out[normalizedBayPath] = doc
			postProbeMetrics.NVMEAdded++
			c.debugSnapshotf("snapshot nvme bay probe hit path=%s", bayPath)
		}
	}
	// Profile-owned policy for numeric collection post-probe.
	postProbeCollections := make([]string, 0)
	for path, docAny := range out {
		normalized := normalizeRedfishPath(path)
		if !shouldPostProbeCollectionPath(normalized, tuning) {
			continue
		}
		postProbeMetrics.CollectionCandidates++
		doc, _ := docAny.(map[string]interface{})
		if shouldAdaptivePostProbeCollectionPath(normalized, doc, tuning) {
			postProbeCollections = append(postProbeCollections, normalized)
			continue
		}
		if redfishCollectionHasExplicitMembers(doc) {
			postProbeMetrics.SkippedExplicit++
		}
	}
	sort.Strings(postProbeCollections)
	postProbeMetrics.CollectionSelected = len(postProbeCollections)
	postProbeStart := time.Now()
	addedPostProbe := 0
	for i, path := range postProbeCollections {
		if emit != nil && len(postProbeCollections) > 0 && (i == 0 || i%8 == 0 || i == len(postProbeCollections)-1) {
			emit(Progress{
				Status:       "running",
				Progress:     98,
				Message:      fmt.Sprintf("Redfish snapshot: post-probe коллекций (%d/%d, ETA≈%s), текущая=%s", i+1, len(postProbeCollections), formatETA(estimateProgressETA(postProbeStart, i, len(postProbeCollections), 3*time.Second)), compactProgressPath(path)),
				CurrentPhase: "snapshot_postprobe_collections",
				ETASeconds:   int(estimateProgressETA(postProbeStart, i, len(postProbeCollections), 3*time.Second).Seconds()),
			})
		}
		for childPath, doc := range c.probeDirectRedfishCollectionChildren(ctx, postProbeClient, req, baseURL, path) {
			if _, exists := out[childPath]; exists {
				continue
			}
			out[childPath] = doc
			addedPostProbe++
		}
	}
	postProbeMetrics.Added = addedPostProbe
	postProbeMetrics.Duration = time.Since(postProbeTotalStart)
	if emit != nil && addedPostProbe > 0 {
		emit(Progress{
			Status:   "running",
			Progress: 98,
			Message:  fmt.Sprintf("Redfish snapshot: post-probe добавлено %d документов", addedPostProbe),
		})
	}
	if emit != nil {
		emit(Progress{
			Status:   "running",
			Progress: 98,
			Message:  fmt.Sprintf("Redfish snapshot: post-probe метрики candidates=%d selected=%d skipped_explicit=%d added=%d", postProbeMetrics.CollectionCandidates, postProbeMetrics.CollectionSelected, postProbeMetrics.SkippedExplicit, postProbeMetrics.Added),
		})
	}

	if emit != nil {
		emit(Progress{
			Status:   "running",
			Progress: 98,
			Message:  fmt.Sprintf("Redfish snapshot: собрано %d документов", len(out)),
		})
	}

	errorList := make([]map[string]interface{}, 0, len(fetchErrors))
	for p, msg := range fetchErrors {
		errorList = append(errorList, map[string]interface{}{
			"path":  p,
			"error": msg,
		})
	}
	sort.Slice(errorList, func(i, j int) bool {
		return asString(errorList[i]["path"]) < asString(errorList[j]["path"])
	})
	if summary := timings.Summary(12); summary != "" {
		log.Printf("redfish-snapshot-timing: %s", summary)
	}
	if emit != nil {
		if summary := timings.Summary(3); summary != "" {
			emit(Progress{
				Status:   "running",
				Progress: 98,
				Message:  fmt.Sprintf("Redfish snapshot: топ веток по времени: %s", summary),
			})
		}
	}

	return out, errorList, postProbeMetrics, timings.Summary(12)
}

func (c *RedfishConnector) probeSupermicroNVMeDiskBays(ctx context.Context, client *http.Client, req Request, baseURL, backplanePath string) []map[string]interface{} {
	return c.probeDirectDiskBayChildren(ctx, client, req, baseURL, joinPath(backplanePath, "/Drives"))
}

func isSupermicroNVMeBackplanePath(path string) bool {
	path = normalizeRedfishPath(path)
	return strings.Contains(path, "/Chassis/NVMeSSD.") && strings.Contains(path, ".StorageBackplane")
}

func supermicroNVMeDiskBayCandidates(backplanePath string) []string {
	return directDiskBayCandidates(joinPath(backplanePath, "/Drives"))
}

func (c *RedfishConnector) probeDirectDiskBayChildren(ctx context.Context, client *http.Client, req Request, baseURL, drivesCollectionPath string) []map[string]interface{} {
	var out []map[string]interface{}
	for _, path := range directDiskBayCandidates(drivesCollectionPath) {
		doc, err := c.getJSON(ctx, client, req, baseURL, path)
		if err != nil || !looksLikeDrive(doc) {
			continue
		}
		out = append(out, doc)
	}
	return out
}

func directDiskBayCandidates(drivesCollectionPath string) []string {
	const maxBays = 128
	prefix := normalizeRedfishPath(drivesCollectionPath)
	out := make([]string, 0, maxBays*3)
	for i := 0; i < maxBays; i++ {
		out = append(out, fmt.Sprintf("%s/Disk.Bay.%d", prefix, i))
		out = append(out, fmt.Sprintf("%s/Disk.Bay%d", prefix, i))
		out = append(out, fmt.Sprintf("%s/%d", prefix, i))
	}
	return out
}

func (c *RedfishConnector) probeDirectRedfishCollectionChildren(ctx context.Context, client *http.Client, req Request, baseURL, collectionPath string) map[string]map[string]interface{} {
	normalized := normalizeRedfishPath(collectionPath)
	maxItems, startIndex, missBudget := directNumericProbePlan(normalized)
	if maxItems <= 0 {
		return nil
	}
	out := make(map[string]map[string]interface{})
	consecutiveMisses := 0
	for i := startIndex; i <= maxItems; i++ {
		path := fmt.Sprintf("%s/%d", normalized, i)
		doc, err := c.getJSON(ctx, client, req, baseURL, path)
		if err != nil {
			consecutiveMisses++
			if consecutiveMisses >= missBudget {
				break
			}
			continue
		}
		consecutiveMisses = 0
		if !looksLikeRedfishResource(doc) {
			continue
		}
		out[normalizeRedfishPath(path)] = doc
	}
	return out
}

func (c *RedfishConnector) probeDirectRedfishCollectionChildrenSlow(ctx context.Context, client *http.Client, req Request, baseURL, collectionPath string) map[string]map[string]interface{} {
	normalized := normalizeRedfishPath(collectionPath)
	maxItems, startIndex, missBudget := directNumericProbePlan(normalized)
	if maxItems <= 0 {
		return nil
	}
	out := make(map[string]map[string]interface{})
	consecutiveMisses := 0
	for i := startIndex; i <= maxItems; i++ {
		if len(out) > 0 || i > startIndex {
			select {
			case <-time.After(redfishCriticalSlowGap()):
			case <-ctx.Done():
				return out
			}
		}
		path := fmt.Sprintf("%s/%d", normalized, i)
		doc, err := c.getJSONWithRetry(ctx, client, req, baseURL, path, redfishCriticalPlanBAttempts(), redfishCriticalRetryBackoff())
		if err != nil {
			consecutiveMisses++
			if consecutiveMisses >= missBudget {
				break
			}
			continue
		}
		consecutiveMisses = 0
		if !looksLikeRedfishResource(doc) {
			continue
		}
		out[normalizeRedfishPath(path)] = doc
	}
	return out
}

func directNumericProbePlan(collectionPath string) (maxItems, startIndex, missBudget int) {
	switch {
	case strings.HasSuffix(collectionPath, "/Systems"):
		return 32, 1, 8
	case strings.HasSuffix(collectionPath, "/Chassis"):
		return 64, 1, 12
	case strings.HasSuffix(collectionPath, "/Managers"):
		return 16, 1, 6
	case strings.HasSuffix(collectionPath, "/Processors"):
		return 32, 1, 12
	case strings.HasSuffix(collectionPath, "/Memory"):
		return 512, 1, 48
	case strings.HasSuffix(collectionPath, "/Storage"):
		return 128, 1, 24
	case strings.HasSuffix(collectionPath, "/Drives"):
		return 256, 0, 24
	case strings.HasSuffix(collectionPath, "/Volumes"):
		return 128, 1, 16
	case strings.HasSuffix(collectionPath, "/PCIeDevices"):
		return 256, 1, 24
	case strings.HasSuffix(collectionPath, "/PCIeFunctions"):
		return 512, 1, 32
	case strings.HasSuffix(collectionPath, "/NetworkAdapters"):
		return 128, 1, 20
	case strings.HasSuffix(collectionPath, "/NetworkPorts"):
		return 256, 1, 24
	case strings.HasSuffix(collectionPath, "/Ports"):
		return 256, 1, 24
	case strings.HasSuffix(collectionPath, "/EthernetInterfaces"):
		return 256, 1, 24
	case strings.HasSuffix(collectionPath, "/Certificates"):
		return 256, 1, 24
	case strings.HasSuffix(collectionPath, "/Accounts"):
		return 128, 1, 16
	case strings.HasSuffix(collectionPath, "/LogServices"):
		return 32, 1, 8
	case strings.HasSuffix(collectionPath, "/Sensors"):
		return 512, 1, 48
	case strings.HasSuffix(collectionPath, "/Temperatures"):
		return 256, 1, 32
	case strings.HasSuffix(collectionPath, "/Fans"):
		return 256, 1, 32
	case strings.HasSuffix(collectionPath, "/Voltages"):
		return 256, 1, 32
	case strings.HasSuffix(collectionPath, "/PowerSupplies"):
		return 64, 1, 16
	default:
		return 0, 0, 0
	}
}

func shouldPostProbeCollectionPath(path string, tuning redfishprofile.AcquisitionTuning) bool {
	path = normalizeRedfishPath(path)
	if !tuning.PostProbePolicy.EnableNumericCollectionProbe {
		return false
	}
	sensorProbeEnabled := tuning.PostProbePolicy.EnableSensorCollectionProbe || redfishSnapshotSensorPostProbeEnabled()
	// Restrict expensive post-probe to collections that historically recover
	// missing inventory/telemetry on partially implemented BMCs.
	switch {
	case strings.HasSuffix(path, "/Sensors"),
		strings.HasSuffix(path, "/ThresholdSensors"),
		strings.HasSuffix(path, "/DiscreteSensors"),
		strings.HasSuffix(path, "/Temperatures"),
		strings.HasSuffix(path, "/Fans"),
		strings.HasSuffix(path, "/Voltages"):
		return sensorProbeEnabled
	case strings.HasSuffix(path, "/PowerSupplies"),
		strings.HasSuffix(path, "/EthernetInterfaces"),
		strings.HasSuffix(path, "/NetworkPorts"),
		strings.HasSuffix(path, "/Ports"),
		strings.HasSuffix(path, "/PCIeDevices"),
		strings.HasSuffix(path, "/PCIeFunctions"),
		strings.HasSuffix(path, "/Drives"),
		strings.HasSuffix(path, "/Volumes"):
		return true
	default:
		return false
	}
}

func shouldAdaptivePostProbeCollectionPath(path string, collectionDoc map[string]interface{}, tuning redfishprofile.AcquisitionTuning) bool {
	path = normalizeRedfishPath(path)
	if !shouldPostProbeCollectionPath(path, tuning) {
		return false
	}
	if len(collectionDoc) == 0 {
		return true
	}
	memberRefs := redfishCollectionMemberRefs(collectionDoc)
	if len(memberRefs) == 0 {
		return true
	}
	// If the collection reports an explicit non-zero member count that already
	// matches the number of discovered member refs, every member is accounted
	// for and numeric probing cannot find anything new.
	if odataCount := asInt(collectionDoc["Members@odata.count"]); odataCount > 0 && odataCount == len(memberRefs) {
		return false
	}
	return redfishCollectionHasNumericMemberRefs(memberRefs)
}

func shouldAdaptiveNVMeProbe(collectionDoc map[string]interface{}) bool {
	if len(collectionDoc) == 0 {
		return true
	}
	return !redfishCollectionHasExplicitMembers(collectionDoc)
}

// chassisTypeCanHaveNVMe returns false for Redfish ChassisType values that
// represent compute/network/management sub-modules with no storage capability.
// Used to skip expensive Disk.Bay.N probing on HGX GPU, NVSwitch, PCIeRetimer,
// RoT and similar component chassis that expose an empty /Drives collection.
func chassisTypeCanHaveNVMe(chassisType string) bool {
	switch strings.ToLower(strings.TrimSpace(chassisType)) {
	case "module", // GPU SXM, NVLinkManagementNIC, PCIeRetimer
		"component", // ERoT, IRoT, BMC, FPGA sub-chassis
		"zone":      // HGX_Chassis_0 fabric zone
		return false
	default:
		return true
	}
}

func redfishCollectionHasNumericMemberRefs(memberRefs []string) bool {
	for _, memberPath := range memberRefs {
		if redfishPathTailIsNumeric(memberPath) {
			return true
		}
	}
	return false
}

func redfishPathTailIsNumeric(path string) bool {
	normalized := normalizeRedfishPath(path)
	if normalized == "" {
		return false
	}
	parts := strings.Split(strings.Trim(normalized, "/"), "/")
	if len(parts) == 0 {
		return false
	}
	tail := strings.TrimSpace(parts[len(parts)-1])
	if tail == "" {
		return false
	}
	for _, r := range tail {
		if r < '0' || r > '9' {
			return false
		}
	}
	return true
}

func looksLikeRedfishResource(doc map[string]interface{}) bool {
	if len(doc) == 0 {
		return false
	}
	if asString(doc["@odata.id"]) != "" {
		return true
	}
	if asString(doc["Id"]) != "" || asString(doc["Name"]) != "" {
		return true
	}
	if _, ok := doc["Status"]; ok {
		return true
	}
	if _, ok := doc["Reading"]; ok {
		return true
	}
	if _, ok := doc["ReadingCelsius"]; ok {
		return true
	}
	return false
}

// isHardwareInventoryCollectionPath reports whether the path is a hardware
// inventory collection that is expected to have members when the machine is
// powered on and the BMC has finished initializing.
func isHardwareInventoryCollectionPath(p string) bool {
	for _, suffix := range []string{
		"/PCIeDevices",
		"/NetworkAdapters",
		"/Processors",
		"/Drives",
		"/Storage",
		"/EthernetInterfaces",
	} {
		if strings.HasSuffix(p, suffix) {
			return true
		}
	}
	return false
}

func shouldSlowProbeCriticalCollection(p string, tuning redfishprofile.AcquisitionTuning) bool {
	p = normalizeRedfishPath(p)
	if !tuning.RecoveryPolicy.EnableCriticalSlowProbe {
		return false
	}
	for _, suffix := range []string{
		"/Processors",
		"/Memory",
		"/Storage",
		"/Drives",
		"/Volumes",
		"/PCIeDevices",
		"/PCIeFunctions",
		"/NetworkAdapters",
		"/EthernetInterfaces",
		"/NetworkInterfaces",
		"/Sensors",
		"/Fans",
		"/Temperatures",
		"/Voltages",
	} {
		if strings.HasSuffix(p, suffix) {
			return true
		}
	}
	return false
}

func mergeRedfishPaths(groups ...[]string) []string {
	seen := make(map[string]struct{})
	out := make([]string, 0)
	for _, group := range groups {
		for _, path := range group {
			path = normalizeRedfishPath(path)
			if path == "" {
				continue
			}
			if _, ok := seen[path]; ok {
				continue
			}
			seen[path] = struct{}{}
			out = append(out, path)
		}
	}
	return out
}

func redfishFetchErrorListToMap(list []map[string]interface{}) map[string]string {
	out := make(map[string]string, len(list))
	for _, item := range list {
		p := normalizeRedfishPath(asString(item["path"]))
		if p == "" {
			continue
		}
		out[p] = asString(item["error"])
	}
	return out
}

func redfishFetchErrorMapToList(m map[string]string) []map[string]interface{} {
	if len(m) == 0 {
		return nil
	}
	out := make([]map[string]interface{}, 0, len(m))
	for p, msg := range m {
		out = append(out, map[string]interface{}{"path": p, "error": msg})
	}
	sort.Slice(out, func(i, j int) bool {
		return asString(out[i]["path"]) < asString(out[j]["path"])
	})
	return out
}

func isRetryableRedfishFetchError(err error) bool {
	if err == nil {
		return false
	}
	msg := strings.ToLower(err.Error())
	if strings.Contains(msg, "timeout") || strings.Contains(msg, "deadline exceeded") || strings.Contains(msg, "connection reset") || strings.Contains(msg, "unexpected eof") {
		return true
	}
	if strings.HasPrefix(msg, "status 500 ") || strings.HasPrefix(msg, "status 502 ") || strings.HasPrefix(msg, "status 503 ") || strings.HasPrefix(msg, "status 504 ") {
		return true
	}
	return false
}

func redfishSnapshotRequestTimeout() time.Duration {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_SNAPSHOT_TIMEOUT")); v != "" {
		if d, err := time.ParseDuration(v); err == nil && d > 0 {
			return d
		}
	}
	return 12 * time.Second
}

func redfishSnapshotPostProbeRequestTimeout() time.Duration {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_POSTPROBE_TIMEOUT")); v != "" {
		if d, err := time.ParseDuration(v); err == nil && d > 0 {
			return d
		}
	}
	// Post-probe probes non-existent numeric paths expecting fast 404s.
	// A short timeout prevents BMCs that hang on unknown paths from stalling
	// the entire collection for minutes (e.g. HPE iLO on NetworkAdapters Ports).
	return 4 * time.Second
}

func redfishSnapshotWorkers(tuning redfishprofile.AcquisitionTuning) int {
	if tuning.SnapshotWorkers >= 1 && tuning.SnapshotWorkers <= 16 {
		return tuning.SnapshotWorkers
	}
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_SNAPSHOT_WORKERS")); v != "" {
		if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 16 {
			return n
		}
	}
	return 4
}

func redfishPrefetchEnabled(tuning redfishprofile.AcquisitionTuning) bool {
	if tuning.PrefetchEnabled != nil {
		return *tuning.PrefetchEnabled
	}
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_PREFETCH_ENABLED")); v != "" {
		switch strings.ToLower(v) {
		case "0", "false", "off", "no":
			return false
		default:
			return true
		}
	}
	return true
}

func redfishPrefetchRequestTimeout() time.Duration {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_PREFETCH_TIMEOUT")); v != "" {
		if d, err := time.ParseDuration(v); err == nil && d > 0 {
			return d
		}
	}
	return 20 * time.Second
}

func redfishPrefetchRetryAttempts() int {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_PREFETCH_RETRIES")); v != "" {
		if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 8 {
			return n
		}
	}
	return 2
}

func redfishPrefetchMemberRetryAttempts() int {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_PREFETCH_MEMBER_RETRIES")); v != "" {
		if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 6 {
			return n
		}
	}
	return 1
}

func redfishPrefetchMemberRecoveryMax() int {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_PREFETCH_MEMBER_RECOVERY_MAX")); v != "" {
		if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 512 {
			return n
		}
	}
	return 48
}

func redfishPrefetchRetryBackoff() time.Duration {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_PREFETCH_BACKOFF")); v != "" {
		if d, err := time.ParseDuration(v); err == nil && d >= 0 {
			return d
		}
	}
	return 900 * time.Millisecond
}

func redfishPrefetchWorkers(tuning redfishprofile.AcquisitionTuning) int {
	if tuning.PrefetchWorkers >= 1 && tuning.PrefetchWorkers <= 8 {
		return tuning.PrefetchWorkers
	}
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_PREFETCH_WORKERS")); v != "" {
		if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 8 {
			return n
		}
	}
	return 2
}

func redfishSnapshotSensorPostProbeEnabled() bool {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_SENSOR_POSTPROBE")); v != "" {
		switch strings.ToLower(v) {
		case "1", "true", "on", "yes":
			return true
		default:
			return false
		}
	}
	return false
}

func redfishNVMePostProbeEnabled(tuning redfishprofile.AcquisitionTuning) bool {
	if tuning.NVMePostProbeEnabled != nil {
		return *tuning.NVMePostProbeEnabled
	}
	return true
}

func redfishPrefetchTargets(criticalPaths []string, tuning redfishprofile.AcquisitionTuning) []string {
	if len(criticalPaths) == 0 {
		return nil
	}
	out := make([]string, 0, len(criticalPaths))
	seen := make(map[string]struct{}, len(criticalPaths))
	for _, p := range criticalPaths {
		p = normalizeRedfishPath(p)
		if p == "" || !shouldPrefetchCriticalPath(p, tuning) {
			continue
		}
		if _, ok := seen[p]; ok {
			continue
		}
		seen[p] = struct{}{}
		out = append(out, p)
	}
	return out
}

func shouldPrefetchCriticalPath(p string, tuning redfishprofile.AcquisitionTuning) bool {
	p = normalizeRedfishPath(p)
	if p == "" {
		return false
	}
	for _, noisy := range tuning.PrefetchPolicy.ExcludeContains {
		if strings.Contains(p, noisy) {
			return false
		}
	}
	for _, suffix := range tuning.PrefetchPolicy.IncludeSuffixes {
		if strings.HasSuffix(p, suffix) {
			return true
		}
	}
	parts := strings.Split(strings.Trim(p, "/"), "/")
	return len(parts) == 4 && parts[0] == "redfish" && parts[1] == "v1" &&
		(parts[2] == "Systems" || parts[2] == "Chassis" || parts[2] == "Managers")
}

func redfishCriticalRequestTimeout() time.Duration {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_CRITICAL_TIMEOUT")); v != "" {
		if d, err := time.ParseDuration(v); err == nil && d > 0 {
			return d
		}
	}
	return 45 * time.Second
}

func redfishCriticalRetryAttempts() int {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_CRITICAL_RETRIES")); v != "" {
		if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 10 {
			return n
		}
	}
	return 3
}

func redfishCriticalPlanBAttempts() int {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_CRITICAL_PLANB_RETRIES")); v != "" {
		if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 10 {
			return n
		}
	}
	return 3
}

func redfishCriticalMemberRetryAttempts() int {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_CRITICAL_MEMBER_RETRIES")); v != "" {
		if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 6 {
			return n
		}
	}
	return 1
}

func redfishCriticalMemberRecoveryMax() int {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_CRITICAL_MEMBER_RECOVERY_MAX")); v != "" {
		if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 1024 {
			return n
		}
	}
	return 48
}

func redfishCriticalRetryBackoff() time.Duration {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_CRITICAL_BACKOFF")); v != "" {
		if d, err := time.ParseDuration(v); err == nil && d >= 0 {
			return d
		}
	}
	return 1500 * time.Millisecond
}

func redfishCriticalCooldown() time.Duration {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_CRITICAL_COOLDOWN")); v != "" {
		if d, err := time.ParseDuration(v); err == nil && d >= 0 {
			return d
		}
	}
	return 4 * time.Second
}

func redfishCriticalSlowGap() time.Duration {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_CRITICAL_SLOW_GAP")); v != "" {
		if d, err := time.ParseDuration(v); err == nil && d >= 0 {
			return d
		}
	}
	return 1200 * time.Millisecond
}

func redfishSnapshotMemoryRequestTimeout() time.Duration {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_MEMORY_TIMEOUT")); v != "" {
		if d, err := time.ParseDuration(v); err == nil && d > 0 {
			return d
		}
	}
	return 25 * time.Second
}

func redfishSnapshotMemoryRetryAttempts() int {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_MEMORY_RETRIES")); v != "" {
		if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 8 {
			return n
		}
	}
	return 2
}

func redfishSnapshotMemoryRetryBackoff() time.Duration {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_MEMORY_BACKOFF")); v != "" {
		if d, err := time.ParseDuration(v); err == nil && d >= 0 {
			return d
		}
	}
	return 800 * time.Millisecond
}

func redfishSnapshotMemoryConcurrency() int {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_MEMORY_WORKERS")); v != "" {
		if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 8 {
			return n
		}
	}
	return 1
}

func redfishSnapshotBranchConcurrency() int {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_BRANCH_WORKERS")); v != "" {
		if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 8 {
			return n
		}
	}
	return 1
}

func redfishSnapshotBranchRequeueBackoff() time.Duration {
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_BRANCH_BACKOFF")); v != "" {
		if d, err := time.ParseDuration(v); err == nil && d >= 0 {
			return d
		}
	}
	return 35 * time.Millisecond
}

type redfishSnapshotBranchLimiter struct {
	limit    int
	mu       sync.Mutex
	inFlight map[string]int
}

func newRedfishSnapshotBranchLimiter(limit int) *redfishSnapshotBranchLimiter {
	if limit < 1 {
		limit = 1
	}
	return &redfishSnapshotBranchLimiter{
		limit:    limit,
		inFlight: make(map[string]int),
	}
}

func (l *redfishSnapshotBranchLimiter) tryAcquire(path string) bool {
	branch := redfishSnapshotBranchKey(path)
	if branch == "" {
		return true
	}
	l.mu.Lock()
	defer l.mu.Unlock()
	if l.inFlight[branch] >= l.limit {
		return false
	}
	l.inFlight[branch]++
	return true
}

func (l *redfishSnapshotBranchLimiter) waitAcquire(ctx context.Context, path string, backoff time.Duration) bool {
	branch := redfishSnapshotBranchKey(path)
	if branch == "" {
		return true
	}
	if backoff < 0 {
		backoff = 0
	}
	for {
		if l.tryAcquire(path) {
			return true
		}
		if ctx.Err() != nil {
			return false
		}
		select {
		case <-time.After(backoff):
		case <-ctx.Done():
			return false
		}
	}
}

func (l *redfishSnapshotBranchLimiter) release(path string) {
	branch := redfishSnapshotBranchKey(path)
	if branch == "" {
		return
	}
	l.mu.Lock()
	defer l.mu.Unlock()
	switch n := l.inFlight[branch]; {
	case n <= 1:
		delete(l.inFlight, branch)
	default:
		l.inFlight[branch] = n - 1
	}
}

func redfishLinkRefs(doc map[string]interface{}, topKey, nestedKey string) []string {
	top, ok := doc[topKey].(map[string]interface{})
	if !ok {
		return nil
	}
	items, ok := top[nestedKey].([]interface{})
	if !ok {
		return nil
	}
	out := make([]string, 0, len(items))
	for _, itemAny := range items {
		item, ok := itemAny.(map[string]interface{})
		if !ok {
			continue
		}
		if p := asString(item["@odata.id"]); p != "" {
			out = append(out, p)
		}
	}
	return out
}

func pcieDeviceDedupKey(dev models.PCIeDevice) string {
	if bdf := strings.TrimSpace(dev.BDF); looksLikeCanonicalBDF(bdf) {
		return strings.ToLower(bdf)
	}
	if s := strings.TrimSpace(dev.SerialNumber); s != "" {
		return s
	}
	return firstNonEmpty(
		strings.TrimSpace(dev.Slot)+"|"+strings.TrimSpace(dev.PartNumber)+"|"+strings.TrimSpace(dev.DeviceClass),
		strings.TrimSpace(dev.Slot)+"|"+strings.TrimSpace(dev.DeviceClass),
		strings.TrimSpace(dev.PartNumber)+"|"+strings.TrimSpace(dev.DeviceClass),
		strings.TrimSpace(dev.Description)+"|"+strings.TrimSpace(dev.DeviceClass),
	)
}

func looksLikeCanonicalBDF(bdf string) bool {
	bdf = strings.TrimSpace(strings.ToLower(bdf))
	if bdf == "" {
		return false
	}
	// Accept common forms: 0000:65:00.0 or 65:00.0
	if strings.Count(bdf, ":") == 2 && strings.Contains(bdf, ".") {
		return true
	}
	if strings.Count(bdf, ":") == 1 && strings.Contains(bdf, ".") {
		return true
	}
	return false
}

func sanitizeRedfishBDF(bdf string) string {
	bdf = strings.TrimSpace(bdf)
	if looksLikeCanonicalBDF(bdf) {
		return bdf
	}
	return ""
}

func shouldCrawlPath(path string) bool {
	if path == "" {
		return false
	}
	normalized := normalizeRedfishPath(path)
	if isAllowedNVSwitchFabricPath(normalized) {
		return true
	}
	if strings.Contains(normalized, "/Chassis/") &&
		strings.Contains(normalized, "/PCIeDevices/") &&
		strings.HasSuffix(normalized, "/PCIeFunctions") {
		// Avoid crawling entire chassis PCIeFunctions collections. Concrete member
		// docs can still be reached through direct links such as
		// NetworkDeviceFunction Links.PCIeFunction.
		return false
	}
	if strings.Contains(normalized, "/Memory/") {
		after := strings.SplitN(normalized, "/Memory/", 2)
		if len(after) == 2 && strings.Count(after[1], "/") >= 1 {
			// Keep direct DIMM resources and selected metrics subresources, but skip
			// unrelated nested branches like Assembly.
			return strings.HasSuffix(normalized, "/MemoryMetrics")
		}
	}
	if strings.Contains(normalized, "/Processors/") {
		after := strings.SplitN(normalized, "/Processors/", 2)
		if len(after) == 2 && strings.Count(after[1], "/") >= 1 {
			return strings.HasSuffix(normalized, "/ProcessorMetrics")
		}
	}
	// Non-inventory top-level service branches.
	for _, prefix := range []string{
		"/redfish/v1/AccountService",
		"/redfish/v1/CertificateService",
		"/redfish/v1/EventService",
		"/redfish/v1/Registries",
		"/redfish/v1/SessionService",
		"/redfish/v1/TaskService",
	} {
		if strings.HasPrefix(normalized, prefix) {
			return false
		}
	}
	// Manager-specific configuration paths (not hardware inventory).
	if strings.Contains(normalized, "/Managers/") {
		for _, part := range []string{
			"/FirewallRules",
			"/KvmService",
			"/LldpService",
			"/SecurityService",
			"/SmtpService",
			"/SnmpService",
			"/SyslogService",
			"/VirtualMedia",
			"/VncService",
			"/Certificates",
		} {
			if strings.Contains(normalized, part) {
				return false
			}
		}
	}
	// Per-CPU operating frequency configurations — not hardware inventory.
	if strings.HasSuffix(normalized, "/OperatingConfigs") {
		return false
	}
	// Per-core/thread sub-processors — inventory is captured at the top processor level.
	if strings.Contains(normalized, "/SubProcessors") {
		return false
	}
	// Non-inventory system endpoints.
	for _, part := range []string{
		"/BootOptions",
		"/HostPostCode",
		"/Bios/Settings",
		"/GetServerAllUSBStatus",
		"/Oem/Public/KVM",
		"/SecureBoot/SecureBootDatabases",
		// HPE iLO WorkloadPerformanceAdvisor — operational/advisory data, not inventory.
		"/WorkloadPerformanceAdvisor",
	} {
		if strings.Contains(normalized, part) {
			return false
		}
	}
	heavyParts := []string{
		"/JsonSchemas",
		"/LogServices/",
		"/Entries/",
		"/TelemetryService/",
		"/MetricReports/",
		"/SessionService/Sessions",
		"/TaskService/Tasks",
	}
	for _, part := range heavyParts {
		if strings.Contains(path, part) {
			return false
		}
	}
	return true
}

func isAllowedNVSwitchFabricPath(path string) bool {
	if !strings.HasPrefix(path, "/redfish/v1/Fabrics/") {
		return false
	}
	if !strings.Contains(path, "/Switches/NVSwitch_") {
		return false
	}
	if strings.HasSuffix(path, "/Switches") || strings.Contains(path, "/Ports/NVLink_") || strings.HasSuffix(path, "/Ports") {
		return true
	}
	if strings.Contains(path, "/Switches/NVSwitch_") && !strings.Contains(path, "/Ports/") {
		return true
	}
	return false
}

func isRedfishMemoryMemberPath(path string) bool {
	normalized := normalizeRedfishPath(path)
	if !strings.Contains(normalized, "/Systems/") {
		return false
	}
	if !strings.Contains(normalized, "/Memory/") {
		return false
	}
	if strings.Contains(normalized, "/MemoryMetrics") || strings.Contains(normalized, "/Assembly") {
		return false
	}
	after := strings.SplitN(normalized, "/Memory/", 2)
	if len(after) != 2 {
		return false
	}
	suffix := strings.TrimSpace(after[1])
	if suffix == "" || strings.Contains(suffix, "/") {
		return false
	}
	return true
}

func redfishCollectionHasExplicitMembers(doc map[string]interface{}) bool {
	return len(redfishCollectionMemberRefs(doc)) > 0
}

func redfishSnapshotBranchKey(path string) string {
	normalized := normalizeRedfishPath(path)
	if normalized == "" || normalized == "/redfish/v1" {
		return ""
	}
	parts := strings.Split(strings.Trim(normalized, "/"), "/")
	if len(parts) < 3 {
		return normalized
	}
	if parts[0] != "redfish" || parts[1] != "v1" {
		return normalized
	}
	// Keep subsystem branches independent, e.g. Systems/1/Memory vs Systems/1/PCIeDevices.
	if len(parts) >= 5 && (parts[2] == "Systems" || parts[2] == "Chassis" || parts[2] == "Managers") {
		return "/" + strings.Join(parts[:5], "/")
	}
	if len(parts) >= 4 {
		return "/" + strings.Join(parts[:4], "/")
	}
	return "/" + strings.Join(parts[:3], "/")
}

func (c *RedfishConnector) getLinkedPCIeFunctions(ctx context.Context, client *http.Client, req Request, baseURL string, doc map[string]interface{}) []map[string]interface{} {
	// Newer Redfish payloads often keep function references in Links.PCIeFunctions.
	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 := c.getJSON(ctx, client, req, baseURL, memberPath)
				if err != nil {
					continue
				}
				out = append(out, memberDoc)
			}
			return out
		}
		if ref, ok := links["PCIeFunction"].(map[string]interface{}); ok {
			memberPath := asString(ref["@odata.id"])
			if memberPath != "" {
				memberDoc, err := c.getJSON(ctx, client, req, baseURL, memberPath)
				if err == nil {
					return []map[string]interface{}{memberDoc}
				}
			}
		}
	}

	// Some implementations expose a collection object in PCIeFunctions.@odata.id.
	if pcieFunctions, ok := doc["PCIeFunctions"].(map[string]interface{}); ok {
		if collectionPath := asString(pcieFunctions["@odata.id"]); collectionPath != "" {
			memberDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, collectionPath)
			if err == nil {
				return memberDocs
			}
		}
	}

	return nil
}

func (c *RedfishConnector) getNetworkAdapterFunctionDocs(ctx context.Context, client *http.Client, req Request, baseURL string, adapterDoc map[string]interface{}) []map[string]interface{} {
	ndfCol, ok := adapterDoc["NetworkDeviceFunctions"].(map[string]interface{})
	if !ok {
		return nil
	}
	colPath := asString(ndfCol["@odata.id"])
	if colPath == "" {
		return nil
	}
	funcDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, colPath)
	if err != nil {
		return nil
	}
	return funcDocs
}

func (c *RedfishConnector) getCollectionMembers(ctx context.Context, client *http.Client, req Request, baseURL, collectionPath string) ([]map[string]interface{}, error) {
	collection, err := c.getJSON(ctx, client, req, baseURL, collectionPath)
	if err != nil {
		return nil, err
	}

	memberPaths := redfishCollectionMemberRefs(collection)
	if len(memberPaths) == 0 {
		return []map[string]interface{}{}, nil
	}

	out := make([]map[string]interface{}, 0, len(memberPaths))
	for _, memberPath := range memberPaths {
		memberDoc, err := c.getJSON(ctx, client, req, baseURL, memberPath)
		if err != nil {
			continue
		}
		if strings.TrimSpace(asString(memberDoc["@odata.id"])) == "" {
			memberDoc["@odata.id"] = normalizeRedfishPath(memberPath)
		}
		out = append(out, memberDoc)
	}
	return out, nil
}

func (c *RedfishConnector) getJSON(ctx context.Context, client *http.Client, req Request, baseURL, requestPath string) (map[string]interface{}, error) {
	start := time.Now()
	rel := requestPath
	if rel == "" {
		rel = "/"
	}
	if !strings.HasPrefix(rel, "/") {
		rel = "/" + rel
	}

	u, err := url.Parse(baseURL)
	if err != nil {
		return nil, err
	}
	u.Path = path.Join(strings.TrimSuffix(u.Path, "/"), rel)

	httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
	if err != nil {
		return nil, err
	}
	httpReq.Header.Set("Accept", "application/json")

	switch req.AuthType {
	case "password":
		httpReq.SetBasicAuth(req.Username, req.Password)
	case "token":
		httpReq.Header.Set("X-Auth-Token", req.Token)
		httpReq.Header.Set("Authorization", "Bearer "+req.Token)
	}

	resp, err := client.Do(httpReq)
	if err != nil {
		recordRedfishTelemetry(ctx, time.Since(start), true)
		c.debugf("http get path=%s error=%v dur=%s", requestPath, err, time.Since(start).Round(time.Millisecond))
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
		err := fmt.Errorf("status %d from %s: %s", resp.StatusCode, requestPath, strings.TrimSpace(string(body)))
		recordRedfishTelemetry(ctx, time.Since(start), true)
		c.debugf("http get path=%s status=%d dur=%s", requestPath, resp.StatusCode, time.Since(start).Round(time.Millisecond))
		return nil, err
	}

	var doc map[string]interface{}
	dec := json.NewDecoder(resp.Body)
	dec.UseNumber()
	if err := dec.Decode(&doc); err != nil {
		recordRedfishTelemetry(ctx, time.Since(start), true)
		c.debugf("http get path=%s decode_error=%v dur=%s", requestPath, err, time.Since(start).Round(time.Millisecond))
		return nil, err
	}
	recordRedfishTelemetry(ctx, time.Since(start), false)
	c.debugf("http get path=%s status=%d dur=%s", requestPath, resp.StatusCode, time.Since(start).Round(time.Millisecond))

	return doc, nil
}

func (c *RedfishConnector) getJSONWithRetry(ctx context.Context, client *http.Client, req Request, baseURL, requestPath string, attempts int, backoff time.Duration) (map[string]interface{}, error) {
	if attempts < 1 {
		attempts = 1
	}
	var lastErr error
	for i := 0; i < attempts; i++ {
		doc, err := c.getJSON(ctx, client, req, baseURL, requestPath)
		if err == nil {
			return doc, nil
		}
		lastErr = err
		if i == attempts-1 || !isRetryableRedfishFetchError(err) {
			break
		}
		if backoff > 0 {
			select {
			case <-time.After(backoff * time.Duration(i+1)):
			case <-ctx.Done():
				return nil, ctx.Err()
			}
		}
	}
	return nil, lastErr
}

func (c *RedfishConnector) collectCriticalCollectionMembersSequential(
	ctx context.Context,
	client *http.Client,
	req Request,
	baseURL string,
	collectionDoc map[string]interface{},
	rawTree map[string]interface{},
	fetchErrs map[string]string,
) (map[string]interface{}, bool) {
	memberPaths := redfishCollectionMemberRefs(collectionDoc)
	if len(memberPaths) == 0 {
		return nil, false
	}

	retryableMissing := make([]string, 0, len(memberPaths))
	unknownMissing := make([]string, 0, len(memberPaths))
	for _, memberPath := range memberPaths {
		memberPath = normalizeRedfishPath(memberPath)
		if memberPath == "" {
			continue
		}
		if _, exists := rawTree[memberPath]; exists {
			continue
		}
		if msg, hasErr := fetchErrs[memberPath]; hasErr {
			if !isRetryableRedfishFetchError(fmt.Errorf("%s", msg)) {
				continue
			}
			retryableMissing = append(retryableMissing, memberPath)
			continue
		}
		unknownMissing = append(unknownMissing, memberPath)
	}

	candidates := append(retryableMissing, unknownMissing...)
	if len(candidates) == 0 {
		return nil, false
	}
	if maxMembers := redfishCriticalMemberRecoveryMax(); maxMembers > 0 && len(candidates) > maxMembers {
		candidates = candidates[:maxMembers]
	}

	out := make(map[string]interface{}, len(candidates))
	for _, memberPath := range candidates {
		doc, err := c.getJSONWithRetry(ctx, client, req, baseURL, memberPath, redfishCriticalMemberRetryAttempts(), redfishCriticalRetryBackoff())
		if err != nil {
			continue
		}
		out[memberPath] = doc
	}
	return out, true
}

func (c *RedfishConnector) recoverCriticalRedfishDocsPlanB(ctx context.Context, client *http.Client, req Request, baseURL string, criticalPaths []string, rawTree map[string]interface{}, fetchErrs map[string]string, tuning redfishprofile.AcquisitionTuning, emit ProgressFn) int {
	planBStart := time.Now()
	timings := newRedfishPathTimingCollector(4)
	var targets []string
	seenTargets := make(map[string]struct{})
	skippedDiagnosticTargets := 0
	addTarget := func(path string) {
		path = normalizeRedfishPath(path)
		if path == "" {
			return
		}
		if !shouldIncludeCriticalPlanBPath(req, path) {
			skippedDiagnosticTargets++
			return
		}
		if _, ok := seenTargets[path]; ok {
			return
		}
		seenTargets[path] = struct{}{}
		targets = append(targets, path)
	}

	for _, p := range criticalPaths {
		p = normalizeRedfishPath(p)
		if p == "" {
			continue
		}
		if _, ok := rawTree[p]; ok {
			continue
		}
		errMsg, hasErr := fetchErrs[p]
		if hasErr && !isRetryableRedfishFetchError(fmt.Errorf("%s", errMsg)) {
			continue
		}
		addTarget(p)
	}

	// If a critical collection document was fetched, but some of its members
	// failed during the initial crawl (common for /Drives on partially loaded BMCs),
	// retry those member resources in plan-B too.
	for _, p := range criticalPaths {
		p = normalizeRedfishPath(p)
		if p == "" {
			continue
		}
		docAny, ok := rawTree[p]
		if !ok {
			continue
		}
		doc, ok := docAny.(map[string]interface{})
		if !ok {
			continue
		}
		for _, memberPath := range redfishCollectionMemberRefs(doc) {
			if _, exists := rawTree[memberPath]; exists {
				continue
			}
			errMsg, hasErr := fetchErrs[memberPath]
			if hasErr && !isRetryableRedfishFetchError(fmt.Errorf("%s", errMsg)) {
				continue
			}
			addTarget(memberPath)
		}
	}

	// Re-probe critical hardware collections that were successfully fetched but
	// returned no members. This happens when the BMC hasn't finished enumerating
	// hardware at collection time (e.g. PCIeDevices or NetworkAdapters empty right
	// after power-on). Only hardware inventory collection suffixes are retried.
	if tuning.RecoveryPolicy.EnableEmptyCriticalCollectionRetry {
		for _, p := range criticalPaths {
			p = normalizeRedfishPath(p)
			if p == "" {
				continue
			}
			if _, queued := seenTargets[p]; queued {
				continue
			}
			docAny, ok := rawTree[p]
			if !ok {
				continue
			}
			doc, ok := docAny.(map[string]interface{})
			if !ok {
				continue
			}
			if redfishCollectionHasExplicitMembers(doc) {
				continue
			}
			if !isHardwareInventoryCollectionPath(p) {
				continue
			}
			addTarget(p)
		}
	}

	if len(targets) == 0 {
		return 0
	}
	if emit != nil {
		if skippedDiagnosticTargets > 0 {
			emit(Progress{
				Status:   "running",
				Progress: 97,
				Message:  fmt.Sprintf("Redfish: расширенная диагностика выключена, пропущено %d тяжелых diagnostic endpoint", skippedDiagnosticTargets),
			})
		}
		totalETA := redfishCriticalCooldown() + estimatePlanBETA(len(targets))
		emit(Progress{
			Status:       "running",
			Progress:     97,
			Message:      fmt.Sprintf("Redfish: cooldown перед повторным добором критичных endpoint... ETA≈%s", formatETA(totalETA)),
			CurrentPhase: "critical_plan_b",
			ETASeconds:   int(totalETA.Seconds()),
		})
	}
	select {
	case <-time.After(redfishCriticalCooldown()):
	case <-ctx.Done():
		return 0
	}

	recovered := 0
	for i, p := range targets {
		if emit != nil {
			remaining := len(targets) - i
			emit(Progress{
				Status:       "running",
				Progress:     97,
				Message:      fmt.Sprintf("Redfish: plan-B (%d/%d, ETA≈%s) %s", i+1, len(targets), formatETA(estimatePlanBETA(remaining)), compactProgressPath(p)),
				CurrentPhase: "critical_plan_b",
				ETASeconds:   int(estimatePlanBETA(remaining).Seconds()),
			})
		}
		if i > 0 {
			select {
			case <-time.After(redfishCriticalSlowGap()):
			case <-ctx.Done():
				return recovered
			}
		}
		reqStart := time.Now()
		doc, err := c.getJSONWithRetry(ctx, client, req, baseURL, p, redfishCriticalPlanBAttempts(), redfishCriticalRetryBackoff())
		timings.Observe(p, time.Since(reqStart), err != nil)
		if err == nil {
			rawTree[p] = doc
			delete(fetchErrs, p)
			recovered++

			if tuning.RecoveryPolicy.EnableCriticalCollectionMemberRetry {
				if members, ok := c.collectCriticalCollectionMembersSequential(ctx, client, req, baseURL, doc, rawTree, fetchErrs); ok {
					for mp, md := range members {
						if _, exists := rawTree[mp]; exists {
							continue
						}
						rawTree[mp] = md
						delete(fetchErrs, mp)
						recovered++
					}
				}
			}

			// Numeric slow-probe is expensive; skip it when collection already advertises explicit members.
			if shouldSlowProbeCriticalCollection(p, tuning) && !redfishCollectionHasExplicitMembers(doc) {
				if children := c.probeDirectRedfishCollectionChildrenSlow(ctx, client, req, baseURL, p); len(children) > 0 {
					for cp, cd := range children {
						if _, exists := rawTree[cp]; exists {
							continue
						}
						rawTree[cp] = cd
						recovered++
					}
				}
			}
			continue
		}

		fetchErrs[p] = err.Error()
		// If collection endpoint times out, still try direct child probing for common numeric paths.
		if shouldSlowProbeCriticalCollection(p, tuning) {
			if children := c.probeDirectRedfishCollectionChildrenSlow(ctx, client, req, baseURL, p); len(children) > 0 {
				for cp, cd := range children {
					if _, exists := rawTree[cp]; exists {
						continue
					}
					rawTree[cp] = cd
					recovered++
				}
				delete(fetchErrs, p)
			}
		}
	}
	if emit != nil {
		if summary := timings.Summary(3); summary != "" {
			emit(Progress{
				Status:   "running",
				Progress: 97,
				Message:  fmt.Sprintf("Redfish: plan-B топ веток по времени: %s", summary),
			})
		}
		emit(Progress{
			Status:   "running",
			Progress: 97,
			Message:  fmt.Sprintf("Redfish: plan-B завершен за %s (targets=%d, recovered=%d)", time.Since(planBStart).Round(time.Second), len(targets), recovered),
		})
	}
	if summary := timings.Summary(12); summary != "" {
		log.Printf("redfish-planb-timing: %s", summary)
	}
	return recovered
}

func shouldIncludeCriticalPlanBPath(req Request, path string) bool {
	if req.DebugPayloads {
		return true
	}
	return !isExtendedDiagnosticCriticalPlanBPath(path)
}

func isExtendedDiagnosticCriticalPlanBPath(path string) bool {
	path = normalizeRedfishPath(path)
	if path == "" {
		return false
	}
	parts := strings.Split(strings.Trim(path, "/"), "/")
	if len(parts) < 5 || parts[0] != "redfish" || parts[1] != "v1" || parts[2] != "Chassis" {
		return false
	}
	if !strings.HasPrefix(parts[3], "HGX_") {
		return false
	}
	for _, suffix := range []string{
		"/Accelerators",
		"/Assembly",
		"/Drives",
		"/NetworkAdapters",
		"/PCIeDevices",
	} {
		if strings.HasSuffix(path, suffix) {
			return true
		}
	}
	return false
}

func (c *RedfishConnector) recoverProfilePlanBDocs(ctx context.Context, client *http.Client, req Request, baseURL string, plan redfishprofile.AcquisitionPlan, rawTree map[string]interface{}, emit ProgressFn) int {
	if len(plan.PlanBPaths) == 0 || plan.Mode == redfishprofile.ModeFallback || !plan.Tuning.RecoveryPolicy.EnableProfilePlanB {
		return 0
	}
	planBStart := time.Now()
	targets := make([]string, 0, len(plan.PlanBPaths))
	for _, p := range plan.PlanBPaths {
		p = normalizeRedfishPath(p)
		if p == "" {
			continue
		}
		if _, exists := rawTree[p]; exists {
			continue
		}
		targets = append(targets, p)
	}
	if len(targets) == 0 {
		return 0
	}
	if emit != nil {
		emit(Progress{
			Status:       "running",
			Progress:     98,
			Message:      fmt.Sprintf("Redfish: profile plan-B добирает %d endpoint...", len(targets)),
			CurrentPhase: "profile_plan_b",
			ETASeconds:   int(estimateProgressETA(planBStart, 0, len(targets), 2*time.Second).Seconds()),
		})
	}
	recovered := 0
	for i, p := range targets {
		if emit != nil {
			emit(Progress{
				Status:       "running",
				Progress:     98,
				Message:      fmt.Sprintf("Redfish: profile plan-B (%d/%d) %s", i+1, len(targets), compactProgressPath(p)),
				CurrentPhase: "profile_plan_b",
				ETASeconds:   int(estimateProgressETA(planBStart, i, len(targets), 2*time.Second).Seconds()),
			})
		}
		doc, err := c.getJSONWithRetry(ctx, client, req, baseURL, p, redfishCriticalPlanBAttempts(), redfishCriticalRetryBackoff())
		if err != nil {
			continue
		}
		rawTree[p] = doc
		recovered++
	}
	if recovered > 0 {
		log.Printf(
			"redfish-profile-planb: mode=%s profiles=%s targets=%d recovered=%d",
			plan.Mode,
			strings.Join(plan.Profiles, ","),
			len(targets),
			recovered,
		)
	}
	if emit != nil {
		emit(Progress{
			Status:       "running",
			Progress:     98,
			Message:      fmt.Sprintf("Redfish: profile plan-B завершен за %s (targets=%d, recovered=%d)", time.Since(planBStart).Round(time.Second), len(targets), recovered),
			CurrentPhase: "profile_plan_b",
		})
	}
	return recovered
}

func parseBoardInfo(system map[string]interface{}) models.BoardInfo {
	return models.BoardInfo{
		Manufacturer: normalizeRedfishIdentityField(asString(system["Manufacturer"])),
		ProductName: normalizeRedfishIdentityField(firstNonEmpty(
			asString(system["Model"]),
			asString(system["ProductName"]),
			asString(system["Name"]),
		)),
		SerialNumber: normalizeRedfishIdentityField(asString(system["SerialNumber"])),
		PartNumber: normalizeRedfishIdentityField(firstNonEmpty(
			asString(system["PartNumber"]),
			asString(system["SKU"]),
		)),
		UUID: normalizeRedfishIdentityField(asString(system["UUID"])),
	}
}

func parseBoardInfoWithFallback(system, chassis, fru map[string]interface{}) models.BoardInfo {
	board := parseBoardInfo(system)
	chassisBoard := parseBoardInfo(chassis)
	fruBoard := parseBoardInfoFromFRUDoc(fru)

	if board.Manufacturer == "" {
		board.Manufacturer = firstNonEmpty(chassisBoard.Manufacturer, fruBoard.Manufacturer)
	}
	if board.ProductName == "" {
		board.ProductName = firstNonEmpty(chassisBoard.ProductName, fruBoard.ProductName)
	}
	if board.SerialNumber == "" {
		board.SerialNumber = firstNonEmpty(chassisBoard.SerialNumber, fruBoard.SerialNumber)
	}
	if board.PartNumber == "" {
		board.PartNumber = firstNonEmpty(chassisBoard.PartNumber, fruBoard.PartNumber)
	}
	if board.UUID == "" {
		board.UUID = chassisBoard.UUID
	}
	return board
}

func parseBoardInfoFromFRUDoc(doc map[string]interface{}) models.BoardInfo {
	if len(doc) == 0 {
		return models.BoardInfo{}
	}
	return models.BoardInfo{
		Manufacturer: findFirstNormalizedStringByKeys(doc, "Manufacturer", "BoardManufacturer", "Vendor"),
		ProductName:  findFirstNormalizedStringByKeys(doc, "ProductName", "BoardName", "PlatformId", "PlatformName", "MachineTypeModel", "Model"),
		SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber", "BoardSerialNumber"),
		PartNumber:   findFirstNormalizedStringByKeys(doc, "PartNumber", "BoardPartNumber", "ProductPartNumber"),
	}
}

func findFirstNormalizedStringByKeys(doc map[string]interface{}, keys ...string) string {
	if len(doc) == 0 || len(keys) == 0 {
		return ""
	}
	keySet := make(map[string]struct{}, len(keys))
	for _, key := range keys {
		k := strings.ToLower(strings.TrimSpace(key))
		if k != "" {
			keySet[k] = struct{}{}
		}
	}

	stack := []any{doc}
	for len(stack) > 0 {
		last := len(stack) - 1
		node := stack[last]
		stack = stack[:last]

		switch v := node.(type) {
		case map[string]interface{}:
			for k, raw := range v {
				if _, ok := keySet[strings.ToLower(strings.TrimSpace(k))]; ok {
					if s, ok := raw.(string); ok {
						if normalized := normalizeRedfishIdentityField(s); normalized != "" {
							return normalized
						}
					}
				}
				switch nested := raw.(type) {
				case map[string]interface{}, []interface{}:
					stack = append(stack, nested)
				}
			}
		case []interface{}:
			for _, item := range v {
				switch nested := item.(type) {
				case map[string]interface{}, []interface{}:
					stack = append(stack, nested)
				}
			}
		}
	}
	return ""
}

func parseCPUs(docs []map[string]interface{}) []models.CPU {
	cpus := make([]models.CPU, 0, len(docs))
	socketIdx := 0
	for _, doc := range docs {
		// Skip non-CPU processors (GPUs, FPGAs, etc.) that some BMCs list in the
		// same Processors collection.
		if pt := strings.TrimSpace(asString(doc["ProcessorType"])); pt != "" &&
			!strings.EqualFold(pt, "CPU") && !strings.EqualFold(pt, "General") {
			continue
		}
		socket := socketIdx
		socketIdx++
		if s := strings.TrimSpace(asString(doc["Socket"])); s != "" {
			// Parse numeric suffix from labels like "CPU0", "Processor 1", etc.
			trimmed := strings.TrimLeft(strings.ToUpper(s), "ABCDEFGHIJKLMNOPQRSTUVWXYZ _")
			if n, err := strconv.Atoi(trimmed); err == nil {
				socket = n
			}
		}
		l1, l2, l3 := parseCPUCachesFromProcessorMemory(doc)
		publicSerial := redfishCPUPublicSerial(doc)
		serial := normalizeRedfishIdentityField(asString(doc["SerialNumber"]))
		if serial == "" && publicSerial == "" {
			serial = findFirstNormalizedStringByKeys(doc, "SerialNumber")
		}
		cpus = append(cpus, models.CPU{
			Socket:       socket,
			Model:        firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
			Cores:        asInt(doc["TotalCores"]),
			Threads:      asInt(doc["TotalThreads"]),
			FrequencyMHz: int(redfishFirstNumeric(doc, "OperatingSpeedMHz", "CurrentSpeedMHz", "FrequencyMHz")),
			MaxFreqMHz:   int(redfishFirstNumeric(doc, "MaxSpeedMHz", "TurboEnableMaxSpeedMHz", "TurboDisableMaxSpeedMHz")),
			PPIN:         firstNonEmpty(findFirstNormalizedStringByKeys(doc, "PPIN", "ProtectedIdentificationNumber"), publicSerial),
			SerialNumber: serial,
			L1CacheKB:    l1,
			L2CacheKB:    l2,
			L3CacheKB:    l3,
			Status:       mapStatus(doc["Status"]),
			Details:      redfishCPUDetails(doc),
		})
	}
	return cpus
}

func redfishCPUPublicSerial(doc map[string]interface{}) string {
	oem, _ := doc["Oem"].(map[string]interface{})
	public, _ := oem["Public"].(map[string]interface{})
	return normalizeRedfishIdentityField(asString(public["SerialNumber"]))
}

// parseCPUCachesFromProcessorMemory reads L1/L2/L3 cache sizes from the
// Redfish ProcessorMemory array (Processor.v1_x spec).
func parseCPUCachesFromProcessorMemory(doc map[string]interface{}) (l1, l2, l3 int) {
	mem, _ := doc["ProcessorMemory"].([]interface{})
	for _, mAny := range mem {
		m, ok := mAny.(map[string]interface{})
		if !ok {
			continue
		}
		capMiB := asInt(m["CapacityMiB"])
		if capMiB == 0 {
			continue
		}
		capKB := capMiB * 1024
		switch strings.ToUpper(strings.TrimSpace(asString(m["MemoryType"]))) {
		case "L1CACHE":
			l1 = capKB
		case "L2CACHE":
			l2 = capKB
		case "L3CACHE":
			l3 = capKB
		}
	}
	return
}

func parseMemory(docs []map[string]interface{}) []models.MemoryDIMM {
	out := make([]models.MemoryDIMM, 0, len(docs))
	for _, doc := range docs {
		slot := firstNonEmpty(
			asString(doc["DeviceLocator"]),
			redfishLocationLabel(doc["Location"]),
			asString(doc["Name"]),
			asString(doc["Id"]),
		)
		present := true
		if strings.EqualFold(asString(doc["Status"]), "Absent") {
			present = false
		}
		if status, ok := doc["Status"].(map[string]interface{}); ok {
			state := asString(status["State"])
			if strings.EqualFold(state, "Absent") || strings.EqualFold(state, "Disabled") {
				present = false
			}
		}

		out = append(out, models.MemoryDIMM{
			Slot:            slot,
			Location:        slot,
			Present:         present,
			SizeMB:          asInt(doc["CapacityMiB"]),
			Type:            firstNonEmpty(asString(doc["MemoryDeviceType"]), asString(doc["MemoryType"])),
			MaxSpeedMHz:     asInt(doc["MaxSpeedMHz"]),
			CurrentSpeedMHz: asInt(doc["OperatingSpeedMhz"]),
			Manufacturer:    asString(doc["Manufacturer"]),
			SerialNumber:    findFirstNormalizedStringByKeys(doc, "SerialNumber"),
			PartNumber:      asString(doc["PartNumber"]),
			Status:          mapStatus(doc["Status"]),
			Details:         redfishMemoryDetails(doc),
		})
	}
	return out
}

func redfishCPUDetails(doc map[string]interface{}) map[string]any {
	return redfishCPUDetailsAcrossDocs(doc)
}

func redfishCPUDetailsAcrossDocs(doc map[string]interface{}, supplementalDocs ...map[string]interface{}) map[string]any {
	lookupDocs := append([]map[string]interface{}{doc}, supplementalDocs...)
	details := make(map[string]any)
	addFloatDetail(details, "temperature_c", redfishFirstNumericAcrossDocs(lookupDocs,
		"TemperatureCelsius", "TemperatureC", "Temperature", "CurrentTemperature", "temperature",
	))
	addFloatDetail(details, "power_w", redfishFirstNumericAcrossDocs(lookupDocs,
		"PowerConsumedWatts", "PowerWatts", "PowerConsumptionWatts",
	))
	addBoolDetail(details, "throttled", redfishFirstBoolAcrossDocs(lookupDocs,
		"Throttled", "ThermalThrottled", "PerformanceThrottled",
	))
	addInt64Detail(details, "correctable_error_count", redfishFirstInt64AcrossDocs(lookupDocs,
		"CorrectableErrorCount", "CorrectableErrors", "CorrectableECCErrorCount",
	))
	addInt64Detail(details, "uncorrectable_error_count", redfishFirstInt64AcrossDocs(lookupDocs,
		"UncorrectableErrorCount", "UncorrectableErrors", "UncorrectableECCErrorCount",
	))
	addFloatDetail(details, "life_remaining_pct", redfishFirstNumericAcrossDocs(lookupDocs,
		"LifeRemainingPercent", "PredictedLifeLeftPercent",
	))
	addFloatDetail(details, "life_used_pct", redfishFirstNumericAcrossDocs(lookupDocs,
		"LifeUsedPercent", "PercentageLifeUsed",
	))
	for _, lookupDoc := range lookupDocs {
		if microcode, ok := redfishLookupValue(lookupDoc, "MicrocodeVersion"); ok {
			if s := strings.TrimSpace(asString(microcode)); s != "" {
				details["microcode"] = s
				break
			}
		}
		if microcode, ok := redfishLookupValue(lookupDoc, "Microcode"); ok {
			if s := strings.TrimSpace(asString(microcode)); s != "" {
				details["microcode"] = s
				break
			}
		}
	}
	if len(details) == 0 {
		return nil
	}
	return details
}

func redfishMemoryDetails(doc map[string]interface{}) map[string]any {
	return redfishMemoryDetailsAcrossDocs(doc)
}

func redfishMemoryDetailsAcrossDocs(doc map[string]interface{}, supplementalDocs ...map[string]interface{}) map[string]any {
	lookupDocs := append([]map[string]interface{}{doc}, supplementalDocs...)
	details := make(map[string]any)
	addFloatDetail(details, "temperature_c", redfishFirstNumericAcrossDocs(lookupDocs,
		"TemperatureCelsius", "TemperatureC", "Temperature", "CurrentTemperature", "temperature",
	))
	addInt64Detail(details, "correctable_ecc_error_count", redfishFirstInt64AcrossDocs(lookupDocs,
		"CorrectableECCErrorCount", "CorrectableErrorCount", "CorrectableErrors",
	))
	addInt64Detail(details, "uncorrectable_ecc_error_count", redfishFirstInt64AcrossDocs(lookupDocs,
		"UncorrectableECCErrorCount", "UncorrectableErrorCount", "UncorrectableErrors",
	))
	addFloatDetail(details, "life_remaining_pct", redfishFirstNumericAcrossDocs(lookupDocs,
		"LifeRemainingPercent", "PredictedLifeLeftPercent",
	))
	addFloatDetail(details, "life_used_pct", redfishFirstNumericAcrossDocs(lookupDocs,
		"LifeUsedPercent", "PercentageLifeUsed",
	))
	addFloatDetail(details, "spare_blocks_remaining_pct", redfishFirstNumericAcrossDocs(lookupDocs,
		"SpareBlocksRemainingPercent", "SpareBlocksRemainingPct",
	))
	addBoolDetail(details, "performance_degraded", redfishFirstBoolAcrossDocs(lookupDocs,
		"PerformanceDegraded", "Degraded",
	))
	addBoolDetail(details, "data_loss_detected", redfishFirstBoolAcrossDocs(lookupDocs,
		"DataLossDetected", "DataLoss",
	))
	if len(details) == 0 {
		return nil
	}
	return details
}

func parseDrive(doc map[string]interface{}) models.Storage {
	return parseDriveWithSupplementalDocs(doc)
}

func parseDriveWithSupplementalDocs(doc map[string]interface{}, supplementalDocs ...map[string]interface{}) models.Storage {
	sizeGB := 0
	if capBytes := asInt64(doc["CapacityBytes"]); capBytes > 0 {
		sizeGB = int(capBytes / (1024 * 1024 * 1024))
	}
	if sizeGB == 0 {
		sizeGB = asInt(doc["CapacityGB"])
	}
	if sizeGB == 0 {
		sizeGB = asInt(doc["CapacityMiB"]) / 1024
	}

	storageType := classifyStorageType(doc)

	slot := normalizeRAIDDriveSlot(firstNonEmpty(asString(doc["Id"]), asString(doc["Name"])))
	return models.Storage{
		Slot:         slot,
		Type:         storageType,
		Model:        firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
		SizeGB:       sizeGB,
		SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
		Manufacturer: asString(doc["Manufacturer"]),
		Firmware:     asString(doc["Revision"]),
		Interface:    asString(doc["Protocol"]),
		Present:      true,
		Details:      redfishDriveDetailsWithSupplementalDocs(doc, supplementalDocs...),
	}
}

// isNumericString returns true if s is a non-empty string of only ASCII digits.
func isNumericString(s string) bool {
	if s == "" {
		return false
	}
	for _, c := range s {
		if c < '0' || c > '9' {
			return false
		}
	}
	return true
}

// normalizeRAIDDriveSlot converts Inspur-style RAID drive IDs to canonical BP notation.
// Example: "PCIe8_RAID_Disk_1:0" → "BP0:0"  (enclosure_id - 1 = backplane_index)
// Other slot names are returned unchanged.
func normalizeRAIDDriveSlot(slot string) string {
	// Pattern: {anything}_RAID_Disk_{enclosure}:{slot}
	const marker = "_RAID_Disk_"
	idx := strings.Index(slot, marker)
	if idx < 0 {
		return slot
	}
	rest := slot[idx+len(marker):] // e.g. "1:0"
	colonIdx := strings.Index(rest, ":")
	if colonIdx < 0 {
		return slot
	}
	encStr := rest[:colonIdx]
	slotStr := rest[colonIdx+1:]
	enc, err := strconv.Atoi(encStr)
	if err != nil || enc < 1 {
		return slot
	}
	return fmt.Sprintf("BP%d:%s", enc-1, slotStr)
}

func parseStorageVolume(doc map[string]interface{}, controller string) models.StorageVolume {
	sizeGB := 0
	capBytes := asInt64(doc["CapacityBytes"])
	if capBytes > 0 {
		sizeGB = int(capBytes / (1024 * 1024 * 1024))
	}
	if sizeGB == 0 {
		sizeGB = asInt(doc["CapacityGB"])
	}
	raidLevel := firstNonEmpty(asString(doc["RAIDType"]), asString(doc["VolumeType"]))
	if raidLevel == "" {
		if v, ok := doc["Oem"].(map[string]interface{}); ok {
			if smc, ok := v["Supermicro"].(map[string]interface{}); ok {
				raidLevel = firstNonEmpty(raidLevel, asString(smc["RAIDType"]), asString(smc["VolumeType"]))
			}
		}
	}
	return models.StorageVolume{
		ID:            asString(doc["Id"]),
		Name:          firstNonEmpty(asString(doc["Name"]), asString(doc["Id"])),
		Controller:    strings.TrimSpace(controller),
		RAIDLevel:     raidLevel,
		SizeGB:        sizeGB,
		CapacityBytes: capBytes,
		Status:        mapStatus(doc["Status"]),
		Bootable:      asBool(doc["Bootable"]),
		Encrypted:     asBool(doc["Encrypted"]),
	}
}

func redfishVolumeCapabilitiesDoc(doc map[string]interface{}) bool {
	if len(doc) == 0 {
		return false
	}
	if strings.Contains(strings.ToLower(strings.TrimSpace(asString(doc["@odata.type"]))), "collectioncapabilities") {
		return true
	}
	path := strings.ToLower(normalizeRedfishPath(asString(doc["@odata.id"])))
	if strings.HasSuffix(path, "/volumes/capabilities") {
		return true
	}
	id := strings.TrimSpace(asString(doc["Id"]))
	name := strings.ToLower(strings.TrimSpace(asString(doc["Name"])))
	return strings.EqualFold(id, "Capabilities") || strings.Contains(name, "capabilities for volumecollection")
}

func parseNIC(doc map[string]interface{}) models.NetworkAdapter {
	vendorID := asHexOrInt(doc["VendorId"])
	deviceID := asHexOrInt(doc["DeviceId"])
	model := firstNonEmpty(asString(doc["Model"]), asString(doc["Name"]))
	if isMissingOrRawPCIModel(model) {
		if resolved := pciids.DeviceName(vendorID, deviceID); resolved != "" {
			model = resolved
		}
	}
	vendor := asString(doc["Manufacturer"])
	if strings.TrimSpace(vendor) == "" {
		vendor = pciids.VendorName(vendorID)
	}
	location := redfishLocationLabel(doc["Location"])
	slot := firstNonEmpty(redfishLocationLabel(doc["Location"]), asString(doc["Id"]), asString(doc["Name"]))
	var firmware string
	var portCount int
	var linkWidth int
	var maxLinkWidth int
	var linkSpeed string
	var maxLinkSpeed string
	if controllers, ok := doc["Controllers"].([]interface{}); ok && len(controllers) > 0 {
		totalPortCount := 0
		for _, ctrlAny := range controllers {
			ctrl, ok := ctrlAny.(map[string]interface{})
			if !ok {
				continue
			}
			ctrlLocation := redfishLocationLabel(ctrl["Location"])
			location = firstNonEmpty(location, ctrlLocation)
			if isWeakRedfishNICSlotLabel(slot) {
				slot = firstNonEmpty(ctrlLocation, slot)
			}
			if normalizeRedfishIdentityField(firmware) == "" {
				firmware = findFirstNormalizedStringByKeys(ctrl, "FirmwarePackageVersion", "FirmwareVersion")
			}
			if caps, ok := ctrl["ControllerCapabilities"].(map[string]interface{}); ok {
				totalPortCount += sanitizeNetworkPortCount(asInt(caps["NetworkPortCount"]))
			}
			if pcieIf, ok := ctrl["PCIeInterface"].(map[string]interface{}); ok && linkWidth == 0 && maxLinkWidth == 0 && linkSpeed == "" && maxLinkSpeed == "" {
				linkWidth = asInt(pcieIf["LanesInUse"])
				maxLinkWidth = firstNonZeroInt(asInt(pcieIf["MaxLanes"]), asInt(pcieIf["Maxlanes"]))
				linkSpeed = firstNonEmpty(asString(pcieIf["PCIeType"]), asString(pcieIf["CurrentLinkSpeedGTs"]), asString(pcieIf["CurrentLinkSpeed"]))
				maxLinkSpeed = firstNonEmpty(asString(pcieIf["MaxPCIeType"]), asString(pcieIf["MaxLinkSpeedGTs"]), asString(pcieIf["MaxLinkSpeed"]))
			}
		}
		portCount = sanitizeNetworkPortCount(totalPortCount)
	}

	return models.NetworkAdapter{
		Slot:         slot,
		Location:     location,
		Present:      !strings.EqualFold(mapStatus(doc["Status"]), "Absent"),
		BDF:          sanitizeRedfishBDF(asString(doc["BDF"])),
		Model:        strings.TrimSpace(model),
		Vendor:       strings.TrimSpace(vendor),
		VendorID:     vendorID,
		DeviceID:     deviceID,
		SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
		PartNumber:   asString(doc["PartNumber"]),
		Firmware:     firmware,
		PortCount:    portCount,
		LinkWidth:    linkWidth,
		LinkSpeed:    linkSpeed,
		MaxLinkWidth: maxLinkWidth,
		MaxLinkSpeed: maxLinkSpeed,
		Status:       mapStatus(doc["Status"]),
		Details:      redfishPCIeDetails(doc, nil),
	}
}

func isWeakRedfishNICSlotLabel(slot string) bool {
	slot = strings.TrimSpace(slot)
	if slot == "" {
		return true
	}
	lower := strings.ToLower(slot)
	if isNumericString(slot) {
		return true
	}
	if strings.EqualFold(slot, "nic") || strings.HasPrefix(lower, "nic") && !strings.Contains(lower, "slot") {
		return true
	}
	if strings.HasPrefix(lower, "devtype") {
		return true
	}
	return false
}

func networkAdapterPCIeDevicePaths(doc map[string]interface{}) []string {
	var out []string
	if controllers, ok := doc["Controllers"].([]interface{}); ok {
		for _, ctrlAny := range controllers {
			ctrl, ok := ctrlAny.(map[string]interface{})
			if !ok {
				continue
			}
			links, ok := ctrl["Links"].(map[string]interface{})
			if !ok {
				continue
			}
			refs, ok := links["PCIeDevices"].([]interface{})
			if !ok {
				continue
			}
			for _, refAny := range refs {
				ref, ok := refAny.(map[string]interface{})
				if !ok {
					continue
				}
				if p := asString(ref["@odata.id"]); p != "" {
					out = append(out, p)
				}
			}
		}
	}
	return out
}

func enrichNICFromPCIe(nic *models.NetworkAdapter, pcieDoc map[string]interface{}, functionDocs []map[string]interface{}, supplementalDocs []map[string]interface{}) {
	if nic == nil {
		return
	}
	pcieSlot := redfishLocationLabel(pcieDoc["Slot"])
	if pcieSlot == "" {
		pcieSlot = redfishLocationLabel(pcieDoc["Location"])
	}
	if isWeakRedfishNICSlotLabel(nic.Slot) && pcieSlot != "" {
		nic.Slot = pcieSlot
	}
	if strings.TrimSpace(nic.Location) == "" && pcieSlot != "" {
		nic.Location = pcieSlot
	}
	if strings.TrimSpace(nic.BDF) == "" {
		nic.BDF = firstNonEmpty(asString(pcieDoc["BDF"]), buildBDFfromOemPublic(pcieDoc))
	}
	if nic.VendorID == 0 {
		nic.VendorID = asHexOrInt(pcieDoc["VendorId"])
	}
	if nic.DeviceID == 0 {
		nic.DeviceID = asHexOrInt(pcieDoc["DeviceId"])
	}
	if nic.LinkWidth == 0 {
		nic.LinkWidth = asInt(pcieDoc["CurrentLinkWidth"])
	}
	if nic.MaxLinkWidth == 0 {
		nic.MaxLinkWidth = asInt(pcieDoc["MaxLinkWidth"])
	}
	if strings.TrimSpace(nic.LinkSpeed) == "" {
		nic.LinkSpeed = firstNonEmpty(asString(pcieDoc["CurrentLinkSpeedGTs"]), asString(pcieDoc["CurrentLinkSpeed"]))
	}
	if strings.TrimSpace(nic.MaxLinkSpeed) == "" {
		nic.MaxLinkSpeed = firstNonEmpty(asString(pcieDoc["MaxLinkSpeedGTs"]), asString(pcieDoc["MaxLinkSpeed"]))
	}
	if nic.LinkWidth == 0 || nic.MaxLinkWidth == 0 || nic.LinkSpeed == "" || nic.MaxLinkSpeed == "" {
		redfishEnrichFromOEMxFusionPCIeLink(pcieDoc, &nic.LinkWidth, &nic.MaxLinkWidth, &nic.LinkSpeed, &nic.MaxLinkSpeed)
	}
	if normalizeRedfishIdentityField(nic.SerialNumber) == "" {
		nic.SerialNumber = findFirstNormalizedStringByKeys(pcieDoc, "SerialNumber")
	}
	if normalizeRedfishIdentityField(nic.PartNumber) == "" {
		nic.PartNumber = findFirstNormalizedStringByKeys(pcieDoc, "PartNumber", "ProductPartNumber")
	}
	if normalizeRedfishIdentityField(nic.Firmware) == "" {
		nic.Firmware = findFirstNormalizedStringByKeys(pcieDoc, "FirmwareVersion", "FirmwarePackageVersion")
	}
	for _, fn := range functionDocs {
		if strings.TrimSpace(nic.BDF) == "" {
			nic.BDF = sanitizeRedfishBDF(asString(fn["FunctionId"]))
		}
		if nic.VendorID == 0 {
			nic.VendorID = asHexOrInt(fn["VendorId"])
		}
		if nic.DeviceID == 0 {
			nic.DeviceID = asHexOrInt(fn["DeviceId"])
		}
		if nic.LinkWidth == 0 {
			nic.LinkWidth = asInt(fn["CurrentLinkWidth"])
		}
		if nic.MaxLinkWidth == 0 {
			nic.MaxLinkWidth = asInt(fn["MaxLinkWidth"])
		}
		if strings.TrimSpace(nic.LinkSpeed) == "" {
			nic.LinkSpeed = firstNonEmpty(asString(fn["CurrentLinkSpeedGTs"]), asString(fn["CurrentLinkSpeed"]))
		}
		if strings.TrimSpace(nic.MaxLinkSpeed) == "" {
			nic.MaxLinkSpeed = firstNonEmpty(asString(fn["MaxLinkSpeedGTs"]), asString(fn["MaxLinkSpeed"]))
		}
		if nic.LinkWidth == 0 || nic.MaxLinkWidth == 0 || nic.LinkSpeed == "" || nic.MaxLinkSpeed == "" {
			redfishEnrichFromOEMxFusionPCIeLink(fn, &nic.LinkWidth, &nic.MaxLinkWidth, &nic.LinkSpeed, &nic.MaxLinkSpeed)
		}
		if normalizeRedfishIdentityField(nic.SerialNumber) == "" {
			nic.SerialNumber = findFirstNormalizedStringByKeys(fn, "SerialNumber")
		}
		if normalizeRedfishIdentityField(nic.PartNumber) == "" {
			nic.PartNumber = findFirstNormalizedStringByKeys(fn, "PartNumber", "ProductPartNumber")
		}
		if normalizeRedfishIdentityField(nic.Firmware) == "" {
			nic.Firmware = findFirstNormalizedStringByKeys(fn, "FirmwareVersion", "FirmwarePackageVersion")
		}
	}
	if strings.TrimSpace(nic.Vendor) == "" {
		nic.Vendor = pciids.VendorName(nic.VendorID)
	}
	if isMissingOrRawPCIModel(nic.Model) {
		if resolved := pciids.DeviceName(nic.VendorID, nic.DeviceID); resolved != "" {
			nic.Model = resolved
		}
	}
	nic.Details = mergeGenericDetails(nic.Details, redfishPCIeDetailsWithSupplementalDocs(pcieDoc, functionDocs, supplementalDocs))
}

func parsePSU(doc map[string]interface{}, idx int) models.PSU {
	return parsePSUWithSupplementalDocs(doc, idx)
}

func parsePSUWithSupplementalDocs(doc map[string]interface{}, idx int, supplementalDocs ...map[string]interface{}) models.PSU {
	status := mapStatus(doc["Status"])
	present := true
	if statusMap, ok := doc["Status"].(map[string]interface{}); ok {
		state := asString(statusMap["State"])
		if strings.EqualFold(state, "Absent") || strings.EqualFold(state, "Disabled") {
			present = false
		}
	}

	slot := firstNonEmpty(
		asString(doc["MemberId"]),
		asString(doc["Id"]),
		asString(doc["Name"]),
	)
	if slot == "" {
		slot = fmt.Sprintf("PSU%d", idx)
	}
	// Normalize numeric-only slots ("0", "1") to "PSU0", "PSU1" for consistency
	// with BMC log parsers (Inspur, Dell etc.) that use the PSU prefix.
	if isNumericString(slot) {
		slot = "PSU" + slot
	}

	return models.PSU{
		Slot:         slot,
		Present:      present,
		Model:        firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
		Vendor:       asString(doc["Manufacturer"]),
		WattageW:     redfishPSUNominalWattage(doc),
		SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
		PartNumber:   asString(doc["PartNumber"]),
		Firmware:     asString(doc["FirmwareVersion"]),
		Status:       status,
		InputType:    asString(doc["LineInputVoltageType"]),
		InputPowerW:  asInt(doc["PowerInputWatts"]),
		OutputPowerW: asInt(doc["LastPowerOutputWatts"]),
		InputVoltage: asFloat(doc["LineInputVoltage"]),
		Details:      redfishPSUDetailsWithSupplementalDocs(doc, supplementalDocs...),
	}
}

func redfishPSUNominalWattage(doc map[string]interface{}) int {
	if ranges, ok := doc["InputRanges"].([]interface{}); ok {
		best := 0
		for _, rawRange := range ranges {
			rangeDoc, ok := rawRange.(map[string]interface{})
			if !ok {
				continue
			}
			if wattage := asInt(rangeDoc["OutputWattage"]); wattage > best {
				best = wattage
			}
		}
		if best > 0 {
			return best
		}
	}
	return asInt(doc["PowerCapacityWatts"])
}

func redfishDriveDetails(doc map[string]interface{}) map[string]any {
	return redfishDriveDetailsWithSupplementalDocs(doc)
}

func redfishDriveDetailsWithSupplementalDocs(doc map[string]interface{}, supplementalDocs ...map[string]interface{}) map[string]any {
	details := make(map[string]any)
	lookupDocs := append([]map[string]interface{}{doc}, supplementalDocs...)
	addFloatDetail(details, "temperature_c", redfishFirstNumericAcrossDocs(lookupDocs,
		"TemperatureCelsius", "TemperatureC", "Temperature", "CurrentTemperature", "temperature",
	))
	addInt64Detail(details, "power_on_hours", redfishFirstInt64AcrossDocs(lookupDocs,
		"PowerOnHours", "PowerOnHour",
	))
	addInt64Detail(details, "power_cycles", redfishFirstInt64AcrossDocs(lookupDocs,
		"PowerCycles", "PowerCycleCount",
	))
	addInt64Detail(details, "unsafe_shutdowns", redfishFirstInt64AcrossDocs(lookupDocs,
		"UnsafeShutdowns", "UnsafeShutdownCount",
	))
	addInt64Detail(details, "media_errors", redfishFirstInt64AcrossDocs(lookupDocs,
		"MediaErrors", "MediaErrorCount",
	))
	addInt64Detail(details, "error_log_entries", redfishFirstInt64AcrossDocs(lookupDocs,
		"ErrorLogEntries", "ErrorLogEntryCount",
	))
	addInt64Detail(details, "written_bytes", redfishFirstInt64AcrossDocs(lookupDocs,
		"WrittenBytes", "BytesWritten",
	))
	addInt64Detail(details, "read_bytes", redfishFirstInt64AcrossDocs(lookupDocs,
		"ReadBytes", "BytesRead",
	))
	addFloatDetail(details, "life_remaining_pct", redfishFirstNumericAcrossDocs(lookupDocs,
		"PredictedMediaLifeLeftPercent", "LifeRemainingPercent", "PercentageDriveLifeUsedInverse", "PercentLifeRemaining",
	))
	addFloatDetail(details, "life_used_pct", redfishFirstNumericAcrossDocs(lookupDocs,
		"PercentageDriveLifeUsed", "LifeUsedPercent", "PercentageUsed", "PercentLifeUsed",
	))
	addFloatDetail(details, "available_spare_pct", redfishFirstNumericAcrossDocs(lookupDocs,
		"AvailableSparePercent", "AvailableSpare", "PercentAvailableSpare",
	))
	addInt64Detail(details, "reallocated_sectors", redfishFirstInt64AcrossDocs(lookupDocs,
		"ReallocatedSectors", "ReallocatedSectorCount",
	))
	addInt64Detail(details, "current_pending_sectors", redfishFirstInt64AcrossDocs(lookupDocs,
		"CurrentPendingSectors", "CurrentPendingSectorCount",
	))
	addInt64Detail(details, "offline_uncorrectable", redfishFirstInt64AcrossDocs(lookupDocs,
		"OfflineUncorrectable", "OfflineUncorrectableSectorCount",
	))
	if len(details) == 0 {
		return nil
	}
	return details
}

func redfishPSUDetails(doc map[string]interface{}) map[string]any {
	return redfishPSUDetailsWithSupplementalDocs(doc)
}

func redfishPSUDetailsWithSupplementalDocs(doc map[string]interface{}, supplementalDocs ...map[string]interface{}) map[string]any {
	details := make(map[string]any)
	lookupDocs := append([]map[string]interface{}{doc}, supplementalDocs...)
	addFloatDetail(details, "temperature_c", redfishFirstNumericAcrossDocs(lookupDocs,
		"TemperatureCelsius", "TemperatureC", "Temperature",
	))
	addFloatDetail(details, "life_remaining_pct", redfishFirstNumericAcrossDocs(lookupDocs,
		"LifeRemainingPercent", "PredictedLifeLeftPercent",
	))
	addFloatDetail(details, "life_used_pct", redfishFirstNumericAcrossDocs(lookupDocs,
		"LifeUsedPercent", "PercentageLifeUsed",
	))
	if len(details) == 0 {
		return nil
	}
	return details
}

func redfishPCIeDetails(doc map[string]interface{}, functionDocs []map[string]interface{}) map[string]any {
	return redfishPCIeDetailsWithSupplementalDocs(doc, functionDocs, nil)
}

func redfishPCIeDetailsWithSupplementalDocs(doc map[string]interface{}, functionDocs []map[string]interface{}, supplementalDocs []map[string]interface{}) map[string]any {
	lookupDocs := make([]map[string]interface{}, 0, 1+len(functionDocs)+len(supplementalDocs))
	lookupDocs = append(lookupDocs, doc)
	lookupDocs = append(lookupDocs, functionDocs...)
	lookupDocs = append(lookupDocs, supplementalDocs...)
	details := make(map[string]any)
	temperatureC := redfishFirstNumericAcrossDocs(lookupDocs,
		"TemperatureCelsius", "TemperatureC", "Temperature",
	)
	if temperatureC == 0 {
		temperatureC = redfishSupplementalThermalMetricForPCIe(doc, supplementalDocs)
	}
	addFloatDetail(details, "temperature_c", temperatureC)
	addFloatDetail(details, "power_w", redfishFirstNumericAcrossDocs(lookupDocs,
		"PowerConsumedWatts", "PowerWatts", "PowerConsumptionWatts",
	))
	addFloatDetail(details, "life_remaining_pct", redfishFirstNumericAcrossDocs(lookupDocs,
		"LifeRemainingPercent", "PredictedLifeLeftPercent",
	))
	addFloatDetail(details, "life_used_pct", redfishFirstNumericAcrossDocs(lookupDocs,
		"LifeUsedPercent", "PercentageLifeUsed",
	))
	addInt64Detail(details, "ecc_corrected_total", redfishFirstInt64AcrossDocs(lookupDocs,
		"ECCCorrectedTotal", "CorrectableECCErrorCount", "CorrectableErrorCount",
	))
	addInt64Detail(details, "ecc_uncorrected_total", redfishFirstInt64AcrossDocs(lookupDocs,
		"ECCUncorrectedTotal", "UncorrectableECCErrorCount", "UncorrectableErrorCount",
	))
	addBoolDetail(details, "hw_slowdown", redfishFirstBoolAcrossDocs(lookupDocs,
		"HWSlowdown", "HardwareSlowdown",
	))
	addFloatDetail(details, "battery_charge_pct", redfishFirstNumericAcrossDocs(lookupDocs,
		"BatteryChargePercent", "BatteryChargePct",
	))
	addFloatDetail(details, "battery_health_pct", redfishFirstNumericAcrossDocs(lookupDocs,
		"BatteryHealthPercent", "BatteryHealthPct",
	))
	addFloatDetail(details, "battery_temperature_c", redfishFirstNumericAcrossDocs(lookupDocs,
		"BatteryTemperatureCelsius", "BatteryTemperatureC",
	))
	addFloatDetail(details, "battery_voltage_v", redfishFirstNumericAcrossDocs(lookupDocs,
		"BatteryVoltage", "BatteryVoltageV",
	))
	addBoolDetail(details, "battery_replace_required", redfishFirstBoolAcrossDocs(lookupDocs,
		"BatteryReplaceRequired", "ReplaceBattery",
	))
	addFloatDetail(details, "sfp_temperature_c", redfishFirstNumericAcrossDocs(lookupDocs,
		"SFPTemperatureCelsius", "SFPTemperatureC", "TransceiverTemperatureCelsius",
	))
	addFloatDetail(details, "sfp_tx_power_dbm", redfishFirstNumericAcrossDocs(lookupDocs,
		"SFPTXPowerDBm", "SFPTransmitPowerDBm", "TxPowerDBm",
	))
	addFloatDetail(details, "sfp_rx_power_dbm", redfishFirstNumericAcrossDocs(lookupDocs,
		"SFPRXPowerDBm", "SFPReceivePowerDBm", "RxPowerDBm",
	))
	addFloatDetail(details, "sfp_voltage_v", redfishFirstNumericAcrossDocs(lookupDocs,
		"SFPVoltageV", "TransceiverVoltageV",
	))
	addFloatDetail(details, "sfp_bias_ma", redfishFirstNumericAcrossDocs(lookupDocs,
		"SFPBiasMA", "BiasCurrentMA", "LaserBiasCurrentMA",
	))
	if len(details) == 0 {
		return nil
	}
	return details
}

func redfishSupplementalThermalMetricForPCIe(doc map[string]interface{}, supplementalDocs []map[string]interface{}) float64 {
	if len(supplementalDocs) == 0 {
		return 0
	}
	deviceNames := redfishPCIeSupplementalDeviceNames(doc)
	if len(deviceNames) == 0 {
		return 0
	}
	for _, supplemental := range supplementalDocs {
		readings, ok := supplemental["TemperatureReadingsCelsius"].([]interface{})
		if !ok || len(readings) == 0 {
			continue
		}
		for _, readingAny := range readings {
			reading, ok := readingAny.(map[string]interface{})
			if !ok {
				continue
			}
			deviceName := strings.TrimSpace(asString(reading["DeviceName"]))
			if deviceName == "" || !matchesAnyFold(deviceName, deviceNames) {
				continue
			}
			if value := asFloat(reading["Reading"]); value != 0 {
				return value
			}
		}
	}
	return 0
}

func redfishPCIeSupplementalDeviceNames(doc map[string]interface{}) []string {
	names := make([]string, 0, 3)
	for _, raw := range []string{
		asString(doc["Id"]),
		asString(doc["Name"]),
		asString(doc["Model"]),
	} {
		name := strings.TrimSpace(raw)
		if name == "" {
			continue
		}
		if !matchesAnyFold(name, names) {
			names = append(names, name)
		}
	}
	return names
}

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

func looksLikeNVSwitchPCIeDoc(doc map[string]interface{}) bool {
	joined := strings.ToLower(strings.TrimSpace(strings.Join([]string{
		asString(doc["Id"]),
		asString(doc["Name"]),
		asString(doc["Model"]),
		asString(doc["Manufacturer"]),
	}, " ")))
	return strings.Contains(joined, "nvswitch")
}

func chassisPathForPCIeDoc(docPath string) string {
	docPath = normalizeRedfishPath(docPath)
	if !strings.Contains(docPath, "/PCIeDevices/") {
		return ""
	}
	idx := strings.Index(docPath, "/PCIeDevices/")
	if idx <= 0 {
		return ""
	}
	chassisPath := docPath[:idx]
	if !strings.HasPrefix(chassisPath, "/redfish/v1/Chassis/") {
		return ""
	}
	return chassisPath
}

func redfishFirstNumeric(doc map[string]interface{}, keys ...string) float64 {
	for _, key := range keys {
		if v, ok := redfishLookupValue(doc, key); ok {
			if f := asFloat(v); f != 0 {
				return f
			}
		}
	}
	return 0
}

func redfishFirstNumericAcrossDocs(docs []map[string]interface{}, keys ...string) float64 {
	for _, doc := range docs {
		if v := redfishFirstNumeric(doc, keys...); v != 0 {
			return v
		}
	}
	return 0
}

func redfishFirstNumericWithFunctions(doc map[string]interface{}, functionDocs []map[string]interface{}, keys ...string) float64 {
	if v := redfishFirstNumeric(doc, keys...); v != 0 {
		return v
	}
	for _, fn := range functionDocs {
		if v := redfishFirstNumeric(fn, keys...); v != 0 {
			return v
		}
	}
	return 0
}

func redfishFirstInt64(doc map[string]interface{}, keys ...string) int64 {
	for _, key := range keys {
		if v, ok := redfishLookupValue(doc, key); ok {
			if n := asInt64(v); n != 0 {
				return n
			}
			if n := int64(asInt(v)); n != 0 {
				return n
			}
		}
	}
	return 0
}

func redfishFirstInt64AcrossDocs(docs []map[string]interface{}, keys ...string) int64 {
	for _, doc := range docs {
		if v := redfishFirstInt64(doc, keys...); v != 0 {
			return v
		}
	}
	return 0
}

func redfishFirstInt64WithFunctions(doc map[string]interface{}, functionDocs []map[string]interface{}, keys ...string) int64 {
	if v := redfishFirstInt64(doc, keys...); v != 0 {
		return v
	}
	for _, fn := range functionDocs {
		if v := redfishFirstInt64(fn, keys...); v != 0 {
			return v
		}
	}
	return 0
}

func redfishFirstBoolAcrossDocs(docs []map[string]interface{}, keys ...string) *bool {
	for _, doc := range docs {
		if v := redfishFirstBool(doc, keys...); v != nil {
			return v
		}
	}
	return nil
}

func redfishFirstString(doc map[string]interface{}, keys ...string) string {
	for _, key := range keys {
		if v, ok := redfishLookupValue(doc, key); ok {
			if s := strings.TrimSpace(asString(v)); s != "" {
				return s
			}
		}
	}
	return ""
}

func redfishFirstStringAcrossDocs(docs []map[string]interface{}, keys ...string) string {
	for _, doc := range docs {
		if v := redfishFirstString(doc, keys...); v != "" {
			return v
		}
	}
	return ""
}

func redfishFirstLocationAcrossDocs(docs []map[string]interface{}, keys ...string) string {
	for _, doc := range docs {
		for _, key := range keys {
			if v, ok := redfishLookupValue(doc, key); ok {
				if loc := redfishLocationLabel(v); loc != "" {
					return loc
				}
			}
		}
	}
	return ""
}

func redfishLookupValue(doc map[string]interface{}, key string) (any, bool) {
	if doc == nil || strings.TrimSpace(key) == "" {
		return nil, false
	}
	if v, ok := doc[key]; ok {
		return v, true
	}
	if oem, ok := doc["Oem"].(map[string]interface{}); ok {
		if v, ok := redfishLookupNestedValue(oem, key); ok {
			return v, true
		}
	}
	return nil, false
}

func redfishFirstBool(doc map[string]interface{}, keys ...string) *bool {
	for _, key := range keys {
		if v, ok := redfishLookupValue(doc, key); ok {
			if b, ok := asBoolPtr(v); ok {
				return &b
			}
		}
	}
	return nil
}

func redfishFirstBoolWithFunctions(doc map[string]interface{}, functionDocs []map[string]interface{}, keys ...string) *bool {
	if v := redfishFirstBool(doc, keys...); v != nil {
		return v
	}
	for _, fn := range functionDocs {
		if v := redfishFirstBool(fn, keys...); v != nil {
			return v
		}
	}
	return nil
}

func redfishLookupNestedValue(doc map[string]interface{}, key string) (any, bool) {
	if doc == nil {
		return nil, false
	}
	if v, ok := doc[key]; ok {
		return v, true
	}
	for _, value := range doc {
		nested, ok := value.(map[string]interface{})
		if !ok {
			continue
		}
		if v, ok := redfishLookupNestedValue(nested, key); ok {
			return v, true
		}
	}
	return nil, false
}

func addFloatDetail(dst map[string]any, key string, value float64) {
	if value == 0 {
		return
	}
	dst[key] = value
}

func addInt64Detail(dst map[string]any, key string, value int64) {
	if value == 0 {
		return
	}
	dst[key] = value
}

func addBoolDetail(dst map[string]any, key string, value *bool) {
	if value == nil {
		return
	}
	dst[key] = *value
}

func asBoolPtr(v any) (bool, bool) {
	switch x := v.(type) {
	case bool:
		return x, true
	case string:
		switch strings.ToLower(strings.TrimSpace(x)) {
		case "true", "yes", "enabled", "1":
			return true, true
		case "false", "no", "disabled", "0":
			return false, true
		}
	case float64:
		return x != 0, true
	case int:
		return x != 0, true
	case int64:
		return x != 0, true
	}
	return false, false
}

func parseGPU(doc map[string]interface{}, functionDocs []map[string]interface{}, idx int) models.GPU {
	return parseGPUWithSupplementalDocs(doc, functionDocs, nil, idx)
}

func parseGPUWithSupplementalDocs(doc map[string]interface{}, functionDocs []map[string]interface{}, supplementalDocs []map[string]interface{}, idx int) models.GPU {
	slot := firstNonEmpty(
		redfishLocationLabel(doc["Slot"]),
		redfishLocationLabel(doc["Location"]),
		redfishLocationLabel(doc["PhysicalLocation"]),
		asString(doc["Name"]),
		asString(doc["Id"]),
	)
	if slot == "" {
		slot = fmt.Sprintf("GPU%d", idx)
	}

	gpu := models.GPU{
		Slot:         slot,
		Location:     firstNonEmpty(redfishLocationLabel(doc["Location"]), redfishLocationLabel(doc["PhysicalLocation"])),
		Model:        firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
		Manufacturer: asString(doc["Manufacturer"]),
		SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
		PartNumber:   asString(doc["PartNumber"]),
		Firmware:     asString(doc["FirmwareVersion"]),
		Status:       mapStatus(doc["Status"]),
		Details:      redfishPCIeDetailsWithSupplementalDocs(doc, functionDocs, supplementalDocs),
	}

	if bdf := sanitizeRedfishBDF(asString(doc["BDF"])); bdf != "" {
		gpu.BDF = bdf
	}
	if gpu.BDF == "" {
		gpu.BDF = buildBDFfromOemPublic(doc)
	}
	if gpu.VendorID == 0 {
		gpu.VendorID = asHexOrInt(doc["VendorId"])
	}
	if gpu.DeviceID == 0 {
		gpu.DeviceID = asHexOrInt(doc["DeviceId"])
	}

	if pcieIf, ok := doc["PCIeInterface"].(map[string]interface{}); ok {
		if gpu.CurrentLinkWidth == 0 {
			gpu.CurrentLinkWidth = asInt(pcieIf["LanesInUse"])
		}
		if gpu.MaxLinkWidth == 0 {
			gpu.MaxLinkWidth = firstNonZeroInt(asInt(pcieIf["MaxLanes"]), asInt(pcieIf["Maxlanes"]))
		}
		if gpu.CurrentLinkSpeed == "" {
			gpu.CurrentLinkSpeed = firstNonEmpty(asString(pcieIf["PCIeType"]), asString(pcieIf["CurrentLinkSpeedGTs"]), asString(pcieIf["CurrentLinkSpeed"]))
		}
		if gpu.MaxLinkSpeed == "" {
			gpu.MaxLinkSpeed = firstNonEmpty(asString(pcieIf["MaxPCIeType"]), asString(pcieIf["MaxLinkSpeedGTs"]), asString(pcieIf["MaxLinkSpeed"]))
		}
	}

	for _, fn := range functionDocs {
		if gpu.BDF == "" {
			gpu.BDF = sanitizeRedfishBDF(asString(fn["FunctionId"]))
		}
		if gpu.VendorID == 0 {
			gpu.VendorID = asHexOrInt(fn["VendorId"])
		}
		if gpu.DeviceID == 0 {
			gpu.DeviceID = asHexOrInt(fn["DeviceId"])
		}
		if gpu.MaxLinkWidth == 0 {
			gpu.MaxLinkWidth = asInt(fn["MaxLinkWidth"])
		}
		if gpu.CurrentLinkWidth == 0 {
			gpu.CurrentLinkWidth = asInt(fn["CurrentLinkWidth"])
		}
		if gpu.MaxLinkSpeed == "" {
			gpu.MaxLinkSpeed = firstNonEmpty(asString(fn["MaxLinkSpeedGTs"]), asString(fn["MaxLinkSpeed"]))
		}
		if gpu.CurrentLinkSpeed == "" {
			gpu.CurrentLinkSpeed = firstNonEmpty(asString(fn["CurrentLinkSpeedGTs"]), asString(fn["CurrentLinkSpeed"]))
		}
		if gpu.CurrentLinkWidth == 0 || gpu.MaxLinkWidth == 0 || gpu.CurrentLinkSpeed == "" || gpu.MaxLinkSpeed == "" {
			redfishEnrichFromOEMxFusionPCIeLink(fn, &gpu.CurrentLinkWidth, &gpu.MaxLinkWidth, &gpu.CurrentLinkSpeed, &gpu.MaxLinkSpeed)
		}
	}

	if isMissingOrRawPCIModel(gpu.Model) {
		if resolved := pciids.DeviceName(gpu.VendorID, gpu.DeviceID); resolved != "" {
			gpu.Model = resolved
		}
	}
	if strings.TrimSpace(gpu.Manufacturer) == "" {
		gpu.Manufacturer = pciids.VendorName(gpu.VendorID)
	}

	return gpu
}

func parsePCIeDevice(doc map[string]interface{}, functionDocs []map[string]interface{}) models.PCIeDevice {
	return parsePCIeDeviceWithSupplementalDocs(doc, functionDocs, nil)
}

func parsePCIeDeviceWithSupplementalDocs(doc map[string]interface{}, functionDocs []map[string]interface{}, supplementalDocs []map[string]interface{}) models.PCIeDevice {
	supplementalSlot := redfishFirstLocationAcrossDocs(supplementalDocs, "Slot", "Location", "PhysicalLocation")
	dev := models.PCIeDevice{
		Slot:         firstNonEmpty(redfishLocationLabel(doc["Slot"]), redfishLocationLabel(doc["Location"]), supplementalSlot, asString(doc["Name"]), asString(doc["Id"])),
		BDF:          sanitizeRedfishBDF(asString(doc["BDF"])),
		DeviceClass:  asString(doc["DeviceType"]),
		Manufacturer: asString(doc["Manufacturer"]),
		PartNumber:   asString(doc["PartNumber"]),
		SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
		VendorID:     asHexOrInt(doc["VendorId"]),
		DeviceID:     asHexOrInt(doc["DeviceId"]),
		Details:      redfishPCIeDetailsWithSupplementalDocs(doc, functionDocs, supplementalDocs),
	}
	if strings.TrimSpace(dev.BDF) == "" {
		dev.BDF = buildBDFfromOemPublic(doc)
	}

	for _, fn := range functionDocs {
		if dev.BDF == "" {
			dev.BDF = sanitizeRedfishBDF(asString(fn["FunctionId"]))
		}
		if dev.DeviceClass == "" || isGenericPCIeClassLabel(dev.DeviceClass) {
			dev.DeviceClass = firstNonEmpty(asString(fn["DeviceClass"]), asString(fn["ClassCode"]))
		}
		if dev.VendorID == 0 {
			dev.VendorID = asHexOrInt(fn["VendorId"])
		}
		if dev.DeviceID == 0 {
			dev.DeviceID = asHexOrInt(fn["DeviceId"])
		}
		if dev.LinkWidth == 0 {
			dev.LinkWidth = asInt(fn["CurrentLinkWidth"])
		}
		if dev.MaxLinkWidth == 0 {
			dev.MaxLinkWidth = asInt(fn["MaxLinkWidth"])
		}
		if dev.LinkSpeed == "" {
			dev.LinkSpeed = firstNonEmpty(asString(fn["CurrentLinkSpeedGTs"]), asString(fn["CurrentLinkSpeed"]))
		}
		if dev.MaxLinkSpeed == "" {
			dev.MaxLinkSpeed = firstNonEmpty(asString(fn["MaxLinkSpeedGTs"]), asString(fn["MaxLinkSpeed"]))
		}
		if dev.LinkWidth == 0 || dev.MaxLinkWidth == 0 || dev.LinkSpeed == "" || dev.MaxLinkSpeed == "" {
			redfishEnrichFromOEMxFusionPCIeLink(fn, &dev.LinkWidth, &dev.MaxLinkWidth, &dev.LinkSpeed, &dev.MaxLinkSpeed)
		}
	}
	if dev.DeviceClass == "" || isGenericPCIeClassLabel(dev.DeviceClass) {
		dev.DeviceClass = firstNonEmpty(redfishFirstStringAcrossDocs(supplementalDocs, "DeviceType"), dev.DeviceClass)
	}

	if dev.DeviceClass == "" {
		dev.DeviceClass = "PCIe device"
	}
	if isGenericPCIeClassLabel(dev.DeviceClass) {
		if resolved := pciids.DeviceName(dev.VendorID, dev.DeviceID); resolved != "" {
			dev.DeviceClass = resolved
		}
	}
	if isGenericPCIeClassLabel(dev.DeviceClass) {
		dev.DeviceClass = "PCIe device"
	}
	if strings.TrimSpace(dev.Manufacturer) == "" {
		dev.Manufacturer = firstNonEmpty(
			redfishFirstStringAcrossDocs(supplementalDocs, "Manufacturer"),
			pciids.VendorName(dev.VendorID),
		)
	}
	if strings.TrimSpace(dev.PartNumber) == "" {
		dev.PartNumber = firstNonEmpty(
			redfishFirstStringAcrossDocs(supplementalDocs, "ProductPartNumber", "PartNumber"),
			pciids.DeviceName(dev.VendorID, dev.DeviceID),
		)
	}
	if normalizeRedfishIdentityField(dev.SerialNumber) == "" {
		dev.SerialNumber = redfishFirstStringAcrossDocs(supplementalDocs, "SerialNumber")
	}
	return dev
}

func parsePCIeFunction(doc map[string]interface{}, idx int) models.PCIeDevice {
	return parsePCIeFunctionWithSupplementalDocs(doc, nil, idx)
}

func parsePCIeFunctionWithSupplementalDocs(doc map[string]interface{}, supplementalDocs []map[string]interface{}, idx int) models.PCIeDevice {
	slot := firstNonEmpty(redfishLocationLabel(doc["Location"]), asString(doc["Id"]), asString(doc["Name"]))
	if slot == "" {
		slot = fmt.Sprintf("PCIeFn%d", idx)
	}

	dev := models.PCIeDevice{
		Slot:         slot,
		BDF:          sanitizeRedfishBDF(asString(doc["BDF"])),
		VendorID:     asHexOrInt(doc["VendorId"]),
		DeviceID:     asHexOrInt(doc["DeviceId"]),
		DeviceClass:  firstNonEmpty(asString(doc["DeviceClass"]), asString(doc["ClassCode"]), "PCIe device"),
		Manufacturer: asString(doc["Manufacturer"]),
		SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
		LinkWidth:    asInt(doc["CurrentLinkWidth"]),
		LinkSpeed:    firstNonEmpty(asString(doc["CurrentLinkSpeedGTs"]), asString(doc["CurrentLinkSpeed"])),
		MaxLinkWidth: asInt(doc["MaxLinkWidth"]),
		MaxLinkSpeed: firstNonEmpty(asString(doc["MaxLinkSpeedGTs"]), asString(doc["MaxLinkSpeed"])),
		Details:      redfishPCIeDetailsWithSupplementalDocs(doc, nil, supplementalDocs),
	}
	if dev.BDF == "" {
		dev.BDF = firstNonEmpty(buildBDFfromOemPublic(doc), sanitizeRedfishBDF(asString(doc["FunctionId"])))
	}
	if isGenericPCIeClassLabel(dev.DeviceClass) {
		if resolved := pciids.DeviceName(dev.VendorID, dev.DeviceID); resolved != "" {
			dev.DeviceClass = resolved
		}
	}
	if strings.TrimSpace(dev.Manufacturer) == "" {
		dev.Manufacturer = pciids.VendorName(dev.VendorID)
	}
	if strings.TrimSpace(dev.PartNumber) == "" {
		dev.PartNumber = pciids.DeviceName(dev.VendorID, dev.DeviceID)
	}
	return dev
}

func isMissingOrRawPCIModel(model string) bool {
	model = strings.TrimSpace(model)
	if model == "" {
		return true
	}
	l := strings.ToLower(model)
	if l == "unknown" || l == "n/a" || l == "na" || l == "none" {
		return true
	}
	if isGenericRedfishInventoryName(l) {
		return true
	}
	if strings.HasPrefix(l, "0x") && len(l) <= 6 {
		return true
	}
	if len(model) <= 4 {
		isHex := true
		for _, c := range l {
			if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
				isHex = false
				break
			}
		}
		if isHex {
			return true
		}
	}
	return false
}

func isGenericRedfishInventoryName(value string) bool {
	value = strings.ToLower(strings.TrimSpace(value))
	switch {
	case value == "":
		return false
	case value == "networkadapter", strings.HasPrefix(value, "networkadapter_"), strings.HasPrefix(value, "networkadapter "):
		return true
	case value == "pciedevice", strings.HasPrefix(value, "pciedevice_"), strings.HasPrefix(value, "pciedevice "):
		return true
	case value == "pciefunction", strings.HasPrefix(value, "pciefunction_"), strings.HasPrefix(value, "pciefunction "):
		return true
	case value == "ethernetinterface", strings.HasPrefix(value, "ethernetinterface_"), strings.HasPrefix(value, "ethernetinterface "):
		return true
	case value == "networkport", strings.HasPrefix(value, "networkport_"), strings.HasPrefix(value, "networkport "):
		return true
	default:
		return false
	}
}

// isUnidentifiablePCIeDevice returns true for PCIe topology entries that carry no
// useful inventory information: generic class (SingleFunction/MultiFunction), no
// resolved model or serial, and no PCI vendor/device IDs for future resolution.
// These are typically PCH bridges, root ports, or other bus infrastructure that
// some BMCs (e.g. MSI) enumerate exhaustively in their PCIeDevices collection.
func isUnidentifiablePCIeDevice(dev models.PCIeDevice) bool {
	if !isGenericPCIeClassLabel(dev.DeviceClass) {
		return false
	}
	if normalizeRedfishIdentityField(dev.PartNumber) != "" {
		return false
	}
	if normalizeRedfishIdentityField(dev.SerialNumber) != "" {
		return false
	}
	if dev.VendorID > 0 || dev.DeviceID > 0 {
		return false
	}
	return true
}

func isGenericPCIeClassLabel(v string) bool {
	switch strings.ToLower(strings.TrimSpace(v)) {
	case "", "pcie device", "display", "display controller", "vga", "3d controller", "network", "network controller", "storage", "storage controller", "other", "unknown", "singlefunction", "multifunction", "simulated":
		return true
	default:
		return strings.HasPrefix(strings.ToLower(strings.TrimSpace(v)), "0x")
	}
}

func redfishPCIeMatchesChassisDeviceDoc(doc, deviceDoc map[string]interface{}) bool {
	if len(doc) == 0 || len(deviceDoc) == 0 || redfishChassisDeviceDocLooksEmpty(deviceDoc) {
		return false
	}
	docSerial := normalizeRedfishIdentityField(findFirstNormalizedStringByKeys(doc, "SerialNumber"))
	deviceSerial := normalizeRedfishIdentityField(findFirstNormalizedStringByKeys(deviceDoc, "SerialNumber"))
	if docSerial != "" && deviceSerial != "" && strings.EqualFold(docSerial, deviceSerial) {
		return true
	}
	docTokens := redfishPCIeMatchTokens(doc)
	deviceTokens := redfishPCIeMatchTokens(deviceDoc)
	if len(docTokens) == 0 || len(deviceTokens) == 0 {
		return false
	}
	for _, token := range docTokens {
		for _, candidate := range deviceTokens {
			if strings.EqualFold(token, candidate) {
				return true
			}
		}
	}
	return false
}

func redfishPCIeMatchTokens(doc map[string]interface{}) []string {
	if len(doc) == 0 {
		return nil
	}
	rawValues := []string{
		asString(doc["Name"]),
		asString(doc["Model"]),
		asString(doc["PartNumber"]),
		asString(doc["ProductPartNumber"]),
	}
	out := make([]string, 0, len(rawValues))
	seen := make(map[string]struct{}, len(rawValues))
	for _, raw := range rawValues {
		value := normalizeRedfishIdentityField(raw)
		if value == "" {
			continue
		}
		key := strings.ToLower(value)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, value)
	}
	return out
}

func redfishChassisDeviceDocLooksEmpty(doc map[string]interface{}) bool {
	name := strings.ToLower(strings.TrimSpace(asString(doc["Name"])))
	if strings.HasPrefix(name, "empty slot") {
		return true
	}
	if strings.ToLower(strings.TrimSpace(asString(doc["DeviceType"]))) != "unknown" {
		return false
	}
	return normalizeRedfishIdentityField(asString(doc["PartNumber"])) == "" &&
		normalizeRedfishIdentityField(asString(doc["ProductPartNumber"])) == "" &&
		normalizeRedfishIdentityField(findFirstNormalizedStringByKeys(doc, "SerialNumber")) == ""
}

func buildBDFfromOemPublic(doc map[string]interface{}) string {
	if len(doc) == 0 {
		return ""
	}
	oem, ok := doc["Oem"].(map[string]interface{})
	if !ok {
		return ""
	}
	public, ok := oem["Public"].(map[string]interface{})
	if !ok {
		return ""
	}

	bus := asHexOrInt(public["BusNumber"])
	dev := asHexOrInt(public["DeviceNumber"])
	fn := asHexOrInt(public["FunctionNumber"])
	if bus < 0 || dev < 0 || fn < 0 {
		return ""
	}
	segment := asHexOrInt(public["Segment"])
	if segment < 0 {
		segment = 0
	}
	// Require at least bus + dev numbers to avoid inventing meaningless BDFs.
	if bus == 0 && dev == 0 && fn == 0 {
		return ""
	}
	return fmt.Sprintf("%04x:%02x:%02x.%x", segment, bus, dev, fn)
}

// redfishEnrichFromOEMxFusionPCIeLink fills in missing PCIe link width/speed
// from the xFusion OEM namespace. xFusion reports link width as a string like
// "X8" in Oem.xFusion.LinkWidth / Oem.xFusion.LinkWidthAbility, and link speed
// as a string like "Gen4 (16.0GT/s)" in Oem.xFusion.LinkSpeed /
// Oem.xFusion.LinkSpeedAbility. These fields appear on PCIeFunction docs.
func redfishEnrichFromOEMxFusionPCIeLink(doc map[string]interface{}, linkWidth, maxLinkWidth *int, linkSpeed, maxLinkSpeed *string) {
	oem, _ := doc["Oem"].(map[string]interface{})
	if oem == nil {
		return
	}
	xf, _ := oem["xFusion"].(map[string]interface{})
	if xf == nil {
		return
	}
	if *linkWidth == 0 {
		*linkWidth = parseXFusionLinkWidth(asString(xf["LinkWidth"]))
	}
	if *maxLinkWidth == 0 {
		*maxLinkWidth = parseXFusionLinkWidth(asString(xf["LinkWidthAbility"]))
	}
	if strings.TrimSpace(*linkSpeed) == "" {
		*linkSpeed = strings.TrimSpace(asString(xf["LinkSpeed"]))
	}
	if strings.TrimSpace(*maxLinkSpeed) == "" {
		*maxLinkSpeed = strings.TrimSpace(asString(xf["LinkSpeedAbility"]))
	}
}

// parseXFusionLinkWidth converts an xFusion link-width string like "X8" or
// "x16" to the integer lane count. Returns 0 for unrecognised values.
func parseXFusionLinkWidth(s string) int {
	s = strings.TrimSpace(s)
	if s == "" {
		return 0
	}
	s = strings.TrimPrefix(strings.ToUpper(s), "X")
	v := asInt(s)
	if v <= 0 {
		return 0
	}
	return v
}

// firstNonZeroInt returns the first argument that is non-zero.
func firstNonZeroInt(vals ...int) int {
	for _, v := range vals {
		if v != 0 {
			return v
		}
	}
	return 0
}

func normalizeRedfishIdentityField(v string) string {
	v = strings.TrimSpace(v)
	if v == "" {
		return ""
	}
	switch strings.ToLower(v) {
	case "n/a", "na", "none", "null", "unknown", "0":
		return ""
	default:
		return v
	}
}

func gpuDedupKey(gpu models.GPU) string {
	if serial := normalizeRedfishIdentityField(gpu.SerialNumber); serial != "" {
		return serial
	}
	if bdf := strings.TrimSpace(gpu.BDF); bdf != "" {
		return bdf
	}
	return firstNonEmpty(strings.TrimSpace(gpu.Slot)+"|"+strings.TrimSpace(gpu.Model), strings.TrimSpace(gpu.Slot))
}

func gpuDocDedupKey(doc map[string]interface{}, gpu models.GPU) string {
	// Prefer stable GPU identifiers (serial, BDF) over path so that the same
	// physical GPU exposed under multiple Chassis PCIeDevice trees (e.g. Supermicro
	// HGX: Chassis/1/PCIeDevices/GPU1 and Chassis/HGX_GPU_SXM_1/PCIeDevices/GPU_SXM_1)
	// is correctly deduplicated.
	//
	// Only stable identifiers (serial, BDF) are used for cross-path dedup.
	// When neither is present we fall back to path, so two genuinely distinct GPUs
	// that happen to share the same model name (e.g. in GraphicsControllers) are
	// not incorrectly collapsed into one.
	if serial := normalizeRedfishIdentityField(gpu.SerialNumber); serial != "" {
		return serial
	}
	if bdf := strings.TrimSpace(gpu.BDF); bdf != "" {
		return bdf
	}
	if path := normalizeRedfishPath(asString(doc["@odata.id"])); path != "" {
		return "path:" + path
	}
	return ""
}

func shouldSkipGenericGPUDuplicate(existing []models.GPU, candidate models.GPU) bool {
	if len(existing) == 0 {
		return false
	}
	if normalizeRedfishIdentityField(candidate.SerialNumber) != "" || strings.TrimSpace(candidate.BDF) != "" {
		return false
	}
	slot := strings.TrimSpace(candidate.Slot)
	model := strings.TrimSpace(candidate.Model)
	if slot == "" || model == "" {
		return false
	}

	// Typical GraphicsControllers fallback on some BMCs reports only model/name
	// as slot and lacks stable identifiers. If we already have concrete GPUs of the
	// same model/manufacturer from PCIe inventory, this candidate is a duplicate.
	if !strings.EqualFold(slot, model) {
		return false
	}
	for _, gpu := range existing {
		if !strings.EqualFold(strings.TrimSpace(gpu.Model), model) {
			continue
		}
		existingMfr := strings.TrimSpace(gpu.Manufacturer)
		candidateMfr := strings.TrimSpace(candidate.Manufacturer)
		if existingMfr != "" && candidateMfr != "" && !strings.EqualFold(existingMfr, candidateMfr) {
			continue
		}
		if normalizeRedfishIdentityField(gpu.SerialNumber) != "" || strings.TrimSpace(gpu.BDF) != "" {
			return true
		}
	}
	return false
}

func dropModelOnlyGPUPlaceholders(items []models.GPU) []models.GPU {
	if len(items) < 2 {
		return items
	}

	// Merge serial from generic GraphicsControllers placeholders (slot ~= model)
	// into concrete PCIe rows (with BDF) when mapping is unambiguous.
	mergedPlaceholder := make(map[int]struct{})
	usedConcrete := make(map[int]struct{})
	unresolvedByGroup := make(map[string][]int)
	for i := range items {
		serial := normalizeRedfishIdentityField(items[i].SerialNumber)
		if serial == "" || strings.TrimSpace(items[i].BDF) != "" || !isModelOnlyGPUPlaceholder(items[i]) {
			continue
		}

		candidates := matchingConcreteGPUIndexes(items, i, usedConcrete)
		candidate := -1
		if len(candidates) == 1 {
			candidate = candidates[0]
		}

		if candidate >= 0 {
			mergeGPUPlaceholderIntoConcrete(&items[candidate], items[i])
			usedConcrete[candidate] = struct{}{}
			mergedPlaceholder[i] = struct{}{}
			continue
		}
		group := gpuModelVendorKey(items[i])
		if group == "" {
			continue
		}
		unresolvedByGroup[group] = append(unresolvedByGroup[group], i)
	}

	// Fallback mapping by order for ambiguous groups (e.g. same model x8).
	for group, placeholders := range unresolvedByGroup {
		donors := make([]int, 0, len(placeholders))
		for j := range items {
			if _, used := usedConcrete[j]; used {
				continue
			}
			if !isConcreteGPUDonor(items[j]) {
				continue
			}
			if gpuModelVendorKey(items[j]) != group {
				continue
			}
			if normalizeRedfishIdentityField(items[j].SerialNumber) != "" {
				continue
			}
			donors = append(donors, j)
		}
		limit := len(placeholders)
		if len(donors) < limit {
			limit = len(donors)
		}
		for k := 0; k < limit; k++ {
			pi := placeholders[k]
			di := donors[k]
			if normalizeRedfishIdentityField(items[pi].SerialNumber) == "" {
				continue
			}
			mergeGPUPlaceholderIntoConcrete(&items[di], items[pi])
			usedConcrete[di] = struct{}{}
			mergedPlaceholder[pi] = struct{}{}
		}
	}

	concreteByModel := make(map[string]struct{}, len(items))
	for _, gpu := range items {
		modelKey := strings.ToLower(strings.TrimSpace(gpu.Model))
		if modelKey == "" {
			continue
		}
		if normalizeRedfishIdentityField(gpu.SerialNumber) != "" || strings.TrimSpace(gpu.BDF) != "" {
			concreteByModel[modelKey] = struct{}{}
		}
	}
	if len(concreteByModel) == 0 {
		return items
	}

	out := make([]models.GPU, 0, len(items))
	for i, gpu := range items {
		modelKey := strings.ToLower(strings.TrimSpace(gpu.Model))
		if _, hasConcrete := concreteByModel[modelKey]; hasConcrete &&
			strings.TrimSpace(gpu.BDF) == "" &&
			isModelOnlyGPUPlaceholder(gpu) &&
			(normalizeRedfishIdentityField(gpu.SerialNumber) == "" || hasMergedPlaceholderIndex(mergedPlaceholder, i)) {
			continue
		}
		out = append(out, gpu)
	}
	return out
}

func isModelOnlyGPUPlaceholder(gpu models.GPU) bool {
	slot := strings.TrimSpace(gpu.Slot)
	model := strings.TrimSpace(gpu.Model)
	if slot == "" || model == "" {
		return false
	}
	return strings.EqualFold(slot, model) || strings.HasPrefix(strings.ToUpper(slot), "GPU")
}

func isConcreteGPUDonor(gpu models.GPU) bool {
	if strings.TrimSpace(gpu.BDF) == "" {
		return false
	}
	return !isModelOnlyGPUPlaceholder(gpu)
}

func gpuModelVendorKey(gpu models.GPU) string {
	model := strings.ToLower(strings.TrimSpace(gpu.Model))
	if model == "" {
		return ""
	}
	mfr := strings.ToLower(strings.TrimSpace(gpu.Manufacturer))
	return model + "|" + mfr
}

func matchingConcreteGPUIndexes(items []models.GPU, placeholderIdx int, usedConcrete map[int]struct{}) []int {
	out := make([]int, 0, 2)
	ph := items[placeholderIdx]
	for j := range items {
		if j == placeholderIdx {
			continue
		}
		if _, used := usedConcrete[j]; used {
			continue
		}
		if !isConcreteGPUDonor(items[j]) {
			continue
		}
		if !strings.EqualFold(strings.TrimSpace(items[j].Model), strings.TrimSpace(ph.Model)) {
			continue
		}
		otherMfr := strings.TrimSpace(items[j].Manufacturer)
		phMfr := strings.TrimSpace(ph.Manufacturer)
		if phMfr != "" && otherMfr != "" && !strings.EqualFold(phMfr, otherMfr) {
			continue
		}
		if normalizeRedfishIdentityField(items[j].SerialNumber) != "" {
			continue
		}
		out = append(out, j)
	}
	return out
}

func mergeGPUPlaceholderIntoConcrete(concrete *models.GPU, placeholder models.GPU) {
	if concrete == nil {
		return
	}
	if normalizeRedfishIdentityField(concrete.SerialNumber) == "" {
		if serial := normalizeRedfishIdentityField(placeholder.SerialNumber); serial != "" {
			concrete.SerialNumber = serial
		}
	}
	if strings.TrimSpace(concrete.UUID) == "" && strings.TrimSpace(placeholder.UUID) != "" {
		concrete.UUID = placeholder.UUID
	}
	if strings.TrimSpace(concrete.PartNumber) == "" && strings.TrimSpace(placeholder.PartNumber) != "" {
		concrete.PartNumber = placeholder.PartNumber
	}
	if strings.TrimSpace(concrete.Firmware) == "" && strings.TrimSpace(placeholder.Firmware) != "" {
		concrete.Firmware = placeholder.Firmware
	}
	if strings.TrimSpace(concrete.Status) == "" && strings.TrimSpace(placeholder.Status) != "" {
		concrete.Status = placeholder.Status
	}
}

func hasMergedPlaceholderIndex(indexes map[int]struct{}, idx int) bool {
	_, ok := indexes[idx]
	return ok
}

func looksLikeGPU(doc map[string]interface{}, functionDocs []map[string]interface{}) bool {
	// "Display Device" is how MSI labels H100 secondary display/audio controller
	// functions — these are not compute GPUs and should be excluded.
	if strings.EqualFold(strings.TrimSpace(asString(doc["Description"])), "Display Device") {
		return false
	}
	// NVSwitch is an NVIDIA NVLink interconnect switch, not a compute GPU.
	if strings.Contains(strings.ToLower(strings.TrimSpace(asString(doc["Model"]))), "nvswitch") {
		return false
	}

	deviceType := strings.ToLower(asString(doc["DeviceType"]))
	if strings.Contains(deviceType, "gpu") || strings.Contains(deviceType, "graphics") || strings.Contains(deviceType, "accelerator") {
		return true
	}
	if strings.Contains(deviceType, "network") {
		return false
	}

	if oem, ok := doc["Oem"].(map[string]interface{}); ok {
		if public, ok := oem["Public"].(map[string]interface{}); ok {
			if dc := strings.ToLower(asString(public["DeviceClass"])); strings.Contains(dc, "network") {
				return false
			}
		}
	}

	modelText := strings.ToLower(strings.Join([]string{
		asString(doc["Name"]),
		asString(doc["Model"]),
		asString(doc["Manufacturer"]),
	}, " "))
	gpuHints := []string{"gpu", "nvidia", "tesla", "a100", "h100", "l40", "rtx", "radeon", "instinct"}
	for _, hint := range gpuHints {
		if strings.Contains(modelText, hint) {
			return true
		}
	}

	for _, fn := range functionDocs {
		classCode := strings.ToLower(strings.TrimPrefix(asString(fn["ClassCode"]), "0x"))
		if strings.HasPrefix(classCode, "03") || strings.HasPrefix(classCode, "12") {
			return true
		}
	}

	return false
}

// isVirtualStorageDrive returns true for BMC-virtual drives that should not
// appear in hardware inventory (e.g. AMI virtual CD/USB sticks with 0 capacity).
func isVirtualStorageDrive(doc map[string]interface{}) bool {
	if strings.EqualFold(asString(doc["Protocol"]), "USB") && asInt64(doc["CapacityBytes"]) == 0 {
		return true
	}
	mfr := strings.ToUpper(strings.TrimSpace(asString(doc["Manufacturer"])))
	model := strings.ToUpper(strings.TrimSpace(asString(doc["Model"])))
	name := strings.ToUpper(strings.TrimSpace(asString(doc["Name"])))
	joined := strings.Join([]string{mfr, model, name}, " ")
	if strings.Contains(mfr, "AMI") && strings.Contains(joined, "VIRTUAL") {
		return true
	}
	for _, marker := range []string{
		"VIRTUAL CDROM",
		"VIRTUAL CD/DVD",
		"VIRTUAL FLOPPY",
		"VIRTUAL FDD",
		"VIRTUAL USB",
		"VIRTUAL MEDIA",
	} {
		if strings.Contains(joined, marker) {
			return true
		}
	}
	if strings.Contains(mfr, "AMERICAN MEGATRENDS") && (strings.Contains(joined, "CDROM") || strings.Contains(joined, "FLOPPY") || strings.Contains(joined, "FDD")) {
		return true
	}
	return false
}

// isAbsentDriveDoc returns true when the drive document represents an empty bay
// with no installed media (Status.State == "Absent"). These should be excluded
// from the storage inventory.
func isAbsentDriveDoc(doc map[string]interface{}) bool {
	if status, ok := doc["Status"].(map[string]interface{}); ok {
		return strings.EqualFold(asString(status["State"]), "Absent")
	}
	return strings.EqualFold(asString(doc["Status"]), "Absent")
}

func looksLikeDrive(doc map[string]interface{}) bool {
	if asString(doc["MediaType"]) != "" {
		return true
	}
	if asString(doc["Protocol"]) != "" && (asInt(doc["CapacityGB"]) > 0 || asInt(doc["CapacityBytes"]) > 0) {
		return true
	}
	if asString(doc["Type"]) != "" && (asString(doc["Model"]) != "" || asInt(doc["CapacityGB"]) > 0 || asInt(doc["CapacityBytes"]) > 0) {
		return true
	}
	return false
}

func classifyStorageType(doc map[string]interface{}) string {
	protocol := strings.ToUpper(asString(doc["Protocol"]))
	if strings.Contains(protocol, "NVME") {
		return "NVMe"
	}

	media := strings.ToUpper(asString(doc["MediaType"]))
	if media == "SSD" {
		return "SSD"
	}
	if media == "HDD" || media == "HDDT" {
		return "HDD"
	}

	nameModel := strings.ToUpper(strings.Join([]string{
		asString(doc["Name"]),
		asString(doc["Model"]),
		asString(doc["Description"]),
	}, " "))
	if strings.Contains(nameModel, "NVME") {
		return "NVMe"
	}
	if strings.Contains(nameModel, "SSD") {
		return "SSD"
	}
	if strings.Contains(nameModel, "HDD") {
		return "HDD"
	}
	if protocol != "" {
		return protocol
	}
	return firstNonEmpty(asString(doc["Type"]), "Storage")
}

func looksLikeVolume(doc map[string]interface{}) bool {
	if redfishVolumeCapabilitiesDoc(doc) {
		return false
	}
	if asString(doc["RAIDType"]) != "" || asString(doc["VolumeType"]) != "" {
		return true
	}
	if strings.Contains(strings.ToLower(asString(doc["@odata.type"])), "volume") && (asInt64(doc["CapacityBytes"]) > 0 || asString(doc["Name"]) != "") {
		return true
	}
	return false
}

func dedupeStorage(items []models.Storage) []models.Storage {
	if len(items) <= 1 {
		return items
	}
	// Pass 1: drop exact duplicates by identity and keep the richer variant.
	out := dedupeStorageByIdentityPreferRich(items)
	if len(out) <= 1 {
		return out
	}
	// Pass 2: replace placeholder slots with rich drive data (slot is preserved).
	merged, consumedDonors := mergeStoragePlaceholders(out)
	if len(consumedDonors) > 0 {
		compacted := make([]models.Storage, 0, len(merged)-len(consumedDonors))
		for i, item := range merged {
			if _, consumed := consumedDonors[i]; consumed {
				continue
			}
			compacted = append(compacted, item)
		}
		out = compacted
	} else {
		out = merged
	}
	// Pass 3: final identity dedupe after placeholder merge.
	return dedupeStorageByIdentityPreferRich(out)
}

func dedupeStorageByIdentityPreferRich(items []models.Storage) []models.Storage {
	if len(items) == 0 {
		return nil
	}
	out := make([]models.Storage, 0, len(items))
	seen := make(map[string]int, len(items))
	for _, item := range items {
		key := storageIdentityKey(item)
		if key == "" {
			continue
		}
		if idx, ok := seen[key]; ok {
			out[idx] = richerStorageEntry(out[idx], item)
			continue
		}
		seen[key] = len(out)
		out = append(out, item)
	}
	return out
}

func storageIdentityKey(item models.Storage) string {
	if serial := normalizeRedfishIdentityField(item.SerialNumber); serial != "" {
		return "sn:" + serial
	}
	slot := strings.TrimSpace(item.Slot)
	model := strings.TrimSpace(item.Model)
	if slot == "" && model == "" {
		return ""
	}
	return "slotmodel:" + slot + "|" + model
}

func richerStorageEntry(a, b models.Storage) models.Storage {
	if storageRichnessScore(b) > storageRichnessScore(a) {
		b.Details = mergeGenericDetails(b.Details, a.Details)
		return b
	}
	a.Details = mergeGenericDetails(a.Details, b.Details)
	return a
}

func storageRichnessScore(item models.Storage) int {
	score := 0
	if normalizeRedfishIdentityField(item.SerialNumber) != "" {
		score += 100
	}
	if item.SizeGB > 0 {
		score += 40
	}
	if normalizedStorageModel(item) != "" {
		score += 20
	}
	if normalizeRedfishIdentityField(item.Manufacturer) != "" {
		score += 10
	}
	if normalizeRedfishIdentityField(item.Firmware) != "" {
		score += 8
	}
	if strings.TrimSpace(item.Interface) != "" {
		score += 5
	}
	if strings.TrimSpace(item.Description) != "" {
		score += 3
	}
	if item.Present {
		score++
	}
	return score
}

func normalizedStorageModel(item models.Storage) string {
	model := normalizeRedfishIdentityField(item.Model)
	if model == "" {
		return ""
	}
	slot := strings.TrimSpace(item.Slot)
	if slot != "" && strings.EqualFold(model, slot) {
		return ""
	}
	return model
}

func isStoragePlaceholder(item models.Storage) bool {
	if normalizeRedfishIdentityField(item.SerialNumber) != "" {
		return false
	}
	if item.SizeGB > 0 {
		return false
	}
	if normalizedStorageModel(item) != "" {
		return false
	}
	if normalizeRedfishIdentityField(item.Manufacturer) != "" {
		return false
	}
	if normalizeRedfishIdentityField(item.Firmware) != "" {
		return false
	}
	if strings.TrimSpace(item.Description) != "" {
		return false
	}
	return true
}

func isRichStorageDonor(item models.Storage) bool {
	if isStoragePlaceholder(item) {
		return false
	}
	return normalizeRedfishIdentityField(item.SerialNumber) != "" ||
		item.SizeGB > 0 ||
		normalizedStorageModel(item) != "" ||
		normalizeRedfishIdentityField(item.Manufacturer) != "" ||
		normalizeRedfishIdentityField(item.Firmware) != ""
}

func mergeStoragePlaceholders(items []models.Storage) ([]models.Storage, map[int]struct{}) {
	if len(items) <= 1 {
		return items, nil
	}
	out := make([]models.Storage, len(items))
	copy(out, items)

	placeholderIdx := make([]int, 0, len(out))
	donorIdx := make([]int, 0, len(out))
	for i, item := range out {
		if isStoragePlaceholder(item) {
			placeholderIdx = append(placeholderIdx, i)
			continue
		}
		if isRichStorageDonor(item) {
			donorIdx = append(donorIdx, i)
		}
	}
	if len(placeholderIdx) == 0 || len(donorIdx) == 0 {
		return out, nil
	}

	consumed := make(map[int]struct{}, len(donorIdx))
	for _, pi := range placeholderIdx {
		di := findStorageDonorIndex(out, donorIdx, consumed, out[pi].Type)
		if di < 0 {
			continue
		}
		out[pi] = mergeStorageIntoPlaceholder(out[pi], out[di])
		consumed[di] = struct{}{}
	}
	if len(consumed) == 0 {
		return out, nil
	}
	return out, consumed
}

func findStorageDonorIndex(items []models.Storage, donors []int, consumed map[int]struct{}, placeholderType string) int {
	placeholderType = strings.TrimSpace(strings.ToUpper(placeholderType))
	if placeholderType != "" {
		for _, idx := range donors {
			if _, used := consumed[idx]; used {
				continue
			}
			if strings.TrimSpace(strings.ToUpper(items[idx].Type)) == placeholderType {
				return idx
			}
		}
	}
	for _, idx := range donors {
		if _, used := consumed[idx]; !used {
			return idx
		}
	}
	return -1
}

func mergeStorageIntoPlaceholder(placeholder, donor models.Storage) models.Storage {
	out := placeholder
	if strings.TrimSpace(out.Type) == "" {
		out.Type = donor.Type
	}
	if normalizedStorageModel(out) == "" && normalizedStorageModel(donor) != "" {
		out.Model = donor.Model
	}
	if out.SizeGB <= 0 && donor.SizeGB > 0 {
		out.SizeGB = donor.SizeGB
	}
	if normalizeRedfishIdentityField(out.SerialNumber) == "" && normalizeRedfishIdentityField(donor.SerialNumber) != "" {
		out.SerialNumber = donor.SerialNumber
	}
	if normalizeRedfishIdentityField(out.Manufacturer) == "" && normalizeRedfishIdentityField(donor.Manufacturer) != "" {
		out.Manufacturer = donor.Manufacturer
	}
	if normalizeRedfishIdentityField(out.Firmware) == "" && normalizeRedfishIdentityField(donor.Firmware) != "" {
		out.Firmware = donor.Firmware
	}
	if strings.TrimSpace(out.Interface) == "" && strings.TrimSpace(donor.Interface) != "" {
		out.Interface = donor.Interface
	}
	if strings.TrimSpace(out.Location) == "" && strings.TrimSpace(donor.Location) != "" {
		out.Location = donor.Location
	}
	if out.BackplaneID == 0 && donor.BackplaneID != 0 {
		out.BackplaneID = donor.BackplaneID
	}
	if strings.TrimSpace(out.Status) == "" && strings.TrimSpace(donor.Status) != "" {
		out.Status = donor.Status
	}
	if strings.TrimSpace(out.Description) == "" && strings.TrimSpace(donor.Description) != "" {
		out.Description = donor.Description
	}
	if !out.Present {
		out.Present = donor.Present
	}
	return out
}

func dedupeNetworkAdapters(items []models.NetworkAdapter) []models.NetworkAdapter {
	if len(items) <= 1 {
		return items
	}
	out := make([]models.NetworkAdapter, 0, len(items))
	bySerial := make(map[string]int, len(items))
	bySlotModel := make(map[string]int, len(items))
	bySlot := make(map[string]int, len(items))

	for _, item := range items {
		serialKey := normalizeRedfishIdentityField(item.SerialNumber)
		slotModelKey := networkAdapterSlotModelKey(item)
		slotKey := strings.TrimSpace(item.Slot)
		idx := -1
		if serialKey != "" {
			if existing, ok := bySerial[serialKey]; ok {
				idx = existing
			}
		}
		if idx < 0 && slotModelKey != "" {
			if existing, ok := bySlotModel[slotModelKey]; ok {
				idx = existing
			}
		}
		if idx < 0 && slotKey != "" {
			if existing, ok := bySlot[slotKey]; ok {
				idx = existing
			}
		}

		if idx >= 0 {
			out[idx] = mergeNetworkAdapterEntries(out[idx], item)
		} else {
			idx = len(out)
			out = append(out, item)
		}

		merged := out[idx]
		if serial := normalizeRedfishIdentityField(merged.SerialNumber); serial != "" {
			bySerial[serial] = idx
		}
		if slotModel := networkAdapterSlotModelKey(merged); slotModel != "" {
			bySlotModel[slotModel] = idx
		}
		if slot := strings.TrimSpace(merged.Slot); slot != "" {
			bySlot[slot] = idx
		}
	}
	return out
}

func networkAdapterSlotModelKey(nic models.NetworkAdapter) string {
	slot := strings.TrimSpace(nic.Slot)
	model := normalizeNetworkAdapterModel(nic)
	if slot == "" && model == "" {
		return ""
	}
	return slot + "|" + model
}

func normalizeNetworkAdapterModel(nic models.NetworkAdapter) string {
	model := normalizeRedfishIdentityField(nic.Model)
	if model == "" {
		return ""
	}
	if isMissingOrRawPCIModel(model) {
		return ""
	}
	slot := strings.TrimSpace(nic.Slot)
	if slot != "" && strings.EqualFold(slot, model) {
		return ""
	}
	return model
}

func networkAdapterRichnessScore(nic models.NetworkAdapter) int {
	score := 0
	if normalizeRedfishIdentityField(nic.SerialNumber) != "" {
		score += 80
	}
	if normalizeNetworkAdapterModel(nic) != "" {
		score += 20
	}
	if normalizeRedfishIdentityField(nic.Vendor) != "" {
		score += 10
	}
	if normalizeRedfishIdentityField(nic.Firmware) != "" {
		score += 8
	}
	if looksLikeCanonicalBDF(strings.TrimSpace(nic.BDF)) {
		score += 10
	}
	if normalizeRedfishIdentityField(nic.PartNumber) != "" {
		score += 6
	}
	if nic.VendorID > 0 {
		score += 5
	}
	if nic.DeviceID > 0 {
		score += 5
	}
	if nic.PortCount > 0 {
		score += 4
	}
	if nic.LinkWidth > 0 || nic.MaxLinkWidth > 0 {
		score += 4
	}
	if strings.TrimSpace(nic.LinkSpeed) != "" || strings.TrimSpace(nic.MaxLinkSpeed) != "" {
		score += 4
	}
	if len(nic.MACAddresses) > 0 {
		score += 4
	}
	if strings.TrimSpace(nic.Location) != "" {
		score += 2
	}
	if nic.Present {
		score++
	}
	return score
}

func mergeNetworkAdapterEntries(a, b models.NetworkAdapter) models.NetworkAdapter {
	base, donor := a, b
	if networkAdapterRichnessScore(donor) > networkAdapterRichnessScore(base) {
		base, donor = donor, base
	}
	out := base
	out.PortCount = sanitizeNetworkPortCount(out.PortCount)
	donor.PortCount = sanitizeNetworkPortCount(donor.PortCount)
	if strings.TrimSpace(out.Slot) == "" && strings.TrimSpace(donor.Slot) != "" {
		out.Slot = donor.Slot
	}
	if strings.TrimSpace(out.Location) == "" && strings.TrimSpace(donor.Location) != "" {
		out.Location = donor.Location
	}
	if strings.TrimSpace(out.BDF) == "" && strings.TrimSpace(donor.BDF) != "" {
		out.BDF = donor.BDF
	}
	if normalizeNetworkAdapterModel(out) == "" && normalizeNetworkAdapterModel(donor) != "" {
		out.Model = donor.Model
	}
	if strings.TrimSpace(out.Description) == "" && strings.TrimSpace(donor.Description) != "" {
		out.Description = donor.Description
	}
	if normalizeRedfishIdentityField(out.Vendor) == "" && normalizeRedfishIdentityField(donor.Vendor) != "" {
		out.Vendor = donor.Vendor
	}
	if out.VendorID == 0 && donor.VendorID != 0 {
		out.VendorID = donor.VendorID
	}
	if out.DeviceID == 0 && donor.DeviceID != 0 {
		out.DeviceID = donor.DeviceID
	}
	if normalizeRedfishIdentityField(out.SerialNumber) == "" && normalizeRedfishIdentityField(donor.SerialNumber) != "" {
		out.SerialNumber = donor.SerialNumber
	}
	if normalizeRedfishIdentityField(out.PartNumber) == "" && normalizeRedfishIdentityField(donor.PartNumber) != "" {
		out.PartNumber = donor.PartNumber
	}
	if normalizeRedfishIdentityField(out.Firmware) == "" && normalizeRedfishIdentityField(donor.Firmware) != "" {
		out.Firmware = donor.Firmware
	}
	if out.PortCount == 0 && donor.PortCount > 0 {
		out.PortCount = donor.PortCount
	}
	if strings.TrimSpace(out.PortType) == "" && strings.TrimSpace(donor.PortType) != "" {
		out.PortType = donor.PortType
	}
	if out.LinkWidth == 0 && donor.LinkWidth > 0 {
		out.LinkWidth = donor.LinkWidth
	}
	if strings.TrimSpace(out.LinkSpeed) == "" && strings.TrimSpace(donor.LinkSpeed) != "" {
		out.LinkSpeed = donor.LinkSpeed
	}
	if out.MaxLinkWidth == 0 && donor.MaxLinkWidth > 0 {
		out.MaxLinkWidth = donor.MaxLinkWidth
	}
	if strings.TrimSpace(out.MaxLinkSpeed) == "" && strings.TrimSpace(donor.MaxLinkSpeed) != "" {
		out.MaxLinkSpeed = donor.MaxLinkSpeed
	}
	if strings.TrimSpace(out.Status) == "" && strings.TrimSpace(donor.Status) != "" {
		out.Status = donor.Status
	}
	out.Present = out.Present || donor.Present
	if len(donor.MACAddresses) > 0 {
		out.MACAddresses = dedupeStrings(append(append([]string{}, out.MACAddresses...), donor.MACAddresses...))
	}
	out.Details = mergeGenericDetails(out.Details, donor.Details)
	return out
}

const maxReasonableNetworkPortCount = 256

func sanitizeNetworkPortCount(v int) int {
	if v <= 0 || v > maxReasonableNetworkPortCount {
		return 0
	}
	return v
}

func dedupePCIeDevices(items []models.PCIeDevice) []models.PCIeDevice {
	if len(items) <= 1 {
		return items
	}
	out := make([]models.PCIeDevice, 0, len(items))
	byPrimary := make(map[string]int, len(items))
	byLoose := make(map[string]int, len(items))

	for _, item := range items {
		primaryKey := pcieDeviceDedupKey(item)
		looseKey := pcieDeviceLooseKey(item)
		idx := -1
		if primaryKey != "" {
			if existing, ok := byPrimary[primaryKey]; ok {
				idx = existing
			}
		}
		if idx < 0 && looseKey != "" {
			if existing, ok := byLoose[looseKey]; ok {
				idx = existing
			}
		}

		if idx >= 0 {
			out[idx] = mergePCIeDeviceEntries(out[idx], item)
		} else {
			idx = len(out)
			out = append(out, item)
		}

		merged := out[idx]
		if k := pcieDeviceDedupKey(merged); k != "" {
			byPrimary[k] = idx
		}
		if k := pcieDeviceLooseKey(merged); k != "" {
			byLoose[k] = idx
		}
	}
	return out
}

func pcieDeviceLooseKey(dev models.PCIeDevice) string {
	return firstNonEmpty(
		strings.TrimSpace(dev.Slot)+"|"+strings.TrimSpace(dev.PartNumber)+"|"+strings.TrimSpace(dev.DeviceClass),
		strings.TrimSpace(dev.Slot)+"|"+strings.TrimSpace(dev.DeviceClass),
		strings.TrimSpace(dev.PartNumber)+"|"+strings.TrimSpace(dev.DeviceClass),
		strings.TrimSpace(dev.Description)+"|"+strings.TrimSpace(dev.DeviceClass),
	)
}

func pcieDeviceRichnessScore(dev models.PCIeDevice) int {
	score := 0
	if bdf := strings.TrimSpace(dev.BDF); looksLikeCanonicalBDF(bdf) {
		score += 120
	}
	if normalizeRedfishIdentityField(dev.SerialNumber) != "" {
		score += 80
	}
	if normalizeRedfishIdentityField(dev.PartNumber) != "" {
		score += 20
	}
	if normalizeRedfishIdentityField(dev.Manufacturer) != "" {
		score += 10
	}
	if dev.VendorID > 0 {
		score += 8
	}
	if dev.DeviceID > 0 {
		score += 8
	}
	if !isGenericPCIeClassLabel(dev.DeviceClass) {
		score += 8
	}
	if dev.LinkWidth > 0 || dev.MaxLinkWidth > 0 {
		score += 6
	}
	if strings.TrimSpace(dev.LinkSpeed) != "" || strings.TrimSpace(dev.MaxLinkSpeed) != "" {
		score += 6
	}
	if strings.TrimSpace(dev.Description) != "" {
		score += 3
	}
	if strings.TrimSpace(dev.Slot) != "" {
		score += 2
	}
	return score
}

func mergePCIeDeviceEntries(a, b models.PCIeDevice) models.PCIeDevice {
	base, donor := a, b
	if pcieDeviceRichnessScore(donor) > pcieDeviceRichnessScore(base) {
		base, donor = donor, base
	}
	out := base
	if strings.TrimSpace(out.Slot) == "" && strings.TrimSpace(donor.Slot) != "" {
		out.Slot = donor.Slot
	}
	if strings.TrimSpace(out.Description) == "" && strings.TrimSpace(donor.Description) != "" {
		out.Description = donor.Description
	}
	if out.VendorID == 0 && donor.VendorID != 0 {
		out.VendorID = donor.VendorID
	}
	if out.DeviceID == 0 && donor.DeviceID != 0 {
		out.DeviceID = donor.DeviceID
	}
	if strings.TrimSpace(out.BDF) == "" && strings.TrimSpace(donor.BDF) != "" {
		out.BDF = donor.BDF
	}
	if isGenericPCIeClassLabel(out.DeviceClass) && !isGenericPCIeClassLabel(donor.DeviceClass) {
		out.DeviceClass = donor.DeviceClass
	}
	if normalizeRedfishIdentityField(out.Manufacturer) == "" && normalizeRedfishIdentityField(donor.Manufacturer) != "" {
		out.Manufacturer = donor.Manufacturer
	}
	if out.LinkWidth == 0 && donor.LinkWidth > 0 {
		out.LinkWidth = donor.LinkWidth
	}
	if strings.TrimSpace(out.LinkSpeed) == "" && strings.TrimSpace(donor.LinkSpeed) != "" {
		out.LinkSpeed = donor.LinkSpeed
	}
	if out.MaxLinkWidth == 0 && donor.MaxLinkWidth > 0 {
		out.MaxLinkWidth = donor.MaxLinkWidth
	}
	if strings.TrimSpace(out.MaxLinkSpeed) == "" && strings.TrimSpace(donor.MaxLinkSpeed) != "" {
		out.MaxLinkSpeed = donor.MaxLinkSpeed
	}
	if normalizeRedfishIdentityField(out.PartNumber) == "" && normalizeRedfishIdentityField(donor.PartNumber) != "" {
		out.PartNumber = donor.PartNumber
	}
	if normalizeRedfishIdentityField(out.SerialNumber) == "" && normalizeRedfishIdentityField(donor.SerialNumber) != "" {
		out.SerialNumber = donor.SerialNumber
	}
	if strings.TrimSpace(out.Status) == "" && strings.TrimSpace(donor.Status) != "" {
		out.Status = donor.Status
	}
	if len(donor.MACAddresses) > 0 {
		out.MACAddresses = dedupeStrings(append(append([]string{}, out.MACAddresses...), donor.MACAddresses...))
	}
	out.Details = mergeGenericDetails(out.Details, donor.Details)
	return out
}

func psuIdentityKeys(psu models.PSU) []string {
	keys := make([]string, 0, 3)
	if serial := normalizeRedfishIdentityField(psu.SerialNumber); serial != "" {
		keys = append(keys, "sn:"+serial)
	}
	slot := strings.TrimSpace(psu.Slot)
	model := strings.TrimSpace(psu.Model)
	if slot != "" && model != "" {
		keys = append(keys, "slotmodel:"+slot+"|"+model)
	}
	if slot != "" {
		keys = append(keys, "slot:"+slot)
	}
	if len(keys) == 0 && model != "" {
		keys = append(keys, "model:"+model)
	}
	return keys
}

func psuRichnessScore(psu models.PSU) int {
	score := 0
	if normalizeRedfishIdentityField(psu.SerialNumber) != "" {
		score += 100
	}
	if normalizeRedfishIdentityField(psu.Model) != "" {
		score += 20
	}
	if psu.WattageW > 0 {
		score += 20
	}
	if normalizeRedfishIdentityField(psu.Vendor) != "" {
		score += 8
	}
	if normalizeRedfishIdentityField(psu.PartNumber) != "" {
		score += 8
	}
	if normalizeRedfishIdentityField(psu.Firmware) != "" {
		score += 8
	}
	if psu.InputPowerW > 0 || psu.OutputPowerW > 0 {
		score += 6
	}
	if psu.InputVoltage > 0 {
		score += 4
	}
	if psu.Present {
		score++
	}
	return score
}

func mergePSUEntries(a, b models.PSU) models.PSU {
	base, donor := a, b
	if psuRichnessScore(donor) > psuRichnessScore(base) {
		base, donor = donor, base
	}
	out := base
	if strings.TrimSpace(out.Slot) == "" && strings.TrimSpace(donor.Slot) != "" {
		out.Slot = donor.Slot
	}
	out.Present = out.Present || donor.Present
	if normalizeRedfishIdentityField(out.Model) == "" && normalizeRedfishIdentityField(donor.Model) != "" {
		out.Model = donor.Model
	}
	if strings.TrimSpace(out.Description) == "" && strings.TrimSpace(donor.Description) != "" {
		out.Description = donor.Description
	}
	if normalizeRedfishIdentityField(out.Vendor) == "" && normalizeRedfishIdentityField(donor.Vendor) != "" {
		out.Vendor = donor.Vendor
	}
	if out.WattageW == 0 && donor.WattageW > 0 {
		out.WattageW = donor.WattageW
	}
	if normalizeRedfishIdentityField(out.SerialNumber) == "" && normalizeRedfishIdentityField(donor.SerialNumber) != "" {
		out.SerialNumber = donor.SerialNumber
	}
	if normalizeRedfishIdentityField(out.PartNumber) == "" && normalizeRedfishIdentityField(donor.PartNumber) != "" {
		out.PartNumber = donor.PartNumber
	}
	if normalizeRedfishIdentityField(out.Firmware) == "" && normalizeRedfishIdentityField(donor.Firmware) != "" {
		out.Firmware = donor.Firmware
	}
	if strings.TrimSpace(out.Status) == "" && strings.TrimSpace(donor.Status) != "" {
		out.Status = donor.Status
	}
	if strings.TrimSpace(out.InputType) == "" && strings.TrimSpace(donor.InputType) != "" {
		out.InputType = donor.InputType
	}
	if out.InputPowerW == 0 && donor.InputPowerW > 0 {
		out.InputPowerW = donor.InputPowerW
	}
	if out.OutputPowerW == 0 && donor.OutputPowerW > 0 {
		out.OutputPowerW = donor.OutputPowerW
	}
	if out.InputVoltage == 0 && donor.InputVoltage > 0 {
		out.InputVoltage = donor.InputVoltage
	}
	if out.OutputVoltage == 0 && donor.OutputVoltage > 0 {
		out.OutputVoltage = donor.OutputVoltage
	}
	if out.TemperatureC == 0 && donor.TemperatureC > 0 {
		out.TemperatureC = donor.TemperatureC
	}
	out.Details = mergeGenericDetails(out.Details, donor.Details)
	return out
}

func mergeGenericDetails(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 dedupeStorageVolumes(items []models.StorageVolume) []models.StorageVolume {
	seen := make(map[string]struct{}, len(items))
	out := make([]models.StorageVolume, 0, len(items))
	for _, v := range items {
		key := firstNonEmpty(strings.TrimSpace(v.ID), strings.TrimSpace(v.Name), strings.TrimSpace(v.Controller)+"|"+fmt.Sprintf("%d", v.CapacityBytes))
		if key == "" {
			continue
		}
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, v)
	}
	return out
}

func storageControllerFromPath(path string) string {
	p := normalizeRedfishPath(path)
	parts := strings.Split(p, "/")
	for i := 0; i < len(parts)-1; i++ {
		if parts[i] == "Storage" && i+1 < len(parts) {
			return parts[i+1]
		}
	}
	return ""
}

func parseFirmware(system, bios, manager, networkProtocol map[string]interface{}) []models.FirmwareInfo {
	var out []models.FirmwareInfo

	appendFW := func(name, version string) {
		version = strings.TrimSpace(version)
		if version == "" {
			return
		}
		out = append(out, models.FirmwareInfo{DeviceName: name, Version: version})
	}

	appendFW("BIOS", asString(system["BiosVersion"]))
	appendFW("BIOS", asString(bios["Version"]))
	appendFW("BMC", asString(manager["FirmwareVersion"]))

	return out
}

func mapStatus(statusAny interface{}) string {
	if statusAny == nil {
		return ""
	}
	if statusMap, ok := statusAny.(map[string]interface{}); ok {
		health := asString(statusMap["Health"])
		state := asString(statusMap["State"])
		return firstNonEmpty(health, state)
	}
	return asString(statusAny)
}

func asString(v interface{}) string {
	switch value := v.(type) {
	case nil:
		return ""
	case string:
		return strings.TrimSpace(value)
	case json.Number:
		return value.String()
	default:
		return strings.TrimSpace(fmt.Sprintf("%v", value))
	}
}

func asInt(v interface{}) int {
	switch value := v.(type) {
	case nil:
		return 0
	case int:
		return value
	case int64:
		return int(value)
	case float64:
		return int(value)
	case json.Number:
		if i, err := value.Int64(); err == nil {
			return int(i)
		}
		if f, err := value.Float64(); err == nil {
			return int(f)
		}
	case string:
		if value == "" {
			return 0
		}
		if i, err := strconv.Atoi(value); err == nil {
			return i
		}
	}
	return 0
}

func asInt64(v interface{}) int64 {
	switch value := v.(type) {
	case nil:
		return 0
	case int:
		return int64(value)
	case int64:
		return value
	case float64:
		return int64(value)
	case json.Number:
		if i, err := value.Int64(); err == nil {
			return i
		}
		if f, err := value.Float64(); err == nil {
			return int64(f)
		}
	case string:
		if value == "" {
			return 0
		}
		if i, err := strconv.ParseInt(value, 10, 64); err == nil {
			return i
		}
	}
	return 0
}

func asBool(v interface{}) bool {
	switch t := v.(type) {
	case bool:
		return t
	case string:
		return strings.EqualFold(strings.TrimSpace(t), "true")
	default:
		return false
	}
}

func asFloat(v interface{}) float64 {
	switch value := v.(type) {
	case nil:
		return 0
	case float64:
		return value
	case int:
		return float64(value)
	case int64:
		return float64(value)
	case json.Number:
		if f, err := value.Float64(); err == nil {
			return f
		}
	case string:
		if value == "" {
			return 0
		}
		if f, err := strconv.ParseFloat(value, 64); err == nil {
			return f
		}
	}
	return 0
}

func asHexOrInt(v interface{}) int {
	switch value := v.(type) {
	case nil:
		return 0
	case int:
		return value
	case int64:
		return int(value)
	case float64:
		return int(value)
	case json.Number:
		if i, err := value.Int64(); err == nil {
			return int(i)
		}
		if f, err := value.Float64(); err == nil {
			return int(f)
		}
	case string:
		s := strings.TrimSpace(value)
		s = strings.TrimPrefix(strings.ToLower(s), "0x")
		if s == "" {
			return 0
		}
		if i, err := strconv.ParseInt(s, 16, 64); err == nil {
			return int(i)
		}
		if i, err := strconv.Atoi(s); err == nil {
			return i
		}
	}
	return 0
}

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

func joinPath(base, suffix string) string {
	base = strings.TrimRight(base, "/")
	if suffix == "" {
		return base
	}
	if strings.HasPrefix(suffix, "/") {
		return base + suffix
	}
	return base + "/" + suffix
}

func firstPathOrDefault(paths []string, fallback string) string {
	if len(paths) == 0 {
		return fallback
	}
	return paths[0]
}

func normalizeRedfishPath(raw string) string {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return ""
	}
	if i := strings.Index(raw, "#"); i >= 0 {
		raw = raw[:i]
	}

	if strings.HasPrefix(raw, "http://") || strings.HasPrefix(raw, "https://") {
		u, err := url.Parse(raw)
		if err != nil {
			return ""
		}
		raw = u.Path
	}

	if !strings.HasPrefix(raw, "/") {
		raw = "/" + raw
	}
	if !strings.HasPrefix(raw, "/redfish/") {
		return ""
	}
	return raw
}

func redfishCollectionMemberRefs(collection map[string]interface{}) []string {
	if len(collection) == 0 {
		return nil
	}
	var out []string
	seen := make(map[string]struct{})
	addRefs := func(raw any) {
		refs, ok := raw.([]interface{})
		if !ok || len(refs) == 0 {
			return
		}
		for _, refAny := range refs {
			ref, ok := refAny.(map[string]interface{})
			if !ok {
				continue
			}
			memberPath := normalizeRedfishPath(asString(ref["@odata.id"]))
			if memberPath == "" {
				continue
			}
			if _, exists := seen[memberPath]; exists {
				continue
			}
			seen[memberPath] = struct{}{}
			out = append(out, memberPath)
		}
	}
	addRefs(collection["Members"])
	if oem, ok := collection["Oem"].(map[string]interface{}); ok {
		if public, ok := oem["Public"].(map[string]interface{}); ok {
			addRefs(public["Members"])
		}
	}
	return out
}

func extractODataIDs(v interface{}) []string {
	var refs []string
	var walk func(any)
	walk = func(node any) {
		switch typed := node.(type) {
		case map[string]interface{}:
			for k, child := range typed {
				if k == "@odata.id" {
					if ref := asString(child); ref != "" {
						refs = append(refs, ref)
					}
					continue
				}
				walk(child)
			}
		case []interface{}:
			for _, child := range typed {
				walk(child)
			}
		}
	}
	walk(v)
	return refs
}

func redfishTopRoot(path string) string {
	path = strings.TrimPrefix(path, "/")
	parts := strings.Split(path, "/")
	if len(parts) < 3 {
		return "root"
	}
	return parts[2]
}

func topRoots(counts map[string]int, limit int) []string {
	if len(counts) == 0 {
		return []string{"n/a"}
	}
	type rootCount struct {
		root  string
		count int
	}
	items := make([]rootCount, 0, len(counts))
	for root, count := range counts {
		items = append(items, rootCount{root: root, count: count})
	}
	sort.Slice(items, func(i, j int) bool {
		return items[i].count > items[j].count
	})
	if len(items) > limit {
		items = items[:limit]
	}
	out := make([]string, 0, len(items))
	for _, item := range items {
		out = append(out, fmt.Sprintf("%s(%d)", item.root, item.count))
	}
	return out
}

func redfishLocationLabel(v interface{}) string {
	switch typed := v.(type) {
	case nil:
		return ""
	case string:
		return strings.TrimSpace(typed)
	case map[string]interface{}:
		// Common shapes:
		// Slot.Location.PartLocation.ServiceLabel
		// Location.PartLocation.ServiceLabel
		// PartLocation.ServiceLabel
		if nested := redfishLocationLabel(typed["Location"]); nested != "" {
			return nested
		}
		if nested := redfishLocationLabel(typed["PartLocation"]); nested != "" {
			return nested
		}
		serviceLabel := asString(typed["ServiceLabel"])
		locationType := asString(typed["LocationType"])
		ordinal := asString(typed["LocationOrdinalValue"])
		if serviceLabel != "" {
			return serviceLabel
		}
		if locationType != "" && ordinal != "" {
			return fmt.Sprintf("%s %s", locationType, ordinal)
		}
		if locationType != "" {
			return locationType
		}
		if ordinal != "" {
			return "Slot " + ordinal
		}
		return ""
	default:
		// Avoid fmt.Sprint(map[]) style garbage for complex objects in UI/export.
		return ""
	}
}

type redfishPathTiming struct {
	Path     string
	Duration time.Duration
	Requests int
	Errors   int
}

type redfishPathTimingCollector struct {
	depth int
	mu    sync.Mutex
	byKey map[string]redfishPathTiming
}

func newRedfishPathTimingCollector(depth int) *redfishPathTimingCollector {
	if depth < 1 {
		depth = 1
	}
	return &redfishPathTimingCollector{
		depth: depth,
		byKey: make(map[string]redfishPathTiming),
	}
}

func (c *redfishPathTimingCollector) Observe(path string, d time.Duration, failed bool) {
	if c == nil {
		return
	}
	key := redfishBranchPathForTiming(path, c.depth)
	if key == "" {
		return
	}
	c.mu.Lock()
	item := c.byKey[key]
	item.Path = key
	item.Duration += d
	item.Requests++
	if failed {
		item.Errors++
	}
	c.byKey[key] = item
	c.mu.Unlock()
}

func (c *redfishPathTimingCollector) Summary(limit int) string {
	if c == nil || limit == 0 {
		return ""
	}
	c.mu.Lock()
	items := make([]redfishPathTiming, 0, len(c.byKey))
	for _, item := range c.byKey {
		items = append(items, item)
	}
	c.mu.Unlock()
	if len(items) == 0 {
		return ""
	}
	sort.Slice(items, func(i, j int) bool {
		if items[i].Duration == items[j].Duration {
			if items[i].Requests == items[j].Requests {
				return items[i].Path < items[j].Path
			}
			return items[i].Requests > items[j].Requests
		}
		return items[i].Duration > items[j].Duration
	})
	if limit < 0 || limit > len(items) {
		limit = len(items)
	}
	parts := make([]string, 0, limit)
	for i := 0; i < limit; i++ {
		item := items[i]
		parts = append(parts, fmt.Sprintf("%s=%s(req=%d,err=%d)", item.Path, item.Duration.Round(time.Millisecond), item.Requests, item.Errors))
	}
	return strings.Join(parts, "; ")
}

func redfishBranchPathForTiming(path string, depth int) string {
	normalized := normalizeRedfishPath(path)
	if normalized == "" {
		return ""
	}
	parts := strings.Split(strings.Trim(normalized, "/"), "/")
	if len(parts) < 2 || parts[0] != "redfish" || parts[1] != "v1" {
		return normalized
	}
	if depth < 1 {
		depth = 1
	}
	maxParts := 2 + depth
	if len(parts) > maxParts {
		parts = parts[:maxParts]
	}
	return "/" + strings.Join(parts, "/")
}

func compactProgressPath(p string) string {
	const maxLen = 72
	if len(p) <= maxLen {
		return p
	}
	return "..." + p[len(p)-maxLen+3:]
}

func boolPointerValue(v *bool) interface{} {
	if v == nil {
		return nil
	}
	return *v
}

func redfishSnapshotMaxDocuments(tuning redfishprofile.AcquisitionTuning) int {
	// Default is intentionally high enough to capture vendor-specific PCIe/GPU trees
	// on modern HGX-class systems while staying within memory budgets of a typical
	// developer workstation.
	const (
		def = 100000
		min = 1200
		max = 500000
	)
	if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_SNAPSHOT_MAX_DOCS")); v != "" {
		if n, err := strconv.Atoi(v); err == nil {
			if n < min {
				return min
			}
			if n > max {
				return max
			}
			return n
		}
	}
	if tuning.SnapshotMaxDocuments > 0 {
		if tuning.SnapshotMaxDocuments < min {
			return min
		}
		if tuning.SnapshotMaxDocuments > max {
			return max
		}
		return tuning.SnapshotMaxDocuments
	}
	return def
}

func formatActiveModuleLog(modules []ModuleActivation) string {
	if len(modules) == 0 {
		return "-"
	}
	parts := make([]string, 0, len(modules))
	for _, module := range modules {
		parts = append(parts, fmt.Sprintf("%s(%d)", module.Name, module.Score))
	}
	return strings.Join(parts, ",")
}

func formatModuleScoreLog(scores []ModuleScore) string {
	if len(scores) == 0 {
		return "-"
	}
	parts := make([]string, 0, len(scores))
	for _, score := range scores {
		state := "inactive"
		if score.Active {
			state = "active"
		}
		parts = append(parts, fmt.Sprintf("%s=%d[%s]", score.Name, score.Score, state))
	}
	return strings.Join(parts, ",")
}

func newRedfishRequestTelemetry() *redfishRequestTelemetry {
	return &redfishRequestTelemetry{
		overall: redfishPhaseTelemetry{
			durations: make([]time.Duration, 0, 128),
		},
		byPhase: make(map[string]*redfishPhaseTelemetry),
	}
}

func withRedfishTelemetryPhase(ctx context.Context, phase string) context.Context {
	if strings.TrimSpace(phase) == "" {
		return ctx
	}
	return context.WithValue(ctx, redfishTelemetryPhaseContextKey{}, phase)
}

func recordRedfishTelemetry(ctx context.Context, duration time.Duration, failed bool) {
	telemetry, _ := ctx.Value(redfishTelemetryContextKey{}).(*redfishRequestTelemetry)
	if telemetry == nil {
		return
	}
	phase, _ := ctx.Value(redfishTelemetryPhaseContextKey{}).(string)
	telemetry.Observe(phase, duration, failed)
}

func (t *redfishRequestTelemetry) Observe(phase string, duration time.Duration, failed bool) {
	if t == nil {
		return
	}
	t.mu.Lock()
	defer t.mu.Unlock()
	observeRedfishPhaseTelemetry(&t.overall, duration, failed)
	phase = strings.TrimSpace(phase)
	if phase == "" {
		return
	}
	phaseTelemetry := t.byPhase[phase]
	if phaseTelemetry == nil {
		phaseTelemetry = &redfishPhaseTelemetry{durations: make([]time.Duration, 0, 64)}
		t.byPhase[phase] = phaseTelemetry
	}
	observeRedfishPhaseTelemetry(phaseTelemetry, duration, failed)
}

func (t *redfishRequestTelemetry) Snapshot() redfishTelemetrySummary {
	if t == nil {
		return redfishTelemetrySummary{}
	}
	t.mu.Lock()
	defer t.mu.Unlock()
	return snapshotRedfishPhaseTelemetry(&t.overall)
}

func (t *redfishRequestTelemetry) PhaseSnapshots() map[string]redfishTelemetrySummary {
	if t == nil {
		return nil
	}
	t.mu.Lock()
	defer t.mu.Unlock()
	out := make(map[string]redfishTelemetrySummary, len(t.byPhase))
	for phase, telemetry := range t.byPhase {
		out[phase] = snapshotRedfishPhaseTelemetry(telemetry)
	}
	return out
}

func observeRedfishPhaseTelemetry(t *redfishPhaseTelemetry, duration time.Duration, failed bool) {
	if t == nil {
		return
	}
	t.requests++
	if failed {
		t.errors++
	}
	t.durations = append(t.durations, duration)
	if len(t.durations) > 512 {
		t.durations = t.durations[len(t.durations)-512:]
	}
	t.lastAvg, t.lastP95 = summarizeRedfishDurations(t.durations)
}

func snapshotRedfishPhaseTelemetry(t *redfishPhaseTelemetry) redfishTelemetrySummary {
	if t == nil {
		return redfishTelemetrySummary{}
	}
	out := redfishTelemetrySummary{
		Requests: t.requests,
		Errors:   t.errors,
		Avg:      t.lastAvg,
		P95:      t.lastP95,
	}
	if t.requests > 0 {
		out.ErrorRate = float64(t.errors) / float64(t.requests)
	}
	return out
}

func summarizeRedfishDurations(durations []time.Duration) (time.Duration, time.Duration) {
	if len(durations) == 0 {
		return 0, 0
	}
	total := time.Duration(0)
	items := append([]time.Duration(nil), durations...)
	for _, duration := range items {
		total += duration
	}
	sort.Slice(items, func(i, j int) bool { return items[i] < items[j] })
	p95Idx := (len(items)*95 - 1) / 100
	if p95Idx < 0 {
		p95Idx = 0
	}
	if p95Idx >= len(items) {
		p95Idx = len(items) - 1
	}
	return total / time.Duration(len(items)), items[p95Idx]
}

func adaptRedfishAcquisitionTuning(tuning redfishprofile.AcquisitionTuning, summary redfishTelemetrySummary) (redfishprofile.AcquisitionTuning, bool) {
	if summary.Requests < 4 {
		return tuning, false
	}
	policy := tuning.RatePolicy
	shouldThrottle := false
	if policy.ThrottleP95LatencyMS > 0 && summary.P95 >= time.Duration(policy.ThrottleP95LatencyMS)*time.Millisecond {
		shouldThrottle = true
	}
	if policy.DisablePrefetchOnErrors && summary.ErrorRate >= 0.20 {
		shouldThrottle = true
		if tuning.PrefetchEnabled == nil {
			tuning.PrefetchEnabled = new(bool)
		}
		*tuning.PrefetchEnabled = false
	}
	if !shouldThrottle {
		return tuning, false
	}
	if tuning.SnapshotWorkers == 0 {
		tuning.SnapshotWorkers = redfishSnapshotWorkers(redfishprofile.AcquisitionTuning{})
	}
	if tuning.SnapshotWorkers > 1 {
		tuning.SnapshotWorkers = maxInt(policy.MinSnapshotWorkers, tuning.SnapshotWorkers/2)
	}
	if tuning.PrefetchWorkers > 1 {
		tuning.PrefetchWorkers = maxInt(policy.MinPrefetchWorkers, tuning.PrefetchWorkers/2)
	}
	return tuning, true
}

func redfishPhaseTelemetryPayload(phases map[string]redfishTelemetrySummary) map[string]any {
	if len(phases) == 0 {
		return nil
	}
	keys := make([]string, 0, len(phases))
	for phase := range phases {
		keys = append(keys, phase)
	}
	sort.Strings(keys)
	out := make(map[string]any, len(keys))
	for _, phase := range keys {
		summary := phases[phase]
		out[phase] = map[string]any{
			"requests":   summary.Requests,
			"errors":     summary.Errors,
			"error_rate": summary.ErrorRate,
			"avg_ms":     summary.Avg.Milliseconds(),
			"p95_ms":     summary.P95.Milliseconds(),
		}
	}
	return out
}

func redfishPhaseTelemetryLogLines(phases map[string]redfishTelemetrySummary) []string {
	if len(phases) == 0 {
		return nil
	}
	keys := make([]string, 0, len(phases))
	for phase := range phases {
		keys = append(keys, phase)
	}
	sort.Strings(keys)
	lines := make([]string, 0, len(keys))
	for _, phase := range keys {
		summary := phases[phase]
		lines = append(lines, fmt.Sprintf("%s req=%d err=%d err_rate=%.2f avg=%dms p95=%dms",
			phase,
			summary.Requests,
			summary.Errors,
			summary.ErrorRate,
			summary.Avg.Milliseconds(),
			summary.P95.Milliseconds(),
		))
	}
	return lines
}

func buildCollectPhaseTelemetry(phases map[string]redfishTelemetrySummary) []PhaseTelemetry {
	if len(phases) == 0 {
		return nil
	}
	keys := make([]string, 0, len(phases))
	for phase := range phases {
		keys = append(keys, phase)
	}
	sort.Strings(keys)
	out := make([]PhaseTelemetry, 0, len(keys))
	for _, phase := range keys {
		summary := phases[phase]
		out = append(out, PhaseTelemetry{
			Phase:     phase,
			Requests:  summary.Requests,
			Errors:    summary.Errors,
			ErrorRate: summary.ErrorRate,
			AvgMS:     summary.Avg.Milliseconds(),
			P95MS:     summary.P95.Milliseconds(),
		})
	}
	return out
}

func shouldReportSnapshotFetchError(err error) bool {
	if err == nil {
		return false
	}
	msg := err.Error()
	if strings.HasPrefix(msg, "status 404 ") ||
		strings.HasPrefix(msg, "status 405 ") ||
		strings.HasPrefix(msg, "status 410 ") ||
		strings.HasPrefix(msg, "status 501 ") {
		return false
	}
	return true
}

func minInt32(a, b int32) int32 {
	if a < b {
		return a
	}
	return b
}

func maxInt(values ...int) int {
	if len(values) == 0 {
		return 0
	}
	max := values[0]
	for _, v := range values[1:] {
		if v > max {
			max = v
		}
	}
	return max
}

func estimateSnapshotETA(start time.Time, processed, seen, queueLen, workers int, requestTimeout time.Duration) time.Duration {
	remaining := maxInt(seen-processed, queueLen, 0)
	if remaining == 0 {
		return 0
	}
	if workers <= 0 {
		workers = 1
	}
	if requestTimeout <= 0 {
		requestTimeout = 10 * time.Second
	}

	timeoutBased := time.Duration(float64(requestTimeout) * float64(remaining) / float64(workers))
	if processed <= 0 {
		return timeoutBased
	}

	elapsed := time.Since(start)
	if elapsed <= 0 {
		return timeoutBased
	}
	rateBased := time.Duration(float64(elapsed) * float64(remaining) / float64(processed))
	if rateBased <= 0 {
		return timeoutBased
	}
	// Blend observed throughput with configured per-request timeout to keep ETA stable
	// and still bounded by timeout assumptions on slower Redfish branches.
	return (rateBased + timeoutBased) / 2
}

func estimatePlanBETA(targets int) time.Duration {
	if targets <= 0 {
		return 0
	}
	attempts := redfishCriticalPlanBAttempts()
	if attempts < 1 {
		attempts = 1
	}
	timeoutPart := time.Duration(attempts) * redfishCriticalRequestTimeout()
	backoffPart := time.Duration(attempts-1) * redfishCriticalRetryBackoff()
	gapPart := redfishCriticalSlowGap()
	perTarget := timeoutPart + backoffPart + gapPart
	return time.Duration(targets) * perTarget
}

func estimateProgressETA(start time.Time, processed, total int, fallbackPerItem time.Duration) time.Duration {
	if total <= 0 || processed >= total {
		return 0
	}
	remaining := total - processed
	if processed <= 0 {
		if fallbackPerItem <= 0 {
			fallbackPerItem = time.Second
		}
		return time.Duration(remaining) * fallbackPerItem
	}
	elapsed := time.Since(start)
	if elapsed <= 0 {
		return 0
	}
	return time.Duration(float64(elapsed) * float64(remaining) / float64(processed))
}

func formatETA(d time.Duration) string {
	if d <= 0 {
		return "<1s"
	}
	if d < time.Second {
		return "<1s"
	}
	if d < time.Minute {
		return fmt.Sprintf("%ds", int(d.Round(time.Second).Seconds()))
	}
	totalSec := int(d.Round(time.Second).Seconds())
	hours := totalSec / 3600
	minutes := (totalSec % 3600) / 60
	seconds := totalSec % 60
	if hours > 0 {
		return fmt.Sprintf("%dh%02dm%02ds", hours, minutes, seconds)
	}
	return fmt.Sprintf("%dm%02ds", minutes, seconds)
}
