package main

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"math"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"
	"unicode"

	smartgo "github.com/anatol/smart.go"
	"github.com/klauspost/cpuid"
	lmsensors "github.com/mt-inside/go-lmsensors"
	"github.com/shirou/gopsutil/v4/cpu"
	"github.com/shirou/gopsutil/v4/disk"
	"github.com/shirou/gopsutil/v4/host"
	"github.com/shirou/gopsutil/v4/mem"
	gopsnet "github.com/shirou/gopsutil/v4/net"
	smbios "github.com/u-root/u-root/pkg/smbios"
	"gopkg.in/yaml.v3"
)

// ==========================================
// 1. TYPES & CONFIGURATION
// ==========================================

// ConfigStructure représente le fichier config.yaml distant
type Config struct {
	ConfigVersion int              `yaml:"config_version"`
	Backend       BackendConfig    `yaml:"backend"`
	Runtime       RuntimeConfig    `yaml:"runtime"`
	Logging       LoggingConfig    `yaml:"logging"`
	Payload       PayloadConfig    `yaml:"payload"`
	Benchmarks    BenchmarkConfig  `yaml:"benchmarks"`
	Collection    CollectionConfig `yaml:"collection"`
	RawInfo       RawInfoConfig    `yaml:"raw_info"`
}

type BackendConfig struct {
	URL  string     `yaml:"url"`
	Auth AuthConfig `yaml:"auth"`
	HTTP HTTPConfig `yaml:"http"`
}

type AuthConfig struct {
	Mode   string `yaml:"mode"`
	EnvVar string `yaml:"env_var"`
}

type HTTPConfig struct {
	TimeoutS      int  `yaml:"timeout_s"`
	Retries       int  `yaml:"retries"`
	RetryBackoffS int  `yaml:"retry_backoff_s"`
	TLSVerify     bool `yaml:"verify"`
}

type RuntimeConfig struct {
	MaxTotalRuntime int      `yaml:"max_total_runtime_s"`
	CommandTimeout  int      `yaml:"command_timeout_s"`
	TempDir         string   `yaml:"temp_dir"`
	Locale          string   `yaml:"locale"`
	PathPrepend     []string `yaml:"path_prepend"`
}

type LoggingConfig struct {
	Level                 string `yaml:"level"`
	Progress              bool   `yaml:"progress"`
	ShowCommandLines      bool   `yaml:"show_command_lines"`
	CaptureRawOutput      bool   `yaml:"capture_raw_output"`
	RawOutputMaxKB        int    `yaml:"raw_output_max_kb"`
	SavePayloadToTmp      bool   `yaml:"save_payload_to_tmp"`
	PayloadTmpDir         string `yaml:"payload_tmp_dir"`
	PayloadFilenamePrefix string `yaml:"payload_filename_prefix"`
	InteractiveDebugPause bool   `yaml:"interactive_pause_on_debug"`
}

type PayloadConfig struct {
	Dir    string `yaml:"dir"`
	Prefix string `yaml:"prefix"`
	Always bool   `yaml:"always_save"`
}

type BenchmarkConfig struct {
	Enabled bool               `yaml:"enabled"`
	Weights map[string]float64 `yaml:"weights"`
	CPU     BenchmarkTool      `yaml:"cpu_sysbench"`
	Memory  BenchmarkTool      `yaml:"memory_sysbench"`
	Disk    BenchmarkTool      `yaml:"disk_fio"`
	Network BenchmarkTool      `yaml:"network_iperf3"`
}

type BenchmarkTool struct {
	Enabled bool                   `yaml:"enabled"`
	Tool    string                 `yaml:"tool"`
	Server  string                 `yaml:"server"`
	Port    int                    `yaml:"port"`
	Params  map[string]interface{} `yaml:"params"`
	Safety  map[string]interface{} `yaml:"safety"`
}

type CollectionConfig struct {
	System  CollectionGroup `yaml:"system"`
	CPU     CollectionGroup `yaml:"cpu"`
	RAM     CollectionGroup `yaml:"ram"`
	Storage CollectionGroup `yaml:"storage"`
	Network CollectionGroup `yaml:"network"`
	GPU     CollectionGroup `yaml:"gpu"`
	Audio   CollectionGroup `yaml:"audio"`
	PCI     CollectionGroup `yaml:"pci_devices"`
	USB     CollectionGroup `yaml:"usb_devices"`
}

type CollectionGroup struct {
	Enabled bool `yaml:"enabled"`
}

type RawInfoConfig struct {
	Enabled   bool     `yaml:"enabled"`
	Include   []string `yaml:"include"`
	MaxSizeKB int      `yaml:"max_size_kb"`
}

// FinalPayload est la structure JSON envoyée au backend
type FinalPayload struct {
	DeviceIdentifier   string            `json:"device_identifier"`
	BenchScriptVersion string            `json:"bench_script_version"`
	BenchClientVersion string            `json:"bench_client_version"`
	Hardware           Hardware          `json:"hardware"`
	Results            Results           `json:"results"`
	RawInfo            map[string]string `json:"raw_info,omitempty"`
}

type Hardware struct {
	CPU           CPUDetails        `json:"cpu"`
	RAM           RAMDetails        `json:"ram"`
	GPU           GPUDetails        `json:"gpu"`
	Storage       StorageDetails    `json:"storage"`
	PCIDevices    []PCIInfo         `json:"pci_devices"`
	USBDevices    []USBInfo         `json:"usb_devices"`
	NetworkShares []NetworkShare    `json:"network_shares"`
	Network       NetworkDetails    `json:"network"`
	Motherboard   MotherboardInfo   `json:"motherboard"`
	OS            OSInfo            `json:"os"`
	Sensors       SensorInfo        `json:"sensors"`
	Audio         AudioInfo         `json:"audio"`
	RawInfo       map[string]string `json:"raw_info"`
}

type CPUDetails struct {
	Vendor            string   `json:"vendor"`
	Model             string   `json:"model"`
	Cores             int      `json:"cores"`
	Threads           int      `json:"threads"`
	BaseFreqGHz       float64  `json:"base_freq_ghz"`
	MaxFreqGHz        float64  `json:"max_freq_ghz"`
	CacheL1KB         int      `json:"cache_l1_kb"`
	CacheL2KB         int      `json:"cache_l2_kb"`
	CacheL3KB         int      `json:"cache_l3_kb"`
	Flags             []string `json:"flags"`
	Microarchitecture string   `json:"microarchitecture"`
	TDPW              *float64 `json:"tdp_w"`
}

type RAMDetails struct {
	TotalMB       int            `json:"total_mb"`
	UsedMB        int            `json:"used_mb"`
	FreeMB        int            `json:"free_mb"`
	SharedMB      int            `json:"shared_mb"`
	SlotsTotal    int            `json:"slots_total"`
	SlotsUsed     int            `json:"slots_used"`
	MaxCapacityMB int            `json:"max_capacity_mb"`
	ECC           bool           `json:"ecc"`
	Layout        []MemoryModule `json:"layout"`
}

type MemoryModule struct {
	Slot          string `json:"slot"`
	SizeMB        int    `json:"size_mb"`
	Type          string `json:"type"`
	SpeedMHz      int    `json:"speed_mhz"`
	ConfiguredMHz int    `json:"configured_speed_mhz"`
	FormFactor    string `json:"form_factor"`
	TypeDetail    string `json:"type_detail"`
	Rank          string `json:"rank"`
	Manufacturer  string `json:"manufacturer"`
	PartNumber    string `json:"part_number"`
}

type GPUDetails struct {
	Vendor            string   `json:"vendor"`
	Model             string   `json:"model"`
	DriverVersion     string   `json:"driver_version"`
	MemoryDedicatedMB *int     `json:"memory_dedicated_mb"`
	MemorySharedMB    *int     `json:"memory_shared_mb"`
	APISupport        []string `json:"api_support"`
}

type StorageDetails struct {
	Devices    []StorageDevice `json:"devices"`
	Partitions []PartitionInfo `json:"partitions"`
}

type StorageDevice struct {
	Name        string  `json:"name"`
	Type        string  `json:"type"`
	Interface   string  `json:"interface"`
	CapacityGB  float64 `json:"capacity_gb"`
	Vendor      string  `json:"vendor"`
	Model       string  `json:"model"`
	Serial      string  `json:"serial"`
	SmartHealth string  `json:"smart_health"`
	Temperature int     `json:"temperature_c"`
}

type PartitionInfo struct {
	Name       string   `json:"name"`
	MountPoint string   `json:"mount_point"`
	FSType     string   `json:"fs_type"`
	TotalGB    float64  `json:"total_gb"`
	UsedGB     *float64 `json:"used_gb"`
	FreeGB     *float64 `json:"free_gb"`
}

type PCIInfo struct {
	Slot   string `json:"slot"`
	Class  string `json:"class"`
	Vendor string `json:"vendor"`
	Device string `json:"device"`
}

type USBInfo struct {
	Bus       string `json:"bus"`
	Device    string `json:"device"`
	VendorID  string `json:"vendor_id"`
	ProductID string `json:"product_id"`
	Name      string `json:"name"`
}

type NetworkShare struct {
	Protocol   string   `json:"protocol"`
	Source     string   `json:"source"`
	MountPoint string   `json:"mount_point"`
	FSType     string   `json:"fs_type"`
	Options    string   `json:"options"`
	TotalGB    *float64 `json:"total_gb"`
	UsedGB     *float64 `json:"used_gb"`
	FreeGB     *float64 `json:"free_gb"`
}

type NetworkDetails struct {
	Interfaces []NetworkInterface `json:"interfaces"`
}

type NetworkInterface struct {
	Name      string `json:"name"`
	Type      string `json:"type"`
	MAC       string `json:"mac"`
	IP        string `json:"ip"`
	SpeedMbps *int   `json:"speed_mbps"`
	Driver    string `json:"driver"`
	SSID      string `json:"ssid"`
	WakeOnLAN bool   `json:"wake_on_lan"`
}

type MotherboardInfo struct {
	Vendor      string `json:"vendor"`
	Model       string `json:"model"`
	BIOSVendor  string `json:"bios_vendor"`
	BIOSVersion string `json:"bios_version"`
	BIOSDate    string `json:"bios_date"`
}

type OSInfo struct {
	Name               string  `json:"name"`
	Version            string  `json:"version"`
	KernelVersion      string  `json:"kernel_version"`
	Architecture       string  `json:"architecture"`
	SessionType        string  `json:"session_type"`
	DisplayServer      string  `json:"display_server"`
	ScreenResolution   string  `json:"screen_resolution"`
	LastBootTime       string  `json:"last_boot_time"`
	UptimeSeconds      float64 `json:"uptime_seconds"`
	BatteryPercentage  *int    `json:"battery_percentage"`
	BatteryStatus      string  `json:"battery_status"`
	BatteryHealth      string  `json:"battery_health"`
	Hostname           string  `json:"hostname"`
	VirtualizationType string  `json:"virtualization_type"`
	DesktopEnvironment string  `json:"desktop_environment"`
}

type SensorInfo struct {
	CPUTempC   *float64           `json:"cpu_temp_c"`
	DiskTempsC map[string]float64 `json:"disk_temps_c"`
	Sensors    []SensorReading    `json:"sensors"`
}

type SensorReading struct {
	Chip  string  `json:"chip"`
	Name  string  `json:"name"`
	Type  string  `json:"type"`
	Value float64 `json:"value"`
	Unit  string  `json:"unit"`
	Alarm bool    `json:"alarm"`
}

type AudioInfo struct {
	Hardware AudioHardware `json:"hardware"`
	Software AudioSoftware `json:"software"`
}

