package xfusion

import (
	"fmt"
	"strconv"
	"strings"
	"time"

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

type xfusionNICCard struct {
	Slot         string
	Model        string
	ProductName  string
	Vendor       string
	VendorID     int
	DeviceID     int
	BDF          string
	SerialNumber string
	PartNumber   string
}

type xfusionNetcardPort struct {
	BDF       string
	MAC       string
	ActualMAC string
}

type xfusionNetcardSnapshot struct {
	Timestamp    time.Time
	Slot         string
	ProductName  string
	Manufacturer string
	Firmware     string
	Ports        []xfusionNetcardPort
}

// ── FRU ──────────────────────────────────────────────────────────────────────

// parseFRUInfo parses fruinfo.txt and populates result.FRU and result.Hardware.BoardInfo.
// The file contains IPMI FRU blocks separated by "FRU Device Description" header lines.
func parseFRUInfo(content []byte, result *models.AnalysisResult) {
	type fruBlock struct {
		header string
		fields map[string]string
	}
	var blocks []fruBlock
	var current *fruBlock

	for _, line := range strings.Split(string(content), "\n") {
		trimmed := strings.TrimSpace(line)
		if strings.HasPrefix(trimmed, "FRU Device Description") {
			if current != nil {
				blocks = append(blocks, *current)
			}
			current = &fruBlock{header: trimmed, fields: make(map[string]string)}
			continue
		}
		if current == nil {
			continue
		}
		idx := strings.Index(trimmed, " : ")
		if idx < 0 {
			continue
		}
		key := strings.TrimSpace(trimmed[:idx])
		val := strings.TrimSpace(trimmed[idx+3:])
		if key != "" && current.fields[key] == "" {
			current.fields[key] = val
		}
	}
	if current != nil {
		blocks = append(blocks, *current)
	}

	for _, b := range blocks {
		f := b.fields
		fru := models.FRUInfo{
			Description:  extractFRUHeaderDesc(b.header),
			Manufacturer: firstNonEmpty(f["Board Manufacturer"], f["Product Manufacturer"]),
			ProductName:  f["Product Name"],
			SerialNumber: firstNonEmpty(f["Product Serial Number"], f["Board Serial Number"]),
			PartNumber:   firstNonEmpty(f["Product Part Number"], f["Board Part Number"]),
			MfgDate:      f["Board Mfg. Date"],
		}
		if fru.Description != "" || fru.ProductName != "" || fru.SerialNumber != "" {
			result.FRU = append(result.FRU, fru)
		}
	}

	// Set BoardInfo from the mainboard block (ID 0).
	for _, b := range blocks {
		hdr := strings.ToLower(b.header)
		if strings.Contains(hdr, "id 0") || strings.Contains(hdr, "mainboard") {
			f := b.fields
			result.Hardware.BoardInfo = models.BoardInfo{
				Manufacturer: firstNonEmpty(f["Product Manufacturer"], f["Board Manufacturer"]),
				ProductName:  firstNonEmpty(f["Product Name"], f["Board Product Name"]),
				SerialNumber: firstNonEmpty(f["Product Serial Number"], f["Board Serial Number"]),
				PartNumber:   firstNonEmpty(f["Product Part Number"], f["Board Part Number"]),
			}
			break
		}
	}
}

func extractFRUHeaderDesc(header string) string {
	// "FRU Device Description : Builtin FRU Device (ID 0, Mainboard)"
	idx := strings.Index(header, " : ")
	if idx >= 0 {
		return strings.TrimSpace(header[idx+3:])
	}
	return header
}

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

// ── Sensors ───────────────────────────────────────────────────────────────────

// parseSensorInfo parses the pipe-delimited IPMI sensor table from sensor_info.txt.
// Columns: sensor id | sensor name | value | unit | status | thresholds...
func parseSensorInfo(content []byte) []models.SensorReading {
	var sensors []models.SensorReading
	inTable := false

	for _, line := range strings.Split(string(content), "\n") {
		if strings.Contains(line, "sensor id") && strings.Contains(line, "sensor name") {
			inTable = true
			continue
		}
		if inTable && strings.HasPrefix(strings.TrimSpace(line), "**") {
			// "*** Detailed Voltage Object Information ***" signals end of main table
			inTable = false
			continue
		}
		if !inTable || !strings.Contains(line, "|") {
			continue
		}
		parts := strings.Split(line, "|")
		if len(parts) < 5 {
			continue
		}
		name := strings.TrimSpace(parts[1])
		valueStr := strings.TrimSpace(parts[2])
		unit := strings.TrimSpace(parts[3])
		status := strings.TrimSpace(parts[4])

		if name == "" || valueStr == "na" || unit == "discrete" || unit == "unspecified" {
			continue
		}
		value, err := strconv.ParseFloat(valueStr, 64)
		if err != nil {
			continue
		}
		sensors = append(sensors, models.SensorReading{
			Name:     name,
			Type:     sensorType(name, unit),
			Value:    value,
			Unit:     mapSensorUnit(unit),
			RawValue: valueStr,
			Status:   status,
		})
	}
	return sensors
}

func mapSensorUnit(u string) string {
	switch strings.ToLower(strings.TrimSpace(u)) {
	case "degrees c":
		return "C"
	case "volts":
		return "V"
	case "watts":
		return "W"
	case "rpm":
		return "RPM"
	case "amps":
		return "A"
	default:
		return u
	}
}

func sensorType(name, unit string) string {
	u := strings.ToLower(unit)
	n := strings.ToLower(name)
	switch {
	case strings.Contains(u, "degrees"):
		return "temperature"
	case strings.Contains(u, "volts"):
		return "voltage"
	case strings.Contains(u, "watts"):
		return "power"
	case strings.Contains(u, "rpm") || strings.Contains(n, "fan") && strings.Contains(n, "speed"):
		return "fan"
	case strings.Contains(u, "amps"):
		return "current"
	default:
		return ""
	}
}

// ── CPU ───────────────────────────────────────────────────────────────────────

// parseCPUInfo parses the comma-separated cpu_info file.
// Columns: slot, presence, model, processorID, cores, threads, flags, L1, L2, L3, partNum, devName, location, SN
func parseCPUInfo(content []byte) []models.CPU {
	var cpus []models.CPU
	lines := strings.Split(string(content), "\n")
	for i, line := range lines {
		if i == 0 { // skip header
			continue
		}
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}
		parts := strings.Split(line, ",")
		if len(parts) < 6 {
			continue
		}
		slot := strings.TrimSpace(parts[0])
		if !strings.HasPrefix(strings.ToLower(slot), "cpu") {
			continue
		}
		if strings.ToLower(strings.TrimSpace(parts[1])) != "present" {
			continue
		}

		socketNum := 0
		fmt.Sscanf(strings.ToLower(slot), "cpu%d", &socketNum)

		model := strings.TrimSpace(parts[2])
		cores := 0
		fmt.Sscanf(strings.TrimSpace(parts[4]), "%d", &cores)
		threads := 0
		fmt.Sscanf(strings.TrimSpace(parts[5]), "%d", &threads)

		l1, l2, l3 := 0, 0, 0
		if len(parts) >= 10 {
			l1 = parseCacheSizeKB(parts[7])
			l2 = parseCacheSizeKB(parts[8])
			l3 = parseCacheSizeKB(parts[9])
		}

		sn := ""
		if len(parts) >= 14 {
			sn = strings.TrimSpace(parts[13])
		}

		cpus = append(cpus, models.CPU{
			Socket:       socketNum,
			Model:        model,
			Cores:        cores,
			Threads:      threads,
			L1CacheKB:    l1,
			L2CacheKB:    l2,
			L3CacheKB:    l3,
			SerialNumber: sn,
			Status:       "ok",
		})
	}
	return cpus
}

