// PerformanceDeck - PC Performance Console (Pro variant).
//
// A composite, weighted benchmark suite: it runs four distinct subsystem
// benchmarks (CPU single-thread, CPU multi-thread, memory bandwidth, disk
// sequential write) and folds them into ONE overall score with a fully
// transparent per-subsystem breakdown, then compares two saved runs to show
// which subsystem actually moved.
package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"math"
	"os"
	"os/signal"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"syscall"
	"time"
)

const (
	toolName      = "performancedeck"
	toolVersion   = "1.0.0"
	formatVersion = 1

	exitOK        = 0
	exitError     = 1
	exitRegressed = 2
	exitSignal    = 130

	profileFull   = "full"
	profileCPUMem = "cpu-mem"

	// --- Reference constants -------------------------------------------------
	// Each subscore is 100 * measurement / reference. The references are fixed
	// constants baked into this build; they are NOT measured, NOT adaptive and
	// NOT derived from your machine. A machine that hits the reference exactly
	// scores 100 on that subsystem. Subscores are uncapped: faster than the
	// reference scores above 100.
	refCPUSingle = 200.0 // Mops/s  (millions of kernel iterations per second, 1 thread)
	refCPUMulti  = 700.0 // Mops/s  (same kernel, one goroutine per logical CPU)
	refMemory    = 12.0  // GB/s    (bytes moved / 1e9 per second, read+write counted)
	refDisk      = 500.0 // MB/s    (bytes written / 1e6 per second, fsync included)

	// --- Base weights --------------------------------------------------------
	// Held as integer weight points so the division below is exact in binary
	// floating point. The four points sum to 100, i.e. weights 0.30/0.30/0.20/
	// 0.20 summing to exactly 1.0. When the disk benchmark is skipped the three
	// remaining points sum to 80, giving 30/80 = 0.375, 30/80 = 0.375 and
	// 20/80 = 0.25 - again exactly 1.0, with no floating-point drift.
	wpCPUSingle = 30
	wpCPUMulti  = 30
	wpMemory    = 20
	wpDisk      = 20
	wpTotal     = 100

	wCPUSingle = float64(wpCPUSingle) / wpTotal
	wCPUMulti  = float64(wpCPUMulti) / wpTotal
	wMemory    = float64(wpMemory) / wpTotal
	wDisk      = float64(wpDisk) / wpTotal

	// --- Fixed benchmark shapes ---------------------------------------------
	warmupMs    = 250     // untimed CPU warm-up before the first measurement
	cpuChunk    = 1 << 16 // kernel iterations per timing check
	diskBlock   = 1 << 20 // 1 MiB write block
	memMinMiB   = 1
	memMaxMiB   = 4096
	diskMinMiB  = 1
	diskMaxMiB  = 65536
	maxSeconds  = 600.0
	scoreMethod = "subscore = 100 * measurement / reference (uncapped); contribution = weight * subscore; overall = sum of contributions in the fixed order cpu_single, cpu_multi, memory, disk"
)

// ---------------------------------------------------------------------------
// Shared Techlosoft UX helpers (identical across the tool family).
// ---------------------------------------------------------------------------

func reorderFlags(args []string, valueFlags map[string]bool) []string {
	var flags, positional []string
	for i := 0; i < len(args); i++ {
		a := args[i]
		name := strings.TrimLeft(a, "-")
		if strings.HasPrefix(a, "-") && valueFlags[name] {
			flags = append(flags, a)
			if i+1 < len(args) {
				i++
				flags = append(flags, args[i])
			}
			continue
		}
		if strings.HasPrefix(a, "-") {
			flags = append(flags, a)
			continue
		}
		positional = append(positional, a)
	}
	return append(flags, positional...)
}

func humanBytes(n int64) string {
	const unit = 1024
	if n < unit {
		return fmt.Sprintf("%d B", n)
	}
	div, exp := int64(unit), 0
	for x := n / unit; x >= unit; x /= unit {
		div *= unit
		exp++
	}
	return fmt.Sprintf("%.1f %ciB", float64(n)/float64(div), "KMGTPE"[exp])
}

// ---------------------------------------------------------------------------
// Temp-file registry: every disk benchmark file is registered here so that a
// signal (Ctrl-C / SIGTERM) still removes it before the process dies.
// ---------------------------------------------------------------------------

var (
	tempMu    sync.Mutex
	tempFiles = map[string]bool{}
)

func registerTemp(p string) {
	tempMu.Lock()
	tempFiles[p] = true
	tempMu.Unlock()
}

func releaseTemp(p string) {
	tempMu.Lock()
	delete(tempFiles, p)
	tempMu.Unlock()
	os.Remove(p)
}

func cleanupTemps() {
	tempMu.Lock()
	paths := make([]string, 0, len(tempFiles))
	for p := range tempFiles {
		paths = append(paths, p)
	}
	tempFiles = map[string]bool{}
	tempMu.Unlock()
	for _, p := range paths {
		os.Remove(p)
	}
}

func installSignalCleanup() {
	ch := make(chan os.Signal, 1)
	signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
	go func() {
		s := <-ch
		cleanupTemps()
		fmt.Fprintf(os.Stderr, "\n%s: interrupted (%s); benchmark temp files removed\n", toolName, s)
		os.Exit(exitSignal)
	}()
}

