// Package lenovo_xcc provides parser for Lenovo XCC mini-log archives.
// Tested with: ThinkSystem SR650 V3 (XCC mini-log zip, exported via XCC UI)
//
// Archive structure: zip with tmp/ directory containing JSON .log files.
//
// IMPORTANT: Increment parserVersion when modifying parser logic!
package lenovo_xcc

import (
	"encoding/json"
	"fmt"
	"regexp"
	"strconv"
	"strings"
	"time"

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

const parserVersion = "1.2"

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

// Parser implements VendorParser for Lenovo XCC mini-log archives.
type Parser struct{}

func (p *Parser) Name() string    { return "Lenovo XCC Mini-Log Parser" }
func (p *Parser) Vendor() string  { return "lenovo_xcc" }
func (p *Parser) Version() string { return parserVersion }

// Detect checks if files match the Lenovo XCC mini-log archive format.
// Returns confidence score 0-100.
func (p *Parser) Detect(files []parser.ExtractedFile) int {
	confidence := 0
	for _, f := range files {
		path := strings.ToLower(f.Path)
		switch {
		case strings.HasSuffix(path, "tmp/basic_sys_info.log"):
			confidence += 60
		case strings.HasSuffix(path, "tmp/inventory_cpu.log"):
			confidence += 20
		case strings.HasSuffix(path, "tmp/xcc_plat_events1.log"):
			confidence += 20
		case strings.HasSuffix(path, "tmp/inventory_dimm.log"):
			confidence += 10
		case strings.HasSuffix(path, "tmp/inventory_fw.log"):
			confidence += 10
		}
		if confidence >= 100 {
			return 100
		}
	}
	return confidence
}

// Parse parses the Lenovo XCC mini-log archive and returns an analysis result.
func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, error) {
	result := &models.AnalysisResult{
		Events:  make([]models.Event, 0),
		FRU:     make([]models.FRUInfo, 0),
		Sensors: make([]models.SensorReading, 0),
		Hardware: &models.HardwareConfig{
			Firmware:    make([]models.FirmwareInfo, 0),
			CPUs:        make([]models.CPU, 0),
			Memory:      make([]models.MemoryDIMM, 0),
			Storage:     make([]models.Storage, 0),
			PCIeDevices: make([]models.PCIeDevice, 0),
			PowerSupply: make([]models.PSU, 0),
		},
	}

	if f := findByPath(files, "tmp/basic_sys_info.log"); f != nil {
		parseBasicSysInfo(f.Content, result)
	}
	if f := findByPath(files, "tmp/inventory_fw.log"); f != nil {
		result.Hardware.Firmware = append(result.Hardware.Firmware, parseFirmware(f.Content)...)
	}
	if f := findByPath(files, "tmp/inventory_cpu.log"); f != nil {
		result.Hardware.CPUs = parseCPUs(f.Content)
	}
	if f := findByPath(files, "tmp/inventory_dimm.log"); f != nil {
		memory, events := parseDIMMs(f.Content)
		result.Hardware.Memory = memory
		result.Events = append(result.Events, events...)
	}
	if f := findByPath(files, "tmp/inventory_disk.log"); f != nil {
		result.Hardware.Storage = parseDisks(f.Content)
	}
	if f := findByPath(files, "tmp/inventory_volume.log"); f != nil {
		result.Hardware.Volumes = parseVolumes(f.Content)
	}
	if f := findByPath(files, "tmp/inventory_card.log"); f != nil {
		result.Hardware.PCIeDevices = parseCards(f.Content)
	}
	if f := findByPath(files, "tmp/inventory_psu.log"); f != nil {
		result.Hardware.PowerSupply = parsePSUs(f.Content)
	}
	if f := findByPath(files, "tmp/inventory_ipmi_fru.log"); f != nil {
		result.FRU = parseFRU(f.Content)
		enrichBoardFromFRU(result)
	}
	if f := findByPath(files, "tmp/inventory_ipmi_sensor.log"); f != nil {
		result.Sensors = parseSensors(f.Content)
		result.Hardware.PowerSupply = enrichPSUsFromSensors(result.Hardware.PowerSupply, result.Sensors)
	}
	for _, f := range findEventFiles(files) {
		result.Events = append(result.Events, parseEvents(f.Content)...)
	}
	applyDIMMWarningsFromEvents(result)

	result.Protocol = "ipmi"
	result.SourceType = models.SourceTypeArchive
	parser.ApplyManufacturedYearWeekFromFRU(result.FRU, result.Hardware)

	return result, nil
}

