// Command hardwarelens is the plain-English system report card of the
// Hardware Health Hub.
//
// One command produces one readable page describing what this computer is,
// how fast it measured, what is filling up the folders you point it at, and
// whether anything about those numbers looks worth mentioning - written for
// somebody who is not a system administrator, and exportable with --out so it
// can be handed to whoever is helping them.
//
// Every number it prints is measured at run time. Facts that portable Go
// cannot read (CPU model name, total system RAM, GPU, temperatures, fan
// speeds, SMART health) are deliberately omitted and listed as not measured
// rather than guessed.
package main

import (
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"time"
)

const (
	appName    = "hardwarelens"
	appVersion = "1.0.0"

	reportSchema    = "hardwarelens.report.v1"
	inventorySchema = "hardwarelens.inventory.v1"
	benchSchema     = "hardwarelens.bench.v1"

	// Benchmark shape. The unit of work is a ROUND: fill a fixed buffer with
	// a deterministic 64-bit mixing sequence, then make mixPasses further
	// mixing passes over that buffer. A round therefore performs exactly
	// benchBufWords*(1+mixPasses) mix operations and returns a checksum that
	// depends only on its seed - never on timing, scheduling or worker count.
	benchBufWords = 8192 // 64 KiB of uint64, sized to sit in cache
	benchPasses   = 32
	benchOpsRound = benchBufWords * (1 + benchPasses)

	// Fixed-workload round counts. These are constants, so the fixed-workload
	// benchmark performs an identical amount of work on every single run and
	// only the elapsed TIME varies.
	benchSingleRounds = 700
	benchMultiRounds  = 1400

	maxBenchSeconds = 60.0
)

// ---------------------------------------------------------------------------
// shared Techlosoft CLI helpers
// ---------------------------------------------------------------------------

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])
}

// comma renders an integer with thousands separators.
func comma(n int64) string {
	s := strconv.FormatInt(n, 10)
	neg := ""
	if strings.HasPrefix(s, "-") {
		neg, s = "-", s[1:]
	}
	var b strings.Builder
	for i, c := range s {
		if i > 0 && (len(s)-i)%3 == 0 {
			b.WriteByte(',')
		}
		b.WriteRune(c)
	}
	return neg + b.String()
}

// plural returns "" for 1 and "s" otherwise.
func plural(n int64) string {
	if n == 1 {
		return ""
	}
	return "s"
}

// ---------------------------------------------------------------------------
// data model
// ---------------------------------------------------------------------------

// Machine is the set of static facts about this computer that portable Go can
// actually read. Nothing in here is inferred or defaulted.
type Machine struct {
	Hostname   string `json:"hostname"`
	OS         string `json:"os"`
	OSLabel    string `json:"os_label"`
	Arch       string `json:"arch"`
	ArchLabel  string `json:"arch_label"`
	NumCPU     int    `json:"logical_cpus"`
	GOMAXPROCS int    `json:"gomaxprocs"`
	GoVersion  string `json:"go_version"`
	Compiler   string `json:"go_compiler"`
	PID        int    `json:"pid"`
	WorkingDir string `json:"working_dir,omitempty"`
}

// MemUsage is this process's own Go runtime memory usage, read from
// runtime.MemStats. It describes hardwarelens itself, NOT the machine's total
// RAM, which portable Go cannot see.
type MemUsage struct {
	AllocBytes      uint64 `json:"alloc_bytes"`
	TotalAllocBytes uint64 `json:"total_alloc_bytes"`
	SysBytes        uint64 `json:"sys_bytes"`
	HeapAllocBytes  uint64 `json:"heap_alloc_bytes"`
	HeapSysBytes    uint64 `json:"heap_sys_bytes"`
	HeapObjects     uint64 `json:"heap_objects"`
	StackSysBytes   uint64 `json:"stack_sys_bytes"`
	NumGC           uint32 `json:"num_gc"`
	NumGoroutine    int    `json:"num_goroutine"`
}

// Phase is one measured half of the benchmark.
type Phase struct {
	Workers     int     `json:"workers"`
	Rounds      int64   `json:"rounds"`
	OpsPerRound int64   `json:"ops_per_round"`
	Ops         int64   `json:"ops"`
	Seconds     float64 `json:"seconds"`
	MOpsPerSec  float64 `json:"mops_per_sec"`
	Checksum    string  `json:"checksum"`
}

// Bench is a complete benchmark measurement.
type Bench struct {
	Mode        string  `json:"mode"` // "fixed" or "timed"
	Measures    string  `json:"measures"`
	BufferBytes int64   `json:"buffer_bytes"`
	Passes      int     `json:"passes_per_round"`
	Single      Phase   `json:"single_core"`
	Multi       Phase   `json:"all_core"`
	Speedup     float64 `json:"all_core_speedup"`
	Rating      string  `json:"rating"`
	RatingBasis string  `json:"rating_basis"`
}

// LargestFile names the biggest regular file found under a watched path.
type LargestFile struct {
	Path  string `json:"path"`
	Bytes int64  `json:"bytes"`
}

// PathUsage is the recursive contents of one --watch path.
type PathUsage struct {
	Path        string       `json:"path"`
	Bytes       int64        `json:"bytes"`
	Files       int64        `json:"files"`
	Dirs        int64        `json:"dirs"`
	Unreadable  int64        `json:"unreadable_entries"`
	MeanBytes   int64        `json:"mean_file_bytes"`
	Largest     *LargestFile `json:"largest_file,omitempty"`
	LargestPct  float64      `json:"largest_file_pct"`
	IsDirectory bool         `json:"is_directory"`
}

