package collector

import (
	"bee/audit/internal/schema"
	"bufio"
	"log/slog"
	"os"
	"os/exec"
	"strings"
)

var execDmidecode = func(typeNum string) (string, error) {
	out, err := exec.Command("dmidecode", "-t", typeNum).Output()
	if err != nil {
		return "", err
	}
	return string(out), nil
}

var execIpmitool = func(args ...string) (string, error) {
	out, err := exec.Command("ipmitool", args...).Output()
	if err != nil {
		return "", err
	}
	return string(out), nil
}

// collectBoard runs dmidecode for types 0, 1, 2 and returns the board record
// plus the BIOS firmware entry. Any failure is logged and returns zero values.
func collectBoard() (schema.HardwareBoard, []schema.HardwareFirmwareRecord) {
	type1, err := runDmidecode("1")
	if err != nil {
		slog.Warn("board: dmidecode type 1 failed", "err", err)
		return schema.HardwareBoard{}, nil
	}

	type2, err := runDmidecode("2")
	if err != nil {
		slog.Warn("board: dmidecode type 2 failed", "err", err)
	}

	type0, err := runDmidecode("0")
	if err != nil {
		slog.Warn("board: dmidecode type 0 failed", "err", err)
	}

	board := parseBoard(type1, type2)
	firmware := parseBIOSFirmware(type0)

	slog.Info("board: collected", "serial", board.SerialNumber)
	return board, firmware
}

// parseBoard extracts HardwareBoard from dmidecode type 1 (System) and type 2 (Baseboard) output.
func parseBoard(type1, type2 string) schema.HardwareBoard {
	sys := parseDMIFields(type1, "System Information")
	base := parseDMIFields(type2, "Base Board Information")

	board := schema.HardwareBoard{}

	if v := cleanDMIValue(sys["Manufacturer"]); v != "" {
		board.Manufacturer = &v
	}
	if v := cleanDMIValue(sys["Product Name"]); v != "" {
		board.ProductName = &v
	}
	if v := cleanDMIValue(sys["Serial Number"]); v != "" {
		board.SerialNumber = v
	}
	if v := cleanDMIValue(sys["UUID"]); v != "" {
		board.UUID = &v
	}
	// part number comes from baseboard Product Name
	if v := cleanDMIValue(base["Product Name"]); v != "" {
		board.PartNumber = &v
	}

	return board
}

// collectBMCFirmware collects BMC firmware version via ipmitool mc info.
// Returns nil if ipmitool is missing, /dev/ipmi0 is absent, or any error occurs.
func collectBMCFirmware() []schema.HardwareFirmwareRecord {
	if _, err := exec.LookPath("ipmitool"); err != nil {
		return nil
	}
	if _, err := os.Stat("/dev/ipmi0"); err != nil {
		return nil
	}
	out, err := execIpmitool("mc", "info")
	if err != nil {
		slog.Info("bmc: ipmitool mc info unavailable", "err", err)
		return nil
	}
	version := parseBMCFirmwareRevision(out)
	if version == "" {
		return nil
	}
	slog.Info("bmc: collected", "version", version)
	return []schema.HardwareFirmwareRecord{
		{DeviceName: "BMC", Version: version},
	}
}

// parseBMCFirmwareRevision extracts the "Firmware Revision" field from ipmitool mc info output.
func parseBMCFirmwareRevision(out string) string {
	for _, line := range strings.Split(out, "\n") {
		line = strings.TrimSpace(line)
		key, val, ok := strings.Cut(line, ":")
		if !ok {
			continue
		}
		if strings.TrimSpace(key) == "Firmware Revision" {
			return strings.TrimSpace(val)
		}
	}
	return ""
}

// parseBIOSFirmware extracts BIOS version from dmidecode type 0 output.
func parseBIOSFirmware(type0 string) []schema.HardwareFirmwareRecord {
	fields := parseDMIFields(type0, "BIOS Information")

	version := cleanDMIValue(fields["Version"])
	if version == "" {
		return nil
	}
	return []schema.HardwareFirmwareRecord{
		{DeviceName: "BIOS", Version: version},
	}
}

// parseDMIFields parses the key-value pairs from a dmidecode section.
// sectionTitle is the section header line to find (e.g. "System Information").
// Returns a map of trimmed field names to raw values.
func parseDMIFields(output, sectionTitle string) map[string]string {
	fields := make(map[string]string)
	inSection := false

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

		if strings.TrimSpace(line) == sectionTitle {
			inSection = true
			continue
		}

		if inSection {
			// blank line or new Handle line = end of section
			if line == "" || strings.HasPrefix(line, "Handle ") {
				break
			}
			// skip sub-list items (double tab indent)
			if strings.HasPrefix(line, "\t\t") {
				continue
			}
			// key: value line (single tab indent)
			trimmed := strings.TrimPrefix(line, "\t")
			if idx := strings.Index(trimmed, ": "); idx >= 0 {
				key := trimmed[:idx]
				val := trimmed[idx+2:]
				fields[key] = val
			}
		}
	}
	return fields
}

// cleanDMIValue returns empty string for known placeholder values that vendors
// use when a field is unpopulated.
func cleanDMIValue(v string) string {
	v = strings.TrimSpace(v)
	if v == "" {
		return ""
	}
	upper := strings.ToUpper(v)
	placeholders := []string{
		"TO BE FILLED BY O.E.M.",
		"NOT SPECIFIED",
		"NOT SETTABLE",
		"NOT PRESENT",
		"UNKNOWN",
		"N/A",
		"NONE",
		"NULL",
		"DEFAULT STRING",
		"0",
	}
	for _, p := range placeholders {
		if upper == p {
			return ""
		}
	}
	return v
}

// runDmidecode executes dmidecode -t <typeNum> and returns its stdout.
func runDmidecode(typeNum string) (string, error) {
	return execDmidecode(typeNum)
}