// findByPath returns the first file whose lowercased path ends with the given suffix.
func findByPath(files []parser.ExtractedFile, suffix string) *parser.ExtractedFile {
	for i := range files {
		if strings.HasSuffix(strings.ToLower(files[i].Path), suffix) {
			return &files[i]
		}
	}
	return nil
}

// findEventFiles returns all xcc_plat_eventsN.log files.
func findEventFiles(files []parser.ExtractedFile) []parser.ExtractedFile {
	var out []parser.ExtractedFile
	for _, f := range files {
		path := strings.ToLower(f.Path)
		if strings.Contains(path, "tmp/xcc_plat_events") && strings.HasSuffix(path, ".log") {
			out = append(out, f)
		}
	}
	return out
}

// --- JSON structures ---

type xccBasicSysInfoDoc struct {
	Items []xccBasicSysInfoItem `json:"items"`
}

type xccBasicSysInfoItem struct {
	MachineName      string `json:"machine_name"`
	MachineTypeModel string `json:"machine_typemodel"`
	SerialNumber     string `json:"serial_number"`
	UUID             string `json:"uuid"`
	PowerState       string `json:"power_state"`
	ServerState      string `json:"server_state"`
	CurrentTime      string `json:"current_time"`
}

// xccFWEntry covers both basic_sys_info firmware (no type_str) and inventory_fw (has type_str).
type xccFWEntry struct {
	Index       int    `json:"index"`
	TypeCode    int    `json:"type"`
	TypeStr     string `json:"type_str"`  // only in inventory_fw.log
	Version     string `json:"version"`
	Build       string `json:"build"`
	ReleaseDate string `json:"release_date"`
}

type xccFirmwareDoc struct {
	Items []xccFWEntry `json:"items"`
}

type xccCPUDoc struct {
	Items []xccCPUItem `json:"items"`
}

type xccCPUItem struct {
	Processors []xccCPU `json:"processors"`
}

type xccCPU struct {
	Name         int             `json:"processors_name"`
	Model        string          `json:"processors_cpu_model"`
	Cores        json.RawMessage `json:"processors_cores"`   // may be int or string
	Threads      json.RawMessage `json:"processors_threads"` // may be int or string
	ClockSpeed   string          `json:"processors_clock_speed"`
	L1DataCache  string          `json:"processors_l1datacache"`
	L2Cache      string          `json:"processors_l2cache"`
	L3Cache      string          `json:"processors_l3cache"`
	Status       string          `json:"processors_status"`
	SerialNumber string          `json:"processors_serial_number"`
}

type xccDIMMDoc struct {
	Items []xccDIMMItem `json:"items"`
}

type xccDIMMItem struct {
	Memory []xccDIMM `json:"memory"`
}

type xccDIMM struct {
	Index        int             `json:"memory_index"`
	Status       string          `json:"memory_status"`
	Name         string          `json:"memory_name"`
	Type         string          `json:"memory_type"`
	Capacity     json.RawMessage `json:"memory_capacity"` // int (GB) or string
	PartNumber   string          `json:"memory_part_number"`
	SerialNumber string          `json:"memory_serial_number"`
	Manufacturer string          `json:"memory_manufacturer"`
	MemSpeed     json.RawMessage `json:"memory_mem_speed"`    // int or string
	ConfigSpeed  json.RawMessage `json:"memory_config_speed"` // int or string
}

type xccDiskDoc struct {
	Items []xccDiskItem `json:"items"`
}

type xccDiskItem struct {
	Disks []xccDisk `json:"disks"`
}

type xccDisk struct {
	ID           int    `json:"id"`
	SlotNo       int    `json:"slotNo"`
	Type         string `json:"type"`
	Interface    string `json:"interface"`
	Media        string `json:"media"`
	SerialNo     string `json:"serialNo"`
	PartNo       string `json:"partNo"`
	CapacityStr  string `json:"capacityStr"` // e.g. "3.20 TB"
	Manufacture  string `json:"manufacture"`
	ProductName  string `json:"productName"`
	RemainLife   int    `json:"remainLife"` // 0-100
	FWVersion    string `json:"fwVersion"`
	Temperature  int    `json:"temperature"`
	HealthStatus int    `json:"healthStatus"` // int code: 2=Normal
	State        int    `json:"state"`
	StateStr     string `json:"statestr"`
}

