package api

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"clicd/internal/lxc"
)

type HostInfo struct {
	CPU     CpuInfo     `json:"cpu"`
	RAM     MemoryInfo  `json:"ram"`
	Disk    DiskInfo    `json:"disk"`
	Network NetworkInfo `json:"network"`
	DiskIO  DiskIOInfo  `json:"disk_io"`
	Load    LoadInfo    `json:"load"`
}

type HostProbeReport struct {
	GeneratedAt       string                `json:"generated_at"`
	Hostname          string                `json:"hostname"`
	Kernel            string                `json:"kernel"`
	OS                string                `json:"os"`
	CPU               HostCPUProbe          `json:"cpu"`
	Memory            HostMemoryProbe       `json:"memory"`
	Disks             []HostDiskProbe       `json:"disks"`
	NetworkInterfaces []HostNICProbe        `json:"network_interfaces"`
	PublicIPv4        []string              `json:"public_ipv4"`
	IPv4Addresses     []HostIPProbe         `json:"ipv4_addresses"`
	IPv4Prefixes      []HostIPv4PrefixProbe `json:"ipv4_prefixes"`
	IPv6Addresses     []HostIPProbe         `json:"ipv6_addresses"`
	IPv6Prefixes      []lxc.IPv6PrefixInfo  `json:"ipv6_prefixes"`
	Gateways          []HostGatewayProbe    `json:"gateways"`
	GPUs              []HostGPUProbe        `json:"gpus"`
	Runtime           HostRuntimeProbe      `json:"runtime"`
	System            HostSystemProbe       `json:"system"`
	Environment       []HostEnvCheck        `json:"environment"`
}

type HostCPUProbe struct {
	Model             string   `json:"model"`
	Cores             int      `json:"cores"`
	Threads           int      `json:"threads"`
	Architecture      string   `json:"architecture"`
	Flags             []string `json:"flags"`
	HasIntegratedGPU  bool     `json:"has_integrated_gpu"`
	Virtualization    bool     `json:"virtualization"`
	VirtualizationKey string   `json:"virtualization_key"`
}

type HostMemoryProbe struct {
	TotalMB int64              `json:"total_mb"`
	UsedMB  int64              `json:"used_mb"`
	FreeMB  int64              `json:"free_mb"`
	Modules []HostMemoryModule `json:"modules"`
}

type HostMemoryModule struct {
	Locator      string `json:"locator"`
	Size         string `json:"size"`
	Type         string `json:"type"`
	Speed        string `json:"speed"`
	Manufacturer string `json:"manufacturer"`
	PartNumber   string `json:"part_number"`
	SerialNumber string `json:"serial_number"`
}

type HostDiskProbe struct {
	Name         string             `json:"name"`
	Path         string             `json:"path"`
	Model        string             `json:"model"`
	Serial       string             `json:"serial"`
	SizeBytes    uint64             `json:"size_bytes"`
	Type         string             `json:"type"`
	Virtual      bool               `json:"virtual"`
	Rotational   bool               `json:"rotational"`
	Mountpoints  []string           `json:"mountpoints"`
	Health       string             `json:"health"`
	HealthDetail string             `json:"health_detail"`
	SMART        HostDiskSMARTProbe `json:"smart"`
}

type HostDiskSMARTProbe struct {
	Available         bool   `json:"available"`
	LifeUsedPercent   *int   `json:"life_used_percent,omitempty"`
	PowerOnHours      int64  `json:"power_on_hours,omitempty"`
	PowerCycleCount   int64  `json:"power_cycle_count,omitempty"`
	ReadDataBytes     uint64 `json:"read_data_bytes,omitempty"`
	WrittenDataBytes  uint64 `json:"written_data_bytes,omitempty"`
	ReadCommands      uint64 `json:"read_commands,omitempty"`
	WriteCommands     uint64 `json:"write_commands,omitempty"`
	WearLevelingCount string `json:"wear_leveling_count,omitempty"`
	EraseCount        string `json:"erase_count,omitempty"`
	MediaErrors       uint64 `json:"media_errors,omitempty"`
}

type HostNICProbe struct {
	Name      string        `json:"name"`
	MAC       string        `json:"mac"`
	State     string        `json:"state"`
	SpeedMbps int           `json:"speed_mbps"`
	Driver    string        `json:"driver"`
	Model     string        `json:"model"`
	IPv4      []HostIPProbe `json:"ipv4"`
	IPv6      []HostIPProbe `json:"ipv6"`
}

type HostIPProbe struct {
	Interface string `json:"interface"`
	Address   string `json:"address"`
	PrefixLen int    `json:"prefix_len"`
	Scope     string `json:"scope"`
	Gateway   string `json:"gateway,omitempty"`
}

type HostIPv4PrefixProbe struct {
	Interface  string `json:"interface"`
	Address    string `json:"address"`
	Prefix     string `json:"prefix"`
	PrefixLen  int    `json:"prefix_len"`
	SubnetMask string `json:"subnet_mask"`
	Gateway    string `json:"gateway"`
	Source     string `json:"source"`
}

type HostGatewayProbe struct {
	Family    string `json:"family"`
	Interface string `json:"interface"`
	Gateway   string `json:"gateway"`
}

type HostGPUProbe struct {
	Name   string `json:"name"`
	Vendor string `json:"vendor"`
	Driver string `json:"driver"`
	Type   string `json:"type"`
}

type HostRuntimeProbe struct {
	LXCAvailable         bool   `json:"lxc_available"`
	KVMAvailable         bool   `json:"kvm_available"`
	DevKVM               bool   `json:"dev_kvm"`
	NestedVirtualization bool   `json:"nested_virtualization"`
	NestedDetail         string `json:"nested_detail"`
	SupportMode          string `json:"support_mode"`
}

type HostSystemProbe struct {
	UptimeSeconds int64  `json:"uptime_seconds"`
	UptimeText    string `json:"uptime_text"`
	ProcessCount  int    `json:"process_count"`
}

type HostEnvCheck struct {
	Key      string `json:"key"`
	Label    string `json:"label"`
	OK       bool   `json:"ok"`
	Required bool   `json:"required"`
	Detail   string `json:"detail"`
}

type LoadInfo struct {
	Load1  float64 `json:"load1"`
	Load5  float64 `json:"load5"`
	Load15 float64 `json:"load15"`
}

type CpuInfo struct {
	Cores int     `json:"cores"`
	Usage float64 `json:"usage_pct"`
}

type MemoryInfo struct {
	TotalMB int64 `json:"total_mb"`
	UsedMB  int64 `json:"used_mb"`
	FreeMB  int64 `json:"free_mb"`
}

type DiskInfo struct {
	TotalGB float64 `json:"total_gb"`
	UsedGB  float64 `json:"used_gb"`
	FreeGB  float64 `json:"free_gb"`
}