// Observation is one plain-English diagnostic remark. Evidence records the
// measured numbers the remark was derived from, so a reader can check it.
type Observation struct {
	Code     string `json:"code"`
	Severity string `json:"severity"` // ok | note | warn
	Text     string `json:"text"`
	Evidence string `json:"evidence"`
}

// Inventory is the static-facts-only view, and the shape of
// "inventory --json".
type Inventory struct {
	Schema      string   `json:"schema"`
	Tool        string   `json:"tool"`
	Version     string   `json:"version"`
	Generated   string   `json:"generated"`
	Machine     Machine  `json:"machine"`
	Memory      MemUsage `json:"tool_memory"`
	NotMeasured []string `json:"not_measured"`
}

// BenchResult is the shape of "bench --json".
type BenchResult struct {
	Schema    string  `json:"schema"`
	Tool      string  `json:"tool"`
	Version   string  `json:"version"`
	Generated string  `json:"generated"`
	Machine   Machine `json:"machine"`
	Benchmark Bench   `json:"benchmark"`
}

// ReportCard is the whole point of the tool, and the shape of
// "report --json".
type ReportCard struct {
	Schema       string        `json:"schema"`
	Tool         string        `json:"tool"`
	Version      string        `json:"version"`
	Generated    string        `json:"generated"`
	Machine      Machine       `json:"machine"`
	Benchmark    Bench         `json:"benchmark"`
	Memory       MemUsage      `json:"tool_memory"`
	Paths        []PathUsage   `json:"watched_paths"`
	TotalBytes   int64         `json:"watched_total_bytes"`
	TotalFiles   int64         `json:"watched_total_files"`
	Observations []Observation `json:"observations"`
	NotMeasured  []string      `json:"not_measured"`
}

// notMeasured is the honest list of things this tool does NOT report because
// portable Go cannot read them without native, per-OS system APIs. It is a
// list of MISSING FIELD NAMES; it contains no hardware values, because
// inventing one would defeat the purpose of the tool.
func notMeasured() []string {
	return []string{
		"CPU model name, vendor and clock speed - needs CPUID / sysctl / WMI",
		"physical core count as distinct from logical CPUs - needs native topology APIs",
		"total and free system RAM - needs sysinfo / sysctl / GlobalMemoryStatusEx",
		"GPU make, model and video memory - needs vendor or platform APIs",
		"CPU, GPU and drive temperatures - needs sensor APIs or a kernel driver",
		"fan speeds - needs embedded-controller access",
		"drive SMART health, wear and error counters - needs raw device ioctls",
		"battery health and cycle count - needs platform power APIs",
		"free space on the filesystem holding a watched path - needs statfs / GetDiskFreeSpaceEx",
		"whole-machine CPU load - needs /proc, host_statistics or PDH counters",
	}
}

// ---------------------------------------------------------------------------
// usage / exits
// ---------------------------------------------------------------------------

func usage(w io.Writer) {
	fmt.Fprintf(w, `%s %s - plain-English system report card

One command, one readable page: what this computer is, how fast it measured,
what is filling up the folders you point it at, and anything about those
numbers worth mentioning. Written for people who are not sysadmins, and
exportable with --out so you can hand it to whoever is helping you.

USAGE
  %s report [--watch <dir> ...] [--out <file>] [--json]
  %s inventory [--json]
  %s bench [--seconds N] [--json]
  %s help

COMMANDS
  report      The report card. Machine facts, a short CPU speed check, this
              tool's own memory use, the contents of every --watch path, and a
              plain-English diagnostic section in which every remark is
              derived from a number printed elsewhere in the same report.

  inventory   The static facts only - hostname, OS, architecture, logical CPU
              count, GOMAXPROCS, Go runtime. No benchmark, so it is instant.

  bench       The CPU speed check on its own.

REPORT FLAGS
  --watch <dir>   Directory (or single file) to measure; repeat for several.
                  Measures total bytes, file count and the largest file.
  --out <file>    Also write the report to this file. "-" means stdout.
                  This is the ONLY file %s ever writes.
  --json          Emit machine-readable JSON instead of the printed page.

BENCH FLAGS
  --seconds N     Run the timed variant for about N seconds (0 < N <= %.0f)
                  instead of the fixed workload. Optional.
  --json          Emit JSON instead of text.

WHAT THE SPEED CHECK MEASURES
  Rounds of 64-bit integer mixing over a %s buffer: %s mix operations per
  round, %s rounds single-threaded and %s rounds spread over all
  available CPUs. It measures integer throughput and cache behaviour of this
  machine as it is running right now. It is not a graphics, disk or memory
  bandwidth test, and it is not comparable to any other benchmark's numbers.
  Without --seconds the workload is a fixed constant, so the operation count
  and the checksum are identical on every run and only the time varies.

WHAT THIS TOOL DOES NOT REPORT
  CPU model name, total system RAM, GPU, temperatures, fan speeds, SMART
  drive health and free disk space are NOT shown. Portable Go cannot read
  them without native per-OS APIs, so %s omits them rather than guessing.
  Every number it does print is measured at run time.

NOTES
  Flags may appear before or after positional arguments.
  %s only ever reads; the sole exception is the --out file you name.

EXAMPLES
  %s report
  %s report --watch ~/Documents --watch ~/Downloads --out report.txt
  %s inventory --json
  %s bench --seconds 3
`, appName, appVersion,
		appName, appName, appName, appName,
		appName, maxBenchSeconds,
		humanBytes(benchBufWords*8), comma(benchOpsRound),
		comma(benchSingleRounds), comma(benchMultiRounds),
		appName, appName,
		appName, appName, appName, appName)
}

func fail(format string, args ...any) {
	fmt.Fprintf(os.Stderr, appName+": "+format+"\n", args...)
	usage(os.Stderr)
	os.Exit(1)
}