type xccCardDoc struct {
	Items []xccCard `json:"items"`
}

type xccCard struct {
	Key            int           `json:"key"`
	SlotNo         int           `json:"slotNo"`
	AdapterName    string        `json:"adapterName"`
	ConnectorLabel string        `json:"connectorLabel"`
	OOBSupported   int           `json:"oobSupported"`
	Location       int           `json:"location"`
	Functions      []xccCardFunc `json:"functions"`
}

type xccCardFunc struct {
	FunType         int    `json:"funType"`
	BusNo           int    `json:"generic_busNo"`
	DevNo           int    `json:"generic_devNo"`
	FunNo           int    `json:"generic_funNo"`
	VendorID        int    `json:"generic_vendorId"` // direct int
	DeviceID        int    `json:"generic_devId"`    // direct int
	SlotDesignation string `json:"generic_slotDesignation"`
}

type xccPSUDoc struct {
	Items []xccPSUItem `json:"items"`
}

type xccPSUItem struct {
	Power []xccPSU `json:"power"`
}

type xccPSU struct {
	Name        int    `json:"name"`
	Status      string `json:"status"`
	RatedPower  int    `json:"rated_power"`
	PartNumber  string `json:"part_number"`
	FRUNumber   string `json:"fru_number"`
	SerialNumber string `json:"serial_number"`
	ManufID     string `json:"manuf_id"`
}

type xccFRUDoc struct {
	Items []xccFRUItem `json:"items"`
}

type xccFRUItem struct {
	BuiltinFRU []map[string]string `json:"builtin_fru_device"`
}

type xccSensorDoc struct {
	Items []xccSensor `json:"items"`
}

type xccSensor struct {
	Name   string `json:"Sensor Name"`
	Value  string `json:"Value"`
	Status string `json:"status"`
	Unit   string `json:"unit"`
}

type xccEventDoc struct {
	Items []xccEvent `json:"items"`
}

type xccVolumeDoc struct {
	Items []xccVolumeItem `json:"items"`
}

type xccVolumeItem struct {
	Volumes          []xccVolume `json:"volumes"`
	TotalCapacityStr string      `json:"totalCapacityStr"`
}

type xccVolume struct {
	ID          int    `json:"id"`
	Name        string `json:"name"`
	Drives      string `json:"drives"`     // e.g. "M.2 Drive 0, M.2 Drive 1"
	RDLvlStr    string `json:"rdlvlstr"`   // e.g. "RAID 1"
	CapacityStr string `json:"capacityStr"` // e.g. "893.750 GiB"
	Status      int    `json:"status"`
	StatusStr   string `json:"statusStr"` // e.g. "Optimal"
}

type xccEvent struct {
	Severity string `json:"severity"` // "I", "W", "E", "C"
	Source   string `json:"source"`
	Date     string `json:"date"` // "2025-12-22T13:24:02.070"
	Index    int    `json:"index"`
	EventID  string `json:"eventid"`
	CmnID    string `json:"cmnid"`
	Message  string `json:"message"`
}

// --- Parsers ---

func parseBasicSysInfo(content []byte, result *models.AnalysisResult) {
	var doc xccBasicSysInfoDoc
	if err := json.Unmarshal(content, &doc); err != nil || len(doc.Items) == 0 {
		return
	}
	item := doc.Items[0]

	result.Hardware.BoardInfo = models.BoardInfo{
		ProductName:  cleanXCCValue(item.MachineTypeModel),
		SerialNumber: cleanXCCValue(item.SerialNumber),
		UUID:         cleanXCCValue(item.UUID),
	}

	if host := cleanXCCValue(item.MachineName); host != "" {
		result.TargetHost = host
	}

	if t, err := parseXCCTime(item.CurrentTime); err == nil {
		result.CollectedAt = t.UTC()
	}
}

func parseFirmware(content []byte) []models.FirmwareInfo {
	var doc xccFirmwareDoc
	if err := json.Unmarshal(content, &doc); err != nil {
		return nil
	}
	var out []models.FirmwareInfo
	for _, fw := range doc.Items {
		if fi := xccFWEntryToModel(fw); fi != nil {
			out = append(out, *fi)
		}
	}
	return out
}