type NetworkInfo struct {
	RXBytes             uint64               `json:"rx_bytes"`
	TXBytes             uint64               `json:"tx_bytes"`
	RXBps               float64              `json:"rx_bps"`
	TXBps               float64              `json:"tx_bps"`
	PublicIPv4          string               `json:"public_ipv4"`
	PublicIPv4Interface string               `json:"public_ipv4_interface"`
	PublicIPv4Addresses []lxc.PublicIPInfo   `json:"public_ipv4_addresses"`
	PublicIPv6          string               `json:"public_ipv6"`
	PublicIPv6Interface string               `json:"public_ipv6_interface"`
	IPv6Prefixes        []lxc.IPv6PrefixInfo `json:"ipv6_prefixes"`
}

type DiskIOInfo struct {
	ReadBytes  uint64  `json:"read_bytes"`
	WriteBytes uint64  `json:"write_bytes"`
	ReadBps    float64 `json:"read_bps"`
	WriteBps   float64 `json:"write_bps"`
}

var hostCPUMu sync.Mutex
var lastHostCPU cpuTimes
var hostIOMu sync.Mutex
var lastHostIO hostIOSample

type cpuTimes struct {
	Total uint64
	Idle  uint64
}

type hostIOSample struct {
	RXBytes    uint64
	TXBytes    uint64
	ReadBytes  uint64
	WriteBytes uint64
	At         int64
}

func getHostInfo() HostInfo {
	info := HostInfo{
		CPU: CpuInfo{Cores: runtime.NumCPU()},
	}

	info.RAM = getMemoryInfo()
	info.Disk = getDiskInfo()
	info.CPU.Usage = getCPUUsage()
	info.Network, info.DiskIO = getHostRates()
	info.Load = getLoadInfo()
	return info
}

func getMemoryInfo() MemoryInfo {
	f, err := os.Open("/proc/meminfo")
	if err != nil {
		return MemoryInfo{TotalMB: 0, UsedMB: 0, FreeMB: 0}
	}
	defer f.Close()

	var total, available, free int64
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Text()
		fields := strings.Fields(line)
		if len(fields) < 2 {
			continue
		}
		val, _ := strconv.ParseInt(fields[1], 10, 64)
		switch fields[0] {
		case "MemTotal:":
			total = val / 1024
		case "MemAvailable:":
			available = val / 1024
		case "MemFree:":
			free = val / 1024
		}
	}

	used := total - available
	if available == 0 {
		used = total - free
	}

	return MemoryInfo{
		TotalMB: total,
		UsedMB:  used,
		FreeMB:  available,
	}
}

func getDiskInfo() DiskInfo {
	if info, ok := getRootDiskInfo(); ok {
		return info
	}

	{
		// Try command-based fallback
		cmd := exec.Command("df", "-BG", "/")
		output, err := cmd.Output()
		if err == nil {
			lines := strings.Split(string(output), "\n")
			if len(lines) >= 2 {
				fields := strings.Fields(lines[1])
				if len(fields) >= 4 {
					total, _ := parseSizeGBf(fields[1])
					used, _ := parseSizeGBf(fields[2])
					free, _ := parseSizeGBf(fields[3])
					return DiskInfo{TotalGB: total, UsedGB: used, FreeGB: free}
				}
			}
		}
		return DiskInfo{}
	}
}

func getCPUUsage() float64 {
	current, err := readCPUTimes()
	if err != nil {
		return 0
	}

	hostCPUMu.Lock()
	defer hostCPUMu.Unlock()

	if lastHostCPU.Total == 0 {
		lastHostCPU = current
		return 0
	}

	totalDelta := current.Total - lastHostCPU.Total
	idleDelta := current.Idle - lastHostCPU.Idle
	lastHostCPU = current

	if totalDelta == 0 {
		return 0
	}

	usage := (1 - float64(idleDelta)/float64(totalDelta)) * 100
	if usage < 0 {
		return 0
	}
	if usage > 100 {
		return 100
	}
	return usage
}

func readCPUTimes() (cpuTimes, error) {
	f, err := os.Open("/proc/stat")
	if err != nil {
		return cpuTimes{}, err
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)
	if !scanner.Scan() {
		return cpuTimes{}, scanner.Err()
	}

	fields := strings.Fields(scanner.Text())
	if len(fields) < 8 || fields[0] != "cpu" {
		return cpuTimes{}, nil
	}

	var values []uint64
	for _, field := range fields[1:] {
		value, _ := strconv.ParseUint(field, 10, 64)
		values = append(values, value)
	}

	var total uint64
	for _, value := range values {
		total += value
	}

	idle := values[3]
	if len(values) > 4 {
		idle += values[4]
	}

	return cpuTimes{Total: total, Idle: idle}, nil
}

func parseSizeGB(s string) (int64, error) {
	s = strings.TrimSuffix(s, "G")
	s = strings.TrimSpace(s)
	val, err := strconv.ParseInt(s, 10, 64)
	return val, err
}

func parseSizeGBf(s string) (float64, error) {
	s = strings.TrimSuffix(s, "G")
	s = strings.TrimSpace(s)
	val, err := strconv.ParseFloat(s, 64)
	return val, err
}

func getHostRates() (NetworkInfo, DiskIOInfo) {
	rx, tx := readHostNetworkBytes()
	readBytes, writeBytes := readHostDiskBytes()
	now := unixNano()

	network := NetworkInfo{RXBytes: rx, TXBytes: tx}
	publicIPv4 := lxc.DetectPublicIPv4()
	network.PublicIPv4 = publicIPv4.Address
	network.PublicIPv4Interface = publicIPv4.Interface
	network.PublicIPv4Addresses = lxc.DetectFreePublicIPv4Candidates(0)
	network.IPv6Prefixes = lxc.DetectHostPublicIPv6Prefixes()
	if len(network.IPv6Prefixes) > 0 {
		network.PublicIPv6 = network.IPv6Prefixes[0].Address
		network.PublicIPv6Interface = network.IPv6Prefixes[0].Interface
	}
	diskIO := DiskIOInfo{ReadBytes: readBytes, WriteBytes: writeBytes}

	hostIOMu.Lock()
	defer hostIOMu.Unlock()

	if lastHostIO.At == 0 {
		lastHostIO = hostIOSample{RXBytes: rx, TXBytes: tx, ReadBytes: readBytes, WriteBytes: writeBytes, At: now}
		return network, diskIO
	}

	elapsed := float64(now-lastHostIO.At) / 1_000_000_000
	if elapsed > 0 {
		if rx >= lastHostIO.RXBytes {
			network.RXBps = float64(rx-lastHostIO.RXBytes) / elapsed
		}
		if tx >= lastHostIO.TXBytes {
			network.TXBps = float64(tx-lastHostIO.TXBytes) / elapsed
		}
		if readBytes >= lastHostIO.ReadBytes {
			diskIO.ReadBps = float64(readBytes-lastHostIO.ReadBytes) / elapsed
		}
		if writeBytes >= lastHostIO.WriteBytes {
			diskIO.WriteBps = float64(writeBytes-lastHostIO.WriteBytes) / elapsed
		}
	}

	lastHostIO = hostIOSample{RXBytes: rx, TXBytes: tx, ReadBytes: readBytes, WriteBytes: writeBytes, At: now}
	return network, diskIO
}