func parseCacheSizeKB(s string) int {
	var n int
	fmt.Sscanf(strings.TrimSpace(s), "%d", &n)
	return n
}

// ── Memory ────────────────────────────────────────────────────────────────────

// parseMemInfo parses the comma-separated mem_info file.
// Columns: slot, location, dimmName, manufacturer, size, maxSpeed, curSpeed, type, SN, voltage, rank, bitWidth, tech, bom, partNum, ..., health
func parseMemInfo(content []byte) []models.MemoryDIMM {
	var dimms []models.MemoryDIMM
	lines := strings.Split(string(content), "\n")
	for i, line := range lines {
		if i == 0 {
			continue
		}
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}
		parts := strings.Split(line, ",")
		if len(parts) < 9 {
			continue
		}

		sn := strings.TrimSpace(parts[8])
		if strings.ToLower(sn) == "no dimm" || sn == "" {
			continue
		}

		slot := strings.TrimSpace(parts[0])
		location := strings.TrimSpace(parts[1])
		manufacturer := strings.TrimSpace(parts[3])
		if strings.ToLower(manufacturer) == "unknown" {
			manufacturer = ""
		}

		sizeMB := 0
		fmt.Sscanf(strings.TrimSpace(parts[4]), "%d MB", &sizeMB)

		maxSpeedMHz := 0
		fmt.Sscanf(strings.TrimSpace(parts[5]), "%d MT/s", &maxSpeedMHz)

		curSpeedMHz := 0
		fmt.Sscanf(strings.TrimSpace(parts[6]), "%d MT/s", &curSpeedMHz)

		memType := strings.TrimSpace(parts[7])
		if strings.ToLower(memType) == "unknown" {
			memType = ""
		}

		ranks := 0
		if len(parts) >= 11 {
			fmt.Sscanf(strings.TrimSpace(parts[10]), "%d rank", &ranks)
		}

		partNum := ""
		if len(parts) >= 15 {
			v := strings.TrimSpace(parts[14])
			if strings.ToLower(v) != "no dimm" && strings.ToLower(v) != "unknown" {
				partNum = v
			}
		}

		status := "ok"
		if len(parts) >= 22 {
			if s := strings.TrimSpace(parts[21]); strings.ToLower(s) != "ok" && s != "" {
				status = strings.ToLower(s)
			}
		}

		dimms = append(dimms, models.MemoryDIMM{
			Slot:            slot,
			Location:        location,
			Present:         true,
			SizeMB:          sizeMB,
			Type:            memType,
			MaxSpeedMHz:     maxSpeedMHz,
			CurrentSpeedMHz: curSpeedMHz,
			Manufacturer:    manufacturer,
			SerialNumber:    sn,
			PartNumber:      partNum,
			Ranks:           ranks,
			Status:          status,
		})
	}
	return dimms
}