// ---------------------------------------------------------------------------
// Data model
// ---------------------------------------------------------------------------

// Subsystem is one measured benchmark and its exact contribution to the score.
type Subsystem struct {
	Name           string  `json:"name"`
	Metric         string  `json:"metric"`
	Raw            float64 `json:"raw"`
	Unit           string  `json:"unit"`
	Reference      float64 `json:"reference"`
	Subscore       float64 `json:"subscore"`
	BaseWeightPts  int     `json:"base_weight_points"`
	TotalWeightPts int     `json:"total_weight_points"`
	BaseWeight     float64 `json:"base_weight"`
	Weight         float64 `json:"weight"`
	Contribution   float64 `json:"contribution"`
	Detail         string  `json:"detail"`
}

// Result is one full suite run. It is also the on-disk save format.
type Result struct {
	Tool          string      `json:"tool"`
	ToolVersion   string      `json:"tool_version"`
	FormatVersion int         `json:"format_version"`
	CreatedUTC    string      `json:"created_utc"`
	OS            string      `json:"os"`
	Arch          string      `json:"arch"`
	NumCPU        int         `json:"num_cpu"`
	Profile       string      `json:"profile"`
	ProfileNote   string      `json:"profile_note"`
	SecondsEach   float64     `json:"seconds_per_subsystem"`
	MemBufMiB     int         `json:"memory_buffer_mib"`
	DiskCapMiB    int         `json:"disk_cap_mib"`
	DiskDir       string      `json:"disk_dir"`
	DiskBytes     int64       `json:"disk_bytes_written"`
	Subsystems    []Subsystem `json:"subsystems"`
	WeightSum     float64     `json:"weight_sum"`
	OverallScore  float64     `json:"overall_score"`
	ScoreMethod   string      `json:"score_method"`
}

// config is the measurement configuration for one suite run.
type config struct {
	seconds    float64
	diskDir    string
	memBufMiB  int
	diskCapMiB int
}

// ---------------------------------------------------------------------------
// Benchmarks
// ---------------------------------------------------------------------------

// cpuKernel runs n iterations of a fixed integer mixing loop (three
// shift/xor steps plus one 64-bit multiply-add). It returns the accumulated
// state so the loop cannot be optimised away. One "op" == one iteration.
func cpuKernel(seed uint64, n int) uint64 {
	x := seed
	for i := 0; i < n; i++ {
		x ^= x << 13
		x ^= x >> 7
		x ^= x << 17
		x = x*6364136223846793005 + 1442695040888963407
	}
	return x
}

var cpuSink uint64

// warmCPU spins the same kernel for a short, untimed period so that CPU
// frequency scaling has ramped up before the first measured benchmark. Its
// duration is fixed by warmupMs and is never included in any measurement.
func warmCPU() {
	x := uint64(0xDEADBEEFCAFEF00D)
	deadline := time.Now().Add(warmupMs * time.Millisecond)
	for time.Now().Before(deadline) {
		x = cpuKernel(x, cpuChunk)
	}
	cpuSink += x
}

// benchCPUSingle measures single-thread throughput in Mops/s.
func benchCPUSingle(d time.Duration) float64 {
	var iters int64
	x := uint64(0x9E3779B97F4A7C15)
	start := time.Now()
	deadline := start.Add(d)
	for time.Now().Before(deadline) {
		x = cpuKernel(x, cpuChunk)
		iters += cpuChunk
	}
	el := time.Since(start).Seconds()
	cpuSink += x
	if el <= 0 {
		return 0
	}
	return float64(iters) / el / 1e6
}

// benchCPUMulti runs the identical kernel on one goroutine per logical CPU and
// reports the aggregate throughput in Mops/s over wall-clock time.
func benchCPUMulti(d time.Duration, threads int) float64 {
	counts := make([]int64, threads)
	sinks := make([]uint64, threads)
	var wg sync.WaitGroup
	start := time.Now()
	deadline := start.Add(d)
	for t := 0; t < threads; t++ {
		wg.Add(1)
		go func(t int) {
			defer wg.Done()
			x := uint64(0x9E3779B97F4A7C15) + uint64(t)*0x1000193
			var n int64
			for time.Now().Before(deadline) {
				x = cpuKernel(x, cpuChunk)
				n += cpuChunk
			}
			counts[t] = n
			sinks[t] = x
		}(t)
	}
	wg.Wait()
	el := time.Since(start).Seconds()
	var total int64
	for i := range counts {
		total += counts[i]
		cpuSink += sinks[i]
	}
	if el <= 0 {
		return 0
	}
	return float64(total) / el / 1e6
}

var memSink byte

// benchMemory measures memory copy bandwidth in GB/s (GB = 1e9 bytes). Two
// buffers of bufMiB each are allocated; every pass copies one into the other,
// which moves 2*bufMiB (one read plus one write) worth of bytes.
func benchMemory(d time.Duration, bufMiB int) (float64, int64) {
	n := bufMiB << 20
	src := make([]byte, n)
	dst := make([]byte, n)
	for i := 0; i < n; i++ {
		src[i] = byte(i*31 + 7)
	}
	copy(dst, src) // warm both buffers so page faults are not timed

	var moved int64
	start := time.Now()
	deadline := start.Add(d)
	for time.Now().Before(deadline) {
		copy(dst, src)
		moved += int64(n) * 2
	}
	el := time.Since(start).Seconds()
	memSink += dst[n-1]
	if el <= 0 {
		return 0, moved
	}
	return float64(moved) / el / 1e9, moved
}