func errExit(format string, args ...any) {
	fmt.Fprintf(os.Stderr, appName+": "+format+"\n", args...)
	os.Exit(1)
}

func isHelp(a string) bool {
	switch a {
	case "-h", "--help", "help", "-help":
		return true
	}
	return false
}

func hasHelp(args []string) bool {
	for _, a := range args {
		if isHelp(a) {
			return true
		}
	}
	return false
}

// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------

func main() {
	args := os.Args[1:]
	if len(args) == 0 {
		// Double-clicked in Explorer rather than run from a prompt: ask the
		// question the program needs and stay on screen. Printing usage
		// and exiting here is what made the window vanish instantly.
		if interactiveConsole() {
			runGuided()
			return
		}
		fmt.Fprintf(os.Stderr, "%s: no command given\n", appName)
		usage(os.Stderr)
		os.Exit(1)
	}
	if isHelp(args[0]) {
		usage(os.Stdout)
		os.Exit(0)
	}
	switch args[0] {
	case "report":
		cmdReport(args[1:])
	case "inventory":
		cmdInventory(args[1:])
	case "bench":
		cmdBench(args[1:])
	case "version", "--version":
		fmt.Printf("%s %s\n", appName, appVersion)
	default:
		fail("unknown command %q", args[0])
	}
}

type multiFlag []string

func (m *multiFlag) String() string { return strings.Join(*m, ",") }

func (m *multiFlag) Set(v string) error {
	if strings.TrimSpace(v) == "" {
		return errors.New("value must not be empty")
	}
	*m = append(*m, v)
	return nil
}

// ---------------------------------------------------------------------------
// machine facts
// ---------------------------------------------------------------------------

func osLabel(goos string) string {
	switch goos {
	case "windows":
		return "Windows"
	case "darwin":
		return "macOS"
	case "linux":
		return "Linux"
	case "freebsd":
		return "FreeBSD"
	case "openbsd":
		return "OpenBSD"
	case "netbsd":
		return "NetBSD"
	default:
		return goos
	}
}

// archLabel names the instruction-set FAMILY reported by the Go runtime. It is
// a translation of runtime.GOARCH, not a CPU model: the tool cannot read the
// model and does not pretend to.
func archLabel(goarch string) string {
	switch goarch {
	case "amd64":
		return "64-bit Intel/AMD (x86-64)"
	case "386":
		return "32-bit Intel/AMD (x86)"
	case "arm64":
		return "64-bit ARM"
	case "arm":
		return "32-bit ARM"
	case "riscv64":
		return "64-bit RISC-V"
	default:
		return goarch
	}
}

func readMachine() Machine {
	host, err := os.Hostname()
	if err != nil || strings.TrimSpace(host) == "" {
		host = "unknown"
	}
	wd, err := os.Getwd()
	if err != nil {
		wd = ""
	}
	return Machine{
		Hostname:   host,
		OS:         runtime.GOOS,
		OSLabel:    osLabel(runtime.GOOS),
		Arch:       runtime.GOARCH,
		ArchLabel:  archLabel(runtime.GOARCH),
		NumCPU:     runtime.NumCPU(),
		GOMAXPROCS: runtime.GOMAXPROCS(0),
		GoVersion:  runtime.Version(),
		Compiler:   runtime.Compiler,
		PID:        os.Getpid(),
		WorkingDir: wd,
	}
}

func readMem() MemUsage {
	var ms runtime.MemStats
	runtime.ReadMemStats(&ms)
	return MemUsage{
		AllocBytes:      ms.Alloc,
		TotalAllocBytes: ms.TotalAlloc,
		SysBytes:        ms.Sys,
		HeapAllocBytes:  ms.HeapAlloc,
		HeapSysBytes:    ms.HeapSys,
		HeapObjects:     ms.HeapObjects,
		StackSysBytes:   ms.StackSys,
		NumGC:           ms.NumGC,
		NumGoroutine:    runtime.NumGoroutine(),
	}
}

// ---------------------------------------------------------------------------
// the benchmark
// ---------------------------------------------------------------------------

// mix is one benchmark operation: a single splitmix64-style 64-bit avalanche
// step. Integer-only, so it produces bit-identical results on every platform.
func mix(x uint64) uint64 {
	x += 0x9E3779B97F4A7C15
	x ^= x >> 30
	x *= 0xBF58476D1CE4E5B9
	x ^= x >> 27
	x *= 0x94D049BB133111EB
	x ^= x >> 31
	return x
}

// runRound performs exactly benchOpsRound mix operations over buf and returns
// a checksum that is a pure function of seed. buf is scratch space, fully
// overwritten on entry, so a round never depends on what ran before it.
func runRound(buf []uint64, seed uint64) uint64 {
	s := seed
	for i := range buf {
		s = mix(s)
		buf[i] = s
	}
	var sum uint64
	for p := 0; p < benchPasses; p++ {
		pv := uint64(p) * 0x1000193
		for i := range buf {
			v := mix(buf[i] ^ pv)
			buf[i] = v
			sum ^= v
		}
	}
	return sum ^ s
}

// runPhaseFixed executes exactly `rounds` rounds spread over `workers`
// goroutines. Round r is always seeded with r, and the per-round checksums are
// XOR-folded, so the result is independent of how rounds are distributed and
// of the order in which workers finish.
func runPhaseFixed(rounds int64, workers int) (checksum uint64, elapsed time.Duration) {
	if workers < 1 {
		workers = 1
	}
	if rounds < 0 {
		rounds = 0
	}
	sums := make([]uint64, workers)
	done := make(chan struct{}, workers)
	start := time.Now()
	for w := 0; w < workers; w++ {
		go func(w int) {
			buf := make([]uint64, benchBufWords)
			var acc uint64
			for r := int64(w); r < rounds; r += int64(workers) {
				acc ^= runRound(buf, uint64(r))
			}
			sums[w] = acc
			done <- struct{}{}
		}(w)
	}
	for w := 0; w < workers; w++ {
		<-done
	}
	elapsed = time.Since(start)
	for _, s := range sums {
		checksum ^= s
	}
	return checksum, elapsed
}