// ── Card Info (GPU + NIC) ─────────────────────────────────────────────────────

// parseCardInfo parses card_info file, extracting GPU and OCP NIC card inventory.
// The file has named sections ("GPU Card Info", "OCP Card Info", etc.) each with a pipe-table.
func parseCardInfo(content []byte) (gpus []models.GPU, nicCards []xfusionNICCard) {
	sections := splitPipeSections(content)

	// Build BDF and VendorID/DeviceID map from PCIe Card Info: slot → info
	type pcieEntry struct {
		bdf      string
		vendorID int
		deviceID int
		desc     string
	}
	slotPCIe := make(map[string]pcieEntry)
	for _, row := range sections["pcie card info"] {
		slot := strings.TrimSpace(row["slot"])
		seg := parseHexInt(row["segment number"])
		bus := parseHexInt(row["bus number"])
		dev := parseHexInt(row["device number"])
		fn := parseHexInt(row["function number"])
		slotPCIe[slot] = pcieEntry{
			bdf:      fmt.Sprintf("%04x:%02x:%02x.%d", seg, bus, dev, fn),
			vendorID: parseHexInt(row["vender id"]),
			deviceID: parseHexInt(row["device id"]),
			desc:     strings.TrimSpace(row["card desc"]),
		}
	}

	// GPU Card Info: slot, name, manufacturer, serialNum, firmVer, SBE/DBE counts
	for _, row := range sections["gpu card info"] {
		slot := strings.TrimSpace(row["slot"])
		name := strings.TrimSpace(row["name"])
		manufacturer := strings.TrimSpace(row["manufacturer"])
		serial := strings.TrimSpace(row["serialnum"])
		firmware := strings.TrimSpace(row["firmver"])

		sbeCount, dbeCount := 0, 0
		fmt.Sscanf(strings.TrimSpace(row["sbe"]), "%d", &sbeCount)
		fmt.Sscanf(strings.TrimSpace(row["dbe"]), "%d", &dbeCount)

		pcie := slotPCIe[slot]
		gpu := models.GPU{
			Slot:         slot,
			Model:        name,
			Manufacturer: manufacturer,
			SerialNumber: serial,
			Firmware:     firmware,
			BDF:          pcie.bdf,
			VendorID:     pcie.vendorID,
			DeviceID:     pcie.deviceID,
			Status:       "ok",
		}
		if dbeCount > 0 {
			gpu.Status = "warning"
		}
		gpus = append(gpus, gpu)
	}

	// OCP Card Info: NIC cards
	for _, row := range sections["ocp card info"] {
		slot := strings.TrimSpace(row["slot"])
		pcie := slotPCIe[slot]
		nicCards = append(nicCards, xfusionNICCard{
			Slot:         slot,
			Model:        strings.TrimSpace(row["card desc"]),
			ProductName:  strings.TrimSpace(row["card desc"]),
			VendorID:     parseHexInt(row["vender id"]),
			DeviceID:     parseHexInt(row["device id"]),
			BDF:          pcie.bdf,
			SerialNumber: strings.TrimSpace(row["serialnumber"]),
			PartNumber:   strings.TrimSpace(row["partnum"]),
		})
	}

	return gpus, nicCards
}