func readHostNetworkBytes() (uint64, uint64) {
	entries, err := os.ReadDir("/sys/class/net")
	if err != nil {
		return 0, 0
	}

	var rx, tx uint64
	for _, entry := range entries {
		name := entry.Name()
		if name == "lo" {
			continue
		}
		rx += readUintFile("/sys/class/net/" + name + "/statistics/rx_bytes")
		tx += readUintFile("/sys/class/net/" + name + "/statistics/tx_bytes")
	}
	return rx, tx
}

func readHostDiskBytes() (uint64, uint64) {
	f, err := os.Open("/proc/diskstats")
	if err != nil {
		return 0, 0
	}
	defer f.Close()

	var readSectors, writeSectors uint64
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		fields := strings.Fields(scanner.Text())
		if len(fields) < 14 {
			continue
		}
		device := fields[2]
		if strings.HasPrefix(device, "loop") ||
			strings.HasPrefix(device, "ram") ||
			strings.HasPrefix(device, "fd") ||
			strings.HasPrefix(device, "sr") {
			continue
		}
		read, _ := strconv.ParseUint(fields[5], 10, 64)
		write, _ := strconv.ParseUint(fields[9], 10, 64)
		readSectors += read
		writeSectors += write
	}
	return readSectors * 512, writeSectors * 512
}

func readUintFile(path string) uint64 {
	data, err := os.ReadFile(path)
	if err != nil {
		return 0
	}
	value, _ := strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64)
	return value
}

func unixNano() int64 {
	return time.Now().UnixNano()
}

func getLoadInfo() LoadInfo {
	f, err := os.Open("/proc/loadavg")
	if err != nil {
		return LoadInfo{}
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)
	if !scanner.Scan() {
		return LoadInfo{}
	}

	fields := strings.Fields(scanner.Text())
	if len(fields) < 3 {
		return LoadInfo{}
	}

	load1, _ := strconv.ParseFloat(fields[0], 64)
	load5, _ := strconv.ParseFloat(fields[1], 64)
	load15, _ := strconv.ParseFloat(fields[2], 64)
	return LoadInfo{Load1: load1, Load5: load5, Load15: load15}
}

func HandleHostReport(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
		return
	}
	if !requireScope(w, r, "host:read") {
		return
	}
	jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: getHostProbeReport()})
}

func getHostProbeReport() HostProbeReport {
	host, _ := os.Hostname()
	mem := getMemoryInfo()
	report := HostProbeReport{
		GeneratedAt:       time.Now().Format("2006-01-02 15:04:05"),
		Hostname:          host,
		Kernel:            strings.TrimSpace(runCommandOutput(2*time.Second, "uname", "-srmo")),
		OS:                detectOSRelease(),
		CPU:               detectHostCPUProbe(),
		Memory:            HostMemoryProbe{TotalMB: mem.TotalMB, UsedMB: mem.UsedMB, FreeMB: mem.FreeMB, Modules: detectMemoryModules()},
		Disks:             detectHostDisks(),
		NetworkInterfaces: detectHostNICs(),
		PublicIPv4:        detectAllPublicIPv4(),
		IPv6Prefixes:      lxc.DetectHostPublicIPv6Prefixes(),
		Gateways:          detectGateways(),
		GPUs:              detectGPUs(),
		System:            detectSystemProbe(),
		Environment:       detectHostEnvironment(),
	}
	report.IPv6Addresses = collectIPv6Addresses(report.NetworkInterfaces)
	report.IPv4Addresses = collectIPv4Addresses(report.NetworkInterfaces)
	report.IPv4Prefixes = detectPublicIPv4Prefixes(report.NetworkInterfaces, report.Gateways)
	report.Runtime = detectRuntimeProbe(report.Environment)
	report.CPU.HasIntegratedGPU = hasIntegratedGPU(report.GPUs)
	return report
}

func detectOSRelease() string {
	values := readKeyValueFile("/etc/os-release", "=")
	if pretty := trimOSReleaseValue(values["PRETTY_NAME"]); pretty != "" {
		return pretty
	}
	if name := trimOSReleaseValue(values["NAME"]); name != "" {
		return name
	}
	return strings.TrimSpace(runCommandOutput(2*time.Second, "uname", "-o"))
}

func trimOSReleaseValue(value string) string {
	return strings.Trim(strings.TrimSpace(value), `"`)
}

func detectHostCPUProbe() HostCPUProbe {
	probe := HostCPUProbe{Cores: runtime.NumCPU(), Threads: runtime.NumCPU(), Architecture: runtime.GOARCH}
	if data, err := os.ReadFile("/proc/cpuinfo"); err == nil {
		seenFlags := map[string]bool{}
		for _, line := range strings.Split(string(data), "\n") {
			fields := strings.SplitN(line, ":", 2)
			if len(fields) != 2 {
				continue
			}
			key := strings.TrimSpace(fields[0])
			value := strings.TrimSpace(fields[1])
			switch key {
			case "model name", "Hardware", "Processor":
				if probe.Model == "" {
					probe.Model = value
				}
			case "cpu cores":
				if cores, err := strconv.Atoi(value); err == nil && cores > probe.Cores {
					probe.Cores = cores
				}
			case "flags", "Features":
				for _, flag := range strings.Fields(value) {
					if flag == "vmx" || flag == "svm" {
						probe.Virtualization = true
						probe.VirtualizationKey = flag
					}
					if !seenFlags[flag] {
						seenFlags[flag] = true
						probe.Flags = append(probe.Flags, flag)
					}
				}
			}
		}
		sort.Strings(probe.Flags)
	}
	if probe.Model == "" {
		probe.Model = "Unknown"
	}
	return probe
}

func detectMemoryModules() []HostMemoryModule {
	if !commandExists("dmidecode") {
		return nil
	}
	out := runCommandOutput(4*time.Second, "dmidecode", "-t", "memory")
	modules := make([]HostMemoryModule, 0)
	var current HostMemoryModule
	inDevice := false
	flush := func() {
		if !inDevice {
			return
		}
		if current.Size != "" && !strings.EqualFold(current.Size, "No Module Installed") {
			modules = append(modules, current)
		}
		current = HostMemoryModule{}
	}
	for _, line := range strings.Split(out, "\n") {
		if strings.HasPrefix(line, "Memory Device") {
			flush()
			inDevice = true
			continue
		}
		if !inDevice {
			continue
		}
		parts := strings.SplitN(strings.TrimSpace(line), ":", 2)
		if len(parts) != 2 {
			continue
		}
		key, value := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
		switch key {
		case "Locator":
			current.Locator = value
		case "Size":
			current.Size = value
		case "Type":
			current.Type = value
		case "Speed":
			current.Speed = value
		case "Manufacturer":
			current.Manufacturer = value
		case "Part Number":
			current.PartNumber = value
		case "Serial Number":
			current.SerialNumber = value
		}
	}
	flush()
	return modules
}

