// Package xigmanas provides parser for XigmaNAS diagnostic dumps.
package xigmanas

import (
	"regexp"
	"strconv"
	"strings"
	"time"

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

// parserVersion - increment when parsing logic changes.
const parserVersion = "2.2"

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

// Parser implements VendorParser for XigmaNAS logs.
type Parser struct{}

func (p *Parser) Name() string   { return "XigmaNAS Parser" }
func (p *Parser) Vendor() string { return "xigmanas" }
func (p *Parser) Version() string {
	return parserVersion
}

// Detect checks if files contain typical XigmaNAS markers.
func (p *Parser) Detect(files []parser.ExtractedFile) int {
	confidence := 0

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

		if strings.Contains(path, "xigmanas") || strings.HasSuffix(path, "dmesg") {
			confidence += 20
		}
		if strings.Contains(content, `loader_brand="xigmanas"`) {
			confidence += 70
		}
		if strings.Contains(content, "xigmanas kernel build") {
			confidence += 35
		}
		if strings.Contains(content, "system uptime:") && strings.Contains(content, "routing tables:") {
			confidence += 20
		}
		if strings.Contains(content, "s.m.a.r.t. [/dev/") {
			confidence += 10
		}
		if confidence >= 100 {
			return 100
		}
	}

	if confidence > 100 {
		return 100
	}
	return confidence
}

// Parse parses XigmaNAS logs and returns normalized data.
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),
		},
	}

	content := joinFileContents(files)
	if strings.TrimSpace(content) == "" {
		return result, nil
	}

	parseSystemInfo(content, result)
	parseCPU(content, result)
	parseMemory(content, result)
	parseUptime(content, result)
	parseZFSState(content, result)
	parseStorageAndSMART(content, result)
	parseJournalLogSections(content, result)

	return result, nil
}

func joinFileContents(files []parser.ExtractedFile) string {
	var b strings.Builder
	for _, f := range files {
		b.Write(f.Content)
		b.WriteString("\n")
	}
	return b.String()
}

func parseSystemInfo(content string, result *models.AnalysisResult) {
	if m := regexp.MustCompile(`(?m)^Version:\s*\n-+\s*\n([^\n]+)`).FindStringSubmatch(content); len(m) == 2 {
		result.Hardware.Firmware = append(result.Hardware.Firmware, models.FirmwareInfo{
			DeviceName: "XigmaNAS",
			Version:    strings.TrimSpace(m[1]),
		})
	}
	if m := regexp.MustCompile(`(?m)^smbios\.bios\.version="([^"]+)"`).FindStringSubmatch(content); len(m) == 2 {
		result.Hardware.Firmware = append(result.Hardware.Firmware, models.FirmwareInfo{
			DeviceName: "System BIOS",
			Version:    strings.TrimSpace(m[1]),
		})
	}

	board := models.BoardInfo{}
	if m := regexp.MustCompile(`(?m)^smbios\.system\.maker="([^"]+)"`).FindStringSubmatch(content); len(m) == 2 {
		board.Manufacturer = strings.TrimSpace(m[1])
	}
	if m := regexp.MustCompile(`(?m)^smbios\.system\.product="([^"]+)"`).FindStringSubmatch(content); len(m) == 2 {
		board.ProductName = strings.TrimSpace(m[1])
	}
	if m := regexp.MustCompile(`(?m)^smbios\.system\.serial="([^"]+)"`).FindStringSubmatch(content); len(m) == 2 {
		board.SerialNumber = strings.TrimSpace(m[1])
	}
	if m := regexp.MustCompile(`(?m)^smbios\.system\.uuid="([^"]+)"`).FindStringSubmatch(content); len(m) == 2 {
		board.UUID = strings.TrimSpace(m[1])
	}
	result.Hardware.BoardInfo = board
}

