// 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"
	"strconv"
	"strings"
	"time"

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

const parserVersion = "1.1"

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_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)
	}
	if f := findByPath(files, "tmp/inventory_ipmi_sensor.log"); f != nil {
		result.Sensors = parseSensors(f.Content)
	}
	for _, f := range findEventFiles(files) {
		result.Events = append(result.Events, parseEvents(f.Content)...)
	}

	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 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:  strings.TrimSpace(item.MachineTypeModel),
		SerialNumber: strings.TrimSpace(item.SerialNumber),
		UUID:         strings.TrimSpace(item.UUID),
	}

	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")
			disk := models.Storage{
				Slot:         fmt.Sprintf("%d", d.SlotNo),
				Type:         strings.TrimSpace(d.Media),
				Model:        strings.TrimSpace(d.ProductName),
				SizeGB:       sizeGB,
				SerialNumber: strings.TrimSpace(d.SerialNo),
				Manufacturer: strings.TrimSpace(d.Manufacture),
				Firmware:     strings.TrimSpace(d.FWVersion),
				Interface:    strings.TrimSpace(d.Interface),
				Present:      present,
				Status:       stateStr,
			}
			if d.RemainLife >= 0 && d.RemainLife <= 100 {
				v := d.RemainLife
				disk.RemainingEndurancePct = &v
			}
			out = append(out, disk)
		}
	}
	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 {
			psu := models.PSU{
				Slot:         fmt.Sprintf("%d", p.Name),
				Present:      true,
				WattageW:     p.RatedPower,
				SerialNumber: strings.TrimSpace(p.SerialNumber),
				PartNumber:   strings.TrimSpace(p.PartNumber),
				Vendor:       strings.TrimSpace(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
		}
		sr := models.SensorReading{
			Name:     name,
			RawValue: strings.TrimSpace(s.Value),
			Unit:     strings.TrimSpace(s.Unit),
			Status:   strings.TrimSpace(s.Status),
		}
		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
}

// --- 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")
}

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 "GB":
		return int(v)
	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)
}