func detectHostDisks() []HostDiskProbe {
	disks := make([]HostDiskProbe, 0)
	entries, err := os.ReadDir("/sys/block")
	if err != nil {
		return disks
	}
	mounts := detectMountpointsByDevice()
	for _, entry := range entries {
		name := entry.Name()
		if strings.HasPrefix(name, "loop") || strings.HasPrefix(name, "ram") || strings.HasPrefix(name, "fd") || strings.HasPrefix(name, "sr") {
			continue
		}
		base := filepath.Join("/sys/block", name)
		path := "/dev/" + name
		model := strings.TrimSpace(readFirstExistingFile(filepath.Join(base, "device/model"), filepath.Join(base, "device/name")))
		vendor := strings.TrimSpace(readFirstExistingFile(filepath.Join(base, "device/vendor")))
		virtual := isVirtualBlockDevice(name, model, vendor)
		disk := HostDiskProbe{
			Name:        name,
			Path:        path,
			Model:       model,
			Serial:      strings.TrimSpace(readFirstExistingFile(filepath.Join(base, "device/serial"), filepath.Join(base, "serial"))),
			SizeBytes:   readUintFile(filepath.Join(base, "size")) * 512,
			Type:        detectDiskType(base, name, virtual),
			Virtual:     virtual,
			Rotational:  strings.TrimSpace(readFirstExistingFile(filepath.Join(base, "queue/rotational"))) == "1",
			Mountpoints: mounts[name],
		}
		if !virtual {
			disk.SMART = detectDiskSMART(path)
		}
		disk.Health = disk.SMARTHealth()
		disk.HealthDetail = disk.SMARTDetail()
		disks = append(disks, disk)
	}
	sort.Slice(disks, func(i, j int) bool { return disks[i].Name < disks[j].Name })
	return disks
}

func detectDiskType(base, name string, virtual bool) string {
	if virtual {
		return "Virtual"
	}
	if strings.HasPrefix(name, "nvme") {
		return "NVMe"
	}
	if strings.TrimSpace(readFirstExistingFile(filepath.Join(base, "queue/rotational"))) == "1" {
		return "HDD"
	}
	return "SSD"
}

func isVirtualBlockDevice(name, model, vendor string) bool {
	lower := strings.ToLower(strings.TrimSpace(name + " " + model + " " + vendor))
	if strings.HasPrefix(name, "vd") || strings.HasPrefix(name, "xvd") {
		return true
	}
	for _, token := range []string{
		"qemu", "virtio", "virtual", "vmware", "vbox", "xen",
		"amazon elastic block store", "google persistentdisk", "microsoft",
	} {
		if strings.Contains(lower, token) {
			return true
		}
	}
	return false
}

func (disk HostDiskProbe) SMARTHealth() string {
	if disk.Virtual {
		return "virtual"
	}
	if disk.SMART.Available && disk.Health != "" {
		return disk.Health
	}
	return disk.SMART.Health()
}

func (disk HostDiskProbe) SMARTDetail() string {
	if disk.Virtual {
		return "虚拟磁盘，真实 SMART/寿命/通电数据需在物理宿主机查看"
	}
	return disk.SMART.Detail()
}

func (smart HostDiskSMARTProbe) Health() string {
	if !smart.Available {
		return "unknown"
	}
	if smart.MediaErrors > 0 {
		return "failed"
	}
	return "ok"
}

func (smart HostDiskSMARTProbe) Detail() string {
	if !smart.Available {
		return "smartctl not installed or no SMART output"
	}
	parts := []string{"SMART passed"}
	if smart.LifeUsedPercent != nil {
		parts = append(parts, fmt.Sprintf("寿命已用 %d%%", *smart.LifeUsedPercent))
	}
	if smart.PowerOnHours > 0 {
		parts = append(parts, fmt.Sprintf("通电 %dh", smart.PowerOnHours))
	}
	if smart.WrittenDataBytes > 0 {
		parts = append(parts, fmt.Sprintf("写入 %s", formatBytesText(smart.WrittenDataBytes)))
	}
	if smart.ReadDataBytes > 0 {
		parts = append(parts, fmt.Sprintf("读取 %s", formatBytesText(smart.ReadDataBytes)))
	}
	if smart.MediaErrors > 0 {
		parts = append(parts, fmt.Sprintf("介质错误 %d", smart.MediaErrors))
	}
	return strings.Join(parts, " | ")
}

func detectDiskSMART(path string) HostDiskSMARTProbe {
	smart := HostDiskSMARTProbe{}
	if !commandExists("smartctl") {
		return smart
	}
	out := runCommandOutput(8*time.Second, "smartctl", "-a", "-j", path)
	if strings.TrimSpace(out) == "" {
		return smart
	}
	var data smartctlOutput
	if err := json.Unmarshal([]byte(out), &data); err != nil {
		return smart
	}
	smart.Available = true
	smart.PowerOnHours = data.PowerOnTime.Hours
	smart.PowerCycleCount = data.PowerCycleCount
	if data.NVMe.PowerOnHours > 0 {
		smart.PowerOnHours = uint64ToInt64(data.NVMe.PowerOnHours)
	}
	if data.NVMe.PowerCycles > 0 {
		smart.PowerCycleCount = uint64ToInt64(data.NVMe.PowerCycles)
	}
	if data.NVMe.PercentageUsed > 0 {
		if used, ok := smartPercentToInt(data.NVMe.PercentageUsed); ok {
			smart.LifeUsedPercent = &used
		}
	}
	smart.ReadDataBytes = data.NVMe.DataUnitsRead * 512000
	smart.WrittenDataBytes = data.NVMe.DataUnitsWritten * 512000
	smart.ReadCommands = data.NVMe.HostReadCommands
	smart.WriteCommands = data.NVMe.HostWriteCommands
	smart.MediaErrors = data.NVMe.MediaErrors

	parseATAAttributes(&smart, data.ATASmartAttributes.Table)
	if data.SmartStatus != nil && !data.SmartStatus.Passed {
		smart.MediaErrors++
	}
	return smart
}

type smartctlOutput struct {
	SmartStatus *struct {
		Passed bool `json:"passed"`
	} `json:"smart_status"`
	PowerOnTime struct {
		Hours int64 `json:"hours"`
	} `json:"power_on_time"`
	PowerCycleCount    int64 `json:"power_cycle_count"`
	ATASmartAttributes struct {
		Table []smartctlAttribute `json:"table"`
	} `json:"ata_smart_attributes"`
	NVMe struct {
		PercentageUsed    uint64 `json:"percentage_used"`
		DataUnitsRead     uint64 `json:"data_units_read"`
		DataUnitsWritten  uint64 `json:"data_units_written"`
		HostReadCommands  uint64 `json:"host_reads"`
		HostWriteCommands uint64 `json:"host_writes"`
		PowerOnHours      uint64 `json:"power_on_hours"`
		PowerCycles       uint64 `json:"power_cycles"`
		MediaErrors       uint64 `json:"media_errors"`
	} `json:"nvme_smart_health_information_log"`
}

