package collector

import (
	"bee/audit/internal/schema"
	"bufio"
	"log/slog"
	"os"
	"path/filepath"
	"strconv"
	"strings"
)

// collectCPUs runs dmidecode -t 4 and enriches CPUs with microcode from sysfs.
func collectCPUs() []schema.HardwareCPU {
	out, err := runDmidecode("4")
	if err != nil {
		slog.Warn("cpu: dmidecode type 4 failed", "err", err)
		return nil
	}

	cpus := parseCPUs(out)
	if mc := readMicrocode(); mc != "" {
		for i := range cpus {
			cpus[i].Firmware = &mc
		}
	}

	slog.Info("cpu: collected", "count", len(cpus))
	return cpus
}

// parseCPUs splits dmidecode output into per-processor sections and parses each.
func parseCPUs(output string) []schema.HardwareCPU {
	sections := splitDMISections(output, "Processor Information")
	cpus := make([]schema.HardwareCPU, 0, len(sections))

	for _, section := range sections {
		cpu, ok := parseCPUSection(section)
		if !ok {
			continue
		}
		cpus = append(cpus, cpu)
	}
	return cpus
}

// parseCPUSection parses one "Processor Information" block into a HardwareCPU.
// Returns false if the socket is unpopulated.
func parseCPUSection(fields map[string]string) (schema.HardwareCPU, bool) {
	status := parseCPUStatus(fields["Status"])
	if status == statusEmpty {
		return schema.HardwareCPU{}, false
	}

	cpu := schema.HardwareCPU{}
	cpu.Status = &status
	present := true
	cpu.Present = &present

	if socket, ok := parseSocketIndex(fields["Socket Designation"]); ok {
		cpu.Socket = &socket
	}

	if v := cleanDMIValue(fields["Version"]); v != "" {
		cpu.Model = &v
	}
	if v := cleanManufacturer(fields["Manufacturer"]); v != "" {
		cpu.Manufacturer = &v
	}
	if v := cleanDMIValue(fields["Serial Number"]); v != "" {
		cpu.SerialNumber = &v
	}

	if v := parseMHz(fields["Max Speed"]); v > 0 {
		cpu.MaxFrequencyMHz = &v
	}
	if v := parseMHz(fields["Current Speed"]); v > 0 {
		cpu.FrequencyMHz = &v
	}
	if v := parseInt(fields["Core Count"]); v > 0 {
		cpu.Cores = &v
	}
	if v := parseInt(fields["Thread Count"]); v > 0 {
		cpu.Threads = &v
	}

	return cpu, true
}

// parseCPUStatus maps dmidecode Status field to our status vocabulary.
func parseCPUStatus(raw string) string {
	raw = strings.TrimSpace(raw)
	upper := strings.ToUpper(raw)
	switch {
	case upper == "" || upper == "UNKNOWN":
		return statusUnknown
	case strings.Contains(upper, "UNPOPULATED") || strings.Contains(upper, "NOT POPULATED"):
		return statusEmpty
	case strings.Contains(upper, "ENABLED"):
		return statusOK
	case strings.Contains(upper, "DISABLED"):
		return statusWarning
	default:
		return statusUnknown
	}
}

// parseSocketIndex extracts the integer socket index from strings like
// "CPU0", "CPU1", "Processor 1", "Socket 0", etc.
func parseSocketIndex(raw string) (int, bool) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return 0, false
	}
	// strip leading non-digit prefix and parse the first integer found
	digits := ""
	for _, r := range raw {
		if r >= '0' && r <= '9' {
			digits += string(r)
		} else if digits != "" {
			break
		}
	}
	if digits == "" {
		return 0, false
	}
	n, err := strconv.Atoi(digits)
	if err != nil {
		return 0, false
	}
	return n, true
}

// cleanManufacturer normalises CPU manufacturer strings.
// "Intel(R) Corporation" → "Intel", "AMD" → "AMD".
func cleanManufacturer(v string) string {
	v = cleanDMIValue(v)
	if v == "" {
		return ""
	}
	// strip "(R)" and "(TM)" suffixes, trim "Corporation" / "Inc."
	v = strings.ReplaceAll(v, "(R)", "")
	v = strings.ReplaceAll(v, "(TM)", "")
	v = strings.ReplaceAll(v, " Corporation", "")
	v = strings.ReplaceAll(v, " Inc.", "")
	v = strings.ReplaceAll(v, " Inc", "")
	return strings.TrimSpace(v)
}

// parseMHz parses "4000 MHz" → 4000. Returns 0 on failure.
func parseMHz(v string) int {
	v = strings.TrimSpace(v)
	v = strings.TrimSuffix(v, " MHz")
	v = strings.TrimSpace(v)
	n, err := strconv.Atoi(v)
	if err != nil {
		return 0
	}
	return n
}

// parseInt parses a plain integer string. Returns 0 on failure or "N/A".
func parseInt(v string) int {
	v = strings.TrimSpace(v)
	n, err := strconv.Atoi(v)
	if err != nil {
		return 0
	}
	return n
}

// readMicrocode reads the CPU microcode revision from sysfs.
// Returns empty string if unavailable.
func readMicrocode() string {
	data, err := os.ReadFile(filepath.Join(cpuSysBaseDir, "cpu0", "microcode", "version"))
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(data))
}

// splitDMISections splits dmidecode output into sections by a given section title.
// Returns a slice of field maps, one per matching section.
func splitDMISections(output, sectionTitle string) []map[string]string {
	var sections []map[string]string
	var current []string
	inSection := false

	scanner := bufio.NewScanner(strings.NewReader(output))
	for scanner.Scan() {
		line := scanner.Text()

		if strings.TrimSpace(line) == sectionTitle {
			if inSection && len(current) > 0 {
				sections = append(sections, parseFieldLines(current))
			}
			current = nil
			inSection = true
			continue
		}

		if inSection {
			if strings.HasPrefix(line, "Handle ") {
				sections = append(sections, parseFieldLines(current))
				current = nil
				inSection = false
				continue
			}
			current = append(current, line)
		}
	}
	if inSection && len(current) > 0 {
		sections = append(sections, parseFieldLines(current))
	}

	return sections
}

// parseFieldLines converts raw section lines into a key→value map.
// Skips sub-list items (double-tab indented lines).
func parseFieldLines(lines []string) map[string]string {
	fields := make(map[string]string)
	for _, line := range lines {
		if strings.HasPrefix(line, "\t\t") {
			continue
		}
		trimmed := strings.TrimPrefix(line, "\t")
		if idx := strings.Index(trimmed, ": "); idx >= 0 {
			fields[trimmed[:idx]] = trimmed[idx+2:]
		}
	}
	return fields
}