func parseCPU(content string, result *models.AnalysisResult) {
	var cores, threads int
	if m := regexp.MustCompile(`(?m)^FreeBSD/SMP:\s+\d+\s+package\(s\)\s+x\s+(\d+)\s+core\(s\)`).FindStringSubmatch(content); len(m) == 2 {
		cores = parseInt(m[1])
		threads = cores
	}

	seen := map[string]struct{}{}
	cpuRe := regexp.MustCompile(`(?m)^CPU:\s+(.+?)\s+\(([\d.]+)-MHz`)
	for _, m := range cpuRe.FindAllStringSubmatch(content, -1) {
		model := strings.TrimSpace(m[1])
		if _, ok := seen[model]; ok {
			continue
		}
		seen[model] = struct{}{}

		result.Hardware.CPUs = append(result.Hardware.CPUs, models.CPU{
			Socket:       len(result.Hardware.CPUs),
			Model:        model,
			Cores:        cores,
			Threads:      threads,
			FrequencyMHz: int(parseFloat(m[2])),
		})
	}
}

func parseMemory(content string, result *models.AnalysisResult) {
	if m := regexp.MustCompile(`(?m)^real memory\s*=\s*\d+\s+\((\d+)\s+MB\)`).FindStringSubmatch(content); len(m) == 2 {
		result.Hardware.Memory = append(result.Hardware.Memory, models.MemoryDIMM{
			Slot:    "system",
			Present: true,
			SizeMB:  parseInt(m[1]),
			Type:    "DRAM",
			Status:  "ok",
		})
		return
	}

	// Fallback for logs that only have active/inactive breakdown.
	if m := regexp.MustCompile(`(?m)^Mem:\s+(.+)$`).FindStringSubmatch(content); len(m) == 2 {
		totalMB := 0
		tokenRe := regexp.MustCompile(`(\d+)M`)
		for _, t := range tokenRe.FindAllStringSubmatch(m[1], -1) {
			totalMB += parseInt(t[1])
		}
		if totalMB > 0 {
			result.Hardware.Memory = append(result.Hardware.Memory, models.MemoryDIMM{
				Slot:    "system",
				Present: true,
				SizeMB:  totalMB,
				Type:    "DRAM",
				Status:  "estimated",
			})
		}
	}
}

func parseUptime(content string, result *models.AnalysisResult) {
	upRe := regexp.MustCompile(`(?m)^(\d+:\d+(?:AM|PM))\s+up\s+(.+?),\s+(\d+)\s+users?,\s+load averages?:\s+([\d.]+),\s+([\d.]+),\s+([\d.]+)$`)
	m := upRe.FindStringSubmatch(content)
	if len(m) != 7 {
		return
	}

	result.Events = append(result.Events, models.Event{
		Timestamp:   time.Now(),
		Source:      "System",
		EventType:   "Uptime",
		Severity:    models.SeverityInfo,
		Description: "System uptime and load averages parsed",
		RawData:     "time=" + m[1] + "; uptime=" + m[2] + "; users=" + m[3] + "; load=" + m[4] + "," + m[5] + "," + m[6],
	})
}

func parseZFSState(content string, result *models.AnalysisResult) {
	m := regexp.MustCompile(`(?m)^state:\s+([A-Z]+)$`).FindStringSubmatch(content)
	if len(m) != 2 {
		return
	}

	state := m[1]
	severity := models.SeverityInfo
	if state != "ONLINE" {
		severity = models.SeverityWarning
	}
	result.Events = append(result.Events, models.Event{
		Timestamp:   time.Now(),
		Source:      "ZFS",
		EventType:   "Pool State",
		Severity:    severity,
		Description: "ZFS pool state: " + state,
		RawData:     state,
	})
}