func xccFWEntryToModel(fw xccFWEntry) *models.FirmwareInfo {
	name := strings.TrimSpace(fw.TypeStr)
	version := strings.TrimSpace(fw.Version)
	if name == "" && version == "" {
		return nil
	}
	build := strings.TrimSpace(fw.Build)
	v := version
	if build != "" {
		v = version + " (" + build + ")"
	}
	return &models.FirmwareInfo{
		DeviceName: name,
		Version:    v,
		BuildTime:  strings.TrimSpace(fw.ReleaseDate),
	}
}

func parseCPUs(content []byte) []models.CPU {
	var doc xccCPUDoc
	if err := json.Unmarshal(content, &doc); err != nil || len(doc.Items) == 0 {
		return nil
	}
	var out []models.CPU
	for _, item := range doc.Items {
		for _, c := range item.Processors {
			cpu := models.CPU{
				Socket:       c.Name,
				Model:        strings.TrimSpace(c.Model),
				Cores:        rawJSONToInt(c.Cores),
				Threads:      rawJSONToInt(c.Threads),
				FrequencyMHz: parseMHz(c.ClockSpeed),
				L1CacheKB:    parseKB(c.L1DataCache),
				L2CacheKB:    parseKB(c.L2Cache),
				L3CacheKB:    parseKB(c.L3Cache),
				Status:       strings.TrimSpace(c.Status),
				SerialNumber: strings.TrimSpace(c.SerialNumber),
			}
			out = append(out, cpu)
		}
	}
	return out
}

func parseDIMMs(content []byte) ([]models.MemoryDIMM, []models.Event) {
	var doc xccDIMMDoc
	if err := json.Unmarshal(content, &doc); err != nil || len(doc.Items) == 0 {
		return nil, nil
	}
	var out []models.MemoryDIMM
	var events []models.Event
	for _, item := range doc.Items {
		for _, m := range item.Memory {
			status := strings.TrimSpace(m.Status)
			present := !strings.EqualFold(status, "not present") &&
				!strings.EqualFold(status, "absent")
			// memory_capacity is in GB (int); convert to MB
			capacityGB := rawJSONToInt(m.Capacity)
			dimm := models.MemoryDIMM{
				Slot:            strings.TrimSpace(m.Name),
				Location:        strings.TrimSpace(m.Name),
				Present:         present,
				SizeMB:          capacityGB * 1024,
				Type:            strings.TrimSpace(m.Type),
				MaxSpeedMHz:     rawJSONToInt(m.MemSpeed),
				CurrentSpeedMHz: rawJSONToInt(m.ConfigSpeed),
				Manufacturer:    strings.TrimSpace(m.Manufacturer),
				SerialNumber:    strings.TrimSpace(m.SerialNumber),
				PartNumber:      strings.TrimSpace(strings.TrimRight(m.PartNumber, " ")),
				Status:          status,
			}
			out = append(out, dimm)
			if isUnqualifiedDIMM(status) {
				events = append(events, models.Event{
					Source:      "Memory",
					SensorType:  "Memory",
					SensorName:  dimm.Slot,
					EventType:   "DIMM Qualification",
					Severity:    models.SeverityWarning,
					Description: status,
				})
			}
		}
	}
	return out, events
}

func parseDisks(content []byte) []models.Storage {
	var doc xccDiskDoc
	if err := json.Unmarshal(content, &doc); err != nil || len(doc.Items) == 0 {
		return nil
	}
	var out []models.Storage
	for _, item := range doc.Items {
		for _, d := range item.Disks {
			sizeGB := parseCapacityToGB(d.CapacityStr)
			stateStr := strings.TrimSpace(d.StateStr)
			present := !strings.EqualFold(stateStr, "absent") &&
				!strings.EqualFold(stateStr, "not present")
			status := mapDiskHealthStatus(d.HealthStatus, stateStr)
			disk := models.Storage{
				Slot:         fmt.Sprintf("%d", d.SlotNo),
				Type:         strings.TrimSpace(d.Media),
				Model:        cleanXCCValue(d.ProductName),
				SizeGB:       sizeGB,
				SerialNumber: cleanXCCValue(d.SerialNo),
				Manufacturer: cleanXCCValue(d.Manufacture),
				Firmware:     cleanXCCValue(d.FWVersion),
				Interface:    strings.TrimSpace(d.Interface),
				Present:      present,
				Status:       status,
			}
			if d.Temperature > 0 {
				disk.Details = map[string]any{"temperature_c": d.Temperature}
			}
			if d.RemainLife >= 0 && d.RemainLife <= 100 {
				v := d.RemainLife
				disk.RemainingEndurancePct = &v
			}
			out = append(out, disk)
		}
	}
	return out
}