// runPhaseTimed keeps executing rounds on every worker until the deadline
// passes. The round SIZE is still the constant benchOpsRound; only how many
// rounds fit in the budget varies.
func runPhaseTimed(budget time.Duration, workers int) (rounds int64, checksum uint64, elapsed time.Duration) {
	if workers < 1 {
		workers = 1
	}
	type res struct {
		rounds int64
		sum    uint64
	}
	out := make(chan res, workers)
	start := time.Now()
	deadline := start.Add(budget)
	for w := 0; w < workers; w++ {
		go func(w int) {
			buf := make([]uint64, benchBufWords)
			var acc uint64
			var n int64
			for r := int64(w); ; r += int64(workers) {
				acc ^= runRound(buf, uint64(r))
				n++
				if time.Now().After(deadline) {
					break
				}
			}
			out <- res{rounds: n, sum: acc}
		}(w)
	}
	for w := 0; w < workers; w++ {
		r := <-out
		rounds += r.rounds
		checksum ^= r.sum
	}
	return rounds, checksum, time.Since(start)
}

func phaseFromFixed(rounds int64, workers int) Phase {
	sum, d := runPhaseFixed(rounds, workers)
	return makePhase(workers, rounds, d, sum)
}

func makePhase(workers int, rounds int64, d time.Duration, sum uint64) Phase {
	ops := rounds * benchOpsRound
	secs := d.Seconds()
	mops := 0.0
	if secs > 0 {
		mops = float64(ops) / secs / 1e6
	}
	return Phase{
		Workers:     workers,
		Rounds:      rounds,
		OpsPerRound: benchOpsRound,
		Ops:         ops,
		Seconds:     secs,
		MOpsPerSec:  mops,
		Checksum:    fmt.Sprintf("%016x", sum),
	}
}

// rateSingleCore turns a measured single-core throughput into a plain-English
// band. The bands are a documented rule of thumb applied to a number this run
// actually measured; they are not a claim about any specific hardware.
func rateSingleCore(mops float64) (string, string) {
	basis := fmt.Sprintf("band applied to the measured single-core %.1f million mix ops/sec "+
		"(<100 very slow, <250 slow, <500 ordinary, <900 fast, else very fast)", mops)
	switch {
	case mops < 100:
		return "very slow - either old hardware or something else is hogging the CPU", basis
	case mops < 250:
		return "slow - fine for browsing and documents, sluggish under load", basis
	case mops < 500:
		return "ordinary - comfortable for everyday work", basis
	case mops < 900:
		return "fast - handles demanding work well", basis
	default:
		return "very fast - this is a quick machine", basis
	}
}

func runBenchFixed() Bench {
	workers := runtime.GOMAXPROCS(0)
	b := Bench{
		Mode:        "fixed",
		Measures:    "64-bit integer mixing throughput over a cache-resident buffer",
		BufferBytes: benchBufWords * 8,
		Passes:      benchPasses,
		Single:      phaseFromFixed(benchSingleRounds, 1),
		Multi:       phaseFromFixed(benchMultiRounds, workers),
	}
	finishBench(&b)
	return b
}

func runBenchTimed(seconds float64) Bench {
	workers := runtime.GOMAXPROCS(0)
	budget := time.Duration(seconds * float64(time.Second) / 2)
	if budget <= 0 {
		budget = time.Millisecond
	}
	sr, ss, sd := runPhaseTimed(budget, 1)
	mr, ms, md := runPhaseTimed(budget, workers)
	b := Bench{
		Mode:        "timed",
		Measures:    "64-bit integer mixing throughput over a cache-resident buffer",
		BufferBytes: benchBufWords * 8,
		Passes:      benchPasses,
		Single:      makePhase(1, sr, sd, ss),
		Multi:       makePhase(workers, mr, md, ms),
	}
	finishBench(&b)
	return b
}

func finishBench(b *Bench) {
	if b.Single.MOpsPerSec > 0 {
		b.Speedup = b.Multi.MOpsPerSec / b.Single.MOpsPerSec
	}
	b.Rating, b.RatingBasis = rateSingleCore(b.Single.MOpsPerSec)
}

// ---------------------------------------------------------------------------
// watched paths
// ---------------------------------------------------------------------------

// walkUsage measures one watched path recursively. Entries that cannot be read
// are counted rather than aborting the walk; a path that does not exist at all
// is a hard error.
func walkUsage(root string) (PathUsage, error) {
	abs, err := filepath.Abs(root)
	if err != nil {
		abs = root
	}
	u := PathUsage{Path: abs}

	info, err := os.Stat(abs)
	if err != nil {
		return u, fmt.Errorf("--watch %s: %s", root, readableFSError(err))
	}
	if !info.IsDir() {
		u.Files = 1
		u.Bytes = info.Size()
		u.MeanBytes = info.Size()
		u.Largest = &LargestFile{Path: abs, Bytes: info.Size()}
		if u.Bytes > 0 {
			u.LargestPct = 100
		}
		return u, nil
	}
	u.IsDirectory = true
	err = filepath.WalkDir(abs, func(p string, d fs.DirEntry, walkErr error) error {
		if walkErr != nil {
			if p == abs {
				return walkErr
			}
			u.Unreadable++
			return nil
		}
		if d.IsDir() {
			if p != abs {
				u.Dirs++
			}
			return nil
		}
		fi, err := d.Info()
		if err != nil {
			u.Unreadable++
			return nil
		}
		if !fi.Mode().IsRegular() {
			return nil
		}
		u.Files++
		u.Bytes += fi.Size()
		if u.Largest == nil || fi.Size() > u.Largest.Bytes {
			u.Largest = &LargestFile{Path: p, Bytes: fi.Size()}
		}
		return nil
	})
	if err != nil {
		return u, fmt.Errorf("--watch %s: %s", root, readableFSError(err))
	}
	if u.Files > 0 {
		u.MeanBytes = u.Bytes / u.Files
	}
	if u.Largest != nil && u.Bytes > 0 {
		u.LargestPct = float64(u.Largest.Bytes) / float64(u.Bytes) * 100
	}
	return u, nil
}

