// Package inspur provides parser for Inspur/Kaytus BMC diagnostic archives
// Tested with: Inspur NF5468M7 / Kaytus KR4268X2 (onekeylog format)
//
// IMPORTANT: Increment parserVersion when modifying parser logic!
// This helps track which version was used to parse specific logs.
package inspur

import (
	"fmt"
	"strings"
	"time"

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

// parserVersion - version of this parser module
// IMPORTANT: Increment this version when making changes to parser logic!
const parserVersion = "1.8"

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

// Parser implements VendorParser for Inspur/Kaytus servers
type Parser struct{}

// Name returns human-readable parser name
func (p *Parser) Name() string {
	return "Inspur/Kaytus BMC Parser"
}

// Vendor returns vendor identifier
func (p *Parser) Vendor() string {
	return "inspur"
}

// Version returns parser version
// IMPORTANT: Update parserVersion constant when modifying parser logic!
func (p *Parser) Version() string {
	return parserVersion
}

// Detect checks if archive matches Inspur/Kaytus format
// Returns confidence 0-100
func (p *Parser) Detect(files []parser.ExtractedFile) int {
	confidence := 0

	for _, f := range files {
		path := strings.ToLower(f.Path)

		// Strong indicators for Inspur/Kaytus onekeylog format
		if strings.Contains(path, "onekeylog/") {
			confidence += 30
		}
		if strings.Contains(path, "devicefrusdr.log") {
			confidence += 25
		}
		if strings.Contains(path, "component/component.log") {
			confidence += 15
		}

		// Check for asset.json with Inspur-specific structure
		if strings.HasSuffix(path, "asset.json") {
			if containsInspurMarkers(f.Content) {
				confidence += 20
			}
		}

		// Cap at 100
		if confidence >= 100 {
			return 100
		}
	}

	return confidence
}

// containsInspurMarkers checks if content has Inspur-specific markers
func containsInspurMarkers(content []byte) bool {
	s := string(content)
	// Check for typical Inspur asset.json structure
	return strings.Contains(s, "VersionInfo") &&
		strings.Contains(s, "CpuInfo") &&
		strings.Contains(s, "MemInfo")
}

// Parse parses Inspur/Kaytus archive
func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, error) {
	selLocation := inferInspurArchiveLocation(files)

	result := &models.AnalysisResult{
		Events:  make([]models.Event, 0),
		FRU:     make([]models.FRUInfo, 0),
		Sensors: make([]models.SensorReading, 0),
	}

	// Pre-parse enrichment maps from devicefrusdr.log for use inside ParseAssetJSON.
	// BMC does not populate HddInfo.ModelName or SerialNumber for NVMe drives.
	var pcieSlotDeviceNames map[int]string
	var nvmeLocToSlot map[int]int
	if f := parser.FindFileByName(files, "devicefrusdr.log"); f != nil {
		pcieSlotDeviceNames = ParsePCIeSlotDeviceNames(f.Content)
		nvmeLocToSlot = ParsePCIeNVMeLocToSlot(f.Content)
	}

	// Parse NVMe serial numbers from audit.log: every disk SN change is logged there.
	// Combine with the NVMe loc→slot mapping to build pcieSlot→serial map.
	// Also parse RAID disk serials by backplane slot key (e.g. "BP0:0").
	var pcieSlotSerials map[int]string
	var raidSlotSerials map[string]string
	if f := parser.FindFileByName(files, "audit.log"); f != nil {
		if len(nvmeLocToSlot) > 0 {
			nvmeDiskSerials := ParseAuditLogNVMeSerials(f.Content)
			if len(nvmeDiskSerials) > 0 {
				pcieSlotSerials = make(map[int]string, len(nvmeDiskSerials))
				for diskNum, serial := range nvmeDiskSerials {
					if slot, ok := nvmeLocToSlot[diskNum]; ok {
						pcieSlotSerials[slot] = serial
					}
				}
				if len(pcieSlotSerials) == 0 {
					pcieSlotSerials = nil
				}
			}
		}
		raidSlotSerials = ParseAuditLogRAIDSerials(f.Content)
	}

	// Parse asset.json first (base hardware info)
	if f := parser.FindFileByName(files, "asset.json"); f != nil {
		if hw, err := ParseAssetJSON(f.Content, pcieSlotDeviceNames, pcieSlotSerials); err == nil {
			result.Hardware = hw
		}
	}

	// Extract BoardInfo from FRU data
	if result.Hardware == nil {
		result.Hardware = &models.HardwareConfig{}
	}

	// Parse devicefrusdr.log (contains SDR, FRU, PCIe and additional data)
	if f := parser.FindFileByName(files, "devicefrusdr.log"); f != nil {
		p.parseDeviceFruSDR(f.Content, result)
	}

	extractBoardInfo(result.FRU, result.Hardware)

	// Extract PlatformId (server model) from ThermalConfig
	if f := parser.FindFileByName(files, "ThermalConfig_Cur.conf"); f != nil {
		extractPlatformId(f.Content, result.Hardware)
	}

	// Parse component.log for additional data (PSU, etc.)
	if f := parser.FindFileByName(files, "component.log"); f != nil {
		ParseComponentLog(f.Content, result.Hardware)

		// Extract events from component.log (memory errors, etc.)
		componentEvents := ParseComponentLogEvents(f.Content)
		result.Events = append(result.Events, componentEvents...)

		// Extract additional telemetry sensors from component.log sections
		// (fan RPM, backplane temperature, PSU summary power, etc.).
		componentSensors := ParseComponentLogSensors(f.Content)
		result.Sensors = mergeSensorReadings(result.Sensors, componentSensors)
	}

	// Enrich runtime component data from Redis snapshot (serials, FW, telemetry),
	// when text logs miss these fields.
	if f := parser.FindFileByName(files, "redis-dump.rdb"); f != nil && result.Hardware != nil {
		enrichFromRedisDump(f.Content, result.Hardware)
	}

	// Parse IDL-like logs (plain and structured JSON logs with embedded IDL messages)
	idlFiles := parser.FindFileByPattern(files, "/idl.log", "idl_json.log", "run_json.log")
	for _, f := range idlFiles {
		idlEvents := ParseIDLLog(f.Content)
		result.Events = append(result.Events, idlEvents...)
	}

	// Parse SEL list (selelist.csv)
	if f := parser.FindFileByName(files, "selelist.csv"); f != nil {
		selEvents := ParseSELListWithLocation(f.Content, selLocation)
		result.Events = append(result.Events, selEvents...)
	}

	// Parse syslog files
	syslogFiles := parser.FindFileByPattern(files, "syslog/alert", "syslog/warning", "syslog/notice", "syslog/info")
	for _, f := range syslogFiles {
		events := ParseSyslog(f.Content, f.Path)
		result.Events = append(result.Events, events...)
	}

	// Fallback for archives where board serial is missing in parsed FRU/asset data:
	// recover it from log content, never from archive filename.
	if strings.TrimSpace(result.Hardware.BoardInfo.SerialNumber) == "" {
		if serial := inferBoardSerialFromFallbackLogs(files); serial != "" {
			result.Hardware.BoardInfo.SerialNumber = serial
		}
	}
	if strings.TrimSpace(result.Hardware.BoardInfo.ProductName) == "" {
		if model := inferBoardModelFromFallbackLogs(files); model != "" {
			result.Hardware.BoardInfo.ProductName = model
		}
	}

	// Enrich GPU inventory from HGX Redfish snapshot (serial/model/part mapping).
	if f := parser.FindFileByName(files, "HGX_HWInfo_FWVersion.log"); f != nil && result.Hardware != nil {
		enrichGPUsFromHGXHWInfo(f.Content, result.Hardware)
		appendHGXFirmwareFromHWInfo(f.Content, result.Hardware)
	}

	// Mark problematic GPUs from IDL errors like "BIOS miss F_GPU6".
	if result.Hardware != nil {
		applyGPUStatusFromEvents(result.Hardware, result.Events)
		enrichStorageFromSerialFallbackFiles(files, result.Hardware)
		// Apply RAID disk serials from audit.log (authoritative: last non-NULL SN change).
		// These override redis/component.log serials which may be stale after disk replacement.
		applyRAIDSlotSerials(result.Hardware, raidSlotSerials)
		parser.ApplyManufacturedYearWeekFromFRU(result.FRU, result.Hardware)
	}

	return result, nil
}