func parseVolumes(content []byte) []models.StorageVolume {
	var doc xccVolumeDoc
	if err := json.Unmarshal(content, &doc); err != nil || len(doc.Items) == 0 {
		return nil
	}
	var out []models.StorageVolume
	for _, item := range doc.Items {
		for _, v := range item.Volumes {
			vol := models.StorageVolume{
				ID:        fmt.Sprintf("%d", v.ID),
				Name:      strings.TrimSpace(v.Name),
				RAIDLevel: strings.TrimSpace(v.RDLvlStr),
				SizeGB:    parseCapacityToGB(v.CapacityStr),
				Status:    strings.TrimSpace(v.StatusStr),
			}
			drives := strings.TrimSpace(v.Drives)
			if drives != "" {
				for _, d := range strings.Split(drives, ",") {
					vol.Drives = append(vol.Drives, strings.TrimSpace(d))
				}
				// M.2 NVMe volumes are managed by Intel VROC (VMD)
				if strings.Contains(strings.ToLower(drives), "m.2") {
					vol.Controller = "Intel VROC"
				}
			}
			out = append(out, vol)
		}
	}
	return out
}

func parseCards(content []byte) []models.PCIeDevice {
	var doc xccCardDoc
	if err := json.Unmarshal(content, &doc); err != nil {
		return nil
	}
	var out []models.PCIeDevice
	for _, card := range doc.Items {
		slot := strings.TrimSpace(card.ConnectorLabel)
		if slot == "" {
			slot = fmt.Sprintf("%d", card.SlotNo)
		}
		dev := models.PCIeDevice{
			Slot:        slot,
			Description: strings.TrimSpace(card.AdapterName),
		}
		if len(card.Functions) > 0 {
			fn := card.Functions[0]
			dev.BDF = fmt.Sprintf("%02x:%02x.%x", fn.BusNo, fn.DevNo, fn.FunNo)
			dev.VendorID = fn.VendorID
			dev.DeviceID = fn.DeviceID
		}
		out = append(out, dev)
	}
	return out
}

func parsePSUs(content []byte) []models.PSU {
	var doc xccPSUDoc
	if err := json.Unmarshal(content, &doc); err != nil || len(doc.Items) == 0 {
		return nil
	}
	var out []models.PSU
	for _, item := range doc.Items {
		for _, p := range item.Power {
			model := cleanXCCValue(p.FRUNumber)
			if model == "" {
				model = cleanXCCValue(p.PartNumber)
			}
			psu := models.PSU{
				Slot:         fmt.Sprintf("%d", p.Name),
				Present:      true,
				Model:        model,
				WattageW:     p.RatedPower,
				SerialNumber: cleanXCCValue(p.SerialNumber),
				PartNumber:   cleanXCCValue(p.PartNumber),
				Vendor:       cleanXCCValue(p.ManufID),
				Status:       strings.TrimSpace(p.Status),
			}
			out = append(out, psu)
		}
	}
	return out
}

func parseFRU(content []byte) []models.FRUInfo {
	var doc xccFRUDoc
	if err := json.Unmarshal(content, &doc); err != nil || len(doc.Items) == 0 {
		return nil
	}
	var out []models.FRUInfo
	for _, item := range doc.Items {
		for _, entry := range item.BuiltinFRU {
			fru := models.FRUInfo{
				Description:  entry["FRU Device Description"],
				Manufacturer: entry["Board Mfg"],
				ProductName:  entry["Board Product"],
				SerialNumber: entry["Board Serial"],
				PartNumber:   entry["Board Part Number"],
				MfgDate:      entry["Board Mfg Date"],
			}
			if fru.ProductName == "" {
				fru.ProductName = entry["Product Name"]
			}
			if fru.SerialNumber == "" {
				fru.SerialNumber = entry["Product Serial"]
			}
			if fru.PartNumber == "" {
				fru.PartNumber = entry["Product Part Number"]
			}
			if fru.Description == "" && fru.ProductName == "" && fru.SerialNumber == "" {
				continue
			}
			out = append(out, fru)
		}
	}
	return out
}