func readableFSError(err error) string {
	switch {
	case errors.Is(err, fs.ErrNotExist):
		return "no such file or directory"
	case errors.Is(err, fs.ErrPermission):
		return "permission denied"
	default:
		return err.Error()
	}
}

// ---------------------------------------------------------------------------
// diagnostics
// ---------------------------------------------------------------------------

// observe derives the plain-English diagnostic section. Every observation is
// produced from measured values passed in here; none is emitted unless its
// trigger condition holds, and each carries the numbers it was derived from.
func observe(m Machine, b Bench, paths []PathUsage) []Observation {
	var obs []Observation
	add := func(code, sev, text, evidence string) {
		obs = append(obs, Observation{Code: code, Severity: sev, Text: text, Evidence: evidence})
	}

	// --- CPU count -------------------------------------------------------
	switch {
	case m.NumCPU <= 2:
		add("cpu-count-low", "warn",
			fmt.Sprintf("This machine has only %d logical CPU%s, so heavy multitasking will feel slow - "+
				"running a big update while you work is likely to make everything stutter.",
				m.NumCPU, plural(int64(m.NumCPU))),
			fmt.Sprintf("runtime.NumCPU() = %d", m.NumCPU))
	case m.NumCPU >= 8:
		add("cpu-count-high", "ok",
			fmt.Sprintf("With %d logical CPUs there is plenty of room to run several demanding "+
				"things at once.", m.NumCPU),
			fmt.Sprintf("runtime.NumCPU() = %d", m.NumCPU))
	default:
		add("cpu-count-ok", "ok",
			fmt.Sprintf("%d logical CPUs is a normal amount for everyday work.", m.NumCPU),
			fmt.Sprintf("runtime.NumCPU() = %d", m.NumCPU))
	}
	if m.GOMAXPROCS < m.NumCPU {
		add("gomaxprocs-capped", "note",
			fmt.Sprintf("Programs started in this environment are limited to %d of the %d CPUs, "+
				"so the all-core figure above understates what the hardware could do.",
				m.GOMAXPROCS, m.NumCPU),
			fmt.Sprintf("GOMAXPROCS = %d, NumCPU = %d", m.GOMAXPROCS, m.NumCPU))
	}

	// --- speed check -----------------------------------------------------
	if b.Single.MOpsPerSec > 0 {
		add("cpu-speed", severityForRating(b.Rating),
			fmt.Sprintf("The speed check rated this processor %q at %.1f million operations per second "+
				"on one core.", b.Rating, b.Single.MOpsPerSec),
			fmt.Sprintf("%s ops in %.3f s single-threaded", comma(b.Single.Ops), b.Single.Seconds))
	}
	if m.GOMAXPROCS >= 2 && b.Single.MOpsPerSec > 0 {
		half := float64(m.GOMAXPROCS) / 2
		if b.Speedup < half {
			add("parallel-poor", "warn",
				fmt.Sprintf("Using all %d CPUs was only %.2fx faster than using one, well short of the "+
					"%dx the core count allows - something else on this machine was probably "+
					"competing for the processor while the check ran.",
					m.GOMAXPROCS, b.Speedup, m.GOMAXPROCS),
				fmt.Sprintf("all-core %.1f Mops/s vs single-core %.1f Mops/s",
					b.Multi.MOpsPerSec, b.Single.MOpsPerSec))
		} else {
			add("parallel-ok", "ok",
				fmt.Sprintf("Using all %d CPUs was %.2fx faster than using one, so the extra cores are "+
					"working as expected.", m.GOMAXPROCS, b.Speedup),
				fmt.Sprintf("all-core %.1f Mops/s vs single-core %.1f Mops/s",
					b.Multi.MOpsPerSec, b.Single.MOpsPerSec))
		}
	}

	// --- watched paths ---------------------------------------------------
	for _, p := range paths {
		short := p.Path
		if p.Files == 0 {
			add("path-empty", "note",
				fmt.Sprintf("There are no files at all under %s.", short),
				fmt.Sprintf("%d file%s, %d director%s", p.Files, plural(p.Files), p.Dirs,
					map[bool]string{true: "y", false: "ies"}[p.Dirs == 1]))
			continue
		}
		if p.Largest != nil && p.LargestPct >= 50 && p.Files > 1 {
			add("path-dominated", "note",
				fmt.Sprintf("Disk usage under %s is dominated by one %s file - %s is %.1f%% of the "+
					"%s stored there. Deleting or moving that single file would reclaim most of the space.",
					short, humanBytes(p.Largest.Bytes), filepath.Base(p.Largest.Path),
					p.LargestPct, humanBytes(p.Bytes)),
				fmt.Sprintf("largest %d B of %d B total across %s files",
					p.Largest.Bytes, p.Bytes, comma(p.Files)))
		}
		if p.Files >= 1000 && p.MeanBytes < 4096 {
			add("path-many-small", "note",
				fmt.Sprintf("%s holds %s files averaging only %s each. Lots of tiny files take up "+
					"little space but make backups and folder listings slow.",
					short, comma(p.Files), humanBytes(p.MeanBytes)),
				fmt.Sprintf("%s files, %s total, mean %d B",
					comma(p.Files), humanBytes(p.Bytes), p.MeanBytes))
		}
		if p.Unreadable > 0 {
			add("path-unreadable", "warn",
				fmt.Sprintf("%s entr%s under %s could not be read, so the totals for that path are a "+
					"lower bound. This is usually a permissions problem.",
					comma(p.Unreadable), map[bool]string{true: "y", false: "ies"}[p.Unreadable == 1], short),
				fmt.Sprintf("%d unreadable entr%s during the walk", p.Unreadable,
					map[bool]string{true: "y", false: "ies"}[p.Unreadable == 1]))
		}
	}

	if len(obs) == 0 {
		add("nothing-notable", "ok", "Nothing measured in this report looks out of the ordinary.", "no trigger condition met")
	}
	return obs
}