// benchDisk measures sequential write throughput in MB/s (MB = 1e6 bytes)
// including the final fsync. It writes exactly one temp file inside dir and
// always removes it - on success, on error, and on signal.
func benchDisk(dir string, d time.Duration, capMiB int) (float64, int64, error) {
	fi, err := os.Stat(dir)
	if err != nil {
		if os.IsNotExist(err) {
			return 0, 0, fmt.Errorf("--disk-dir not found: %s", dir)
		}
		return 0, 0, fmt.Errorf("--disk-dir %s: %v", dir, err)
	}
	if !fi.IsDir() {
		return 0, 0, fmt.Errorf("--disk-dir is not a directory: %s", dir)
	}

	f, err := os.CreateTemp(dir, ".performancedeck-bench-*.tmp")
	if err != nil {
		return 0, 0, fmt.Errorf("cannot create benchmark file in %s: %v", dir, err)
	}
	path := f.Name()
	registerTemp(path)
	defer func() {
		f.Close()
		releaseTemp(path)
	}()

	block := make([]byte, diskBlock)
	for i := range block {
		block[i] = byte(i*17 + 3)
	}
	limit := int64(capMiB) * diskBlock

	var written int64
	start := time.Now()
	deadline := start.Add(d)
	for written < limit && time.Now().Before(deadline) {
		n, werr := f.Write(block)
		written += int64(n)
		if werr != nil {
			return 0, written, fmt.Errorf("write to %s failed after %s: %v", dir, humanBytes(written), werr)
		}
	}
	if err := f.Sync(); err != nil {
		return 0, written, fmt.Errorf("fsync of benchmark file in %s failed: %v", dir, err)
	}
	el := time.Since(start).Seconds()
	if written == 0 || el <= 0 {
		return 0, written, fmt.Errorf("no bytes written to %s (is it writable?)", dir)
	}
	return float64(written) / el / 1e6, written, nil
}

// ---------------------------------------------------------------------------
// Scoring
// ---------------------------------------------------------------------------

// runSuite executes every applicable benchmark and folds the measurements into
// a single weighted score. Nothing is ever fabricated: a subsystem appears in
// the result only if it was actually measured.
func runSuite(cfg config) (*Result, error) {
	d := time.Duration(cfg.seconds * float64(time.Second))
	threads := runtime.NumCPU()

	res := &Result{
		Tool:          toolName,
		ToolVersion:   toolVersion,
		FormatVersion: formatVersion,
		CreatedUTC:    time.Now().UTC().Format(time.RFC3339Nano),
		OS:            runtime.GOOS,
		Arch:          runtime.GOARCH,
		NumCPU:        threads,
		SecondsEach:   cfg.seconds,
		MemBufMiB:     cfg.memBufMiB,
		DiskCapMiB:    cfg.diskCapMiB,
		DiskDir:       cfg.diskDir,
		ScoreMethod:   scoreMethod,
	}

	warmCPU()
	single := benchCPUSingle(d)
	multi := benchCPUMulti(d, threads)
	mem, memMoved := benchMemory(d, cfg.memBufMiB)

	subs := []Subsystem{
		{
			Name:          "cpu_single",
			Metric:        "single-thread integer mixing throughput",
			Raw:           single,
			Unit:          "Mops/s",
			Reference:     refCPUSingle,
			BaseWeightPts: wpCPUSingle,
			Detail:        fmt.Sprintf("1 thread, xorshift+LCG kernel, 1 op = 1 loop iteration, %dms untimed warm-up first", warmupMs),
		},
		{
			Name:          "cpu_multi",
			Metric:        "multi-thread integer mixing throughput",
			Raw:           multi,
			Unit:          "Mops/s",
			Reference:     refCPUMulti,
			BaseWeightPts: wpCPUMulti,
			Detail:        fmt.Sprintf("%d goroutines (one per logical CPU), identical kernel, aggregate over wall clock", threads),
		},
		{
			Name:          "memory",
			Metric:        "memory copy bandwidth",
			Raw:           mem,
			Unit:          "GB/s",
			Reference:     refMemory,
			BaseWeightPts: wpMemory,
			Detail:        fmt.Sprintf("2 x %d MiB buffers, %s moved (read+write counted), GB = 1e9 bytes", cfg.memBufMiB, humanBytes(memMoved)),
		},
	}

	if cfg.diskDir != "" {
		raw, written, err := benchDisk(cfg.diskDir, d, cfg.diskCapMiB)
		if err != nil {
			return nil, err
		}
		res.DiskBytes = written
		subs = append(subs, Subsystem{
			Name:          "disk",
			Metric:        "sequential write throughput (fsync included)",
			Raw:           raw,
			Unit:          "MB/s",
			Reference:     refDisk,
			BaseWeightPts: wpDisk,
			Detail:        fmt.Sprintf("1 MiB blocks into %s, %s written then fsync, MB = 1e6 bytes", cfg.diskDir, humanBytes(written)),
		})
		res.Profile = profileFull
		res.ProfileNote = "full: CPU single-thread + CPU multi-thread + memory + disk"
	} else {
		res.Profile = profileCPUMem
		res.ProfileNote = "cpu-mem: CPU and memory ONLY - no disk benchmark was run, so this score is NOT comparable with a 'full' score"
	}

	// Renormalise the base weights over the subsystems that were actually
	// measured. Integer weight points make the division exact, so the active
	// weights always sum to exactly 1.0 in IEEE-754 double arithmetic.
	baseTotal := 0
	for _, s := range subs {
		baseTotal += s.BaseWeightPts
	}
	var overall, wsum float64
	for i := range subs {
		subs[i].TotalWeightPts = baseTotal
		subs[i].BaseWeight = float64(subs[i].BaseWeightPts) / wpTotal
		subs[i].Subscore = 100 * subs[i].Raw / subs[i].Reference
		subs[i].Weight = float64(subs[i].BaseWeightPts) / float64(baseTotal)
		subs[i].Contribution = subs[i].Weight * subs[i].Subscore
		overall += subs[i].Contribution
		wsum += subs[i].Weight
	}

	res.Subsystems = subs
	res.WeightSum = wsum
	res.OverallScore = overall
	return res, nil
}