func parseSensors(content []byte) []models.SensorReading {
	var doc xccSensorDoc
	if err := json.Unmarshal(content, &doc); err != nil {
		return nil
	}
	var out []models.SensorReading
	for _, s := range doc.Items {
		name := strings.TrimSpace(s.Name)
		if name == "" {
			continue
		}
		unit := strings.TrimSpace(s.Unit)
		sr := models.SensorReading{
			Name:     name,
			RawValue: strings.TrimSpace(s.Value),
			Unit:     unit,
			Status:   strings.TrimSpace(s.Status),
			Type:     classifySensorType(name, unit),
		}
		if v, err := strconv.ParseFloat(sr.RawValue, 64); err == nil {
			sr.Value = v
		}
		out = append(out, sr)
	}
	return out
}

func parseEvents(content []byte) []models.Event {
	var doc xccEventDoc
	if err := json.Unmarshal(content, &doc); err != nil {
		return nil
	}
	var out []models.Event
	for _, e := range doc.Items {
		ev := models.Event{
			ID:          e.EventID,
			Source:      strings.TrimSpace(e.Source),
			Description: strings.TrimSpace(e.Message),
			Severity:    xccSeverity(e.Severity, e.Message),
		}
		if t, err := parseXCCTime(e.Date); err == nil {
			ev.Timestamp = t.UTC()
		}
		out = append(out, ev)
	}
	return out
}

// --- Cross-reference enrichment ---

// enrichBoardFromFRU sets BoardInfo.Manufacturer from the system board FRU entry
// when it is not already populated. Mirrors bee's board parsing from dmidecode type 1.
func enrichBoardFromFRU(result *models.AnalysisResult) {
	if result.Hardware.BoardInfo.Manufacturer != "" {
		return
	}
	for _, fru := range result.FRU {
		desc := strings.ToLower(fru.Description)
		if !strings.Contains(desc, "system board") &&
			!strings.Contains(desc, "planar") &&
			!strings.Contains(desc, "backplane") {
			continue
		}
		if mfg := cleanXCCValue(fru.Manufacturer); mfg != "" {
			result.Hardware.BoardInfo.Manufacturer = mfg
			return
		}
	}
}

// psuSensorSlot extracts a 1-based PSU slot number from a sensor name.
// Recognises patterns: "PSU1 ...", "PSU 2 ...", "Power Supply 1 ...", "PWS1 ..."
var psuSensorSlotPattern = regexp.MustCompile(`(?i)(?:PSU|Power\s+Supply|PWS)\s*(\d+)`)

// enrichPSUsFromSensors cross-references sensor readings into PSU InputPowerW /
// OutputPowerW / InputVoltage. Mirrors bee's enrichPSUsWithTelemetry approach.
func enrichPSUsFromSensors(psus []models.PSU, sensors []models.SensorReading) []models.PSU {
	if len(psus) == 0 || len(sensors) == 0 {
		return psus
	}
	for i := range psus {
		slot, err := strconv.Atoi(psus[i].Slot)
		if err != nil {
			continue
		}
		for _, s := range sensors {
			m := psuSensorSlotPattern.FindStringSubmatch(s.Name)
			if len(m) < 2 {
				continue
			}
			sensorSlot, err := strconv.Atoi(m[1])
			if err != nil || sensorSlot != slot {
				continue
			}
			nameLower := strings.ToLower(s.Name)
			switch {
			case isPSUInputPower(nameLower):
				psus[i].InputPowerW = int(s.Value)
			case isPSUOutputPower(nameLower):
				psus[i].OutputPowerW = int(s.Value)
			case isPSUInputVoltage(nameLower):
				psus[i].InputVoltage = s.Value
			}
		}
	}
	return psus
}

func isPSUInputPower(name string) bool {
	return strings.Contains(name, "input power") ||
		strings.Contains(name, "input watts") ||
		strings.Contains(name, "_pin") ||
		strings.Contains(name, " pin")
}

func isPSUOutputPower(name string) bool {
	return strings.Contains(name, "output power") ||
		strings.Contains(name, "output watts") ||
		strings.Contains(name, "_pout") ||
		strings.Contains(name, " pout")
}

func isPSUInputVoltage(name string) bool {
	return strings.Contains(name, "input voltage") ||
		strings.Contains(name, "ac voltage") ||
		strings.Contains(name, "_vin") ||
		strings.Contains(name, " vin")
}