// splitPipeSections parses a multi-section file where each section starts with a
// plain header line (no "|") ending in "Info" or "info", followed by a pipe-table.
// Returns a map from lowercased section name → rows (each row is a map of lowercase header → value).
func splitPipeSections(content []byte) map[string][]map[string]string {
	result := make(map[string][]map[string]string)
	var sectionName string
	var headers []string

	for _, line := range strings.Split(string(content), "\n") {
		trimmed := strings.TrimSpace(line)
		if trimmed == "" {
			continue
		}
		if !strings.Contains(trimmed, "|") {
			if strings.HasSuffix(strings.ToLower(trimmed), "info") {
				sectionName = strings.ToLower(trimmed)
				headers = nil
			}
			continue
		}
		parts := strings.Split(line, "|")
		if len(parts) < 2 {
			continue
		}
		cols := make([]string, len(parts))
		for i, p := range parts {
			cols[i] = strings.TrimSpace(p)
		}
		if headers == nil {
			headers = make([]string, len(cols))
			for i, h := range cols {
				headers[i] = strings.ToLower(h)
			}
			continue
		}
		row := make(map[string]string, len(headers))
		for i, h := range headers {
			if i < len(cols) {
				row[h] = cols[i]
			}
		}
		result[sectionName] = append(result[sectionName], row)
	}
	return result
}

func parseHexInt(s string) int {
	s = strings.TrimSpace(s)
	s = strings.TrimPrefix(strings.ToLower(s), "0x")
	n, _ := strconv.ParseInt(s, 16, 64)
	return int(n)
}

func parseNetcardInfo(content []byte) []xfusionNetcardSnapshot {
	if len(content) == 0 {
		return nil
	}

	var snapshots []xfusionNetcardSnapshot
	var current *xfusionNetcardSnapshot
	var currentPort *xfusionNetcardPort

	flushPort := func() {
		if current == nil || currentPort == nil {
			return
		}
		current.Ports = append(current.Ports, *currentPort)
		currentPort = nil
	}
	flushSnapshot := func() {
		if current == nil || !current.hasData() {
			return
		}
		flushPort()
		snapshots = append(snapshots, *current)
		current = nil
	}

	for _, rawLine := range strings.Split(string(content), "\n") {
		line := strings.TrimSpace(rawLine)
		if line == "" {
			flushPort()
			continue
		}
		if ts, ok := parseXFusionUTCTimestamp(line); ok {
			if current == nil {
				current = &xfusionNetcardSnapshot{Timestamp: ts}
				continue
			}
			if current.hasData() {
				flushSnapshot()
				current = &xfusionNetcardSnapshot{Timestamp: ts}
				continue
			}
			current.Timestamp = ts
			continue
		}
		if current == nil {
			current = &xfusionNetcardSnapshot{}
		}
		if port := parseNetcardPortHeader(line); port != nil {
			flushPort()
			currentPort = port
			continue
		}
		if currentPort != nil {
			if value, ok := parseSimpleKV(line, "MacAddr"); ok {
				currentPort.MAC = value
				continue
			}
			if value, ok := parseSimpleKV(line, "ActualMac"); ok {
				currentPort.ActualMAC = value
				continue
			}
		}
		if value, ok := parseSimpleKV(line, "ProductName"); ok {
			current.ProductName = value
			continue
		}
		if value, ok := parseSimpleKV(line, "Manufacture"); ok {
			current.Manufacturer = value
			continue
		}
		if value, ok := parseSimpleKV(line, "FirmwareVersion"); ok {
			current.Firmware = value
			continue
		}
		if value, ok := parseSimpleKV(line, "SlotId"); ok {
			current.Slot = value
		}
	}
	flushSnapshot()

	bestIndexBySlot := make(map[string]int)
	for i, snapshot := range snapshots {
		slot := strings.TrimSpace(snapshot.Slot)
		if slot == "" {
			continue
		}
		prevIdx, exists := bestIndexBySlot[slot]
		if !exists || snapshot.isBetterThan(snapshots[prevIdx]) {
			bestIndexBySlot[slot] = i
		}
	}

	ordered := make([]xfusionNetcardSnapshot, 0, len(bestIndexBySlot))
	for i, snapshot := range snapshots {
		slot := strings.TrimSpace(snapshot.Slot)
		bestIdx, ok := bestIndexBySlot[slot]
		if !ok || bestIdx != i {
			continue
		}
		ordered = append(ordered, snapshot)
		delete(bestIndexBySlot, slot)
	}
	return ordered
}