type AudioHardware struct {
	PCIAudioDevices []string `json:"pci_audio_devices"`
	AlsaPlayback    string   `json:"alsa_playback"`
	AlsaCapture     string   `json:"alsa_capture"`
	PCMDevices      []string `json:"pcm_devices"`
}

type AudioSoftware struct {
	Backend       string   `json:"backend"`
	ServerName    string   `json:"server_name"`
	ServerVersion string   `json:"server_version"`
	DefaultSink   *string  `json:"default_sink"`
	DefaultSource *string  `json:"default_source"`
	Sinks         []string `json:"sinks"`
	Sources       []string `json:"sources"`
}

type Results struct {
	CPU         CPUResult  `json:"cpu"`
	Memory      MemResult  `json:"memory"`
	Disk        DiskResult `json:"disk"`
	Network     NetResult  `json:"network"`
	GPU         GPUResult  `json:"gpu"`
	GlobalScore float64    `json:"global_score"`
}

type CPUResult struct {
	EventsPerSec       float64 `json:"events_per_sec"`
	EventsPerSecSingle float64 `json:"events_per_sec_single"`
	EventsPerSecMulti  float64 `json:"events_per_sec_multi"`
	DurationSec        float64 `json:"duration_s"`
	Score              float64 `json:"score"`
	ScoreSingle        float64 `json:"score_single"`
	ScoreMulti         float64 `json:"score_multi"`
}

type MemResult struct {
	Throughput float64 `json:"throughput_mib_s"`
	Score      float64 `json:"score"`
}

type DiskResult struct {
	ReadMBs   float64 `json:"read_mb_s"`
	WriteMBs  float64 `json:"write_mb_s"`
	IOPSRead  float64 `json:"iops_read"`
	IOPSWrite float64 `json:"iops_write"`
	LatencyMs float64 `json:"latency_ms"`
	Score     float64 `json:"score"`
}

type DirectDiskMetrics struct {
	WriteMBs float64
	ReadMBs  float64
}

type CacheSummary struct {
	Sizes  map[int]int
	Counts map[int]int
}

type NetResult struct {
	Upload        float64  `json:"upload_mbps"`
	Download      float64  `json:"download_mbps"`
	PingMs        float64  `json:"ping_ms"`
	JitterMs      *float64 `json:"jitter_ms"`
	PacketLossPct *float64 `json:"packet_loss_percent"`
	Score         float64  `json:"score"`
}

type GPUResult struct {
	GLMark2Score *float64 `json:"glmark2_score"`
	Score        *float64 `json:"score"`
}

// ==========================================
// 2. GLOBALS & UTILS
// ==========================================

var (
	cfg    Config
	debug  = false
	dryRun = false
)

const benchScriptVersion = "1.6.6"
const benchClientVersion = "1.0.2"

var (
	sensorInitOnce sync.Once
	sensorInitErr  error
)

var (
	sizeValueRegex      = regexp.MustCompile(`(?i)^([\d\.]+)\s*([kmgtp]i?)?b?$`)
	quotedStringRegexp  = regexp.MustCompile(`"([^"]+)"`)
	sysbenchEventsRegex = regexp.MustCompile(`events per second:\s+([\d\.]+)`)
	sysbenchTimeRegex   = regexp.MustCompile(`total time:\s+([\d\.]+)s`)
)

var (
	debugLogFile *os.File
	debugLogOnce sync.Once
	debugLogErr  error
	debugLogMu   sync.Mutex
)

const debugLogFilename = "debug.log"

func debugf(format string, args ...interface{}) {
	if debug {
		message := fmt.Sprintf("[DEBUG] "+format+"\n", args...)
		fmt.Print(message)
		if err := appendDebugLog(message); err != nil {
			fmt.Printf("[WARN] impossible d'écrire dans %s: %v\n", debugLogFilename, err)
		}
	}
}

// safeRun exécute une commande avec timeout et gestion d'erreur
func safeRun(ctx context.Context, name string, args ...string) (string, error) {
	if debug {
		debugf("Exec: %s %v", name, args)
	}
	if dryRun {
		return "DRY RUN OUTPUT", nil
	}

	cmd := exec.CommandContext(ctx, name, args...)
	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	err := cmd.Run()
	if err != nil {
		return "", fmt.Errorf("%w: %s", err, stderr.String())
	}
	return stdout.String(), nil
}

func appendDebugLog(message string) error {
	if err := ensureDebugLog(); err != nil {
		return err
	}
	debugLogMu.Lock()
	defer debugLogMu.Unlock()
	_, err := debugLogFile.WriteString(message)
	return err
}

func ensureDebugLog() error {
	debugLogOnce.Do(func() {
		wd, err := os.Getwd()
		if err != nil {
			debugLogErr = err
			return
		}
		path := filepath.Join(wd, debugLogFilename)
		file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o777)
		if err != nil {
			debugLogErr = err
			return
		}
		if err := file.Chmod(0o777); err != nil {
			// ignore error
		}
		debugLogFile = file
	})
	return debugLogErr
}

func closeDebugLog() {
	if debugLogFile != nil {
		_ = debugLogFile.Close()
	}
}

func collectOSInfo(ctx context.Context) OSInfo {
	info := OSInfo{}

	if h, err := host.InfoWithContext(ctx); err == nil {
		info.Name = h.Platform
		info.Version = h.PlatformVersion
		info.KernelVersion = h.KernelVersion
		info.Architecture = h.KernelArch
		info.Hostname = h.Hostname
		info.VirtualizationType = h.VirtualizationSystem
		if h.VirtualizationSystem != "" && h.VirtualizationRole != "" {
			info.VirtualizationType = fmt.Sprintf("%s-%s", h.VirtualizationSystem, h.VirtualizationRole)
		}
		info.UptimeSeconds = float64(h.Uptime)
	}

	if host, err := os.Hostname(); err == nil {
		info.Hostname = host
	}

	if data, err := os.ReadFile("/etc/os-release"); err == nil {
		for _, line := range strings.Split(string(data), "\n") {
			line = strings.TrimSpace(line)
			if line == "" || strings.HasPrefix(line, "#") {
				continue
			}
			parts := strings.SplitN(line, "=", 2)
			if len(parts) != 2 {
				continue
			}
			key := parts[0]
			value := strings.Trim(parts[1], "\"")
			switch key {
			case "NAME":
				info.Name = value
			case "VERSION":
				info.Version = value
			}
		}
	}

	if out, err := safeRun(ctx, "uname", "-r"); err == nil {
		info.KernelVersion = strings.TrimSpace(out)
	}
	if out, err := safeRun(ctx, "uname", "-m"); err == nil {
		info.Architecture = strings.TrimSpace(out)
	}

	if vt, err := safeRun(ctx, "systemd-detect-virt"); err == nil {
		vt = strings.TrimSpace(vt)
		if vt == "none" {
			info.VirtualizationType = "none"
		} else if vt != "" {
			info.VirtualizationType = vt
		}
	}

	info.SessionType = os.Getenv("XDG_SESSION_TYPE")
	if info.SessionType == "" {
		if sessionID := os.Getenv("XDG_SESSION_ID"); sessionID != "" {
			if out, err := safeRun(ctx, "loginctl", "show-session", sessionID, "-p", "Type"); err == nil {
				info.SessionType = strings.TrimPrefix(strings.TrimSpace(out), "Type=")
			}
		}
	}

	info.DisplayServer = info.SessionType
	info.DesktopEnvironment = os.Getenv("XDG_CURRENT_DESKTOP")

	if out, err := safeRun(ctx, "xrandr"); err == nil {
		if strings.Contains(out, "current") {
			if parts := strings.Split(out, "\n")[0]; strings.Contains(parts, "current") {
				fields := strings.Split(parts, ",")
				for _, f := range fields {
					f = strings.TrimSpace(f)
					if strings.HasPrefix(f, "current") {
						info.ScreenResolution = strings.TrimPrefix(f, "current ")
						break
					}
				}
			}
		}
	}

	if out, err := safeRun(ctx, "xrandr", "--current"); err == nil && info.ScreenResolution == "" {
		for _, line := range strings.Split(out, "\n") {
			if strings.Contains(line, "*") && strings.Contains(line, "+") {
				info.ScreenResolution = strings.TrimSpace(strings.Fields(line)[0])
				break
			}
		}
	}

	if out, err := safeRun(ctx, "who", "-b"); err == nil {
		info.LastBootTime = strings.TrimSpace(strings.ReplaceAll(out, "system boot", ""))
	}

	if uptimeRaw, err := os.ReadFile("/proc/uptime"); err == nil {
		if parts := strings.Fields(string(uptimeRaw)); len(parts) > 0 {
			if val, err := strconv.ParseFloat(parts[0], 64); err == nil {
				info.UptimeSeconds = val
			}
		}
	}

	info.BatteryPercentage = parseBatteryInt("/sys/class/power_supply/BAT0/capacity")
	info.BatteryStatus = parseBatteryString("/sys/class/power_supply/BAT0/status")
	info.BatteryHealth = parseBatteryString("/sys/class/power_supply/BAT0/health")

	return info
}

func parseBatteryInt(path string) *int {
	if data, err := os.ReadFile(path); err == nil {
		if val, err := strconv.Atoi(strings.TrimSpace(string(data))); err == nil {
			return &val
		}
	}
	return nil
}

func parseBatteryString(path string) string {
	if data, err := os.ReadFile(path); err == nil {
		return strings.TrimSpace(string(data))
	}
	return ""
}

func collectCPUInfo(ctx context.Context) CPUDetails {
	info := CPUDetails{}
	cpuInfos, err := cpu.InfoWithContext(ctx)
	if err != nil || len(cpuInfos) == 0 {
		return info
	}
	first := cpuInfos[0]
	info.Vendor = first.VendorID
	info.Model = first.ModelName

	if cores, err := cpu.CountsWithContext(ctx, false); err == nil && cores > 0 {
		info.Cores = cores
	} else if int(first.Cores) > 0 {
		info.Cores = int(first.Cores)
	}
	if info.Cores == 0 {
		info.Cores = len(cpuInfos)
	}

	if threads, err := cpu.CountsWithContext(ctx, true); err == nil && threads > 0 {
		info.Threads = threads
	} else {
		info.Threads = len(cpuInfos)
	}

	info.Flags = first.Flags
	if lscpuOut, err := safeRun(ctx, "lscpu"); err == nil {
		l1, l2, l3 := parseLscpuCacheSizes(lscpuOut)
		if l1 > 0 {
			info.CacheL1KB = l1
		}
		if l2 > 0 {
			info.CacheL2KB = l2
		}
		if l3 > 0 {
			info.CacheL3KB = l3
		}
	}
	cacheSummary := collectCacheSizes(ctx, info.Cores)
	if info.CacheL1KB == 0 && cacheSummary.Sizes[1] > 0 {
		info.CacheL1KB = cacheSummary.Sizes[1]
	}
	if info.CacheL2KB == 0 && cacheSummary.Sizes[2] > 0 {
		info.CacheL2KB = cacheSummary.Sizes[2]
	}
	if info.CacheL3KB == 0 && cacheSummary.Sizes[3] > 0 {
		info.CacheL3KB = cacheSummary.Sizes[3]
	}
	applyCpuidCacheInfo(&info, cacheSummary)
	info.Microarchitecture = detectMicroarchitecture(first.ModelName, first.Family)
	info.BaseFreqGHz, info.MaxFreqGHz = determineCPUFreqs(ctx, first)
	return info
}