func pctChange(base, now float64) float64 {
	if base == 0 {
		if now == 0 {
			return 0
		}
		return math.Inf(1)
	}
	return (now - base) / base * 100
}

// ---------------------------------------------------------------------------
// Persistence
// ---------------------------------------------------------------------------

func saveResult(path string, r *Result) error {
	if dir := filepath.Dir(path); dir != "" && dir != "." {
		if err := os.MkdirAll(dir, 0o755); err != nil {
			return err
		}
	}
	b, err := json.MarshalIndent(r, "", "  ")
	if err != nil {
		return err
	}
	b = append(b, '\n')
	return os.WriteFile(path, b, 0o644)
}

func loadResult(path string) (*Result, error) {
	b, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, fmt.Errorf("result file not found: %s", path)
		}
		return nil, fmt.Errorf("cannot read result %s: %v", path, err)
	}
	var r Result
	if err := json.Unmarshal(b, &r); err != nil {
		return nil, fmt.Errorf("corrupt result %s: invalid JSON: %v", path, err)
	}
	if r.Tool != toolName {
		return nil, fmt.Errorf("corrupt result %s: not a %s result (tool=%q)", path, toolName, r.Tool)
	}
	if r.FormatVersion != formatVersion {
		return nil, fmt.Errorf("unsupported result %s: format_version %d, this build understands %d", path, r.FormatVersion, formatVersion)
	}
	if len(r.Subsystems) == 0 {
		return nil, fmt.Errorf("corrupt result %s: no subsystems recorded", path)
	}
	if r.Profile != profileFull && r.Profile != profileCPUMem {
		return nil, fmt.Errorf("corrupt result %s: unknown profile %q", path, r.Profile)
	}
	if math.IsNaN(r.OverallScore) || math.IsInf(r.OverallScore, 0) {
		return nil, fmt.Errorf("corrupt result %s: overall_score is not a finite number", path)
	}
	return &r, nil
}

// ---------------------------------------------------------------------------
// Reporting
// ---------------------------------------------------------------------------

func signed(v float64) string {
	if math.IsInf(v, 1) {
		return "+inf%"
	}
	if math.IsInf(v, -1) {
		return "-inf%"
	}
	return fmt.Sprintf("%+.2f%%", v)
}