type smartctlAttribute struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Value int    `json:"value"`
	Raw   struct {
		Value  json.Number `json:"value"`
		String string      `json:"string"`
	} `json:"raw"`
}

func parseATAAttributes(smart *HostDiskSMARTProbe, attrs []smartctlAttribute) {
	for _, attr := range attrs {
		name := normalizeSMARTAttrName(attr.Name)
		raw := smartAttrRawUint(attr)
		rawText := smartAttrRawText(attr)
		switch name {
		case "poweronhours":
			if smart.PowerOnHours == 0 {
				if parsed, ok := smartAttrRawInt64(attr); ok {
					smart.PowerOnHours = parsed
				}
			}
		case "powercyclecount":
			if smart.PowerCycleCount == 0 {
				if parsed, ok := smartAttrRawInt64(attr); ok {
					smart.PowerCycleCount = parsed
				}
			}
		case "totallbaswritten":
			if smart.WrittenDataBytes == 0 {
				smart.WrittenDataBytes = raw * 512
			}
		case "totallbasread":
			if smart.ReadDataBytes == 0 {
				smart.ReadDataBytes = raw * 512
			}
		case "hostwrites32mib":
			if smart.WrittenDataBytes == 0 {
				smart.WrittenDataBytes = raw * 32 * 1024 * 1024
			}
		case "hostreads32mib":
			if smart.ReadDataBytes == 0 {
				smart.ReadDataBytes = raw * 32 * 1024 * 1024
			}
		case "hostwritecommands":
			smart.WriteCommands = raw
		case "hostreadcommands":
			smart.ReadCommands = raw
		case "wearlevelingcount":
			smart.WearLevelingCount = rawText
			if smart.LifeUsedPercent == nil && attr.Value > 0 && attr.Value <= 100 {
				used := 100 - attr.Value
				if used < 0 {
					used = 0
				}
				smart.LifeUsedPercent = &used
			}
		case "percentlifetimeremain", "mediawearoutindicator":
			if smart.LifeUsedPercent == nil {
				remaining := attr.Value
				if raw > 0 && raw <= 100 {
					if parsed, ok := smartPercentToInt(raw); ok {
						remaining = parsed
					}
				}
				used := 100 - remaining
				if used < 0 {
					used = 0
				}
				if used <= 100 {
					smart.LifeUsedPercent = &used
				}
			}
		case "percentageused":
			if smart.LifeUsedPercent == nil && raw <= 255 {
				if used, ok := smartPercentToInt(raw); ok {
					smart.LifeUsedPercent = &used
				}
			}
		case "erasefailcounttotal", "erasecount", "nandwrites", "programfailcnttotal":
			if smart.EraseCount == "" && rawText != "" {
				smart.EraseCount = rawText
			}
		case "mediaerrors":
			smart.MediaErrors = raw
		}
	}
}

func uint64ToInt64(value uint64) int64 {
	if value > 9223372036854775807 {
		return 0
	}
	return int64(value)
}

func smartPercentToInt(value uint64) (int, bool) {
	if value > 2147483647 {
		return 0, false
	}
	return int(value), true
}

func normalizeSMARTAttrName(name string) string {
	name = strings.ToLower(name)
	var b strings.Builder
	for _, ch := range name {
		if ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9' {
			b.WriteRune(ch)
		}
	}
	return b.String()
}

func smartAttrRawText(attr smartctlAttribute) string {
	if attr.Raw.String != "" {
		return attr.Raw.String
	}
	if attr.Raw.Value != "" {
		return attr.Raw.Value.String()
	}
	return ""
}

func smartAttrRawInt64(attr smartctlAttribute) (int64, bool) {
	text := smartAttrRawText(attr)
	if value, err := strconv.ParseInt(text, 10, 64); err == nil {
		return value, true
	}
	digits := firstUintText(text)
	if digits == "" {
		return 0, false
	}
	value, err := strconv.ParseInt(digits, 10, 64)
	if err != nil {
		return 0, false
	}
	return value, true
}

func smartAttrRawUint(attr smartctlAttribute) uint64 {
	text := smartAttrRawText(attr)
	if value, err := strconv.ParseUint(text, 10, 64); err == nil {
		return value
	}
	digits := firstUintText(text)
	if digits == "" {
		return 0
	}
	value, _ := strconv.ParseUint(digits, 10, 64)
	return value
}

func firstUintText(value string) string {
	start := -1
	for i, ch := range value {
		if ch >= '0' && ch <= '9' {
			if start < 0 {
				start = i
			}
			continue
		}
		if start >= 0 {
			return value[start:i]
		}
	}
	if start >= 0 {
		return value[start:]
	}
	return ""
}

func detectMountpointsByDevice() map[string][]string {
	result := map[string][]string{}
	f, err := os.Open("/proc/mounts")
	if err != nil {
		return result
	}
	defer f.Close()
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		fields := strings.Fields(scanner.Text())
		if len(fields) < 2 || !strings.HasPrefix(fields[0], "/dev/") {
			continue
		}
		dev := strings.TrimPrefix(filepath.Base(fields[0]), "/dev/")
		parent := diskParentName(dev)
		result[parent] = append(result[parent], fields[1])
	}
	return result
}

func diskParentName(dev string) string {
	for _, suffix := range []string{"p1", "p2", "p3", "p4", "p5", "p6", "p7", "p8", "p9"} {
		if strings.HasSuffix(dev, suffix) && strings.HasPrefix(dev, "nvme") {
			return strings.TrimSuffix(dev, suffix)
		}
	}
	for len(dev) > 0 && dev[len(dev)-1] >= '0' && dev[len(dev)-1] <= '9' {
		dev = dev[:len(dev)-1]
	}
	return dev
}

func detectHostNICs() []HostNICProbe {
	entries, err := os.ReadDir("/sys/class/net")
	if err != nil {
		return nil
	}
	ipv4, ipv6 := detectInterfaceIPs()
	nics := make([]HostNICProbe, 0)
	for _, entry := range entries {
		name := entry.Name()
		if name == "lo" {
			continue
		}
		base := filepath.Join("/sys/class/net", name)
		speed, _ := strconv.Atoi(strings.TrimSpace(readFirstExistingFile(filepath.Join(base, "speed"))))
		nic := HostNICProbe{
			Name:      name,
			MAC:       strings.TrimSpace(readFirstExistingFile(filepath.Join(base, "address"))),
			State:     strings.TrimSpace(readFirstExistingFile(filepath.Join(base, "operstate"))),
			SpeedMbps: speed,
			Driver:    strings.TrimSpace(runCommandOutput(2*time.Second, "sh", "-c", fmt.Sprintf("basename $(readlink -f /sys/class/net/%s/device/driver 2>/dev/null) 2>/dev/null", shellQuoteSimple(name)))),
			Model:     detectNICModel(name),
			IPv4:      ipv4[name],
			IPv6:      ipv6[name],
		}
		nics = append(nics, nic)
	}
	sort.Slice(nics, func(i, j int) bool { return nics[i].Name < nics[j].Name })
	return nics
}