func collectCacheSizes(ctx context.Context, physicalCores int) CacheSummary {
	cacheDir := "/sys/devices/system/cpu/cpu0/cache"
	entries, err := os.ReadDir(cacheDir)
	summary := CacheSummary{
		Sizes:  map[int]int{},
		Counts: map[int]int{},
	}
	if err != nil {
		return summary
	}
	for _, entry := range entries {
		if !strings.HasPrefix(entry.Name(), "index") {
			continue
		}
		levelPath := filepath.Join(cacheDir, entry.Name(), "level")
		level := readIntFromFile(levelPath)
		if level <= 0 {
			continue
		}
		sizePath := filepath.Join(cacheDir, entry.Name(), "size")
		sizeKB := parseCacheSize(readStringFromFile(sizePath))
		if sizeKB > 0 {
			summary.Sizes[level] += sizeKB
			summary.Counts[level]++
		}
	}
	return summary
}

func applyCpuidCacheInfo(info *CPUDetails, summary CacheSummary) {
	cache := cpuid.CPU.Cache
	if cache.L1I < 0 && cache.L1D < 0 && cache.L2 < 0 && cache.L3 < 0 {
		return
	}
	cores := info.Cores
	if cores <= 0 {
		cores = cpuid.CPU.PhysicalCores
		if cores <= 0 {
			cores = 1
		}
	}
	if summary.Counts == nil {
		summary.Counts = map[int]int{}
	}
	l1Bytes := 0
	if cache.L1I > 0 {
		l1Bytes += cache.L1I
	}
	if cache.L1D > 0 {
		l1Bytes += cache.L1D
	}
	if l1Bytes > 0 && info.CacheL1KB == 0 {
		info.CacheL1KB = int(int64(l1Bytes) * int64(cores) / 1024)
	}
	if cache.L2 > 0 && info.CacheL2KB == 0 {
		l2Slices := summary.Counts[2]
		if l2Slices <= 0 {
			l2Slices = cores
		}
		info.CacheL2KB = maxInt(info.CacheL2KB, int((int64(cache.L2)*int64(l2Slices))/1024))
	}
	if cache.L3 > 0 && info.CacheL3KB == 0 {
		l3Slices := summary.Counts[3]
		if l3Slices <= 0 {
			l3Slices = 1
		}
		info.CacheL3KB = maxInt(info.CacheL3KB, int((int64(cache.L3)*int64(l3Slices))/1024))
	}
}

func parseLscpuCacheSizes(output string) (l1, l2, l3 int) {
	l1d := parseCacheValue(output, "L1d")
	l1i := parseCacheValue(output, "L1i")
	l2 = parseCacheValue(output, "L2")
	l3 = parseCacheValue(output, "L3")
	if l1d > 0 || l1i > 0 {
		l1 = l1d + l1i
	}
	return
}

func parseCacheValue(output, label string) int {
	re := regexp.MustCompile(fmt.Sprintf(`(?m)^Cache\s+%s\s*:\s*([\d\.]+)\s*([KMGT]iB?|B|K|M|G)`, regexp.QuoteMeta(label)))
	matches := re.FindStringSubmatch(output)
	if len(matches) != 3 && len(matches) != 4 {
		return 0
	}
	value := matches[1]
	unit := matches[len(matches)-1]
	num, err := strconv.ParseFloat(value, 64)
	if err != nil {
		return 0
	}
	switch strings.ToUpper(unit) {
	case "KIB", "KB", "K":
		return int(num)
	case "MIB", "MB", "M":
		return int(num * 1024)
	case "GIB", "GB", "G":
		return int(num * 1024 * 1024)
	default:
		return int(num)
	}
}

var (
	dmidecodeCurrentSpeedRE = regexp.MustCompile(`(?m)^Current Speed:\s+([\d\.]+)\s+MHz`)
	dmidecodeMaxSpeedRE     = regexp.MustCompile(`(?m)^Max Speed:\s+([\d\.]+)\s+MHz`)
	microarchitectureMap    = map[string]string{
		"25": "Zen",
	}
)

func detectMicroarchitecture(modelName string, family string) string {
	if strings.Contains(strings.ToLower(modelName), "ryzen") {
		return "Zen"
	}
	if value, ok := microarchitectureMap[family]; ok {
		return value
	}
	if family != "" {
		return family
	}
	return ""
}

func determineCPUFreqs(ctx context.Context, info cpu.InfoStat) (float64, float64) {
	base := info.Mhz / 1000
	max := info.Mhz / 1000
	if val := readFrequencyFromFile("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq"); val > 0 {
		max = float64(val) / 1_000_000
	}
	if val := readFrequencyFromFile("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq"); val > 0 && base == 0 {
		base = float64(val) / 1_000_000
	}
	if out, err := safeRun(ctx, "dmidecode", "-t", "4"); err == nil {
		if curSpeed := parseDMIDecodeSpeed(out, dmidecodeCurrentSpeedRE); curSpeed > 0 {
			base = curSpeed / 1000
		}
		if maxSpeed := parseDMIDecodeSpeed(out, dmidecodeMaxSpeedRE); maxSpeed > 0 {
			max = maxSpeed / 1000
		}
	}
	return round2(base), round2(max)
}

func parseDMIDecodeSpeed(output string, re *regexp.Regexp) float64 {
	if match := re.FindStringSubmatch(output); len(match) == 2 {
		if val, err := strconv.ParseFloat(match[1], 64); err == nil {
			return val
		}
	}
	return 0
}

func readFrequencyFromFile(path string) int64 {
	data, err := os.ReadFile(path)
	if err != nil {
		return 0
	}
	val := strings.TrimSpace(string(data))
	if parsed, err := strconv.ParseInt(val, 10, 64); err == nil {
		return parsed
	}
	return 0
}

func parseCacheSize(value string) int {
	value = strings.TrimSpace(value)
	if value == "" {
		return 0
	}
	units := []struct {
		suffix     string
		multiplier int
	}{
		{"KIB", 1},
		{"KB", 1},
		{"K", 1},
		{"MIB", 1024},
		{"MB", 1024},
		{"M", 1024},
		{"GIB", 1024 * 1024},
		{"GB", 1024 * 1024},
		{"G", 1024 * 1024},
	}
	upper := strings.ToUpper(value)
	multiplier := 1
	for _, unit := range units {
		if strings.HasSuffix(upper, unit.suffix) {
			value = strings.TrimSpace(value[:len(value)-len(unit.suffix)])
			multiplier = unit.multiplier
			break
		}
	}
	if value == "" {
		return 0
	}
	num, err := strconv.ParseFloat(value, 64)
	if err != nil {
		return 0
	}
	return int(num * float64(multiplier))
}

func readIntFromFile(path string) int {
	value := readStringFromFile(path)
	if value == "" {
		return 0
	}
	if parsed, err := strconv.Atoi(value); err == nil {
		return parsed
	}
	return 0
}

func readStringFromFile(path string) string {
	data, err := os.ReadFile(path)
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(data))
}

func collectRAMInfo(ctx context.Context) RAMDetails {
	res := RAMDetails{}
	if vm, err := mem.VirtualMemoryWithContext(ctx); err == nil {
		res.TotalMB = int(vm.Total / (1024 * 1024))
		res.UsedMB = int(vm.Used / (1024 * 1024))
		res.FreeMB = int(vm.Available / (1024 * 1024))
		res.SharedMB = int(vm.Shared / (1024 * 1024))
	}

	slotsTotal, slotsUsed, maxCapacity, ecc, layout := collectSMBIOSMemory()
	if len(layout) > 0 {
		correctSlots := len(layout)
		if slotsTotal == 0 || slotsTotal < correctSlots || slotsTotal > correctSlots*4 {
			debugf("Correction RAM slots_total=%d => %d (layout=%d)", slotsTotal, correctSlots, len(layout))
			slotsTotal = correctSlots
		}
		if slotsUsed == 0 {
			slotsUsed = len(layout)
		}
	}
	if slotsTotal > 0 {
		res.SlotsTotal = slotsTotal
	}
	if slotsUsed > 0 {
		res.SlotsUsed = slotsUsed
	}
	if maxCapacity > 0 {
		res.MaxCapacityMB = maxCapacity
	}
	res.ECC = ecc
	res.Layout = layout

	if res.MaxCapacityMB == 0 {
		debugf("RAM fallback max capacity: SMBIOS=0 => utiliser total=%dMB", res.TotalMB)
		res.MaxCapacityMB = res.TotalMB
	}
	if res.SlotsTotal == 0 && len(layout) > 0 {
		debugf("RAM fallback slots total: SMBIOS=0 => %d modules détectés", len(layout))
		res.SlotsTotal = len(layout)
	}
	if res.SlotsUsed == 0 && len(layout) > 0 {
		debugf("RAM fallback slots used: SMBIOS=0 => %d modules détectés", len(layout))
		res.SlotsUsed = len(layout)
	}
	debugf("RAM result: max=%dMB slotsTotal=%d slotsUsed=%d layout=%d modules", res.MaxCapacityMB, res.SlotsTotal, res.SlotsUsed, len(layout))
	return res
}

const smbiosTableTypePhysicalMemoryArray smbios.TableType = 16

func collectSMBIOSMemory() (slotsTotal, slotsUsed, maxCapacityMB int, ecc bool, layout []MemoryModule) {
	info, err := smbios.FromSysfs()
	if err != nil {
		debugf("Lecture SMBIOS impossible : %v", err)
		return
	}
	debugf("SMBIOS détecté : %s", info)

	slotsTotal, ecc, maxCapacityMB = parseSMBIOSPhysicalMemoryArray(info)
	modules, err := info.GetMemoryDevices()
	if err != nil {
		debugf("Impossible de lire les modules mémoire SMBIOS : %v", err)
		return
	}

	for _, md := range modules {
		module := MemoryModule{
			Slot:          md.DeviceLocator,
			Type:          md.Type.String(),
			TypeDetail:    md.TypeDetail.String(),
			FormFactor:    md.FormFactor.String(),
			Manufacturer:  md.Manufacturer,
			PartNumber:    md.PartNumber,
			SpeedMHz:      int(md.Speed),
			ConfiguredMHz: int(md.ConfiguredSpeed),
			Rank:          memoryRank(md.Attributes),
		}
		if size := md.GetSizeBytes(); size > 0 {
			module.SizeMB = int(size / (1024 * 1024))
			slotsUsed++
		}
		debugf("Module SMBIOS détecté : %s %+v", module.Slot, module)
		layout = append(layout, module)
	}

	if slotsTotal == 0 {
		slotsTotal = len(layout)
	}
	if slotsUsed == 0 {
		slotsUsed = len(layout)
	}
	debugf("Mémoire SMBIOS : slots_total=%d slots_used=%d ecc=%t max=%dMB", slotsTotal, slotsUsed, ecc, maxCapacityMB)
	return
}

func parseSMBIOSPhysicalMemoryArray(info *smbios.Info) (slotsTotal int, ecc bool, maxCapacityMB int) {
	for _, table := range info.Tables {
		if table.Type != smbiosTableTypePhysicalMemoryArray {
			continue
		}
		if val, err := table.GetByteAt(13); err == nil && val > 0 {
			debugf("[SMBIOS RAW] Handle=%s slots_byte=0x%X (%d)", table.Handle, val, val)
			slotsTotal = int(val)
		}
		if val, err := table.GetByteAt(6); err == nil {
			ecc = val != 0 && val != 3
		}
		if dw, err := table.GetDWordAt(7); err == nil {
			if dw == 0x80000000 && table.Len() >= 0x10 {
				if ext, err := table.GetDWordAt(12); err == nil && ext > 0 {
					maxCapacityMB = int(ext / (1024 * 1024))
				}
			} else if dw > 0 {
				maxCapacityMB = int(dw / 1024)
			}
		}
		break
	}
	return
}

func memoryRank(attrs uint8) string {
	rank := attrs & 0x0f
	if rank == 0 {
		return "Unknown"
	}
	return strconv.Itoa(int(rank))
}