func printResult(w io.Writer, r *Result) {
	fmt.Fprintf(w, "PerformanceDeck %s - composite system benchmark\n", r.ToolVersion)
	fmt.Fprintf(w, "  host     : %s/%s, %d logical CPUs\n", r.OS, r.Arch, r.NumCPU)
	fmt.Fprintf(w, "  recorded : %s\n", r.CreatedUTC)
	fmt.Fprintf(w, "  profile  : %s\n", r.ProfileNote)
	fmt.Fprintf(w, "  settings : %.2fs per subsystem, memory buffers 2 x %d MiB", r.SecondsEach, r.MemBufMiB)
	if r.Profile == profileFull {
		fmt.Fprintf(w, ", disk cap %d MiB in %s", r.DiskCapMiB, r.DiskDir)
	}
	fmt.Fprintf(w, "\n\n")

	fmt.Fprintf(w, "  %-11s %-17s %-17s %10s %8s %14s\n",
		"SUBSYSTEM",
		fmt.Sprintf("%10s %-6s", "MEASURED", ""),
		fmt.Sprintf("%10s %-6s", "REFERENCE", ""),
		"SUBSCORE", "WEIGHT", "CONTRIBUTION")
	fmt.Fprintf(w, "  %s\n", strings.Repeat("-", 84))
	for _, s := range r.Subsystems {
		fmt.Fprintf(w, "  %-11s %-17s %-17s %10.4f %8.4f %14.4f\n",
			s.Name,
			fmt.Sprintf("%10.2f %-6s", s.Raw, s.Unit),
			fmt.Sprintf("%10.2f %-6s", s.Reference, s.Unit),
			s.Subscore, s.Weight, s.Contribution)
	}
	fmt.Fprintf(w, "  %s\n", strings.Repeat("-", 84))
	fmt.Fprintf(w, "  %-11s %-17s %-17s %10s %8.4f %14.4f\n", "TOTAL", "", "", "", r.WeightSum, r.OverallScore)
	fmt.Fprintf(w, "\n  OVERALL SCORE: %.4f   (profile: %s)\n", r.OverallScore, r.Profile)
	if r.Profile == profileCPUMem {
		fmt.Fprintf(w, "  NOTE: no --disk-dir was given, so DISK WAS NOT MEASURED. This is a\n")
		fmt.Fprintf(w, "        CPU/memory-only score: the remaining weights were renormalised\n")
		fmt.Fprintf(w, "        (weight points 30/30/20 divided by their sum 80 -> 0.375/0.375/0.25)\n")
		fmt.Fprintf(w, "        and the run is\n")
		fmt.Fprintf(w, "        labelled profile=%s. compare refuses to diff it against a %s run.\n", profileCPUMem, profileFull)
	}
	fmt.Fprintf(w, "\n  HOW THIS SCORE IS COMPOSED\n")
	fmt.Fprintf(w, "    subscore_i     = 100 * measured_i / reference_i        (uncapped)\n")
	fmt.Fprintf(w, "    contribution_i = weight_i * subscore_i\n")
	fmt.Fprintf(w, "    OVERALL        = sum of contributions, in the order printed above\n")
	fmt.Fprintf(w, "    weights are fixed constants renormalised over the measured subsystems,\n")
	fmt.Fprintf(w, "    and always sum to %.4f. References are build-time constants:\n", r.WeightSum)
	fmt.Fprintf(w, "      cpu_single %.2f Mops/s   cpu_multi %.2f Mops/s   memory %.2f GB/s   disk %.2f MB/s\n",
		refCPUSingle, refCPUMulti, refMemory, refDisk)
	fmt.Fprintf(w, "    A machine that matches a reference exactly scores 100 on that subsystem.\n")
}

// SubsystemDelta is one subsystem's movement between two runs.
type SubsystemDelta struct {
	Name             string  `json:"name"`
	Unit             string  `json:"unit"`
	BaselineRaw      float64 `json:"baseline_raw"`
	CurrentRaw       float64 `json:"current_raw"`
	BaselineSubscore float64 `json:"baseline_subscore"`
	CurrentSubscore  float64 `json:"current_subscore"`
	Weight           float64 `json:"weight"`
	ChangePct        float64 `json:"change_pct"`
	ContributionDiff float64 `json:"contribution_delta"`
}

// Comparison is the JSON shape emitted by `compare`.
type Comparison struct {
	Tool          string           `json:"tool"`
	ToolVersion   string           `json:"tool_version"`
	Profile       string           `json:"profile"`
	ThresholdPct  float64          `json:"threshold_pct"`
	BaselineScore float64          `json:"baseline_score"`
	CurrentScore  float64          `json:"current_score"`
	ScoreChangePc float64          `json:"score_change_pct"`
	RegressionPct float64          `json:"regression_pct"`
	BiggestMover  string           `json:"biggest_mover"`
	MoverPct      float64          `json:"biggest_mover_change_pct"`
	Subsystems    []SubsystemDelta `json:"subsystems"`
	Notes         []string         `json:"notes"`
	Regression    bool             `json:"regression"`
	Status        string           `json:"status"`
	ExitCode      int              `json:"exit_code"`
	Baseline      *Result          `json:"baseline"`
	Current       *Result          `json:"current"`
}

func subsystemMap(r *Result) map[string]Subsystem {
	m := make(map[string]Subsystem, len(r.Subsystems))
	for _, s := range r.Subsystems {
		m[s.Name] = s
	}
	return m
}