func detectNICModel(name string) string {
	out := runCommandOutput(2*time.Second, "sh", "-c", fmt.Sprintf("lspci -D 2>/dev/null | grep -iE 'ethernet|network' | head -n 1 || true"))
	if out != "" {
		return strings.TrimSpace(out)
	}
	return strings.TrimSpace(readFirstExistingFile(filepath.Join("/sys/class/net", name, "device", "uevent")))
}

func detectInterfaceIPs() (map[string][]HostIPProbe, map[string][]HostIPProbe) {
	ipv4 := map[string][]HostIPProbe{}
	ipv6 := map[string][]HostIPProbe{}
	for _, family := range []struct {
		arg string
		dst map[string][]HostIPProbe
	}{{"-4", ipv4}, {"-6", ipv6}} {
		out := runCommandOutput(3*time.Second, "ip", "-o", family.arg, "addr", "show")
		for _, line := range strings.Split(out, "\n") {
			fields := strings.Fields(line)
			if len(fields) < 4 {
				continue
			}
			iface := strings.TrimSuffix(fields[1], ":")
			addr := fields[3]
			ip, network, err := net.ParseCIDR(addr)
			if err != nil || ip == nil || network == nil {
				continue
			}
			ones, _ := network.Mask.Size()
			scope := ""
			for i, field := range fields {
				if field == "scope" && i+1 < len(fields) {
					scope = fields[i+1]
				}
			}
			family.dst[iface] = append(family.dst[iface], HostIPProbe{
				Interface: iface,
				Address:   ip.String(),
				PrefixLen: ones,
				Scope:     scope,
			})
		}
	}
	return ipv4, ipv6
}

func detectAllPublicIPv4() []string {
	seen := map[string]bool{}
	result := make([]string, 0)
	if pub := lxc.DetectPublicIPv4(); pub.Address != "" {
		if ip := net.ParseIP(pub.Address); isPublicIPv4(ip) {
			seen[pub.Address] = true
			result = append(result, pub.Address)
		}
	}
	out := runCommandOutput(3*time.Second, "ip", "-o", "-4", "addr", "show", "scope", "global")
	for _, line := range strings.Split(out, "\n") {
		fields := strings.Fields(line)
		if len(fields) < 4 {
			continue
		}
		ip, _, err := net.ParseCIDR(fields[3])
		if err != nil || ip == nil {
			continue
		}
		if !isPublicIPv4(ip) {
			continue
		}
		value := ip.String()
		if !seen[value] {
			seen[value] = true
			result = append(result, value)
		}
	}
	return result
}

func collectIPv4Addresses(nics []HostNICProbe) []HostIPProbe {
	result := make([]HostIPProbe, 0)
	for _, nic := range nics {
		for _, ip := range nic.IPv4 {
			parsed := net.ParseIP(ip.Address)
			if isPublicIPv4(parsed) {
				result = append(result, ip)
			}
		}
	}
	return result
}

func detectPublicIPv4Prefixes(nics []HostNICProbe, gateways []HostGatewayProbe) []HostIPv4PrefixProbe {
	result := make([]HostIPv4PrefixProbe, 0)
	seen := map[string]bool{}
	gatewayByIface := map[string]string{}
	for _, gateway := range gateways {
		if gateway.Family == "ipv4" && gateway.Interface != "" && gateway.Gateway != "" {
			gatewayByIface[gateway.Interface] = gateway.Gateway
		}
	}
	add := func(item HostIPv4PrefixProbe) {
		if item.Prefix == "" || item.Interface == "" {
			return
		}
		key := item.Interface + "|" + item.Prefix
		if seen[key] {
			return
		}
		seen[key] = true
		result = append(result, item)
	}
	for _, nic := range nics {
		for _, ip := range nic.IPv4 {
			parsed := net.ParseIP(ip.Address)
			if !isPublicIPv4(parsed) || ip.PrefixLen <= 0 || ip.PrefixLen > 32 {
				continue
			}
			prefix, subnet := ipv4PrefixAndMask(parsed, ip.PrefixLen)
			add(HostIPv4PrefixProbe{
				Interface:  nic.Name,
				Address:    ip.Address,
				Prefix:     prefix,
				PrefixLen:  ip.PrefixLen,
				SubnetMask: subnet,
				Gateway:    gatewayByIface[nic.Name],
				Source:     "address",
			})
		}
	}
	for _, item := range detectIPv4RoutePrefixes(gatewayByIface) {
		add(item)
	}
	sort.SliceStable(result, func(i, j int) bool {
		if result[i].Interface == result[j].Interface {
			return result[i].Prefix < result[j].Prefix
		}
		return result[i].Interface < result[j].Interface
	})
	return result
}

func detectIPv4RoutePrefixes(gatewayByIface map[string]string) []HostIPv4PrefixProbe {
	out := runCommandOutput(3*time.Second, "ip", "-4", "route", "show")
	result := make([]HostIPv4PrefixProbe, 0)
	for _, line := range strings.Split(out, "\n") {
		fields := strings.Fields(line)
		if len(fields) == 0 || fields[0] == "default" {
			continue
		}
		_, network, err := net.ParseCIDR(fields[0])
		if err != nil || network == nil || network.IP.To4() == nil {
			continue
		}
		if !isPublicIPv4(network.IP) {
			continue
		}
		iface := ""
		gateway := ""
		src := ""
		for i, field := range fields {
			if field == "dev" && i+1 < len(fields) {
				iface = fields[i+1]
			}
			if field == "via" && i+1 < len(fields) {
				gateway = fields[i+1]
			}
			if field == "src" && i+1 < len(fields) {
				src = fields[i+1]
			}
		}
		if iface == "" || isContainerLikeInterfaceName(iface) {
			continue
		}
		ones, bits := network.Mask.Size()
		if bits != 32 || ones <= 0 || ones > 32 {
			continue
		}
		if gateway == "" {
			gateway = gatewayByIface[iface]
		}
		prefix, subnet := ipv4PrefixAndMask(network.IP, ones)
		result = append(result, HostIPv4PrefixProbe{
			Interface:  iface,
			Address:    src,
			Prefix:     prefix,
			PrefixLen:  ones,
			SubnetMask: subnet,
			Gateway:    gateway,
			Source:     "route",
		})
	}
	return result
}

func ipv4PrefixAndMask(ip net.IP, prefixLen int) (string, string) {
	v4 := ip.To4()
	if v4 == nil {
		return "", ""
	}
	mask := net.CIDRMask(prefixLen, 32)
	network := v4.Mask(mask)
	subnet := fmt.Sprintf("%d.%d.%d.%d", mask[0], mask[1], mask[2], mask[3])
	return fmt.Sprintf("%s/%d", network.String(), prefixLen), subnet
}