func collectGPUInfo(ctx context.Context) GPUDetails {
	info := GPUDetails{}
	if out, err := safeRun(ctx, "lspci"); err == nil {
		for _, line := range strings.Split(out, "\n") {
			if strings.Contains(line, "VGA compatible controller") || strings.Contains(line, "3D controller") {
				parts := strings.SplitN(line, ":", 3)
				if len(parts) >= 3 {
					desc := strings.TrimSpace(parts[2])
					info.Model = desc
					info.Vendor = strings.Split(desc, " ")[0]
					break
				}
			}
		}
	}

	if out, err := safeRun(ctx, "nvidia-smi", "--query-gpu=name,driver_version,memory.total", "--format=csv,noheader,nounits"); err == nil {
		lines := strings.Split(strings.TrimSpace(out), "\n")
		if len(lines) > 0 {
			fields := strings.Split(lines[0], ",")
			if len(fields) >= 3 {
				info.Model = strings.TrimSpace(fields[0])
				info.DriverVersion = strings.TrimSpace(fields[1])
				if v := atoi(strings.TrimSpace(fields[2])); v > 0 {
					info.MemoryDedicatedMB = &v
				}
			}
		}
	}

	return info
}

func collectStorageInfo(ctx context.Context) StorageDetails {
	detail := StorageDetails{}
	partitions, err := disk.PartitionsWithContext(ctx, true)
	if err != nil {
		return detail
	}

	deviceMap := map[string]*StorageDevice{}
	for _, part := range partitions {
		if shouldSkipPartition(part) {
			continue
		}
		var usage *disk.UsageStat
		if part.Mountpoint != "" {
			if u, err := disk.UsageWithContext(ctx, part.Mountpoint); err == nil {
				usage = u
			}
		}

		record := PartitionInfo{
			Name:       part.Device,
			MountPoint: part.Mountpoint,
			FSType:     part.Fstype,
			TotalGB:    bytesToGB(int64(usageTotal(usage))),
		}
		if usage != nil {
			record.UsedGB = floatPtr(bytesToGB(int64(usage.Used)))
			record.FreeGB = floatPtr(bytesToGB(int64(usage.Free)))
		}
		detail.Partitions = append(detail.Partitions, record)

		devName := deriveDeviceRoot(part.Device)
		if !isPhysicalDevice(devName) {
			continue
		}
		if _, ok := deviceMap[devName]; !ok {
			deviceMap[devName] = &StorageDevice{
				Name: devName,
				Type: guessDiskType(devName),
			}
		}
		if usage != nil {
			deviceMap[devName].CapacityGB = bytesToGB(int64(usage.Total))
		}
	}

	populateDevicesFromLsblk(ctx, deviceMap)

	for _, dev := range deviceMap {
		annotateSmartDevice(dev)
	}

	if len(deviceMap) > 0 {
		names := make([]string, 0, len(deviceMap))
		for name := range deviceMap {
			names = append(names, name)
		}
		sort.Strings(names)
		for _, name := range names {
			detail.Devices = append(detail.Devices, *deviceMap[name])
		}
	}

	return detail
}

func usageTotal(stats *disk.UsageStat) int64 {
	if stats == nil {
		return 0
	}
	return int64(stats.Total)
}

func guessDiskType(device string) string {
	lower := strings.ToLower(device)
	switch {
	case strings.Contains(lower, "nvme"):
		return "ssd"
	case strings.Contains(lower, "sd"):
		return "ssd"
	case strings.Contains(lower, "hd"):
		return "hdd"
	default:
		return "disk"
	}
}

func populateDevicesFromLsblk(ctx context.Context, deviceMap map[string]*StorageDevice) {
	if len(deviceMap) == 0 {
		// still want to capture drives even if no partitions were added
	}
	raw, err := safeRun(ctx, "lsblk", "-J", "-b", "-o", "NAME,TYPE,TRAN,VENDOR,MODEL,SERIAL,SIZE")
	if err != nil {
		debugf("lsblk JSON échoue: %v", err)
		return
	}
	var output lsblkOutput
	if err := json.Unmarshal([]byte(raw), &output); err != nil {
		debugf("lsblk JSON parse échoue: %v", err)
		return
	}
	for _, entry := range output.BlockDevices {
		addLsblkDevice(entry, deviceMap)
	}
}

func addLsblkDevice(entry lsblkDevice, deviceMap map[string]*StorageDevice) {
	if entry.Type == "" {
		for _, child := range entry.Children {
			addLsblkDevice(child, deviceMap)
		}
		return
	}
	if entry.Type == "disk" {
		path := "/dev/" + entry.Name
		if !isPhysicalDevice(path) || isIgnoredDevice(path) {
			return
		}
		dev := deviceMap[path]
		if dev == nil {
			dev = &StorageDevice{Name: path}
			deviceMap[path] = dev
		}
		if entry.Tran != "" {
			dev.Interface = strings.ToLower(entry.Tran)
		}
		if entry.Vendor != "" {
			dev.Vendor = entry.Vendor
		}
		if entry.Model != "" {
			dev.Model = entry.Model
		}
		if entry.Serial != "" {
			dev.Serial = entry.Serial
		}
		if sizeBytes, err := entry.Size.Int64(); err == nil && sizeBytes > 0 {
			dev.CapacityGB = float64(sizeBytes) / (1024 * 1024 * 1024)
		}
	}
	for _, child := range entry.Children {
		addLsblkDevice(child, deviceMap)
	}
}

type lsblkOutput struct {
	BlockDevices []lsblkDevice `json:"blockdevices"`
}

type lsblkDevice struct {
	Name     string        `json:"name"`
	Type     string        `json:"type"`
	Tran     string        `json:"tran"`
	Vendor   string        `json:"vendor"`
	Model    string        `json:"model"`
	Serial   string        `json:"serial"`
	Size     json.Number   `json:"size"`
	Children []lsblkDevice `json:"children"`
}

func deriveDeviceRoot(device string) string {
	if device == "" {
		return device
	}
	root := device
	if strings.HasPrefix(device, "/dev/") {
		if idx := strings.LastIndex(device, "p"); idx > strings.LastIndex(device, "/") {
			if _, err := strconv.Atoi(device[idx+1:]); err == nil {
				candidate := device[:idx]
				if _, err := os.Stat(candidate); err == nil {
					return candidate
				}
			}
		}
		candidate := strings.TrimRightFunc(device, unicode.IsDigit)
		if candidate != "" && candidate != device {
			if _, err := os.Stat(candidate); err == nil {
				return candidate
			}
		}
	}
	return root
}

func shouldSkipPartition(part disk.PartitionStat) bool {
	if part.Device == "" {
		return true
	}
	lowerFs := strings.ToLower(part.Fstype)
	if lowerFs != "" {
		if _, ok := pseudoFSTypes[lowerFs]; ok {
			return true
		}
	}
	if strings.Contains(lowerFs, "nsfs") {
		return true
	}
	if part.Mountpoint != "" {
		switch {
		case strings.HasPrefix(part.Mountpoint, "/run/docker"),
			strings.HasPrefix(part.Mountpoint, "/sys"),
			strings.HasPrefix(part.Mountpoint, "/proc"),
			strings.HasPrefix(part.Mountpoint, "/dev"):
			return true
		}
	}
	if isIgnoredDevice(part.Device) {
		return true
	}
	return false
}

func isPhysicalDevice(device string) bool {
	device = strings.TrimSpace(device)
	if device == "" || !strings.HasPrefix(device, "/dev/") {
		return false
	}
	normalized := strings.TrimPrefix(device, "/dev/")
	normalized = strings.TrimSuffix(normalized, "p")
	lower := strings.ToLower(normalized)
	physicalPrefixes := []string{"sd", "hd", "vd", "nvme", "mmcblk", "xvd", "mapper", "dm-"}
	for _, prefix := range physicalPrefixes {
		if strings.HasPrefix(lower, prefix) {
			return true
		}
	}
	return false
}

func isIgnoredDevice(device string) bool {
	lower := strings.ToLower(strings.TrimSpace(device))
	ignored := []string{"/dev/loop", "/dev/ram", "/dev/fd", "/dev/sr", "/dev/mapper", "/dev/nbd", "none"}
	for _, prefix := range ignored {
		if strings.HasPrefix(lower, prefix) {
			return true
		}
	}
	return false
}

type mountEntry struct {
	MountPoint string
	FSName     string
	Source     string
}

func isPseudoMountType(name string) bool {
	_, ok := mountPseudoTypes[strings.ToLower(name)]
	return ok
}

func chooseBenchDir() (string, mountEntry) {
	mounts := parseProcMounts()
	candidates := []string{cfg.Runtime.TempDir, "/var/tmp", os.TempDir()}
	var fallback mountEntry
	for _, base := range candidates {
		if base == "" {
			continue
		}
		absPath, err := filepath.Abs(base)
		if err != nil {
			continue
		}
		if err := os.MkdirAll(absPath, 0o755); err != nil {
			continue
		}
		entry := findMountForPath(absPath, mounts)
		if entry.MountPoint != "" {
			if !isPseudoMountType(entry.FSName) && isPhysicalMount(entry) {
				bench := filepath.Join(absPath, "bench-client")
				if err := os.MkdirAll(bench, 0o755); err != nil {
					continue
				}
				return bench, entry
			}
			if fallback.MountPoint == "" {
				fallback = entry
			}
		}
	}

	for _, entry := range mounts {
		if entry.MountPoint == "" || isPseudoMountType(entry.FSName) {
			continue
		}
		if !isPhysicalMount(entry) {
			if fallback.MountPoint == "" {
				fallback = entry
			}
			continue
		}
		bench := filepath.Join(entry.MountPoint, "bench-client")
		if err := os.MkdirAll(bench, 0o755); err != nil {
			continue
		}
		return bench, entry
	}

	fallbackDir := filepath.Join(os.TempDir(), "bench-client")
	_ = os.MkdirAll(fallbackDir, 0o755)
	return fallbackDir, fallback
}

func parseProcMounts() []mountEntry {
	data, err := os.ReadFile("/proc/mounts")
	if err != nil {
		return nil
	}
	lines := strings.Split(string(data), "\n")
	entries := make([]mountEntry, 0, len(lines))
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}
		fields := strings.Fields(line)
		if len(fields) < 3 {
			continue
		}
		entries = append(entries, mountEntry{
			Source:     decodeMountField(fields[0]),
			MountPoint: decodeMountField(fields[1]),
			FSName:     fields[2],
		})
	}
	sort.Slice(entries, func(i, j int) bool {
		return len(entries[i].MountPoint) > len(entries[j].MountPoint)
	})
	return entries
}

func findMountForPath(path string, entries []mountEntry) mountEntry {
	path = filepath.Clean(path)
	var best mountEntry
	bestLen := -1
	for _, entry := range entries {
		mp := entry.MountPoint
		if mp == "" {
			continue
		}
		if mp != "/" && !strings.HasPrefix(path, mp) {
			continue
		}
		if len(mp) > bestLen {
			best = entry
			bestLen = len(mp)
		}
	}
	return best
}

func isPhysicalMount(entry mountEntry) bool {
	return entry.Source != "" && isPhysicalDevice(entry.Source)
}

func decodeMountField(field string) string {
	return strings.ReplaceAll(field, "\\040", " ")
}

var pseudoFSTypes = map[string]struct{}{
	"tmpfs":      {},
	"sysfs":      {},
	"proc":       {},
	"devtmpfs":   {},
	"cgroup":     {},
	"cgroup2":    {},
	"devpts":     {},
	"securityfs": {},
	"squashfs":   {},
	"overlay":    {},
}

var mountPseudoTypes = map[string]struct{}{
	"tmpfs":      {},
	"overlay":    {},
	"squashfs":   {},
	"ramfs":      {},
	"devtmpfs":   {},
	"cgroup":     {},
	"cgroup2":    {},
	"devpts":     {},
	"securityfs": {},
	"tracefs":    {},
	"fusectl":    {},
	"debugfs":    {},
}