func compareResults(base, cur *Result, threshold float64) (*Comparison, error) {
	if base.Profile != cur.Profile {
		hint := "pass --disk-dir so the current run also measures disk"
		if cur.Profile == profileFull {
			hint = "omit --disk-dir so the current run is also CPU/memory-only"
		}
		return nil, fmt.Errorf("profile mismatch: baseline is %q but the run being compared is %q; these scores weight different subsystems and are not comparable (%s)",
			base.Profile, cur.Profile, hint)
	}
	bm := subsystemMap(base)
	for _, s := range cur.Subsystems {
		if _, ok := bm[s.Name]; !ok {
			return nil, fmt.Errorf("subsystem mismatch: %q is present in the current run but not in the baseline", s.Name)
		}
	}

	c := &Comparison{
		Tool:          toolName,
		ToolVersion:   toolVersion,
		Profile:       cur.Profile,
		ThresholdPct:  threshold,
		BaselineScore: base.OverallScore,
		CurrentScore:  cur.OverallScore,
		Notes:         []string{},
		Baseline:      base,
		Current:       cur,
	}

	worst := math.Inf(-1)
	for _, s := range cur.Subsystems {
		b := bm[s.Name]
		ch := pctChange(b.Subscore, s.Subscore)
		c.Subsystems = append(c.Subsystems, SubsystemDelta{
			Name:             s.Name,
			Unit:             s.Unit,
			BaselineRaw:      b.Raw,
			CurrentRaw:       s.Raw,
			BaselineSubscore: b.Subscore,
			CurrentSubscore:  s.Subscore,
			Weight:           s.Weight,
			ChangePct:        ch,
			ContributionDiff: s.Contribution - b.Contribution,
		})
		if a := math.Abs(ch); a > worst {
			worst = a
			c.BiggestMover = s.Name
			c.MoverPct = ch
		}
	}

	c.ScoreChangePc = pctChange(base.OverallScore, cur.OverallScore)
	c.RegressionPct = -c.ScoreChangePc // positive means the score dropped
	c.Regression = c.RegressionPct > threshold
	if c.Regression {
		c.Status = "FAIL"
		c.ExitCode = exitRegressed
	} else {
		c.Status = "PASS"
		c.ExitCode = exitOK
	}

	if base.ToolVersion != cur.ToolVersion {
		c.Notes = append(c.Notes, fmt.Sprintf("tool version differs (baseline %s, current %s): scores are only comparable within one version", base.ToolVersion, cur.ToolVersion))
	}
	if base.OS != cur.OS || base.Arch != cur.Arch {
		c.Notes = append(c.Notes, fmt.Sprintf("platform differs (baseline %s/%s, current %s/%s)", base.OS, base.Arch, cur.OS, cur.Arch))
	}
	if base.NumCPU != cur.NumCPU {
		c.Notes = append(c.Notes, fmt.Sprintf("logical CPU count differs (baseline %d, current %d): cpu_multi is not comparable", base.NumCPU, cur.NumCPU))
	}
	if base.MemBufMiB != cur.MemBufMiB {
		c.Notes = append(c.Notes, fmt.Sprintf("memory buffer size differs (baseline %d MiB, current %d MiB): memory bandwidth depends heavily on buffer size", base.MemBufMiB, cur.MemBufMiB))
	}
	if base.SecondsEach != cur.SecondsEach {
		c.Notes = append(c.Notes, fmt.Sprintf("per-subsystem budget differs (baseline %.2fs, current %.2fs)", base.SecondsEach, cur.SecondsEach))
	}
	return c, nil
}

func printComparison(w io.Writer, c *Comparison) {
	fmt.Fprintf(w, "PerformanceDeck %s - composite comparison\n", c.ToolVersion)
	fmt.Fprintf(w, "  profile  : %s (both runs)\n", c.Profile)
	fmt.Fprintf(w, "  baseline : %s  score %.4f\n", c.Baseline.CreatedUTC, c.BaselineScore)
	fmt.Fprintf(w, "  current  : %s  score %.4f\n", c.Current.CreatedUTC, c.CurrentScore)
	fmt.Fprintf(w, "\n")
	fmt.Fprintf(w, "  %-11s %11s %11s %10s %10s %8s %12s\n",
		"SUBSYSTEM", "BASE RAW", "CUR RAW", "BASE SCORE", "CUR SCORE", "WEIGHT", "CHANGE")
	fmt.Fprintf(w, "  %s\n", strings.Repeat("-", 81))
	for _, s := range c.Subsystems {
		fmt.Fprintf(w, "  %-11s %11.2f %11.2f %10.4f %10.4f %8.4f %12s\n",
			s.Name, s.BaselineRaw, s.CurrentRaw, s.BaselineSubscore, s.CurrentSubscore, s.Weight, signed(s.ChangePct))
	}
	fmt.Fprintf(w, "  %s\n", strings.Repeat("-", 81))
	fmt.Fprintf(w, "  %-11s %11s %11s %10.4f %10.4f %8s %12s\n",
		"OVERALL", "", "", c.BaselineScore, c.CurrentScore, "", signed(c.ScoreChangePc))
	fmt.Fprintf(w, "\n")
	fmt.Fprintf(w, "  biggest mover : %s (%s)\n", c.BiggestMover, signed(c.MoverPct))
	fmt.Fprintf(w, "  threshold     : %.2f%% (applied to the DROP in the overall score)\n", c.ThresholdPct)
	fmt.Fprintf(w, "  measured drop : %.2f%%\n", c.RegressionPct)
	for _, n := range c.Notes {
		fmt.Fprintf(w, "  note          : %s\n", n)
	}
	fmt.Fprintf(w, "\n")
	if c.Regression {
		fmt.Fprintf(w, "  RESULT: FAIL - overall score dropped %.2f%%, exceeding the %.2f%% threshold;\n", c.RegressionPct, c.ThresholdPct)
		fmt.Fprintf(w, "          the subsystem that moved most is %s (%s)\n", c.BiggestMover, signed(c.MoverPct))
		return
	}
	fmt.Fprintf(w, "  RESULT: PASS - overall change %s is within the %.2f%% drop threshold\n", signed(c.ScoreChangePc), c.ThresholdPct)
}

func emitJSON(v any) error {
	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	return enc.Encode(v)
}

// ---------------------------------------------------------------------------
// CLI
// ---------------------------------------------------------------------------