// mapDiskHealthStatus maps an XCC disk healthStatus integer to a canonical status
// string. Mirrors bee's mapRAIDDriveStatus logic.
// XCC codes: 1=Warning, 2=Normal, 3=Critical, 4=PredictiveFailure; 0=Unknown.
func mapDiskHealthStatus(code int, stateStr string) string {
	switch code {
	case 2:
		return "OK"
	case 1, 4:
		return "Warning"
	case 3:
		return "Critical"
	default:
		if stateStr != "" {
			return stateStr
		}
		return "Unknown"
	}
}

// classifySensorType returns a sensor category based on bee's classification logic:
// fan / temperature / power / voltage / current / other.
func classifySensorType(name, unit string) string {
	u := strings.ToLower(strings.TrimSpace(unit))
	switch u {
	case "rpm":
		return "fan"
	case "c", "celsius", "°c":
		return "temperature"
	case "w", "watts":
		return "power"
	case "v", "volts":
		return "voltage"
	case "a", "amps":
		return "current"
	}
	n := strings.ToLower(name)
	switch {
	case strings.Contains(n, "fan"):
		return "fan"
	case strings.Contains(n, "temp"):
		return "temperature"
	case strings.Contains(n, "power") || strings.Contains(n, " pwr"):
		return "power"
	case strings.Contains(n, "volt") || strings.Contains(n, " vin") || strings.Contains(n, " vout"):
		return "voltage"
	case strings.Contains(n, "curr") || strings.Contains(n, " amp"):
		return "current"
	default:
		return "other"
	}
}

// cleanXCCValue strips XCC placeholder strings, returning "" for non-values.
// Mirrors bee's cleanDMIValue for IPMI/XCC context.
func cleanXCCValue(v string) string {
	v = strings.TrimSpace(v)
	switch strings.ToLower(v) {
	case "", "n/a", "na", "none", "unknown", "not available",
		"not applicable", "not present", "not specified", "-":
		return ""
	}
	return v
}

// --- Helpers ---

func xccSeverity(s, message string) models.Severity {
	if isUnqualifiedDIMM(message) {
		return models.SeverityWarning
	}
	switch strings.ToUpper(strings.TrimSpace(s)) {
	case "C":
		return models.SeverityCritical
	case "E":
		return models.SeverityCritical
	case "W":
		return models.SeverityWarning
	default:
		return models.SeverityInfo
	}
}

func isUnqualifiedDIMM(value string) bool {
	return strings.Contains(strings.ToLower(strings.TrimSpace(value)), "unqualified dimm")
}

var (
	unqualifiedDIMMSlotRE   = regexp.MustCompile(`(?i)\bunqualified dimm\s+(\d+)\b`)
	unqualifiedDIMMSerialRE = regexp.MustCompile(`(?i)\bserial number is\s+([A-Z0-9-]+)`)
)

func applyDIMMWarningsFromEvents(result *models.AnalysisResult) {
	if result == nil || result.Hardware == nil || len(result.Hardware.Memory) == 0 || len(result.Events) == 0 {
		return
	}

	for _, ev := range result.Events {
		if !isUnqualifiedDIMM(ev.Description) {
			continue
		}
		idx := findDIMMIndexForUnqualifiedEvent(result.Hardware.Memory, ev.Description)
		if idx < 0 {
			continue
		}

		dimm := &result.Hardware.Memory[idx]
		dimm.Status = "Warning"
		dimm.ErrorDescription = ev.Description
		if !ev.Timestamp.IsZero() {
			ts := ev.Timestamp.UTC()
			dimm.StatusChangedAt = &ts
			dimm.StatusCheckedAt = &ts
		}
		appendDIMMStatusHistory(dimm, ev)
	}
}

func findDIMMIndexForUnqualifiedEvent(memory []models.MemoryDIMM, description string) int {
	slot := extractUnqualifiedDIMMSlot(description)
	serial := normalizeUnqualifiedDIMMSerial(extractUnqualifiedDIMMSerial(description))

	for i := range memory {
		if slot != "" && strings.EqualFold(strings.TrimSpace(memory[i].Slot), slot) {
			return i
		}
	}
	for i := range memory {
		if serial != "" && normalizeUnqualifiedDIMMSerial(memory[i].SerialNumber) == serial {
			return i
		}
	}
	return -1
}