func annotateSmartDevice(dev *StorageDevice) {
	if dev == nil || dev.Name == "" {
		return
	}
	handle, err := smartgo.Open(dev.Name)
	if err != nil {
		debugf("SMART open %s impossible: %v", dev.Name, err)
		dev.SmartHealth = "UNAVAILABLE"
		return
	}
	defer handle.Close()

	health := "UNKNOWN"
	if attrs, err := handle.ReadGenericAttributes(); err == nil {
		health = "PASSED"
		if attrs.Temperature > 0 {
			dev.Temperature = int(attrs.Temperature)
		}
	} else {
		debugf("SMART lecture générique %s échouée: %v", dev.Name, err)
		health = fmt.Sprintf("FAILED: %v", err)
	}

	var vendor, model, serial string

	switch specific := handle.(type) {
	case *smartgo.SataDevice:
		if id, err := specific.Identify(); err == nil {
			model = strings.TrimSpace(id.ModelNumber())
			serial = strings.TrimSpace(id.SerialNumber())
			if vendor == "" {
				vendor = guessVendorFromModel(model)
			}
		} else {
			debugf("SMART Identify SATA %s échoué: %v", dev.Name, err)
		}
	case *smartgo.NVMeDevice:
		if ctrl, _, err := specific.Identify(); err == nil {
			model = strings.TrimSpace(ctrl.ModelNumber())
			serial = strings.TrimSpace(ctrl.SerialNumber())
			if vendor == "" {
				vendor = guessVendorFromModel(model)
			}
		} else {
			debugf("SMART Identify NVMe %s échoué: %v", dev.Name, err)
		}
	case *smartgo.ScsiDevice:
		if inq, err := specific.Inquiry(); err == nil {
			vendor = strings.TrimSpace(string(bytes.TrimSpace(inq.VendorIdent[:])))
			model = strings.TrimSpace(string(bytes.TrimSpace(inq.ProductIdent[:])))
		} else {
			debugf("SMART Inquiry SCSI %s échouée: %v", dev.Name, err)
		}
		if serialNum, err := specific.SerialNumber(); err == nil && serialNum != "" {
			serial = strings.TrimSpace(serialNum)
		} else if err != nil {
			debugf("SMART Serial SCSI %s échouée: %v", dev.Name, err)
		}
	default:
		debugf("SMART type inconnu pour %s", dev.Name)
	}

	if vendor != "" && dev.Vendor == "" {
		dev.Vendor = vendor
	}
	if model != "" {
		dev.Model = model
	}
	if serial != "" {
		dev.Serial = serial
	}
	dev.SmartHealth = health
}

func guessVendorFromModel(model string) string {
	if model == "" {
		return ""
	}
	fields := strings.Fields(model)
	if len(fields) == 0 {
		return ""
	}
	return fields[0]
}

func bytesToGB(value int64) float64 {
	return float64(value) / (1024 * 1024 * 1024)
}

func collectPCIDevices(ctx context.Context) []PCIInfo {
	devices := []PCIInfo{}
	out, err := safeRun(ctx, "lspci", "-mm")
	if err != nil {
		return devices
	}
	for _, line := range strings.Split(out, "\n") {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}
		slot := ""
		rest := line
		if idx := strings.Index(line, " "); idx >= 0 {
			slot = line[:idx]
			rest = line[idx+1:]
		}
		slot = strings.TrimSpace(slot)
		if slot == "" {
			continue
		}
		matches := quotedStringRegexp.FindAllString(rest, -1)
		if len(matches) < 3 {
			continue
		}
		class := strings.Trim(matches[0], `"`)
		vendor := strings.Trim(matches[1], `"`)
		device := strings.Trim(matches[2], `"`)
		devices = append(devices, PCIInfo{
			Slot:   slot,
			Class:  class,
			Vendor: vendor,
			Device: device,
		})
	}
	return devices
}

func collectUSBDevices(ctx context.Context) []USBInfo {
	var devices []USBInfo
	out, err := safeRun(ctx, "lsusb")
	if err != nil {
		return devices
	}
	for _, line := range strings.Split(out, "\n") {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}
		parts := strings.Fields(line)
		if len(parts) < 6 {
			continue
		}
		bus := parts[1]
		device := strings.TrimSuffix(parts[3], ":")
		idParts := strings.Split(parts[5], ":")
		vendorID := ""
		productID := ""
		if len(idParts) == 2 {
			vendorID = idParts[0]
			productID = idParts[1]
		}
		name := strings.Join(parts[6:], " ")
		devices = append(devices, USBInfo{
			Bus:       bus,
			Device:    device,
			VendorID:  vendorID,
			ProductID: productID,
			Name:      name,
		})
	}
	return devices
}

func collectNetworkShares(ctx context.Context) []NetworkShare {
	shares := []NetworkShare{}
	data, err := os.ReadFile("/proc/mounts")
	if err != nil {
		return shares
	}
	for _, line := range strings.Split(string(data), "\n") {
		if line = strings.TrimSpace(line); line == "" {
			continue
		}
		parts := strings.Fields(line)
		if len(parts) < 6 {
			continue
		}
		fs := strings.ToLower(parts[2])
		if !isNetworkFSType(fs) {
			continue
		}
		share := NetworkShare{
			Protocol:   fs,
			Source:     parts[0],
			MountPoint: parts[1],
			FSType:     fs,
			Options:    parts[3],
		}
		if sz, used, free := dfStats(ctx, parts[1]); sz > 0 {
			share.TotalGB = floatPtr(sz)
			if used != 0 {
				share.UsedGB = floatPtr(used)
			}
			if free != 0 {
				share.FreeGB = floatPtr(free)
			}
		}
		shares = append(shares, share)
	}
	return shares
}

func isNetworkFSType(fs string) bool {
	networkFS := []string{"nfs", "nfs4", "cifs", "smbfs", "fuse.cifs", "fuse.smbfs", "afp", "afpfs", "fuse.afpfs", "sshfs", "fuse.sshfs", "ftpfs", "curlftpfs", "fuse.ftpfs", "davfs", "davfs2", "fuse.webdavfs"}
	for _, candidate := range networkFS {
		if fs == candidate {
			return true
		}
	}
	return false
}

func dfStats(ctx context.Context, mountPoint string) (total, used, free float64) {
	out, err := safeRun(ctx, "df", "-k", mountPoint)
	if err != nil {
		return 0, 0, 0
	}
	lines := strings.Split(string(out), "\n")
	if len(lines) < 2 {
		return 0, 0, 0
	}
	fields := strings.Fields(lines[1])
	if len(fields) < 5 {
		return 0, 0, 0
	}
	total = kbToGB(atoi(fields[1]))
	used = kbToGB(atoi(fields[2]))
	free = kbToGB(atoi(fields[3]))
	return total, used, free
}

func kbToGB(value int) float64 {
	return float64(value) / (1024 * 1024)
}

func floatPtr(v float64) *float64 {
	return &v
}

func collectNetworkInfo(ctx context.Context) NetworkDetails {
	details := NetworkDetails{}
	ifaces, err := gopsnet.InterfacesWithContext(ctx)
	if err != nil {
		return details
	}
	for _, iface := range ifaces {
		ifaceName := iface.Name
		if isLoopbackInterface(iface) || isVirtualInterface(ifaceName) {
			continue
		}
		netType := guessNetworkType(ifaceName)
		if netType != "ethernet" && netType != "wifi" {
			continue
		}
		n := NetworkInterface{
			Name: ifaceName,
			MAC:  iface.HardwareAddr,
			Type: netType,
		}
		for _, addr := range iface.Addrs {
			ip := addr.Addr
			if idx := strings.Index(ip, "/"); idx >= 0 {
				ip = ip[:idx]
			}
			if ip == "" {
				continue
			}
			n.IP = ip
			break
		}
		speed, driver, wake := getInterfaceStats(ctx, ifaceName)
		if speed != nil {
			n.SpeedMbps = speed
		}
		n.Driver = driver
		n.WakeOnLAN = wake
		if netType == "wifi" {
			if ssid := getWirelessSSID(ctx, ifaceName); ssid != "" {
				n.SSID = ssid
			}
		}
		details.Interfaces = append(details.Interfaces, n)
	}
	return details
}

func guessNetworkType(name string) string {
	switch {
	case strings.HasPrefix(name, "en"), strings.HasPrefix(name, "eth"):
		return "ethernet"
	case strings.HasPrefix(name, "wl"):
		return "wifi"
	default:
		return "unknown"
	}
}

func isLoopbackInterface(iface gopsnet.InterfaceStat) bool {
	for _, flag := range iface.Flags {
		if strings.EqualFold(flag, "loopback") {
			return true
		}
	}
	return false
}

func isVirtualInterface(name string) bool {
	name = strings.ToLower(name)
	virtualPrefixes := []string{
		"lo", "br-", "docker", "veth", "virbr", "vmnet",
		"vmbr", "vboxnet", "tap", "tun", "zerotier", "wg",
	}
	for _, prefix := range virtualPrefixes {
		if strings.HasPrefix(name, prefix) {
			return true
		}
	}
	return false
}

func getInterfaceStats(ctx context.Context, iface string) (*int, string, bool) {
	var speed *int
	driver := ""
	wake := false

	if out, err := safeRun(ctx, "ethtool", iface); err == nil {
		for _, line := range strings.Split(out, "\n") {
			line = strings.TrimSpace(line)
			if strings.HasPrefix(line, "Speed:") {
				if val := parseSpeedValue(line); val != nil {
					speed = val
				}
			}
			if strings.HasPrefix(line, "Wake-on:") {
				wake = parseWakeOnValue(line)
			}
		}
	}

	if out, err := safeRun(ctx, "ethtool", "-i", iface); err == nil {
		for _, line := range strings.Split(out, "\n") {
			line = strings.TrimSpace(line)
			if strings.HasPrefix(line, "driver:") {
				driver = strings.TrimSpace(strings.TrimPrefix(line, "driver:"))
				break
			}
		}
	}

	return speed, driver, wake
}

func parseSpeedValue(line string) *int {
	if idx := strings.Index(line, ":"); idx >= 0 {
		value := strings.TrimSpace(line[idx+1:])
		value = strings.TrimSuffix(value, "Mb/s")
		value = strings.TrimSpace(value)
		if strings.EqualFold(value, "unknown!") || value == "" {
			return nil
		}
		if parsed, err := strconv.Atoi(value); err == nil {
			return &parsed
		}
	}
	return nil
}

func parseWakeOnValue(line string) bool {
	if idx := strings.Index(line, ":"); idx >= 0 {
		value := strings.TrimSpace(line[idx+1:])
		return strings.Contains(strings.ToLower(value), "g")
	}
	return false
}

func getWirelessSSID(ctx context.Context, iface string) string {
	if out, err := safeRun(ctx, "iwgetid", iface, "-r"); err == nil {
		if ssid := strings.TrimSpace(out); ssid != "" {
			return ssid
		}
	}
	if out, err := safeRun(ctx, "iw", iface, "link"); err == nil {
		for _, line := range strings.Split(out, "\n") {
			line = strings.TrimSpace(line)
			if strings.HasPrefix(line, "SSID:") {
				return strings.TrimSpace(strings.TrimPrefix(line, "SSID:"))
			}
		}
	}
	return ""
}