func inferInspurArchiveLocation(files []parser.ExtractedFile) *time.Location {
	fallback := parser.DefaultArchiveLocation()
	f := parser.FindFileByName(files, "timezone.conf")
	if f == nil {
		return fallback
	}
	locName := parseTimezoneConfigLocation(f.Content)
	if strings.TrimSpace(locName) == "" {
		return fallback
	}
	loc, err := time.LoadLocation(locName)
	if err != nil {
		return fallback
	}
	return loc
}

func parseTimezoneConfigLocation(content []byte) string {
	lines := strings.Split(string(content), "\n")
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line == "" || strings.HasPrefix(line, "[") || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
			continue
		}
		parts := strings.SplitN(line, "=", 2)
		if len(parts) != 2 {
			continue
		}
		key := strings.ToLower(strings.TrimSpace(parts[0]))
		val := strings.TrimSpace(parts[1])
		if key == "timezone" && val != "" {
			return val
		}
	}
	return ""
}

func (p *Parser) parseDeviceFruSDR(content []byte, result *models.AnalysisResult) {
	lines := string(content)

	// Find SDR section
	sdrStart := strings.Index(lines, "BMC sdr Info:")
	fruStart := strings.Index(lines, "BMC fru Info:")

	if sdrStart != -1 {
		var sdrContent string
		if fruStart != -1 && fruStart > sdrStart {
			sdrContent = lines[sdrStart:fruStart]
		} else {
			sdrContent = lines[sdrStart:]
		}
		result.Sensors = ParseSDR([]byte(sdrContent))
	}

	// Find FRU section
	if fruStart != -1 {
		fruContent := lines[fruStart:]
		result.FRU = ParseFRU([]byte(fruContent))
	}

	// Parse PCIe devices from RESTful PCIE Device info
	// This supplements data from asset.json with serial numbers, firmware, etc.
	pcieDevicesFromREST := ParsePCIeDevices(content)

	// Merge PCIe data: asset.json is the base inventory, RESTful data enriches names/links/serials.
	if result.Hardware != nil {
		result.Hardware.PCIeDevices = MergePCIeDevices(result.Hardware.PCIeDevices, pcieDevicesFromREST)
	}

	// Parse GPU devices and add temperature data from sensors
	if len(result.Sensors) > 0 && result.Hardware != nil {
		// Use existing GPU data from asset.json and enrich with sensor data
		for i := range result.Hardware.GPUs {
			gpu := &result.Hardware.GPUs[i]

			// Extract GPU number from slot name
			slotNum := extractSlotNumberFromGPU(gpu.Slot)

			// Find temperature sensors for this GPU
			for _, sensor := range result.Sensors {
				sensorName := strings.ToUpper(sensor.Name)

				// Match GPU temperature sensor
				if strings.Contains(sensorName, fmt.Sprintf("GPU%d_TEMP", slotNum)) && !strings.Contains(sensorName, "MEM") {
					if sensor.RawValue != "" {
						fmt.Sscanf(sensor.RawValue, "%d", &gpu.Temperature)
					}
				}

				// Match GPU memory temperature
				if strings.Contains(sensorName, fmt.Sprintf("GPU%d_MEM_TEMP", slotNum)) {
					if sensor.RawValue != "" {
						fmt.Sscanf(sensor.RawValue, "%d", &gpu.MemTemperature)
					}
				}

				// Match PCIe slot temperature as fallback
				if strings.Contains(sensorName, fmt.Sprintf("PCIE%d_GPU_TLM_T", slotNum)) && gpu.Temperature == 0 {
					if sensor.RawValue != "" {
						fmt.Sscanf(sensor.RawValue, "%d", &gpu.Temperature)
					}
				}
			}
		}
	}
}

// extractSlotNumberFromGPU extracts slot number from GPU slot string
func extractSlotNumberFromGPU(slot string) int {
	parts := strings.Split(slot, "_")
	for _, part := range parts {
		if strings.HasPrefix(part, "PCIE") {
			var num int
			fmt.Sscanf(part, "PCIE%d", &num)
			if num > 0 {
				return num
			}
		}
	}
	return 0
}

func mergeSensorReadings(base, extra []models.SensorReading) []models.SensorReading {
	if len(extra) == 0 {
		return base
	}

	out := append([]models.SensorReading{}, base...)
	seen := make(map[string]struct{}, len(out))
	for _, s := range out {
		if key := sensorMergeKey(s); key != "" {
			seen[key] = struct{}{}
		}
	}

	for _, s := range extra {
		key := sensorMergeKey(s)
		if key != "" {
			if _, ok := seen[key]; ok {
				continue
			}
			seen[key] = struct{}{}
		}
		out = append(out, s)
	}

	return out
}

func sensorMergeKey(s models.SensorReading) string {
	name := strings.ToLower(strings.TrimSpace(s.Name))
	if name == "" {
		return ""
	}
	return name
}