func mergeNetworkAdapters(cards []xfusionNICCard, snapshots []xfusionNetcardSnapshot) ([]models.NetworkAdapter, []models.NIC) {
	bySlotCard := make(map[string]xfusionNICCard, len(cards))
	bySlotSnapshot := make(map[string]xfusionNetcardSnapshot, len(snapshots))
	orderedSlots := make([]string, 0, len(cards)+len(snapshots))
	seenSlots := make(map[string]struct{}, len(cards)+len(snapshots))

	for _, card := range cards {
		slot := strings.TrimSpace(card.Slot)
		if slot == "" {
			continue
		}
		bySlotCard[slot] = card
		if _, seen := seenSlots[slot]; !seen {
			orderedSlots = append(orderedSlots, slot)
			seenSlots[slot] = struct{}{}
		}
	}
	for _, snapshot := range snapshots {
		slot := strings.TrimSpace(snapshot.Slot)
		if slot == "" {
			continue
		}
		bySlotSnapshot[slot] = snapshot
		if _, seen := seenSlots[slot]; !seen {
			orderedSlots = append(orderedSlots, slot)
			seenSlots[slot] = struct{}{}
		}
	}

	adapters := make([]models.NetworkAdapter, 0, len(orderedSlots))
	legacyNICs := make([]models.NIC, 0, len(orderedSlots))
	for _, slot := range orderedSlots {
		card := bySlotCard[slot]
		snapshot := bySlotSnapshot[slot]

		model := firstNonEmpty(card.Model, snapshot.ProductName)
		description := ""
		if !strings.EqualFold(strings.TrimSpace(model), strings.TrimSpace(snapshot.ProductName)) {
			description = strings.TrimSpace(snapshot.ProductName)
		}
		macs := snapshot.macAddresses()
		bdf := firstNonEmpty(snapshot.primaryBDF(), card.BDF)
		firmware := normalizeXFusionValue(snapshot.Firmware)
		manufacturer := firstNonEmpty(snapshot.Manufacturer, card.Vendor)
		portCount := len(snapshot.Ports)
		if portCount == 0 && len(macs) > 0 {
			portCount = len(macs)
		}
		if portCount == 0 {
			portCount = 1
		}

		adapters = append(adapters, models.NetworkAdapter{
			Slot:         slot,
			Location:     "OCP",
			Present:      true,
			BDF:          bdf,
			Model:        model,
			Description:  description,
			Vendor:       manufacturer,
			VendorID:     card.VendorID,
			DeviceID:     card.DeviceID,
			SerialNumber: card.SerialNumber,
			PartNumber:   card.PartNumber,
			Firmware:     firmware,
			PortCount:    portCount,
			PortType:     "ethernet",
			MACAddresses: macs,
			Status:       "ok",
		})
		legacyNICs = append(legacyNICs, models.NIC{
			Name:         fmt.Sprintf("OCP%s", slot),
			Model:        model,
			Description:  description,
			MACAddress:   firstNonEmpty(macs...),
			SerialNumber: card.SerialNumber,
		})
	}

	return adapters, legacyNICs
}

func parseXFusionUTCTimestamp(line string) (time.Time, bool) {
	ts, err := time.Parse("2006-01-02 15:04:05 MST", strings.TrimSpace(line))
	if err != nil {
		return time.Time{}, false
	}
	return ts, true
}