func collectMotherboardInfo(ctx context.Context) MotherboardInfo {
	info := MotherboardInfo{}
	if base, err := safeRun(ctx, "dmidecode", "-t", "2"); err == nil {
		for _, line := range strings.Split(base, "\n") {
			line = strings.TrimSpace(line)
			switch {
			case strings.HasPrefix(line, "Manufacturer:"):
				info.Vendor = strings.TrimSpace(strings.TrimPrefix(line, "Manufacturer:"))
			case strings.HasPrefix(line, "Product Name:"):
				info.Model = strings.TrimSpace(strings.TrimPrefix(line, "Product Name:"))
			}
		}
	}
	if bios, err := safeRun(ctx, "dmidecode", "-t", "0"); err == nil {
		for _, line := range strings.Split(bios, "\n") {
			line = strings.TrimSpace(line)
			switch {
			case strings.HasPrefix(line, "Vendor:"):
				info.BIOSVendor = strings.TrimSpace(strings.TrimPrefix(line, "Vendor:"))
			case strings.HasPrefix(line, "Version:"):
				info.BIOSVersion = strings.TrimSpace(strings.TrimPrefix(line, "Version:"))
			case strings.HasPrefix(line, "Release Date:"):
				info.BIOSDate = strings.TrimSpace(strings.TrimPrefix(line, "Release Date:"))
			}
		}
	}
	return info
}

func collectSensors(ctx context.Context) SensorInfo {
	info := SensorInfo{
		DiskTempsC: make(map[string]float64),
		Sensors:    []SensorReading{},
	}

	if err := ensureSensorsInitialized(); err == nil {
		if system, err := lmsensors.Get(); err == nil {
			for chipID, chip := range system.Chips {
				chipType := strings.ToLower(chip.Type)
				for _, sensor := range chip.Sensors {
					reading := SensorReading{
						Chip:  chipID,
						Name:  sensor.Name,
						Type:  strings.ToLower(sensor.SensorType.String()),
						Value: sensor.Value,
						Unit:  sensor.Unit,
						Alarm: sensor.Alarm,
					}
					info.Sensors = append(info.Sensors, reading)

					if reading.Type == "temperature" && info.CPUTempC == nil && isCpuSensor(chipType, reading.Name) {
						val := reading.Value
						info.CPUTempC = &val
					}
					if reading.Type == "temperature" && isDiskSensor(chipType, reading.Name) {
						info.DiskTempsC[fmt.Sprintf("%s:%s", chipID, reading.Name)] = reading.Value
					}
				}
			}
		} else {
			debugf("lecture go-lmsensors échoue: %v", err)
		}
	} else {
		debugf("initialisation go-lmsensors échoue: %v", err)
	}

	fillThermalFallback(&info)
	return info
}

func ensureSensorsInitialized() error {
	sensorInitOnce.Do(func() {
		sensorInitErr = lmsensors.Init()
	})
	return sensorInitErr
}

func fillThermalFallback(info *SensorInfo) {
	if info.DiskTempsC == nil {
		info.DiskTempsC = make(map[string]float64)
	}
	if zones, err := os.ReadDir("/sys/class/thermal"); err == nil {
		for _, zone := range zones {
			zonePath := filepath.Join("/sys/class/thermal", zone.Name())
			kind, err := os.ReadFile(filepath.Join(zonePath, "type"))
			if err != nil {
				continue
			}
			kindStr := strings.TrimSpace(string(kind))
			tempRaw, err := os.ReadFile(filepath.Join(zonePath, "temp"))
			if err != nil {
				continue
			}
			tempVal := atoi(strings.TrimSpace(string(tempRaw)))
			if tempVal == 0 {
				continue
			}
			tempC := float64(tempVal) / 1000
			lower := strings.ToLower(kindStr)
			if info.CPUTempC == nil && (strings.Contains(lower, "package") || strings.Contains(lower, "cpu")) {
				val := tempC
				info.CPUTempC = &val
			}
			if strings.Contains(lower, "nvme") || strings.Contains(lower, "disk") || strings.Contains(lower, "ssd") {
				info.DiskTempsC[kindStr] = tempC
			}
		}
	}
}

func isCpuSensor(chipType, name string) bool {
	lower := strings.ToLower(name)
	return strings.Contains(lower, "cpu") || strings.Contains(lower, "package") || strings.Contains(chipType, "core") || strings.Contains(chipType, "cpu")
}

func isDiskSensor(chipType, name string) bool {
	lower := strings.ToLower(name)
	return strings.Contains(lower, "nvme") || strings.Contains(lower, "disk") || strings.Contains(lower, "ssd") || strings.Contains(chipType, "nvme") || strings.Contains(chipType, "sata")
}

func collectAudioInfo(ctx context.Context) AudioInfo {
	info := AudioInfo{
		Hardware: AudioHardware{},
		Software: AudioSoftware{},
	}
	if out, err := safeRun(ctx, "lspci"); err == nil {
		for _, line := range strings.Split(out, "\n") {
			if strings.Contains(line, "Audio device") {
				info.Hardware.PCIAudioDevices = append(info.Hardware.PCIAudioDevices, strings.TrimSpace(line))
			}
		}
	}
	if out, err := safeRun(ctx, "aplay", "-l"); err == nil {
		info.Hardware.AlsaPlayback = out
	}
	if out, err := safeRun(ctx, "arecord", "-l"); err == nil {
		info.Hardware.AlsaCapture = out
	}
	if out, err := safeRun(ctx, "aplay", "-L"); err == nil {
		for _, line := range strings.Split(out, "\n") {
			line = strings.TrimSpace(line)
			if line != "" {
				info.Hardware.PCMDevices = append(info.Hardware.PCMDevices, line)
			}
		}
	}
	if out, err := safeRun(ctx, "pactl", "info"); err == nil {
		for _, line := range strings.Split(out, "\n") {
			line = strings.TrimSpace(line)
			switch {
			case strings.HasPrefix(line, "Server Name:"):
				info.Software.ServerName = strings.TrimSpace(strings.TrimPrefix(line, "Server Name:"))
			case strings.HasPrefix(line, "Server Version:"):
				info.Software.ServerVersion = strings.TrimSpace(strings.TrimPrefix(line, "Server Version:"))
			case strings.HasPrefix(line, "Default Sink:"):
				value := strings.TrimSpace(strings.TrimPrefix(line, "Default Sink:"))
				info.Software.DefaultSink = &value
			case strings.HasPrefix(line, "Default Source:"):
				value := strings.TrimSpace(strings.TrimPrefix(line, "Default Source:"))
				info.Software.DefaultSource = &value
			}
		}
		if strings.Contains(out, "PipeWire") {
			info.Software.Backend = "pipewire"
		} else if strings.Contains(out, "PulseAudio") {
			info.Software.Backend = "pulseaudio"
		} else {
			info.Software.Backend = "alsa"
		}
	}
	return info
}

func collectRawInfo(ctx context.Context, include []string, maxKB int) map[string]string {
	raw := make(map[string]string)
	if maxKB <= 0 {
		maxKB = 5120
	}
	for _, item := range include {
		item = strings.ToLower(item)
		var cmd []string
		switch item {
		case "lscpu":
			cmd = []string{"lscpu"}
		case "lsblk":
			cmd = []string{"lsblk", "-a"}
		case "dmidecode":
			cmd = []string{"dmidecode", "-t", "0,1,2,16,17"}
		default:
			continue
		}
		if len(cmd) == 0 {
			continue
		}
		out, err := safeRun(ctx, cmd[0], cmd[1:]...)
		if err != nil {
			continue
		}
		raw[item] = truncateOutput(out, maxKB)
	}
	return raw
}

func truncateOutput(s string, maxKB int) string {
	if len(s) <= maxKB*1024 {
		return s
	}
	return s[:maxKB*1024]
}
func atoi(value string) int {
	value = strings.TrimSpace(strings.Split(value, " ")[0])
	if res, err := strconv.Atoi(value); err == nil {
		return res
	}
	return 0
}

func parseMHz(value string) float64 {
	if f, err := strconv.ParseFloat(strings.TrimSpace(strings.Split(value, " ")[0]), 64); err == nil {
		return f / 1000
	}
	return 0
}

func parseSizeToKB(value string) int {
	value = strings.TrimSpace(value)
	if idx := strings.Index(value, "("); idx >= 0 {
		value = strings.TrimSpace(value[:idx])
	}
	match := sizeValueRegex.FindStringSubmatch(value)
	if len(match) < 2 {
		return 0
	}
	num, err := strconv.ParseFloat(match[1], 64)
	if err != nil {
		return 0
	}
	unit := strings.ToLower(match[2])
	multiplier := 1.0
	switch unit {
	case "k", "kb", "kib":
		multiplier = 1
	case "m", "mb", "mib":
		multiplier = 1024
	case "g", "gb", "gib":
		multiplier = 1024 * 1024
	case "t", "tb", "tib":
		multiplier = 1024 * 1024 * 1024
	default:
		multiplier = 1
	}
	return int(num * multiplier)
}

func printProgress(step, total int, name, status string) {
	// Simple affichage ANSI compatible
	statusColor := "\033[32m" // Vert
	if status == "WARN" || status == "SKIPPED" {
		statusColor = "\033[33m" // Jaune
	} else if strings.Contains(status, "ERROR") {
		statusColor = "\033[31m" // Rouge
	}
	reset := "\033[0m"
	fmt.Printf("[%d/%d] %-20s %s%s%s\n", step, total, name, statusColor, status, reset)
}

// ==========================================
// 3. CONFIG LOADER
// ==========================================

func loadConfig(url string) error {
	var configData []byte
	var err error
	wd, wdErr := os.Getwd()
	localPath := ""

	if wdErr == nil {
		localPath = filepath.Join(wd, "config.yaml")
		if info, statErr := os.Stat(localPath); statErr == nil && !info.IsDir() {
			configData, err = os.ReadFile(localPath)
			if err == nil {
				fmt.Printf("INFO: Configuration locale chargée depuis %s.\n", localPath)
				debugf("Contenu de %s :\n%s", localPath, string(configData))
				goto parse
			}
			fmt.Printf("WARN: Impossible de lire %s (%v), tentative fallback...\n", localPath, err)
		} else {
			debugf("Aucun config.yaml local (%s) : %v", localPath, statErr)
		}
	} else {
		debugf("Impossible de déterminer le dossier d'exécution pour chercher config.yaml : %v", wdErr)
	}

	// Tentative HTTP
	if url != "" {
		client := &http.Client{Timeout: 10 * time.Second}
		resp, err := client.Get(url)
		if err == nil {
			defer resp.Body.Close()
			configData, err = io.ReadAll(resp.Body)
			if err == nil {
				fmt.Println("INFO: Configuration distante chargée.")
				goto parse
			}
		}
		fmt.Printf("WARN: Impossible de charger la config distante (%s), tentative fallback...\n", err)
	}

	// Fallback local (ou simulation pour cet exemple)
	// Dans le réel, on lirait /var/cache/bench-client/config.yaml
	fmt.Println("INFO: Utilisation de la configuration embarquée (fallback).")
	configData = []byte(defaultConfigYAML) // Utilisation d'une constante pour la démo

parse:
	err = yaml.Unmarshal(configData, &cfg)
	if err == nil {
		debugf("Config chargée : %+v", cfg)
	}
	return err
}

// ==========================================
// 4. COLLECTORS
// ==========================================

// ==========================================
// 5. BENCHMARKS
// ==========================================

func runCPUBench(ctx context.Context) CPUResult {
	res := CPUResult{}
	if !cfg.Benchmarks.CPU.Enabled {
		return res
	}
	singleEvents, singleDuration := runSysbenchCPU(ctx, "1")
	multiThreads := resolveThreadCount(ctx)
	multiEvents, multiDuration := runSysbenchCPU(ctx, multiThreads)
	res.EventsPerSecSingle = round2(singleEvents)
	res.EventsPerSecMulti = round2(multiEvents)
	res.EventsPerSec = round2(averagePositive(singleEvents, multiEvents))
	res.DurationSec = round2(singleDuration + multiDuration)
	res.ScoreSingle = res.EventsPerSecSingle
	res.ScoreMulti = res.EventsPerSecMulti
	res.Score = res.EventsPerSec
	return res
}