func parseStorageAndSMART(content string, result *models.AnalysisResult) {
	type smartInfo struct {
		model     string
		serial    string
		firmware  string
		health    string
		tempC     int
		capacityB int64
	}

	storageBySlot := make(map[string]*models.Storage)
	scsiRe := regexp.MustCompile(`(?m)^<([^>]+)>\s+at\s+scbus\d+\s+target\s+\d+\s+lun\s+\d+\s+\(([^,]+),([^)]+)\)$`)
	for _, m := range scsiRe.FindAllStringSubmatch(content, -1) {
		slot := strings.TrimSpace(m[3])
		model, fw := splitModelAndFirmware(strings.TrimSpace(m[1]))
		entry := &models.Storage{
			Slot:      slot,
			Type:      guessStorageType(slot),
			Model:     model,
			Firmware:  fw,
			Present:   true,
			Interface: "SCSI/SATA",
		}
		storageBySlot[slot] = entry
	}

	smartBySlot := make(map[string]smartInfo)
	sectionRe := regexp.MustCompile(`(?m)^S\.M\.A\.R\.T\.\s+\[(/dev/[^\]]+)\]:\s*\n-+\n`)
	sections := sectionRe.FindAllStringSubmatchIndex(content, -1)
	for i, sec := range sections {
		// sec indexes:
		// [0]=full start, [1]=full end, [2]=capture 1 start, [3]=capture 1 end
		if len(sec) < 4 {
			continue
		}
		slot := strings.TrimPrefix(strings.TrimSpace(content[sec[2]:sec[3]]), "/dev/")
		bodyStart := sec[1]
		bodyEnd := len(content)
		if i+1 < len(sections) {
			bodyEnd = sections[i+1][0]
		}
		body := content[bodyStart:bodyEnd]

		info := smartInfo{
			model:    findFirst(body, `(?m)^Device Model:\s+(.+)$`),
			serial:   findFirst(body, `(?m)^Serial Number:\s+(.+)$`),
			firmware: findFirst(body, `(?m)^Firmware Version:\s+(.+)$`),
			health:   findFirst(body, `(?m)^SMART overall-health self-assessment test result:\s+(.+)$`),
		}
		info.capacityB = parseCapacityBytes(findFirst(body, `(?m)^User Capacity:\s+([\d,]+)\s+bytes`))
		if t := findFirst(body, `(?m)^\s*194\s+Temperature_Celsius.*?-\s+(\d+)(?:\s|\()`); t != "" {
			info.tempC = parseInt(t)
		}
		smartBySlot[slot] = info

		if info.tempC > 0 {
			status := "ok"
			if info.health != "" && !strings.EqualFold(info.health, "PASSED") {
				status = "warning"
			}
			result.Sensors = append(result.Sensors, models.SensorReading{
				Name:     "disk_temp_" + slot,
				Type:     "temperature",
				Value:    float64(info.tempC),
				Unit:     "C",
				Status:   status,
				RawValue: strconv.Itoa(info.tempC),
			})
		}
		if info.health != "" && !strings.EqualFold(info.health, "PASSED") {
			result.Events = append(result.Events, models.Event{
				Timestamp:   time.Now(),
				Source:      "SMART",
				EventType:   "Disk Health",
				Severity:    models.SeverityWarning,
				Description: "SMART health is not PASSED for " + slot,
				RawData:     info.health,
			})
		}
	}

	// Merge SMART data into storage entries and add missing entries.
	for slot, info := range smartBySlot {
		s := storageBySlot[slot]
		if s == nil {
			s = &models.Storage{
				Slot:      slot,
				Type:      guessStorageType(slot),
				Present:   true,
				Interface: "SATA",
			}
			storageBySlot[slot] = s
		}

		if s.Model == "" && info.model != "" {
			s.Model = info.model
		}
		if info.serial != "" {
			s.SerialNumber = info.serial
		}
		if s.Firmware == "" && info.firmware != "" {
			s.Firmware = info.firmware
		}
		if info.capacityB > 0 {
			s.SizeGB = int(info.capacityB / 1_000_000_000)
		}
	}

	for _, s := range storageBySlot {
		result.Hardware.Storage = append(result.Hardware.Storage, *s)
	}
}

func parseJournalLogSections(content string, result *models.AnalysisResult) {
	sections := []struct {
		heading   string
		eventType string
		source    string
	}{
		{heading: "Last 275 System log entries:", eventType: "System Log", source: "system.log"},
		{heading: "Last 275 SMARTD log entries:", eventType: "SMARTD Log", source: "smartd.log"},
		{heading: "Last 275 Daemon log entries:", eventType: "Daemon Log", source: "daemon.log"},
	}

	for _, sec := range sections {
		body := extractLogSection(content, sec.heading)
		if body == "" {
			continue
		}

		for _, line := range strings.Split(body, "\n") {
			line = strings.TrimSpace(line)
			if line == "" {
				continue
			}

			msg := extractSyslogMessage(line)
			if msg == "" {
				msg = line
			}

			result.Events = append(result.Events, models.Event{
				Timestamp:   parseEventTimestamp(line),
				Source:      sec.source,
				EventType:   sec.eventType,
				Severity:    classifyEventSeverity(line),
				Description: msg,
				RawData:     line,
			})
		}
	}
}