func parseNetcardPortHeader(line string) *xfusionNetcardPort {
	fields := strings.Fields(strings.TrimSpace(line))
	if len(fields) < 2 || !strings.HasPrefix(strings.ToLower(fields[0]), "port") {
		return nil
	}
	joined := strings.Join(fields[1:], " ")
	if !strings.HasPrefix(strings.ToLower(joined), "bdf:") {
		return nil
	}
	return &xfusionNetcardPort{BDF: strings.TrimSpace(joined[len("BDF:"):])}
}

func parseSimpleKV(line, key string) (string, bool) {
	idx := strings.Index(line, ":")
	if idx < 0 {
		return "", false
	}
	gotKey := strings.TrimSpace(line[:idx])
	if !strings.EqualFold(gotKey, key) {
		return "", false
	}
	return strings.TrimSpace(line[idx+1:]), true
}

func normalizeXFusionValue(value string) string {
	value = strings.TrimSpace(value)
	switch strings.ToUpper(value) {
	case "", "N/A", "NA", "UNKNOWN":
		return ""
	default:
		return value
	}
}

func (s xfusionNetcardSnapshot) hasData() bool {
	return strings.TrimSpace(s.Slot) != "" ||
		strings.TrimSpace(s.ProductName) != "" ||
		strings.TrimSpace(s.Manufacturer) != "" ||
		strings.TrimSpace(s.Firmware) != "" ||
		len(s.Ports) > 0
}

func (s xfusionNetcardSnapshot) score() int {
	score := len(s.Ports)
	if normalizeXFusionValue(s.Firmware) != "" {
		score += 10
	}
	score += len(s.macAddresses()) * 2
	return score
}

func (s xfusionNetcardSnapshot) isBetterThan(other xfusionNetcardSnapshot) bool {
	if s.score() != other.score() {
		return s.score() > other.score()
	}
	if !s.Timestamp.Equal(other.Timestamp) {
		return s.Timestamp.After(other.Timestamp)
	}
	return len(s.Ports) > len(other.Ports)
}

func (s xfusionNetcardSnapshot) primaryBDF() string {
	for _, port := range s.Ports {
		if bdf := strings.TrimSpace(port.BDF); bdf != "" {
			return bdf
		}
	}
	return ""
}

func (s xfusionNetcardSnapshot) macAddresses() []string {
	out := make([]string, 0, len(s.Ports))
	seen := make(map[string]struct{}, len(s.Ports))
	for _, port := range s.Ports {
		for _, candidate := range []string{port.ActualMAC, port.MAC} {
			mac := normalizeMAC(candidate)
			if mac == "" {
				continue
			}
			if _, exists := seen[mac]; exists {
				continue
			}
			seen[mac] = struct{}{}
			out = append(out, mac)
			break
		}
	}
	return out
}

func normalizeMAC(value string) string {
	value = strings.ToUpper(strings.TrimSpace(value))
	switch value {
	case "", "N/A", "NA", "UNKNOWN", "00:00:00:00:00:00":
		return ""
	default:
		return value
	}
}

// ── PSU ───────────────────────────────────────────────────────────────────────

// parsePSUInfo parses the pipe-delimited psu_info.txt.
// Columns: Slot | presence | Manufacturer | Type | SN | Version | Rated Power | InputMode | PartNum | DeviceName | Vin | ...
func parsePSUInfo(content []byte) []models.PSU {
	var psus []models.PSU
	var headers []string

	for _, line := range strings.Split(string(content), "\n") {
		if !strings.Contains(line, "|") {
			continue
		}
		parts := strings.Split(line, "|")
		cols := make([]string, len(parts))
		for i, p := range parts {
			cols[i] = strings.TrimSpace(p)
		}
		if headers == nil {
			headers = make([]string, len(cols))
			for i, h := range cols {
				headers[i] = strings.ToLower(h)
			}
			continue
		}
		row := make(map[string]string, len(headers))
		for i, h := range headers {
			if i < len(cols) {
				row[h] = cols[i]
			}
		}
		if strings.ToLower(row["presence"]) != "present" {
			continue
		}

		wattage := 0
		fmt.Sscanf(row["rated power"], "%d", &wattage)

		inputVoltage := 0.0
		fmt.Sscanf(row["vin"], "%f", &inputVoltage)

		psus = append(psus, models.PSU{
			Slot:         row["slot"],
			Present:      true,
			Model:        row["type"],
			Vendor:       row["manufacturer"],
			SerialNumber: row["sn"],
			PartNumber:   row["partnum"],
			Firmware:     row["version"],
			WattageW:     wattage,
			InputType:    row["inputmode"],
			InputVoltage: inputVoltage,
			Status:       "ok",
		})
	}
	return psus
}