func severityForRating(rating string) string {
	if strings.HasPrefix(rating, "very slow") || strings.HasPrefix(rating, "slow") {
		return "note"
	}
	return "ok"
}

// ---------------------------------------------------------------------------
// report
// ---------------------------------------------------------------------------

func cmdReport(argv []string) {
	if hasHelp(argv) {
		usage(os.Stdout)
		os.Exit(0)
	}
	argv = reorderFlags(argv, map[string]bool{"watch": true, "out": true})

	fset := flag.NewFlagSet("report", flag.ContinueOnError)
	fset.SetOutput(io.Discard)
	out := fset.String("out", "", "also write the report to this file")
	asJSON := fset.Bool("json", false, "emit JSON")
	var watches multiFlag
	fset.Var(&watches, "watch", "directory to measure")
	if err := fset.Parse(argv); err != nil {
		fail("report: %v", err)
	}
	if rest := fset.Args(); len(rest) > 0 {
		fail("report: unexpected argument %q (did you mean --watch %s?)", rest[0], rest[0])
	}

	card := &ReportCard{
		Schema:      reportSchema,
		Tool:        appName,
		Version:     appVersion,
		Generated:   time.Now().UTC().Format(time.RFC3339),
		Machine:     readMachine(),
		Paths:       []PathUsage{},
		NotMeasured: notMeasured(),
	}
	for _, w := range watches {
		u, err := walkUsage(w)
		if err != nil {
			errExit("report: %v", err)
		}
		card.Paths = append(card.Paths, u)
		card.TotalBytes += u.Bytes
		card.TotalFiles += u.Files
	}
	card.Benchmark = runBenchFixed()
	card.Memory = readMem()
	card.Observations = observe(card.Machine, card.Benchmark, card.Paths)

	var body []byte
	if *asJSON {
		blob, err := json.MarshalIndent(card, "", "  ")
		if err != nil {
			errExit("report: encoding: %v", err)
		}
		body = append(blob, '\n')
	} else {
		var sb strings.Builder
		printReport(&sb, card)
		body = []byte(sb.String())
	}

	os.Stdout.Write(body)
	if strings.TrimSpace(*out) == "" || *out == "-" {
		return
	}
	if err := os.WriteFile(*out, body, 0o644); err != nil {
		errExit("report: writing %s: %s", *out, readableFSError(err))
	}
	fmt.Fprintf(os.Stderr, "%s: report also written to %s\n", appName, *out)
}

func rule(w io.Writer, title string) {
	fmt.Fprintf(w, "\n%s\n%s\n", title, strings.Repeat("-", len(title)))
}