func usage(w io.Writer) {
	fmt.Fprintf(w, `%s %s - composite weighted benchmark suite (Techlosoft PC Performance Console)

USAGE
  performancedeck run     [--disk-dir DIR] [--seconds N] [--mem-mib N]
                          [--disk-cap-mib N] [--save FILE] [--json]
  performancedeck compare --baseline FILE [--current FILE] [--disk-dir DIR]
                          [--seconds N] [--mem-mib N] [--disk-cap-mib N]
                          [--threshold-pct P] [--json]
  performancedeck show    --result FILE [--json]
  performancedeck help | -h | --help

COMMANDS
  run       Run every subsystem benchmark and print the weighted breakdown.
  compare   Diff a saved run against another saved run (--current) or against
            a fresh measurement taken now. Exits %d when the overall score drop
            exceeds --threshold-pct.
  show      Print a previously saved result file.

FLAGS
  --disk-dir DIR      Directory to benchmark disk I/O in. One temp file is
                      created inside it and ALWAYS deleted again. Omit this
                      flag to skip the disk test (see PROFILES).
  --seconds N         Wall-clock budget per subsystem (default 2, max %.0f).
  --mem-mib N         Size of EACH of the two memory buffers (default 64).
  --disk-cap-mib N    Maximum bytes written by the disk test (default 256).
  --save FILE         Write the run as a JSON result file.
  --baseline FILE     Saved result to compare against.
  --current FILE      Saved result to use as the current side (default: measure
                      now).
  --threshold-pct P   Overall-score DROP that counts as a regression (default 10).
                      Disk throughput on shared or virtualised storage can vary
                      by 2x between runs; widen this for full-profile gating.
  --json              Machine-readable output.

SCORING - NOTHING HERE IS MAGIC
  Every subscore comes from a real measurement normalised against a fixed
  reference constant compiled into this build:

    subsystem    measures                                   reference = 100 pts
    cpu_single   1-thread xorshift+LCG kernel throughput     %.2f Mops/s
    cpu_multi    same kernel, 1 goroutine per logical CPU    %.2f Mops/s
    memory       memcpy bandwidth, read+write counted        %.2f GB/s
    disk         sequential 1 MiB writes incl. fsync         %.2f MB/s

    subscore_i     = 100 * measured_i / reference_i    (uncapped)
    contribution_i = weight_i * subscore_i
    OVERALL SCORE  = sum of contributions, in the order listed above

  Base weights, held as integer weight points so renormalisation is exact:
    cpu_single %d pts   cpu_multi %d pts   memory %d pts   disk %d pts   (total %d)
    weight_i = base_points_i / (sum of base points of the MEASURED subsystems)
    With all four measured that is 0.30 / 0.30 / 0.20 / 0.20, summing to exactly 1.0.

PROFILES
  full      --disk-dir given: all four subsystems, weights %.2f/%.2f/%.2f/%.2f.
  cpu-mem   --disk-dir omitted: disk is NOT measured and NOT invented. The
            three remaining subsystems keep points 30/30/20, now divided by
            their own total of 80, giving weights 0.375/0.375/0.25 which again
            sum to exactly 1.0. The result is labelled profile=cpu-mem.
            compare REFUSES to diff a cpu-mem run against a full run.

EXIT CODES
  %d  success / no regression
  %d  usage or runtime error
  %d  regression: overall score dropped by more than --threshold-pct
  %d  interrupted by signal (benchmark temp files are removed first)

CAVEATS
  Scores are comparable only against runs from this same tool version on the
  same OS and CPU count. GPU is not measured. CPU temperature and thermal
  throttling are invisible to this tool.
`, toolName, toolVersion, exitRegressed, maxSeconds,
		refCPUSingle, refCPUMulti, refMemory, refDisk,
		wpCPUSingle, wpCPUMulti, wpMemory, wpDisk, wpTotal,
		wCPUSingle, wCPUMulti, wMemory, wDisk,
		exitOK, exitError, exitRegressed, exitSignal)
}

func fail(format string, a ...any) {
	cleanupTemps()
	fmt.Fprintf(os.Stderr, "%s: %s\n", toolName, fmt.Sprintf(format, a...))
	os.Exit(exitError)
}

func failUsage(format string, a ...any) {
	cleanupTemps()
	fmt.Fprintf(os.Stderr, "%s: %s\n\n", toolName, fmt.Sprintf(format, a...))
	usage(os.Stderr)
	os.Exit(exitError)
}

func isHelp(a string) bool {
	return a == "-h" || a == "--help" || a == "help" || a == "-help"
}

func newFlagSet(name string) *flag.FlagSet {
	fs := flag.NewFlagSet(name, flag.ContinueOnError)
	fs.SetOutput(io.Discard)
	fs.Usage = func() {}
	return fs
}

var valueFlags = map[string]bool{
	"disk-dir":      true,
	"seconds":       true,
	"mem-mib":       true,
	"disk-cap-mib":  true,
	"save":          true,
	"baseline":      true,
	"current":       true,
	"result":        true,
	"threshold-pct": true,
}

func main() {
	args := os.Args[1:]
	if len(args) == 0 {
		// Double-clicked in Explorer rather than run from a prompt: ask the
		// one question the program needs and stay on screen. Printing usage
		// and exiting here is what made the window vanish instantly.
		if interactiveConsole() {
			runGuided()
			return
		}
		usage(os.Stderr)
		os.Exit(exitError)
	}
	for _, a := range args {
		if isHelp(a) {
			usage(os.Stdout)
			os.Exit(exitOK)
		}
	}
	installSignalCleanup()

	sub := args[0]
	rest := reorderFlags(args[1:], valueFlags)

	switch sub {
	case "run":
		cmdRun(rest)
	case "compare":
		cmdCompare(rest)
	case "show":
		cmdShow(rest)
	default:
		failUsage("unknown subcommand %q", sub)
	}
}