// ── Storage ───────────────────────────────────────────────────────────────────

// parseStorageControllerInfo parses RAID_Controller_Info.txt and adds firmware entries.
func parseStorageControllerInfo(content []byte, result *models.AnalysisResult) {
	// File may contain multiple controller blocks; parse key:value pairs from each.
	// We only look at the first occurrence of each key (first controller).
	seen := make(map[string]struct{}, len(result.Hardware.Firmware))
	for _, fw := range result.Hardware.Firmware {
		key := strings.ToLower(strings.TrimSpace(fw.DeviceName + "\x00" + fw.Version + "\x00" + fw.Description))
		seen[key] = struct{}{}
	}
	text := string(content)
	blocks := strings.Split(text, "RAID Controller #")
	for _, block := range blocks[1:] { // skip pre-block preamble
		fields := parseKeyValueBlock([]byte(block))
		name := firstNonEmpty(fields["Component Name"], fields["Controller Name"], fields["Controller Type"])
		firmware := fields["Firmware Version"]
		if name != "" && firmware != "" {
			appendXFusionFirmware(result, seen, models.FirmwareInfo{
				DeviceName:  name,
				Description: fields["Controller Name"],
				Version:     firmware,
			})
		}
	}
}

func parseAppRevision(content []byte, result *models.AnalysisResult) {
	type firmwareLine struct {
		deviceName  string
		description string
		buildKey    string
	}

	known := map[string]firmwareLine{
		"Active iBMC    Version":           {deviceName: "iBMC", description: "active iBMC", buildKey: "Active iBMC      Built"},
		"Active BIOS    Version":           {deviceName: "BIOS", description: "active BIOS", buildKey: "Active BIOS      Built"},
		"CPLD           Version":           {deviceName: "CPLD", description: "mainboard CPLD"},
		"SDK            Version":           {deviceName: "SDK", description: "iBMC SDK", buildKey: "SDK              Built"},
		"Active Uboot   Version":           {deviceName: "U-Boot", description: "active U-Boot"},
		"Active Secure Bootloader Version": {deviceName: "Secure Bootloader", description: "active secure bootloader"},
		"Active Secure Firmware   Version": {deviceName: "Secure Firmware", description: "active secure firmware"},
	}

	values := parseAlignedKeyValues(content)
	if result.Hardware.BoardInfo.ProductName == "" {
		if productName := values["Product           Name"]; productName != "" {
			result.Hardware.BoardInfo.ProductName = productName
		}
	}

	seen := make(map[string]struct{}, len(result.Hardware.Firmware))
	for _, fw := range result.Hardware.Firmware {
		key := strings.ToLower(strings.TrimSpace(fw.DeviceName + "\x00" + fw.Version + "\x00" + fw.Description))
		seen[key] = struct{}{}
	}

	for key, meta := range known {
		version := normalizeXFusionValue(values[key])
		if version == "" {
			continue
		}
		appendXFusionFirmware(result, seen, models.FirmwareInfo{
			DeviceName:  meta.deviceName,
			Description: meta.description,
			Version:     version,
			BuildTime:   normalizeXFusionValue(values[meta.buildKey]),
		})
	}
}

func parseAlignedKeyValues(content []byte) map[string]string {
	values := make(map[string]string)
	for _, rawLine := range strings.Split(string(content), "\n") {
		line := strings.TrimRight(rawLine, "\r")
		if !strings.Contains(line, ":") {
			continue
		}
		idx := strings.Index(line, ":")
		if idx < 0 {
			continue
		}
		key := strings.TrimRight(line[:idx], " \t")
		value := strings.TrimSpace(line[idx+1:])
		if key == "" || value == "" || values[key] != "" {
			continue
		}
		values[key] = value
	}
	return values
}