func extractLogSection(content, heading string) string {
	start := strings.Index(content, heading)
	if start == -1 {
		return ""
	}

	tail := content[start+len(heading):]
	lines := strings.Split(tail, "\n")
	i := 0
	for i < len(lines) && strings.TrimSpace(lines[i]) == "" {
		i++
	}
	if i < len(lines) && isDashLine(lines[i]) {
		i++
	}

	out := make([]string, 0, 64)
	for ; i < len(lines); i++ {
		line := lines[i]
		trimmed := strings.TrimSpace(line)
		if strings.HasPrefix(trimmed, "Last 275 ") && strings.HasSuffix(trimmed, " log entries:") {
			break
		}
		out = append(out, line)
	}

	return strings.TrimSpace(strings.Join(out, "\n"))
}

func isDashLine(s string) bool {
	s = strings.TrimSpace(s)
	if s == "" {
		return false
	}
	for _, r := range s {
		if r != '-' {
			return false
		}
	}
	return true
}

func parseEventTimestamp(line string) time.Time {
	isoRe := regexp.MustCompile(`\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?[+-]\d{2}:\d{2}\b`)
	if iso := isoRe.FindString(line); iso != "" {
		if ts, err := time.Parse(time.RFC3339Nano, iso); err == nil {
			return ts
		}
	}

	prefixRe := regexp.MustCompile(`^[A-Z][a-z]{2}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}`)
	if prefix := prefixRe.FindString(line); prefix != "" {
		year := time.Now().Year()
		if ts, err := parser.ParseInDefaultArchiveLocation("Jan 2 15:04:05 2006", prefix+" "+strconv.Itoa(year)); err == nil {
			return ts
		}
	}

	return time.Now()
}

func classifyEventSeverity(line string) models.Severity {
	lower := strings.ToLower(line)
	switch {
	case strings.Contains(lower, "panic"), strings.Contains(lower, "fatal"), strings.Contains(lower, "critical"):
		return models.SeverityCritical
	case strings.Contains(lower, "warning"),
		strings.Contains(lower, "error"),
		strings.Contains(lower, "failed"),
		strings.Contains(lower, "failure"),
		strings.Contains(lower, "login failure"),
		strings.Contains(lower, "limiting open port"):
		return models.SeverityWarning
	default:
		return models.SeverityInfo
	}
}

func extractSyslogMessage(line string) string {
	if idx := strings.Index(line, ": "); idx != -1 && idx+2 < len(line) {
		return strings.TrimSpace(line[idx+2:])
	}

	// RFC5424-like segment in XigmaNAS dumps: "... <host> <proc> <pid> - - <message>"
	fields := strings.Fields(line)
	if len(fields) > 10 {
		return strings.TrimSpace(strings.Join(fields[10:], " "))
	}

	return strings.TrimSpace(line)
}

func splitModelAndFirmware(raw string) (string, string) {
	fields := strings.Fields(raw)
	if len(fields) < 2 {
		return raw, ""
	}
	last := fields[len(fields)-1]
	// Firmware token is usually compact (e.g. GKAOAB0A, 1.00).
	if regexp.MustCompile(`^[A-Za-z0-9._-]{2,12}$`).MatchString(last) {
		return strings.TrimSpace(strings.Join(fields[:len(fields)-1], " ")), last
	}
	return raw, ""
}

func guessStorageType(slot string) string {
	switch {
	case strings.HasPrefix(slot, "cd"):
		return "optical"
	case strings.HasPrefix(slot, "da"), strings.HasPrefix(slot, "ada"):
		return "hdd"
	default:
		return "unknown"
	}
}

func findFirst(content, expr string) string {
	m := regexp.MustCompile(expr).FindStringSubmatch(content)
	if len(m) != 2 {
		return ""
	}
	return strings.TrimSpace(m[1])
}

func parseCapacityBytes(s string) int64 {
	clean := strings.ReplaceAll(strings.TrimSpace(s), ",", "")
	if clean == "" {
		return 0
	}
	v, err := strconv.ParseInt(clean, 10, 64)
	if err != nil {
		return 0
	}
	return v
}

func parseInt(s string) int {
	v, _ := strconv.Atoi(strings.TrimSpace(s))
	return v
}

func parseFloat(s string) float64 {
	v, _ := strconv.ParseFloat(strings.TrimSpace(s), 64)
	return v
}