func isPublicIPv4(ip net.IP) bool {
	ip = ip.To4()
	if ip == nil {
		return false
	}
	return !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsLinkLocalUnicast() && !ip.IsMulticast() && !ip.IsUnspecified()
}

func isContainerLikeInterfaceName(iface string) bool {
	prefixes := []string{"lo", "lxc", "docker", "br-", "veth", "virbr", "cni", "flannel", "cali", "kube", "dummy", "ifb"}
	for _, prefix := range prefixes {
		if iface == prefix || strings.HasPrefix(iface, prefix) {
			return true
		}
	}
	return false
}

func collectIPv6Addresses(nics []HostNICProbe) []HostIPProbe {
	result := make([]HostIPProbe, 0)
	for _, nic := range nics {
		for _, ip := range nic.IPv6 {
			if ip.Scope == "global" {
				result = append(result, ip)
			}
		}
	}
	return result
}

func detectGateways() []HostGatewayProbe {
	gateways := make([]HostGatewayProbe, 0)
	for _, item := range []struct {
		family string
		args   []string
	}{{"ipv4", []string{"-4", "route", "show", "default"}}, {"ipv6", []string{"-6", "route", "show", "default"}}} {
		out := runCommandOutput(3*time.Second, "ip", item.args...)
		for _, line := range strings.Split(out, "\n") {
			fields := strings.Fields(line)
			gw := ""
			iface := ""
			for i, field := range fields {
				if field == "via" && i+1 < len(fields) {
					gw = fields[i+1]
				}
				if field == "dev" && i+1 < len(fields) {
					iface = fields[i+1]
				}
			}
			if gw != "" || iface != "" {
				gateways = append(gateways, HostGatewayProbe{Family: item.family, Interface: iface, Gateway: gw})
			}
		}
	}
	return gateways
}

func detectGPUs() []HostGPUProbe {
	gpus := make([]HostGPUProbe, 0)
	out := runCommandOutput(3*time.Second, "sh", "-c", "lspci -nnk 2>/dev/null | grep -iEA3 'vga|3d|display' || true")
	var current *HostGPUProbe
	for _, line := range strings.Split(out, "\n") {
		trimmed := strings.TrimSpace(line)
		lower := strings.ToLower(trimmed)
		if strings.Contains(lower, "vga") || strings.Contains(lower, "3d controller") || strings.Contains(lower, "display controller") {
			gpus = append(gpus, HostGPUProbe{Name: trimmed, Vendor: detectGPUVendor(trimmed), Type: detectGPUType(trimmed)})
			current = &gpus[len(gpus)-1]
			continue
		}
		if current != nil && strings.HasPrefix(trimmed, "Kernel driver in use:") {
			current.Driver = strings.TrimSpace(strings.TrimPrefix(trimmed, "Kernel driver in use:"))
		}
	}
	return gpus
}

func detectGPUVendor(value string) string {
	lower := strings.ToLower(value)
	switch {
	case strings.Contains(lower, "intel"):
		return "Intel"
	case strings.Contains(lower, "nvidia"):
		return "NVIDIA"
	case strings.Contains(lower, "amd") || strings.Contains(lower, "ati"):
		return "AMD"
	default:
		return "Unknown"
	}
}

func detectGPUType(value string) string {
	lower := strings.ToLower(value)
	if strings.Contains(lower, "intel") {
		return "integrated"
	}
	return "discrete"
}

func hasIntegratedGPU(gpus []HostGPUProbe) bool {
	for _, gpu := range gpus {
		if gpu.Type == "integrated" {
			return true
		}
	}
	return false
}

func detectRuntimeProbe(env []HostEnvCheck) HostRuntimeProbe {
	devKVM := fileExists("/dev/kvm")
	nested, detail := detectNestedVirtualization()
	lxcOK := envCheckOK(env, "lxc-create")
	probe := HostRuntimeProbe{
		LXCAvailable:         lxcOK,
		KVMAvailable:         devKVM && envCheckOK(env, "virsh"),
		DevKVM:               devKVM,
		NestedVirtualization: nested,
		NestedDetail:         detail,
		SupportMode:          "unsupported",
	}
	if probe.KVMAvailable {
		probe.SupportMode = "kvm_lxc"
	} else if probe.LXCAvailable {
		probe.SupportMode = "lxc_only"
	}
	return probe
}

func detectNestedVirtualization() (bool, string) {
	paths := []string{
		"/sys/module/kvm_intel/parameters/nested",
		"/sys/module/kvm_amd/parameters/nested",
	}
	for _, path := range paths {
		value := strings.TrimSpace(readFirstExistingFile(path))
		if value == "" {
			continue
		}
		enabled := strings.EqualFold(value, "Y") || value == "1"
		return enabled, filepath.Base(filepath.Dir(filepath.Dir(path))) + "=" + value
	}
	if fileExists("/dev/kvm") {
		return true, "/dev/kvm present"
	}
	return false, "no kvm nested parameter or /dev/kvm"
}

func detectSystemProbe() HostSystemProbe {
	uptime := int64(0)
	if data, err := os.ReadFile("/proc/uptime"); err == nil {
		first := strings.Fields(string(data))
		if len(first) > 0 {
			value, _ := strconv.ParseFloat(first[0], 64)
			uptime = int64(value)
		}
	}
	return HostSystemProbe{
		UptimeSeconds: uptime,
		UptimeText:    formatDurationText(uptime),
		ProcessCount:  countProcesses(),
	}
}

func detectHostEnvironment() []HostEnvCheck {
	checks := []HostEnvCheck{
		commandCheck("service-manager", "服务管理器 systemd/OpenRC", true, "systemctl", "systemd"),
		commandCheck("lxc-create", "LXC 创建工具", true, "lxc-create", ""),
		commandCheck("lxc-start", "LXC 启动工具", true, "lxc-start", ""),
		commandCheck("iptables", "iptables 网络规则", true, "iptables", ""),
		commandCheck("ip", "iproute2 网络工具", true, "ip", ""),
		commandCheck("conntrack", "conntrack 安全扫描", false, "conntrack", ""),
		commandCheck("virsh", "libvirt virsh", false, "virsh", ""),
		commandCheck("qemu-system-x86_64", "QEMU/KVM 虚拟机", false, "qemu-system-x86_64", ""),
		commandCheck("genisoimage", "KVM cloud-init ISO 工具", false, "genisoimage", "xorriso/mkisofs 可替代"),
		commandCheck("xorriso", "ISO 备用工具", false, "xorriso", ""),
		commandCheck("smartctl", "硬盘健康检测", false, "smartctl", ""),
		certbotCheck(),
	}
	checks = append(checks, HostEnvCheck{Key: "dev-kvm", Label: "/dev/kvm 硬件虚拟化", OK: fileExists("/dev/kvm"), Required: false, Detail: boolDetail(fileExists("/dev/kvm"))})
	checks = append(checks, HostEnvCheck{Key: "ipv4-forward", Label: "IPv4 转发", OK: strings.TrimSpace(readFirstExistingFile("/proc/sys/net/ipv4/ip_forward")) == "1", Required: true, Detail: strings.TrimSpace(readFirstExistingFile("/proc/sys/net/ipv4/ip_forward"))})
	checks = append(checks, HostEnvCheck{Key: "lxcfs", Label: "lxcfs 服务", OK: serviceActive("lxcfs"), Required: false, Detail: serviceDetail("lxcfs")})
	checks = append(checks, HostEnvCheck{Key: "libvirt", Label: "libvirt 服务", OK: serviceActive("libvirtd") || serviceActive("virtqemud"), Required: false, Detail: firstNonEmpty(serviceDetail("libvirtd"), serviceDetail("virtqemud"))})
	return checks
}