func runSysbenchCPU(ctx context.Context, threads string) (float64, float64) {
	args := []string{"cpu", fmt.Sprintf("--threads=%s", threads), "--time=10", "run"}
	out, err := safeRun(ctx, "sysbench", args...)
	if err != nil {
		return 0, 0
	}
	return parseSysbenchCPU(out)
}

func parseSysbenchCPU(output string) (float64, float64) {
	var events, duration float64
	if matches := sysbenchEventsRegex.FindStringSubmatch(output); len(matches) > 1 {
		events, _ = strconv.ParseFloat(matches[1], 64)
	}
	if matches := sysbenchTimeRegex.FindStringSubmatch(output); len(matches) > 1 {
		duration, _ = strconv.ParseFloat(matches[1], 64)
	}
	return events, duration
}

func resolveThreadCount(ctx context.Context) string {
	if v := os.Getenv("GOMAXPROCS"); v != "" {
		return v
	}
	if out, err := safeRun(ctx, "nproc"); err == nil {
		return strings.TrimSpace(out)
	}
	return "4"
}

func averagePositive(values ...float64) float64 {
	var sum float64
	var count int
	for _, v := range values {
		if v > 0 {
			sum += v
			count++
		}
	}
	if count == 0 {
		return 0
	}
	return sum / float64(count)
}

func maxFloat(a, b float64) float64 {
	if b > a {
		return b
	}
	return a
}

func runMemBench(ctx context.Context) MemResult {
	res := MemResult{}
	if !cfg.Benchmarks.Memory.Enabled {
		return res
	}
	args := []string{"memory", "--memory-block-size=1K", "--memory-total-size=1G", "run"}
	if out, err := safeRun(ctx, "sysbench", args...); err == nil {
		if matches := regexp.MustCompile(`([\d\.]+)\s+MiB/sec`).FindStringSubmatch(out); len(matches) > 1 {
			if val, err := strconv.ParseFloat(matches[1], 64); err == nil {
				res.Throughput = round2(val)
				res.Score = res.Throughput
			}
		} else if matches := regexp.MustCompile(`transferred:\s+([\d\.]+)\s+MiB`).FindStringSubmatch(out); len(matches) > 1 {
			if val, err := strconv.ParseFloat(matches[1], 64); err == nil {
				res.Throughput = round2(val)
				res.Score = res.Throughput
			}
		}
	}
	return res
}

func runDiskBench(ctx context.Context) DiskResult {
	res := DiskResult{}
	if !cfg.Benchmarks.Disk.Enabled {
		return res
	}
	benchDir, mountInfo := chooseBenchDir()
	debugf("[3/6] Bench disque cible : %s (mount=%s fstype=%s source=%s)", benchDir, mountInfo.MountPoint, mountInfo.FSName, mountInfo.Source)

	directMetrics := runDirectDiskTests(ctx, benchDir, mountInfo)
	res.ReadMBs = directMetrics.ReadMBs
	res.WriteMBs = directMetrics.WriteMBs
	res.Score = averagePositive(res.ReadMBs, res.WriteMBs)

	tmpFile := filepath.Join(benchDir, fmt.Sprintf("bench-%d.fio", time.Now().UnixNano()))
	defer os.Remove(tmpFile)
	args := []string{
		"--name=bench", "--ioengine=libaio", "--rw=randrw", "--bs=4k",
		"--direct=1", "--size=500M", "--numjobs=1", "--runtime=30",
		"--time_based", "--output-format=json", "--filename=" + tmpFile,
	}
	out, err := safeRun(ctx, "fio", args...)
	if err != nil {
		return res
	}
	var fioJSON map[string]interface{}
	if err := json.Unmarshal([]byte(out), &fioJSON); err != nil {
		return res
	}
	if jobs, ok := fioJSON["jobs"].([]interface{}); ok && len(jobs) > 0 {
		job := jobs[0].(map[string]interface{})
		if read, ok := job["read"].(map[string]interface{}); ok {
			res.ReadMBs = maxFloat(res.ReadMBs, convertBW(read["bw"]))
			res.IOPSRead = convertFloat(read["iops"])
			if hist, ok := read["clat_ns"].(map[string]interface{}); ok {
				res.LatencyMs = convertFloat(hist["mean"]) / 1000000
			}
		}
		if write, ok := job["write"].(map[string]interface{}); ok {
			res.WriteMBs = maxFloat(res.WriteMBs, convertBW(write["bw"]))
			res.IOPSWrite = convertFloat(write["iops"])
		}
	}
	res.Score = round2(averagePositive(res.ReadMBs, res.WriteMBs))
	res.ReadMBs = round2(res.ReadMBs)
	res.WriteMBs = round2(res.WriteMBs)
	res.LatencyMs = round2(res.LatencyMs)
	res.IOPSRead = round2(res.IOPSRead)
	res.IOPSWrite = round2(res.IOPSWrite)
	debugf("[3/6] Bench disque final -> read=%.2f MB/s write=%.2f MB/s score=%.2f", res.ReadMBs, res.WriteMBs, res.Score)
	return res
}

func convertBW(value interface{}) float64 {
	if v, ok := value.(float64); ok {
		return v / 1024
	}
	if num, ok := value.(json.Number); ok {
		if parsed, err := num.Float64(); err == nil {
			return parsed / 1024
		}
	}
	return 0
}

func maxInt(a, b int) int {
	if b > a {
		return b
	}
	return a
}

func runDirectDiskTests(ctx context.Context, benchDir string, mountInfo mountEntry) DirectDiskMetrics {
	if benchDir == "" {
		return DirectDiskMetrics{}
	}
	debugf("[3/6] Bench disque direct sur %s (mnt=%s fs=%s)", mountInfo.Source, mountInfo.MountPoint, mountInfo.FSName)
	tests := []struct {
		suffix string
		sizeMB int64
	}{
		{"500mb", 500},
		{"1gb", 1024},
	}

	var cleanup []string
	var totalWriteMB, totalReadMB float64
	var totalWriteDur, totalReadDur time.Duration

	for _, test := range tests {
		path := filepath.Join(benchDir, fmt.Sprintf("direct-%s.bin", test.suffix))
		cleanup = append(cleanup, path)
		debugf("[3/6] Bench direct : écriture+lecture %s (%d MB)", path, test.sizeMB)
		writeDur, readDur, err := benchmarkFile(ctx, path, test.sizeMB)
		if err != nil {
			debugf("[3/6] Bench direct %s échoué: %v", path, err)
			continue
		}
		totalWriteMB += float64(test.sizeMB)
		totalReadMB += float64(test.sizeMB)
		totalWriteDur += writeDur
		totalReadDur += readDur
	}

	const chunkCount = 50
	const chunkSizeMB = 1
	for idx := 1; idx <= chunkCount; idx++ {
		path := filepath.Join(benchDir, fmt.Sprintf("direct-chunk-%02d.bin", idx))
		cleanup = append(cleanup, path)
		debugf("[3/6] Bench direct chunk %d/%d (%s)", idx, chunkCount, path)
		writeDur, readDur, err := benchmarkFile(ctx, path, chunkSizeMB)
		if err != nil {
			debugf("[3/6] Bench direct chunk %s échoué: %v", path, err)
			continue
		}
		totalWriteMB += float64(chunkSizeMB)
		totalReadMB += float64(chunkSizeMB)
		totalWriteDur += writeDur
		totalReadDur += readDur
	}

	cleanupPaths(cleanup)
	metrics := DirectDiskMetrics{
		WriteMBs: round2(safeThroughput(totalWriteMB, totalWriteDur)),
		ReadMBs:  round2(safeThroughput(totalReadMB, totalReadDur)),
	}
	debugf("[3/6] Bench disque direct résumé -> write=%.2f MB/s read=%.2f MB/s", metrics.WriteMBs, metrics.ReadMBs)
	return metrics
}

func cleanupPaths(paths []string) {
	for _, path := range paths {
		if path == "" {
			continue
		}
		if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
			debugf("Impossible de supprimer %s: %v", path, err)
		}
	}
}

func benchmarkFile(ctx context.Context, path string, sizeMB int64) (time.Duration, time.Duration, error) {
	if sizeMB <= 0 {
		return 0, 0, fmt.Errorf("taille invalide %d", sizeMB)
	}
	totalBytes := int64(sizeMB) * 1024 * 1024
	if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
		// ignore
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return 0, 0, err
	}

	file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
	if err != nil {
		return 0, 0, err
	}

	writeBuf := make([]byte, 64*1024)
	fillBuffer(writeBuf)
	var written int64
	start := time.Now()
	for written < totalBytes {
		if err := ctx.Err(); err != nil {
			return 0, 0, err
		}
		toWrite := int64(len(writeBuf))
		if totalBytes-written < toWrite {
			toWrite = totalBytes - written
		}
		if _, err := file.Write(writeBuf[:toWrite]); err != nil {
			return 0, 0, err
		}
		written += toWrite
	}
	if err := file.Sync(); err != nil {
		return 0, 0, err
	}
	writeDur := time.Since(start)

	if err := file.Close(); err != nil {
		return writeDur, 0, err
	}

	readFile, err := os.Open(path)
	if err != nil {
		return writeDur, 0, err
	}
	defer readFile.Close()

	readBuf := make([]byte, 64*1024)
	start = time.Now()
	for {
		if err := ctx.Err(); err != nil {
			return writeDur, 0, err
		}
		if _, err := readFile.Read(readBuf); err != nil {
			if errors.Is(err, io.EOF) {
				break
			}
			return writeDur, 0, err
		}
	}
	readDur := time.Since(start)
	return writeDur, readDur, nil
}

func safeThroughput(mb float64, duration time.Duration) float64 {
	if duration <= 0 || mb <= 0 {
		return 0
	}
	return mb / duration.Seconds()
}

func fillBuffer(buf []byte) {
	for i := range buf {
		buf[i] = byte(i % 256)
	}
}

func round2(v float64) float64 {
	if math.IsNaN(v) || math.IsInf(v, 0) {
		return v
	}
	return math.Round(v*100) / 100
}
func convertFloat(value interface{}) float64 {
	if v, ok := value.(float64); ok {
		return v
	}
	return 0
}

func runNetBench(ctx context.Context) NetResult {
	res := NetResult{}
	if !cfg.Benchmarks.Network.Enabled {
		return res
	}
	duration := 10
	if params := cfg.Benchmarks.Network.Params; params != nil {
		if raw, ok := params["duration_s"]; ok {
			switch v := raw.(type) {
			case int:
				duration = v
			case float64:
				duration = int(v)
			case string:
				if parsed, err := strconv.Atoi(v); err == nil {
					duration = parsed
				}
			}
		}
	}
	args := []string{
		"-c", cfg.Benchmarks.Network.Server,
		"-p", strconv.Itoa(cfg.Benchmarks.Network.Port),
		"-J", "-t", strconv.Itoa(duration),
	}
	out, err := safeRun(ctx, "iperf3", args...)
	if err != nil {
		return res
	}
	var iperfJSON map[string]interface{}
	if err := json.Unmarshal([]byte(out), &iperfJSON); err != nil {
		return res
	}
	if end, ok := iperfJSON["end"].(map[string]interface{}); ok {
		if sumSent, ok := end["sum_sent"].(map[string]interface{}); ok {
			if val := convertFloat(sumSent["bits_per_second"]); val > 0 {
				res.Upload = round2(val / 1000 / 1000)
			}
			if val := convertFloat(sumSent["jitter_ms"]); val > 0 {
				res.JitterMs = floatPtr(round2(val))
			}
			if val := convertFloat(sumSent["lost_percent"]); val > 0 {
				res.PacketLossPct = floatPtr(round2(val))
			}
		}
		if sumRecv, ok := end["sum_received"].(map[string]interface{}); ok {
			if val := convertFloat(sumRecv["bits_per_second"]); val > 0 {
				res.Download = round2(val / 1000 / 1000)
			}
		}
	}
	res.Score = round2(averagePositive(res.Upload, res.Download))
	// Ping test
	if cfg.Benchmarks.Network.Server != "" {
		if ping := measurePing(ctx, cfg.Benchmarks.Network.Server); ping > 0 {
			res.PingMs = round2(ping)
		}
	}
	return res
}