func printReport(w io.Writer, c *ReportCard) {
	fmt.Fprintf(w, "%s %s - SYSTEM REPORT CARD\n", strings.ToUpper(appName), appVersion)
	fmt.Fprintf(w, "%s   generated %s\n", strings.Repeat("=", 58), c.Generated)

	const lw = 22
	rule(w, "WHAT THIS COMPUTER IS")
	fmt.Fprintf(w, "  %-*s%s\n", lw, "name on the network", c.Machine.Hostname)
	fmt.Fprintf(w, "  %-*s%s (%s)\n", lw, "operating system", c.Machine.OSLabel, c.Machine.OS)
	fmt.Fprintf(w, "  %-*s%s (%s)\n", lw, "processor family", c.Machine.ArchLabel, c.Machine.Arch)
	fmt.Fprintf(w, "  %-*s%d\n", lw, "logical CPUs", c.Machine.NumCPU)
	fmt.Fprintf(w, "  %-*s%d of %d usable by this program\n", lw, "GOMAXPROCS",
		c.Machine.GOMAXPROCS, c.Machine.NumCPU)
	fmt.Fprintf(w, "  %-*s%s (%s)\n", lw, "built with", c.Machine.GoVersion, c.Machine.Compiler)

	b := c.Benchmark
	rule(w, "SPEED CHECK")
	fmt.Fprintf(w, "  What it measures: %s.\n", b.Measures)
	fmt.Fprintf(w, "  Each round performs %s mix operations over a %s buffer; the workload is a\n",
		comma(b.Single.OpsPerRound), humanBytes(b.BufferBytes))
	fmt.Fprintf(w, "  fixed constant, so only the elapsed time varies between runs.\n\n")
	fmt.Fprintf(w, "  %-*s%.1f million ops/sec   (%s ops in %.3f s)\n", lw, "one core",
		b.Single.MOpsPerSec, comma(b.Single.Ops), b.Single.Seconds)
	fmt.Fprintf(w, "  %-*s%.1f million ops/sec   (%s ops in %.3f s on %d worker%s)\n", lw, "all cores",
		b.Multi.MOpsPerSec, comma(b.Multi.Ops), b.Multi.Seconds, b.Multi.Workers,
		plural(int64(b.Multi.Workers)))
	fmt.Fprintf(w, "  %-*s%.2fx faster than one core\n", lw, "using every core", b.Speedup)
	fmt.Fprintf(w, "  %-*s%s\n", lw, "in plain English", b.Rating)
	fmt.Fprintf(w, "  %-*s%s / %s (identical every run - same work done)\n", lw, "work checksums",
		b.Single.Checksum, b.Multi.Checksum)

	m := c.Memory
	rule(w, "MEMORY USED BY THIS TOOL")
	fmt.Fprintf(w, "  These are hardwarelens' OWN figures from the Go runtime. They are not the\n")
	fmt.Fprintf(w, "  machine's total RAM, which this tool cannot see.\n\n")
	fmt.Fprintf(w, "  %-*s%s\n", lw, "in use right now", humanBytes(int64(m.AllocBytes)))
	fmt.Fprintf(w, "  %-*s%s\n", lw, "reserved from the OS", humanBytes(int64(m.SysBytes)))
	fmt.Fprintf(w, "  %-*s%s\n", lw, "allocated in total", humanBytes(int64(m.TotalAllocBytes)))
	fmt.Fprintf(w, "  %-*s%s / %s\n", lw, "heap in use / held", humanBytes(int64(m.HeapAllocBytes)),
		humanBytes(int64(m.HeapSysBytes)))
	fmt.Fprintf(w, "  %-*s%s\n", lw, "live heap objects", comma(int64(m.HeapObjects)))
	fmt.Fprintf(w, "  %-*s%d collection%s, %d goroutine%s\n", lw, "garbage collector",
		m.NumGC, plural(int64(m.NumGC)), m.NumGoroutine, plural(int64(m.NumGoroutine)))

	rule(w, "WHAT IS IN THE FOLDERS YOU ASKED ABOUT")
	if len(c.Paths) == 0 {
		fmt.Fprintf(w, "  No folders were checked. Add --watch <dir> (repeatable) to measure some.\n")
	} else {
		for _, p := range c.Paths {
			fmt.Fprintf(w, "  %s\n", p.Path)
			fmt.Fprintf(w, "    %-*s%s  (%s bytes)\n", lw-4, "total size", humanBytes(p.Bytes), comma(p.Bytes))
			fmt.Fprintf(w, "    %-*s%s file%s in %s folder%s\n", lw-4, "contents",
				comma(p.Files), plural(p.Files), comma(p.Dirs), plural(p.Dirs))
			if p.Files > 0 {
				fmt.Fprintf(w, "    %-*s%s\n", lw-4, "average file", humanBytes(p.MeanBytes))
			}
			if p.Largest != nil {
				fmt.Fprintf(w, "    %-*s%s  %s  (%.1f%% of this folder)\n", lw-4, "largest file",
					humanBytes(p.Largest.Bytes), p.Largest.Path, p.LargestPct)
			}
			if p.Unreadable > 0 {
				fmt.Fprintf(w, "    %-*s%s entr%s skipped\n", lw-4, "unreadable",
					comma(p.Unreadable), map[bool]string{true: "y", false: "ies"}[p.Unreadable == 1])
			}
		}
		if len(c.Paths) > 1 {
			fmt.Fprintf(w, "  %-*s%s across %s file%s\n", lw, "ALL WATCHED PATHS",
				humanBytes(c.TotalBytes), comma(c.TotalFiles), plural(c.TotalFiles))
		}
	}

	rule(w, "WHAT THIS LOOKS LIKE")
	fmt.Fprintf(w, "  Every remark below was derived from a number printed above.\n\n")
	for i, o := range c.Observations {
		if i > 0 {
			fmt.Fprintln(w)
		}
		fmt.Fprintf(w, "  [%s] %s\n", o.Severity, wrapIndent(o.Text, 72, "        "))
		fmt.Fprintf(w, "        evidence: %s\n", o.Evidence)
	}

	rule(w, "WHAT THIS TOOL CANNOT SEE")
	fmt.Fprintf(w, "  Portable Go cannot read the following without native, per-OS system APIs.\n")
	fmt.Fprintf(w, "  They are left out on purpose rather than guessed at:\n\n")
	for _, n := range c.NotMeasured {
		fmt.Fprintf(w, "    - %s\n", n)
	}
	fmt.Fprintf(w, "\n  Every number in this report was measured while it ran.\n")
}

// wrapIndent word-wraps s to width columns, prefixing continuation lines with
// indent. The first line is returned unprefixed so the caller controls it.
func wrapIndent(s string, width int, indent string) string {
	words := strings.Fields(s)
	if len(words) == 0 {
		return ""
	}
	var b strings.Builder
	line := words[0]
	for _, wd := range words[1:] {
		if len(line)+1+len(wd) > width {
			b.WriteString(line)
			b.WriteString("\n")
			b.WriteString(indent)
			line = wd
			continue
		}
		line += " " + wd
	}
	b.WriteString(line)
	return b.String()
}

// ---------------------------------------------------------------------------
// inventory
// ---------------------------------------------------------------------------