func commandCheck(key, label string, required bool, cmd string, fallback string) HostEnvCheck {
	ok := commandExists(cmd)
	detail := "missing"
	if ok {
		detail = commandVersionDetail(cmd)
		if detail == "" {
			detail = "installed"
		}
	} else if fallback != "" {
		detail = fallback
	}
	return HostEnvCheck{Key: key, Label: label, OK: ok, Required: required, Detail: detail}
}

func commandVersionDetail(cmd string) string {
	switch cmd {
	case "ip":
		return strings.TrimSpace(runCommandOutput(2*time.Second, "sh", "-c", "ip -V 2>&1 | head -n 1"))
	default:
		return strings.TrimSpace(runCommandOutput(2*time.Second, "sh", "-c", cmd+" --version 2>&1 | head -n 1"))
	}
}

func certbotCheck() HostEnvCheck {
	check := HostEnvCheck{Key: "certbot", Label: "Certbot 证书工具 >= 5.4", Required: false, Detail: "missing"}
	if !commandExists("certbot") {
		return check
	}
	detail := strings.TrimSpace(runCommandOutput(3*time.Second, "certbot", "--version"))
	if detail == "" {
		detail = strings.TrimSpace(runCommandOutput(3*time.Second, "sh", "-c", "certbot --version 2>&1 | head -n 1"))
	}
	if detail == "" {
		detail = "installed, version unknown"
	}
	check.Detail = detail
	version := extractCertbotVersion(detail)
	check.OK = certbotVersionAtLeast(version, 5, 4)
	if version == "" {
		check.Detail = detail + " (version unknown, need >= 5.4)"
	} else if !check.OK {
		check.Detail = detail + " (need >= 5.4)"
	}
	return check
}

func extractCertbotVersion(output string) string {
	for _, field := range strings.Fields(output) {
		field = strings.Trim(field, "vV,;:()[]{}")
		if field == "" || field[0] < '0' || field[0] > '9' {
			continue
		}
		return field
	}
	return ""
}

func certbotVersionAtLeast(version string, minMajor, minMinor int) bool {
	parts := strings.Split(version, ".")
	if len(parts) < 2 {
		return false
	}
	major, err := strconv.Atoi(numericPrefix(parts[0]))
	if err != nil {
		return false
	}
	minor, err := strconv.Atoi(numericPrefix(parts[1]))
	if err != nil {
		return false
	}
	if major != minMajor {
		return major > minMajor
	}
	return minor >= minMinor
}

func numericPrefix(value string) string {
	var b strings.Builder
	for _, r := range value {
		if r < '0' || r > '9' {
			break
		}
		b.WriteRune(r)
	}
	return b.String()
}

func envCheckOK(checks []HostEnvCheck, key string) bool {
	for _, check := range checks {
		if check.Key == key {
			return check.OK
		}
	}
	return false
}

func serviceActive(name string) bool {
	if commandExists("systemctl") {
		return strings.TrimSpace(runCommandOutput(2*time.Second, "systemctl", "is-active", name)) == "active"
	}
	if commandExists("rc-service") {
		return strings.Contains(runCommandOutput(2*time.Second, "rc-service", name, "status"), "started")
	}
	return false
}

func serviceDetail(name string) string {
	if commandExists("systemctl") {
		return strings.TrimSpace(runCommandOutput(2*time.Second, "systemctl", "is-active", name))
	}
	if commandExists("rc-service") {
		return strings.TrimSpace(runCommandOutput(2*time.Second, "rc-service", name, "status"))
	}
	return "unknown"
}

func readKeyValueFile(path, sep string) map[string]string {
	result := map[string]string{}
	data, err := os.ReadFile(path)
	if err != nil {
		return result
	}
	for _, line := range strings.Split(string(data), "\n") {
		parts := strings.SplitN(line, sep, 2)
		if len(parts) == 2 {
			result[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
		}
	}
	return result
}

func readFirstExistingFile(paths ...string) string {
	for _, path := range paths {
		data, err := os.ReadFile(path)
		if err == nil {
			return string(data)
		}
	}
	return ""
}

func commandExists(name string) bool {
	_, err := exec.LookPath(name)
	return err == nil
}

func fileExists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}

func runCommandOutput(timeout time.Duration, name string, args ...string) string {
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()
	out, err := exec.CommandContext(ctx, name, args...).CombinedOutput()
	if err != nil && len(out) == 0 {
		return ""
	}
	return strings.TrimSpace(string(out))
}

func shortCommandDetail(value string) string {
	value = strings.TrimSpace(value)
	lines := strings.Split(value, "\n")
	if len(lines) > 4 {
		lines = lines[:4]
	}
	return strings.Join(lines, " | ")
}

func shellQuoteSimple(value string) string {
	return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'"
}

func formatDurationText(seconds int64) string {
	days := seconds / 86400
	seconds %= 86400
	hours := seconds / 3600
	seconds %= 3600
	minutes := seconds / 60
	if days > 0 {
		return fmt.Sprintf("%dd %dh %dm", days, hours, minutes)
	}
	return fmt.Sprintf("%dh %dm", hours, minutes)
}

func formatBytesText(value uint64) string {
	if value == 0 {
		return "0 B"
	}
	units := []string{"B", "KB", "MB", "GB", "TB", "PB"}
	next := float64(value)
	index := 0
	for next >= 1024 && index < len(units)-1 {
		next /= 1024
		index++
	}
	if index == 0 {
		return fmt.Sprintf("%d %s", value, units[index])
	}
	return fmt.Sprintf("%.1f %s", next, units[index])
}

func countProcesses() int {
	entries, err := os.ReadDir("/proc")
	if err != nil {
		return 0
	}
	count := 0
	for _, entry := range entries {
		if _, err := strconv.Atoi(entry.Name()); err == nil {
			count++
		}
	}
	return count
}

func boolDetail(ok bool) string {
	if ok {
		return "available"
	}
	return "missing"
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value != "" && value != "inactive" && value != "unknown" {
			return value
		}
	}
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return strings.TrimSpace(value)
		}
	}
	return ""
}