func measurePing(ctx context.Context, target string) float64 {
	out, err := safeRun(ctx, "ping", "-c", "1", "-W", "1", target)
	if err != nil {
		return 0
	}
	re := regexp.MustCompile(`time=([\d\.]+)\s*ms`)
	if matches := re.FindStringSubmatch(out); len(matches) > 1 {
		if val, err := strconv.ParseFloat(matches[1], 64); err == nil {
			return val
		}
	}
	return 0
}

// ==========================================
// 6. MAIN ORCHESTRATOR
// ==========================================

const defaultConfigYAML = `
config_version: 1
runtime:
  max_total_runtime_s: 480
  command_timeout_s: 30
  temp_dir: "/tmp"
backend:
  url: "http://10.0.0.50:8007/api/benchmark"
logging:
  level: info
  progress: true
  show_command_lines: false
  capture_raw_output: true
  raw_output_max_kb: 5120
  save_payload_to_tmp: true
  payload_tmp_dir: "/tmp"
  payload_filename_prefix: "bench_payload_"
  interactive_pause_on_debug: true
payload:
  dir: "."
  prefix: "bench_payload_"
  always_save: true
collection:
  system: { enabled: true }
  cpu: { enabled: true }
  ram: { enabled: true }
  storage: { enabled: true }
  network: { enabled: true }
raw_info:
  enabled: true
  include: [lscpu, lsblk, dmidecode]
  max_size_kb: 5120
benchmarks:
  enabled: true
  weights: { cpu: 0.4, memory: 0.2, disk: 0.2, network: 0.1, gpu: 0.1 }
  cpu_sysbench:
    enabled: true
    params: { cpu_max_prime: 20000 }
  memory_sysbench:
    enabled: true
    params: { total_size: "1G" }
  disk_fio:
    enabled: true
    safety: { max_runtime_s: 60 }
  network_iperf3:
    enabled: true
    server: "127.0.0.1" # Default pour test
    port: 5201
    params: { duration_s: 10 }
`

func calculateScore(h Hardware, r Results) float64 {
	// Normalisation arbitraire pour l'exemple
	// CPU: assume 2000 events/sec is 100 pts
	cpuScore := ((r.CPU.ScoreSingle + r.CPU.ScoreMulti) / 2) / 20
	memScore := r.Memory.Throughput / 100 // assume 1000 MB/s is 100 pts
	diskScore := r.Disk.Score / 10        // assume 500 MB/s is 50 pts
	netScore := r.Network.Score / 10      // assume 1000 Mbps is 100 pts
	gpuScore := 0.0
	if r.GPU.Score != nil {
		gpuScore = *r.GPU.Score
	}

	w := cfg.Benchmarks.Weights
	debugf("Poids benchmarks : %+v", w)
	debugf("Scores normalisés : cpu=%.2f mem=%.2f disk=%.2f net=%.2f", cpuScore, memScore, diskScore, netScore)
	return (cpuScore*w["cpu"] + memScore*w["memory"] + diskScore*w["disk"] + netScore*w["network"] + gpuScore*w["gpu"]) * 100
}

func persistPayload(data []byte) string {
	cacheDir := cfg.Payload.Dir
	if cacheDir == "" {
		if wd, err := os.Getwd(); err == nil {
			cacheDir = wd
		} else {
			fmt.Printf("WARN: impossible de déterminer le dossier d'exécution : %v\n", err)
			cacheDir = os.TempDir()
		}
	}
	if err := os.MkdirAll(cacheDir, 0o755); err != nil {
		fmt.Printf("WARN: impossible de créer %s : %v\n", cacheDir, err)
		return ""
	}
	prefix := cfg.Payload.Prefix
	if prefix == "" {
		prefix = cfg.Logging.PayloadFilenamePrefix
	}
	if prefix == "" {
		prefix = "bench_payload_"
	}
	filename := prefix + "last.json"
	path := filepath.Join(cacheDir, filename)
	if err := os.WriteFile(path, data, 0o777); err != nil {
		fmt.Printf("WARN: impossible d'écrire %s : %v\n", path, err)
		return ""
	}
	debugf("Payload sauvegardé au format JSON dans %s (%d octets)", path, len(data))
	return path
}

func main() {
	// Flags
	configURL := flag.String("config-url", "", "URL de la config distante")
	flag.BoolVar(&debug, "debug", false, "Mode debug")
	flag.BoolVar(&dryRun, "dry-run", false, "Simulation d'exécution")
	flag.Parse()
	defer closeDebugLog()

	// 1. Chargement Config
	fmt.Println("[1/6] Configuration...")
	if err := loadConfig(*configURL); err != nil {
		fmt.Printf("FATAL: Erreur chargement config: %v\n", err)
		os.Exit(1)
	}
	printProgress(1, 6, "Configuration", "OK")
	debugf("Configuration complète : %+v", cfg)

	// 2. Contexte global
	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(cfg.Runtime.MaxTotalRuntime)*time.Second)
	defer cancel()

	// 3. Collecte Info
	fmt.Println("[2/6] Collecte informations système...")
	debugf("[2/6] Début collecte CPU")
	cpuDetails := collectCPUInfo(ctx)
	debugf("[2/6] Détails CPU: %+v", cpuDetails)

	debugf("[2/6] Début collecte RAM")
	ramDetails := collectRAMInfo(ctx)
	debugf("[2/6] Détails RAM: %+v", ramDetails)

	debugf("[2/6] Début collecte GPU")
	gpuDetails := collectGPUInfo(ctx)
	debugf("[2/6] Détails GPU: %+v", gpuDetails)

	debugf("[2/6] Début collecte stockage")
	storageDetails := collectStorageInfo(ctx)
	debugf("[2/6] Détails stockage: %+v", storageDetails)

	debugf("[2/6] Début collecte PCI devices")
	pciDetails := collectPCIDevices(ctx)
	debugf("[2/6] Détails PCI: %+v", pciDetails)

	debugf("[2/6] Début collecte USB devices")
	usbDetails := collectUSBDevices(ctx)
	debugf("[2/6] Détails USB: %+v", usbDetails)

	debugf("[2/6] Début collecte partages réseau")
	networkShares := collectNetworkShares(ctx)
	debugf("[2/6] Détails partages réseau: %+v", networkShares)

	debugf("[2/6] Début collecte interfaces réseau")
	networkDetails := collectNetworkInfo(ctx)
	debugf("[2/6] Détails réseau: %+v", networkDetails)

	debugf("[2/6] Début collecte carte mère")
	motherboardInfo := collectMotherboardInfo(ctx)
	debugf("[2/6] Détails carte mère: %+v", motherboardInfo)

	debugf("[2/6] Début collecte OS")
	osInfo := collectOSInfo(ctx)
	debugf("[2/6] Détails OS: %+v", osInfo)

	debugf("[2/6] Début collecte capteurs")
	sensorInfo := collectSensors(ctx)
	debugf("[2/6] Détails capteurs: %+v", sensorInfo)

	debugf("[2/6] Début collecte audio")
	audioInfo := collectAudioInfo(ctx)
	debugf("[2/6] Détails audio: %+v", audioInfo)

	hw := Hardware{
		CPU:           cpuDetails,
		RAM:           ramDetails,
		GPU:           gpuDetails,
		Storage:       storageDetails,
		PCIDevices:    pciDetails,
		USBDevices:    usbDetails,
		NetworkShares: networkShares,
		Network:       networkDetails,
		Motherboard:   motherboardInfo,
		OS:            osInfo,
		Sensors:       sensorInfo,
		Audio:         audioInfo,
	}
	if cfg.Logging.CaptureRawOutput {
		hw.RawInfo = collectRawInfo(ctx, cfg.RawInfo.Include, cfg.RawInfo.MaxSizeKB)
	}
	printProgress(2, 6, "Collecte", "OK")
	debugf("Matériel collecté : %+v", hw)

	// 4. Benchmarks
	fmt.Println("[3/6] Exécution Benchmarks...")
	debugf("[3/6] Benchmark CPU démarré")
	cpuResult := runCPUBench(ctx)
	debugf("[3/6] Résultat CPU: %+v", cpuResult)

	debugf("[3/6] Benchmark mémoire démarré")
	memResult := runMemBench(ctx)
	debugf("[3/6] Résultat mémoire: %+v", memResult)

	debugf("[3/6] Benchmark disque démarré")
	diskResult := runDiskBench(ctx)
	debugf("[3/6] Résultat disque: %+v", diskResult)

	debugf("[3/6] Benchmark réseau démarré")
	netResult := runNetBench(ctx)
	debugf("[3/6] Résultat réseau: %+v", netResult)

	results := Results{
		CPU:     cpuResult,
		Memory:  memResult,
		Disk:    diskResult,
		Network: netResult,
	}
	printProgress(3, 6, "Benchmarks", "OK")
	debugf("Résultats des benchmarks : %+v", results)

	// 5. Score
	fmt.Println("[4/6] Calcul Score...")
	results.GlobalScore = calculateScore(hw, results)
	printProgress(4, 6, "Score", fmt.Sprintf("%.2f", results.GlobalScore))
	debugf("Score global calculé : %.2f", results.GlobalScore)

	// 6. Payload & Envoi
	rawData := hw.RawInfo
	if rawData == nil {
		rawData = make(map[string]string)
	}
	if debug {
		rawData["debug"] = "active"
	}
	payload := FinalPayload{
		DeviceIdentifier:   hw.OS.Hostname,
		BenchScriptVersion: benchScriptVersion,
		BenchClientVersion: benchClientVersion,
		Hardware:           hw,
		Results:            results,
		RawInfo:            rawData,
	}

	jsonData, err := json.MarshalIndent(payload, "", "  ")
	if err != nil {
		fmt.Printf("FATAL: JSON error: %v\n", err)
		os.Exit(1)
	}
	printProgress(5, 6, "JSON", "OK")
	debugf("Payload JSON (%d octets) : %s", len(jsonData), string(jsonData))

	fmt.Println("[6/6] Envoi Backend...")
	if !dryRun {
		token, tokenSet := os.LookupEnv("API_TOKEN")
		if !tokenSet {
			token = "test_hardware_perf"
			fmt.Println("INFO: API_TOKEN absent, envoi avec token de test 'test_hardware_perf'.")
		} else {
			debugf("API_TOKEN fourni via l'environnement.")
		}

		req, _ := http.NewRequest("POST", cfg.Backend.URL, bytes.NewBuffer(jsonData))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+token)

		client := &http.Client{Timeout: 15 * time.Second}
		resp, err := client.Do(req)
		if err != nil {
			printProgress(6, 6, "Envoi HTTP", "ERROR")
			fmt.Printf("Erreur: %v\n", err)
		} else {
			defer resp.Body.Close()
			if resp.StatusCode >= 200 && resp.StatusCode < 300 {
				printProgress(6, 6, "Envoi HTTP", "OK")
			} else {
				printProgress(6, 6, "Envoi HTTP", fmt.Sprintf("FAIL %d", resp.StatusCode))
			}
		}
	} else {
		fmt.Println("DRY RUN: Payload non envoyé.")
		fmt.Println(string(jsonData))
	}
	if path := persistPayload(jsonData); path != "" {
		fmt.Printf("INFO: Payload enregistré dans %s\n", path)
	}
}