func cmdInventory(argv []string) {
	if hasHelp(argv) {
		usage(os.Stdout)
		os.Exit(0)
	}
	argv = reorderFlags(argv, map[string]bool{})

	fset := flag.NewFlagSet("inventory", flag.ContinueOnError)
	fset.SetOutput(io.Discard)
	asJSON := fset.Bool("json", false, "emit JSON")
	if err := fset.Parse(argv); err != nil {
		fail("inventory: %v", err)
	}
	if rest := fset.Args(); len(rest) > 0 {
		fail("inventory: unexpected argument %q", rest[0])
	}

	inv := &Inventory{
		Schema:      inventorySchema,
		Tool:        appName,
		Version:     appVersion,
		Generated:   time.Now().UTC().Format(time.RFC3339),
		Machine:     readMachine(),
		Memory:      readMem(),
		NotMeasured: notMeasured(),
	}
	if *asJSON {
		blob, err := json.MarshalIndent(inv, "", "  ")
		if err != nil {
			errExit("inventory: encoding: %v", err)
		}
		os.Stdout.Write(append(blob, '\n'))
		return
	}

	const lw = 22
	fmt.Printf("%s %s - INVENTORY (static facts only, no benchmark)\n", strings.ToUpper(appName), appVersion)
	fmt.Printf("%s   %s\n", strings.Repeat("=", 58), inv.Generated)
	m := inv.Machine
	rule(os.Stdout, "WHAT THIS COMPUTER IS")
	fmt.Printf("  %-*s%s\n", lw, "name on the network", m.Hostname)
	fmt.Printf("  %-*s%s (%s)\n", lw, "operating system", m.OSLabel, m.OS)
	fmt.Printf("  %-*s%s (%s)\n", lw, "processor family", m.ArchLabel, m.Arch)
	fmt.Printf("  %-*s%d\n", lw, "logical CPUs", m.NumCPU)
	fmt.Printf("  %-*s%d of %d usable by this program\n", lw, "GOMAXPROCS", m.GOMAXPROCS, m.NumCPU)
	fmt.Printf("  %-*s%s (%s)\n", lw, "built with", m.GoVersion, m.Compiler)
	fmt.Printf("  %-*s%d\n", lw, "this process id", m.PID)
	if m.WorkingDir != "" {
		fmt.Printf("  %-*s%s\n", lw, "working directory", m.WorkingDir)
	}
	rule(os.Stdout, "WHAT THIS TOOL CANNOT SEE")
	for _, n := range inv.NotMeasured {
		fmt.Printf("    - %s\n", n)
	}
	fmt.Printf("\nRun \"%s report\" for the full report card, or \"%s bench\" for the speed check.\n",
		appName, appName)
}

// ---------------------------------------------------------------------------
// bench
// ---------------------------------------------------------------------------

func cmdBench(argv []string) {
	if hasHelp(argv) {
		usage(os.Stdout)
		os.Exit(0)
	}
	argv = reorderFlags(argv, map[string]bool{"seconds": true})

	fset := flag.NewFlagSet("bench", flag.ContinueOnError)
	fset.SetOutput(io.Discard)
	seconds := fset.Float64("seconds", 0, "run the timed variant for about N seconds")
	asJSON := fset.Bool("json", false, "emit JSON")
	if err := fset.Parse(argv); err != nil {
		fail("bench: %v", err)
	}
	if rest := fset.Args(); len(rest) > 0 {
		fail("bench: unexpected argument %q", rest[0])
	}

	timed := false
	fset.Visit(func(f *flag.Flag) {
		if f.Name == "seconds" {
			timed = true
		}
	})
	if timed && !(*seconds > 0 && *seconds <= maxBenchSeconds) {
		fail("bench: --seconds must be greater than 0 and at most %.0f (got %v)", maxBenchSeconds, *seconds)
	}

	res := &BenchResult{
		Schema:    benchSchema,
		Tool:      appName,
		Version:   appVersion,
		Generated: time.Now().UTC().Format(time.RFC3339),
		Machine:   readMachine(),
	}
	if timed {
		res.Benchmark = runBenchTimed(*seconds)
	} else {
		res.Benchmark = runBenchFixed()
	}

	if *asJSON {
		blob, err := json.MarshalIndent(res, "", "  ")
		if err != nil {
			errExit("bench: encoding: %v", err)
		}
		os.Stdout.Write(append(blob, '\n'))
		return
	}

	b := res.Benchmark
	const lw = 22
	fmt.Printf("%s %s - SPEED CHECK (%s workload)\n", strings.ToUpper(appName), appVersion, b.Mode)
	fmt.Printf("%s   %s\n", strings.Repeat("=", 58), res.Generated)
	fmt.Printf("\n  What it measures: %s.\n", b.Measures)
	fmt.Printf("  Round size: %s mix operations over a %s buffer (%d passes).\n",
		comma(b.Single.OpsPerRound), humanBytes(b.BufferBytes), b.Passes)
	if b.Mode == "fixed" {
		fmt.Printf("  Round count is a constant, so the work done is identical on every run.\n")
	} else {
		fmt.Printf("  Round SIZE is constant; the number of rounds varies to fill the time budget.\n")
	}
	fmt.Println()
	printPhase("one core ", b.Single)
	printPhase("all cores", b.Multi)
	fmt.Printf("\n  %-*s%.2fx faster than one core\n", lw, "using every core", b.Speedup)
	fmt.Printf("  %-*s%s\n", lw, "in plain English", b.Rating)
	fmt.Printf("  %-*s%s\n", lw, "rating basis", b.RatingBasis)
	fmt.Printf("\n  This is an integer-throughput check only. It says nothing about graphics,\n")
	fmt.Printf("  disk or memory bandwidth, and is not comparable to other benchmarks.\n")
}

func printPhase(label string, p Phase) {
	fmt.Printf("  %s  %8.1f million ops/sec  |  %s rounds x %s ops = %s ops in %.3f s on %d worker%s  |  checksum %s\n",
		label, p.MOpsPerSec, comma(p.Rounds), comma(p.OpsPerRound), comma(p.Ops),
		p.Seconds, p.Workers, plural(int64(p.Workers)), p.Checksum)
}