// bindCommon registers the measurement flags shared by run and compare.
func bindCommon(fs *flag.FlagSet) (*string, *float64, *int, *int) {
	diskDir := fs.String("disk-dir", "", "")
	seconds := fs.Float64("seconds", 2, "")
	memMiB := fs.Int("mem-mib", 64, "")
	diskCap := fs.Int("disk-cap-mib", 256, "")
	return diskDir, seconds, memMiB, diskCap
}

func validateCommon(what string, seconds float64, memMiB, diskCap int) config {
	if !(seconds > 0) || math.IsNaN(seconds) {
		failUsage("%s: --seconds must be > 0 (got %g)", what, seconds)
	}
	if seconds > maxSeconds {
		failUsage("%s: --seconds must be <= %g (got %g)", what, maxSeconds, seconds)
	}
	if memMiB < memMinMiB || memMiB > memMaxMiB {
		failUsage("%s: --mem-mib must be between %d and %d (got %d)", what, memMinMiB, memMaxMiB, memMiB)
	}
	if diskCap < diskMinMiB || diskCap > diskMaxMiB {
		failUsage("%s: --disk-cap-mib must be between %d and %d (got %d)", what, diskMinMiB, diskMaxMiB, diskCap)
	}
	return config{seconds: seconds, memBufMiB: memMiB, diskCapMiB: diskCap}
}

func cmdRun(rest []string) {
	fs := newFlagSet("run")
	diskDir, seconds, memMiB, diskCap := bindCommon(fs)
	save := fs.String("save", "", "")
	asJSON := fs.Bool("json", false, "")
	if err := fs.Parse(rest); err != nil {
		failUsage("run: %v", err)
	}
	if fs.NArg() > 0 {
		failUsage("run: unexpected argument %q", fs.Arg(0))
	}
	cfg := validateCommon("run", *seconds, *memMiB, *diskCap)
	cfg.diskDir = *diskDir

	res, err := runSuite(cfg)
	if err != nil {
		fail("%v", err)
	}
	if *save != "" {
		if err := saveResult(*save, res); err != nil {
			fail("cannot save result: %v", err)
		}
	}
	if *asJSON {
		if err := emitJSON(res); err != nil {
			fail("cannot encode JSON: %v", err)
		}
		return
	}
	printResult(os.Stdout, res)
	if *save != "" {
		fmt.Printf("\n  result saved to %s\n", *save)
	}
}

func cmdCompare(rest []string) {
	fs := newFlagSet("compare")
	diskDir, seconds, memMiB, diskCap := bindCommon(fs)
	baseline := fs.String("baseline", "", "")
	current := fs.String("current", "", "")
	threshold := fs.Float64("threshold-pct", 10, "")
	asJSON := fs.Bool("json", false, "")
	if err := fs.Parse(rest); err != nil {
		failUsage("compare: %v", err)
	}
	if fs.NArg() > 0 {
		failUsage("compare: unexpected argument %q", fs.Arg(0))
	}
	if *baseline == "" {
		failUsage("compare: --baseline is required")
	}
	if *threshold < 0 || math.IsNaN(*threshold) {
		failUsage("compare: --threshold-pct must be >= 0 (got %g)", *threshold)
	}
	cfg := validateCommon("compare", *seconds, *memMiB, *diskCap)
	cfg.diskDir = *diskDir

	base, err := loadResult(*baseline)
	if err != nil {
		fail("%v", err)
	}

	var cur *Result
	if *current != "" {
		cur, err = loadResult(*current)
		if err != nil {
			fail("%v", err)
		}
	} else {
		cur, err = runSuite(cfg)
		if err != nil {
			fail("%v", err)
		}
	}

	c, err := compareResults(base, cur, *threshold)
	if err != nil {
		fail("%v", err)
	}
	if *asJSON {
		if err := emitJSON(c); err != nil {
			fail("cannot encode JSON: %v", err)
		}
	} else {
		printComparison(os.Stdout, c)
	}
	os.Exit(c.ExitCode)
}

func cmdShow(rest []string) {
	fs := newFlagSet("show")
	result := fs.String("result", "", "")
	asJSON := fs.Bool("json", false, "")
	if err := fs.Parse(rest); err != nil {
		failUsage("show: %v", err)
	}
	path := *result
	if path == "" && fs.NArg() > 0 {
		path = fs.Arg(0)
	}
	if path == "" {
		failUsage("show: --result is required")
	}
	r, err := loadResult(path)
	if err != nil {
		fail("%v", err)
	}
	if *asJSON {
		if err := emitJSON(r); err != nil {
			fail("cannot encode JSON: %v", err)
		}
		return
	}
	printResult(os.Stdout, r)
	if fi, err := os.Stat(path); err == nil {
		fmt.Printf("\n  source: %s (%s)\n", path, humanBytes(fi.Size()))
	}
}