func appendXFusionFirmware(result *models.AnalysisResult, seen map[string]struct{}, fw models.FirmwareInfo) {
	if result == nil || result.Hardware == nil {
		return
	}
	key := strings.ToLower(strings.TrimSpace(fw.DeviceName + "\x00" + fw.Version + "\x00" + fw.Description))
	if key == "" {
		return
	}
	if _, exists := seen[key]; exists {
		return
	}
	seen[key] = struct{}{}
	result.Hardware.Firmware = append(result.Hardware.Firmware, fw)
}

// parseDiskInfo parses a single PhysicalDrivesInfo/DiskN/disk_info file.
func parseDiskInfo(content []byte) *models.Storage {
	fields := parseKeyValueBlock(content)

	model := fields["Model"]
	sn := fields["Serial Number"]
	if model == "" && sn == "" {
		return nil
	}

	sizeGB := 0
	var capFloat float64
	if _, err := fmt.Sscanf(fields["Capacity"], "%f GB", &capFloat); err == nil {
		sizeGB = int(capFloat)
	}

	var wearPct *int
	if wearStr := fields["Remnant Media Wearout"]; wearStr != "" {
		var pct int
		if _, err := fmt.Sscanf(wearStr, "%d%%", &pct); err == nil {
			wearPct = &pct
		}
	}

	status := "ok"
	if h := strings.ToLower(fields["Health Status"]); h != "" && h != "normal" {
		status = h
	}

	return &models.Storage{
		Slot:                  firstNonEmpty(fields["Device Name"], fields["ID"]),
		Type:                  fields["Media Type"],
		Model:                 model,
		SizeGB:                sizeGB,
		SerialNumber:          sn,
		Manufacturer:          fields["Manufacturer"],
		Firmware:              fields["Firmware Version"],
		Interface:             fields["Interface Type"],
		Present:               true,
		RemainingEndurancePct: wearPct,
		Status:                status,
	}
}

// parseKeyValueBlock parses "Key (spaces) : Value" lines from a text block.
func parseKeyValueBlock(content []byte) map[string]string {
	result := make(map[string]string)
	for _, line := range strings.Split(string(content), "\n") {
		line = strings.TrimSpace(line)
		if line == "" || strings.HasPrefix(line, "=") || strings.HasPrefix(line, "-") {
			continue
		}
		idx := strings.Index(line, " : ")
		if idx < 0 {
			continue
		}
		key := strings.TrimSpace(line[:idx])
		val := strings.TrimSpace(line[idx+3:])
		if key != "" && result[key] == "" {
			result[key] = val
		}
	}
	return result
}

// ── Events ────────────────────────────────────────────────────────────────────

// parseMaintenanceLog parses the iBMC maintenance_log file.
// Line format: "YYYY-MM-DD HH:MM:SS LEVEL : CODE,description"
func parseMaintenanceLog(content []byte) []models.Event {
	var events []models.Event
	for _, line := range strings.Split(string(content), "\n") {
		line = strings.TrimSpace(line)
		if len(line) < 20 {
			continue
		}
		ts, err := time.Parse("2006-01-02 15:04:05", line[:19])
		if err != nil || ts.Year() <= 1970 {
			continue // skip epoch-0 boot artifacts
		}

		rest := strings.TrimSpace(line[19:])
		sepIdx := strings.Index(rest, " : ")
		if sepIdx < 0 {
			sepIdx = strings.Index(rest, ": ")
			if sepIdx < 0 {
				continue
			}
		} else {
			sepIdx++ // skip leading space for " : "
		}

		levelStr := strings.TrimSpace(rest[:sepIdx-1])
		body := strings.TrimSpace(rest[sepIdx+2:])

		code := body
		description := ""
		if ci := strings.Index(body, ","); ci >= 0 {
			code = body[:ci]
			description = strings.TrimSpace(body[ci+1:])
		}

		var severity models.Severity
		switch strings.ToUpper(levelStr) {
		case "WARN", "WARNING":
			severity = models.SeverityWarning
		case "ERROR", "ERR", "CRIT", "CRITICAL":
			severity = models.SeverityCritical
		default:
			severity = models.SeverityInfo
		}

		events = append(events, models.Event{
			Timestamp:   ts,
			Source:      "ibmc",
			EventType:   code,
			Severity:    severity,
			Description: description,
			RawData:     line,
		})
	}
	return events
}

// ── unused import guard ───────────────────────────────────────────────────────
var _ = parser.ExtractedFile{}