func extractUnqualifiedDIMMSlot(description string) string {
	m := unqualifiedDIMMSlotRE.FindStringSubmatch(description)
	if len(m) < 2 {
		return ""
	}
	return "DIMM " + strings.TrimSpace(m[1])
}

func extractUnqualifiedDIMMSerial(description string) string {
	m := unqualifiedDIMMSerialRE.FindStringSubmatch(description)
	if len(m) < 2 {
		return ""
	}
	return strings.TrimSpace(m[1])
}

func normalizeUnqualifiedDIMMSerial(serial string) string {
	serial = strings.ToUpper(strings.TrimSpace(serial))
	if idx := strings.Index(serial, "-"); idx >= 0 {
		serial = serial[:idx]
	}
	return serial
}

func appendDIMMStatusHistory(dimm *models.MemoryDIMM, ev models.Event) {
	if dimm == nil || ev.Timestamp.IsZero() {
		return
	}
	for _, item := range dimm.StatusHistory {
		if strings.EqualFold(strings.TrimSpace(item.Status), "Warning") &&
			item.ChangedAt.Equal(ev.Timestamp.UTC()) &&
			strings.TrimSpace(item.Details) == strings.TrimSpace(ev.Description) {
			return
		}
	}
	dimm.StatusHistory = append(dimm.StatusHistory, models.StatusHistoryEntry{
		Status:    "Warning",
		ChangedAt: ev.Timestamp.UTC(),
		Details:   ev.Description,
	})
}

func parseXCCTime(s string) (time.Time, error) {
	s = strings.TrimSpace(s)
	formats := []string{
		"2006-01-02T15:04:05.000",
		"2006-01-02T15:04:05",
		"2006-01-02 15:04:05",
	}
	for _, f := range formats {
		if t, err := time.Parse(f, s); err == nil {
			return t, nil
		}
	}
	return time.Time{}, fmt.Errorf("unparseable time: %q", s)
}

// parseMHz parses "4100 MHz" → 4100
func parseMHz(s string) int {
	s = strings.TrimSpace(s)
	parts := strings.Fields(s)
	if len(parts) == 0 {
		return 0
	}
	v, _ := strconv.Atoi(parts[0])
	return v
}

// parseKB parses "384 KB" → 384
func parseKB(s string) int {
	s = strings.TrimSpace(s)
	parts := strings.Fields(s)
	if len(parts) == 0 {
		return 0
	}
	v, _ := strconv.Atoi(parts[0])
	return v
}

// parseMB parses "32768 MB" → 32768
func parseMB(s string) int {
	return parseKB(s)
}

// parseMTs parses "4800 MT/s" → 4800 (treated as MHz equivalent)
func parseMTs(s string) int {
	return parseKB(s)
}

// parseCapacityToGB parses "3.20 TB" or "480 GB" → GB integer
func parseCapacityToGB(s string) int {
	s = strings.TrimSpace(s)
	parts := strings.Fields(s)
	if len(parts) < 2 {
		return 0
	}
	v, err := strconv.ParseFloat(parts[0], 64)
	if err != nil {
		return 0
	}
	switch strings.ToUpper(parts[1]) {
	case "TB":
		return int(v * 1000)
	case "TIB":
		return int(v * 1099.511627776) // 1 TiB = 1099.511... GB
	case "GB":
		return int(v)
	case "GIB":
		return int(v * 1.073741824) // 1 GiB = 1.073741824 GB
	case "MB":
		return int(v / 1024)
	}
	return int(v)
}

// rawJSONToInt parses a json.RawMessage that may be an int or a quoted string → int
func rawJSONToInt(raw json.RawMessage) int {
	if len(raw) == 0 {
		return 0
	}
	// try direct int
	var n int
	if err := json.Unmarshal(raw, &n); err == nil {
		return n
	}
	// try string
	var s string
	if err := json.Unmarshal(raw, &s); err == nil {
		v, _ := strconv.Atoi(strings.TrimSpace(s))
		return v
	}
	return 0
}

// parseHexID parses "0x15b3" → 5555
func parseHexID(s string) int {
	s = strings.TrimSpace(strings.ToLower(s))
	s = strings.TrimPrefix(s, "0x")
	v, _ := strconv.ParseInt(s, 16, 32)
	return int(v)
}
