// SystemPulse - PC Performance Console (Vertical variant).
//
// A command latency profiler: it measures how long a specific command takes
// across many runs, reports the full distribution, and compares against a
// saved baseline to detect regressions.
package main

import (
	"context"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"math"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"
)

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

	percentileMethod = "nearest-rank: rank = ceil(p/100 * n) over ascending sorted samples, 1-based, clamped to [1,n] (IEEE-754 double arithmetic)"
	stddevMethod     = "sample standard deviation (Bessel-corrected, divisor n-1); 0 when n < 2"

	exitOK        = 0
	exitError     = 1
	exitRegressed = 2
)

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

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

// Stats is the distribution summary. All durations are nanoseconds.
// min/max/median/pNN are always real observed samples (nearest-rank), so they
// are exact integers. mean/stddev are derived floats.
type Stats struct {
	Count    int     `json:"count"`
	MinNs    int64   `json:"min_ns"`
	MaxNs    int64   `json:"max_ns"`
	MeanNs   float64 `json:"mean_ns"`
	MedianNs int64   `json:"median_ns"`
	StddevNs float64 `json:"stddev_ns"`
	P50Ns    int64   `json:"p50_ns"`
	P90Ns    int64   `json:"p90_ns"`
	P95Ns    int64   `json:"p95_ns"`
	P99Ns    int64   `json:"p99_ns"`
}

// Result is one profiling run. It is also the on-disk baseline format.
type Result struct {
	Tool             string         `json:"tool"`
	ToolVersion      string         `json:"tool_version"`
	FormatVersion    int            `json:"format_version"`
	Label            string         `json:"label"`
	CreatedUTC       string         `json:"created_utc"`
	Command          []string       `json:"command"`
	Runs             int            `json:"runs"`
	Warmup           int            `json:"warmup"`
	TimeoutMs        int64          `json:"timeout_ms"`
	Failures         int            `json:"failures"`
	Timeouts         int            `json:"timeouts"`
	ExitCodes        map[string]int `json:"exit_codes"`
	SamplesNs        []int64        `json:"samples_ns"`
	Stats            Stats          `json:"stats"`
	PercentileMethod string         `json:"percentile_method"`
	StddevMethod     string         `json:"stddev_method"`
}

// ---------------------------------------------------------------------------
// Statistics
// ---------------------------------------------------------------------------

// percentileRank is the 1-based nearest-rank index for percentile p over n
// samples, clamped to [1,n].
func percentileRank(n int, p float64) int {
	rank := int(math.Ceil(p / 100 * float64(n)))
	if rank < 1 {
		rank = 1
	}
	if rank > n {
		rank = n
	}
	return rank
}

// percentile implements nearest-rank on an ascending sorted slice.
func percentile(sorted []int64, p float64) int64 {
	n := len(sorted)
	if n == 0 {
		return 0
	}
	return sorted[percentileRank(n, p)-1]
}

// degenerateTail reports whether the nearest-rank index for p lands on the
// last element, meaning the reported percentile is literally the maximum and
// is therefore decided by a single outlier sample.
func degenerateTail(n int, p float64) bool {
	return n > 0 && percentileRank(n, p) == n
}

// computeStats summarises samples. The mean and the sum of squared deviations
// are accumulated in the samples' original (execution) order so that an
// independent reimplementation walking samples_ns front to back reproduces the
// identical IEEE-754 double.
func computeStats(samples []int64) Stats {
	n := len(samples)
	if n == 0 {
		return Stats{}
	}
	var sum float64
	for _, v := range samples {
		sum += float64(v)
	}
	mean := sum / float64(n)

	var ss float64
	for _, v := range samples {
		d := float64(v) - mean
		ss += d * d
	}
	sd := 0.0
	if n > 1 {
		sd = math.Sqrt(ss / float64(n-1))
	}

	sorted := make([]int64, n)
	copy(sorted, samples)
	sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })

	return Stats{
		Count:    n,
		MinNs:    sorted[0],
		MaxNs:    sorted[n-1],
		MeanNs:   mean,
		MedianNs: percentile(sorted, 50),
		StddevNs: sd,
		P50Ns:    percentile(sorted, 50),
		P90Ns:    percentile(sorted, 90),
		P95Ns:    percentile(sorted, 95),
		P99Ns:    percentile(sorted, 99),
	}
}

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

// pooledStddev is the classic pooled sample standard deviation of two groups.
func pooledStddev(s1 float64, n1 int, s2 float64, n2 int) float64 {
	if n1+n2-2 <= 0 {
		return 0
	}
	num := float64(n1-1)*s1*s1 + float64(n2-1)*s2*s2
	return math.Sqrt(num / float64(n1+n2-2))
}

// ---------------------------------------------------------------------------
// Formatting
// ---------------------------------------------------------------------------

func ms(ns float64) string { return fmt.Sprintf("%.3f ms", ns/1e6) }
func msi(ns int64) string  { return ms(float64(ns)) }
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 quoteCommand(argv []string) string {
	parts := make([]string, 0, len(argv))
	for _, a := range argv {
		if a == "" || strings.ContainsAny(a, " \t\n\"'\\") {
			parts = append(parts, strconv.Quote(a))
			continue
		}
		parts = append(parts, a)
	}
	return strings.Join(parts, " ")
}

func exitCodeSummary(m map[string]int) string {
	if len(m) == 0 {
		return "none"
	}
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	sort.Slice(keys, func(i, j int) bool {
		a, _ := strconv.Atoi(keys[i])
		b, _ := strconv.Atoi(keys[j])
		return a < b
	})
	parts := make([]string, 0, len(keys))
	for _, k := range keys {
		parts = append(parts, fmt.Sprintf("exit %s x%d", k, m[k]))
	}
	return strings.Join(parts, ", ")
}

// ---------------------------------------------------------------------------
// Execution
// ---------------------------------------------------------------------------

type runOutcome struct {
	dur      time.Duration
	exitCode int
	timedOut bool
}

// runOnce executes argv once with stdio detached, returning the wall-clock
// duration of the whole spawn+wait cycle. A non-nil error means the process
// could not be started at all (e.g. binary not found).
func runOnce(argv []string, timeout time.Duration) (runOutcome, error) {
	ctx := context.Background()
	if timeout > 0 {
		c, cancel := context.WithTimeout(ctx, timeout)
		defer cancel()
		ctx = c
	}
	cmd := exec.CommandContext(ctx, argv[0], argv[1:]...)
	cmd.Stdin = nil
	cmd.Stdout = nil
	cmd.Stderr = nil

	start := time.Now()
	err := cmd.Run()
	d := time.Since(start)

	if ctx.Err() == context.DeadlineExceeded {
		return runOutcome{dur: d, exitCode: -1, timedOut: true}, nil
	}
	if err != nil {
		var ee *exec.ExitError
		if errors.As(err, &ee) {
			return runOutcome{dur: d, exitCode: ee.ExitCode()}, nil
		}
		return runOutcome{}, err
	}
	return runOutcome{dur: d, exitCode: 0}, nil
}

// profile performs warmup runs (discarded) followed by runs measured runs.
func profile(argv []string, runs, warmup int, timeout time.Duration, label string) (*Result, error) {
	for i := 0; i < warmup; i++ {
		if _, err := runOnce(argv, timeout); err != nil {
			return nil, fmt.Errorf("cannot execute %q: %v", argv[0], err)
		}
	}

	res := &Result{
		Tool:             toolName,
		ToolVersion:      toolVersion,
		FormatVersion:    formatVersion,
		Label:            label,
		CreatedUTC:       time.Now().UTC().Format(time.RFC3339Nano),
		Command:          argv,
		Runs:             runs,
		Warmup:           warmup,
		TimeoutMs:        timeout.Milliseconds(),
		ExitCodes:        map[string]int{},
		SamplesNs:        []int64{},
		PercentileMethod: percentileMethod,
		StddevMethod:     stddevMethod,
	}

	for i := 0; i < runs; i++ {
		out, err := runOnce(argv, timeout)
		if err != nil {
			return nil, fmt.Errorf("cannot execute %q: %v", argv[0], err)
		}
		if out.timedOut {
			res.Timeouts++
			res.Failures++
			res.ExitCodes["timeout"]++
			// Truncated durations are not real measurements: excluded.
			continue
		}
		res.ExitCodes[strconv.Itoa(out.exitCode)]++
		if out.exitCode != 0 {
			res.Failures++
		}
		res.SamplesNs = append(res.SamplesNs, out.dur.Nanoseconds())
	}

	if len(res.SamplesNs) == 0 {
		return nil, fmt.Errorf("no timing samples collected (%d of %d runs timed out)", res.Timeouts, runs)
	}
	res.Stats = computeStats(res.SamplesNs)
	return res, nil
}

// ---------------------------------------------------------------------------
// Baseline persistence
// ---------------------------------------------------------------------------

func saveBaseline(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 loadBaseline(path string) (*Result, error) {
	b, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, fmt.Errorf("baseline file not found: %s", path)
		}
		return nil, fmt.Errorf("cannot read baseline %s: %v", path, err)
	}
	var r Result
	dec := json.NewDecoder(strings.NewReader(string(b)))
	if err := dec.Decode(&r); err != nil {
		return nil, fmt.Errorf("corrupt baseline %s: invalid JSON: %v", path, err)
	}
	if r.Tool != toolName {
		return nil, fmt.Errorf("corrupt baseline %s: not a %s baseline (tool=%q)", path, toolName, r.Tool)
	}
	if r.FormatVersion != formatVersion {
		return nil, fmt.Errorf("unsupported baseline %s: format_version %d, this build understands %d", path, r.FormatVersion, formatVersion)
	}
	if len(r.SamplesNs) == 0 {
		return nil, fmt.Errorf("corrupt baseline %s: no samples recorded", path)
	}
	if r.Stats.Count != len(r.SamplesNs) {
		return nil, fmt.Errorf("corrupt baseline %s: stats.count=%d but %d samples present", path, r.Stats.Count, len(r.SamplesNs))
	}
	return &r, nil
}

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

func printResult(w io.Writer, r *Result) {
	fmt.Fprintf(w, "SystemPulse - command latency profile\n")
	fmt.Fprintf(w, "  command  : %s\n", quoteCommand(r.Command))
	label := r.Label
	if label == "" {
		label = "(none)"
	}
	fmt.Fprintf(w, "  label    : %s\n", label)
	fmt.Fprintf(w, "  recorded : %s\n", r.CreatedUTC)
	fmt.Fprintf(w, "  runs     : %d measured, %d warmup discarded\n", r.Runs, r.Warmup)
	fmt.Fprintf(w, "  samples  : %d   failures: %d   timeouts: %d   [%s]\n",
		r.Stats.Count, r.Failures, r.Timeouts, exitCodeSummary(r.ExitCodes))
	fmt.Fprintf(w, "\n")
	fmt.Fprintf(w, "  %-8s %14s\n", "min", msi(r.Stats.MinNs))
	fmt.Fprintf(w, "  %-8s %14s   (median)\n", "p50", msi(r.Stats.P50Ns))
	fmt.Fprintf(w, "  %-8s %14s\n", "p90", msi(r.Stats.P90Ns))
	fmt.Fprintf(w, "  %-8s %14s\n", "p95", msi(r.Stats.P95Ns))
	fmt.Fprintf(w, "  %-8s %14s\n", "p99", msi(r.Stats.P99Ns))
	fmt.Fprintf(w, "  %-8s %14s\n", "max", msi(r.Stats.MaxNs))
	fmt.Fprintf(w, "  %-8s %14s\n", "mean", ms(r.Stats.MeanNs))
	fmt.Fprintf(w, "  %-8s %14s\n", "stddev", ms(r.Stats.StddevNs))
	fmt.Fprintf(w, "\n")
	if degenerateTail(r.Stats.Count, 95) {
		fmt.Fprintf(w, "  NOTE: with n=%d the nearest-rank p95 (and p99) land on the maximum, so the\n", r.Stats.Count)
		fmt.Fprintf(w, "        tail figures reflect a single outlier run. Use --runs 20 or more.\n\n")
	}
	fmt.Fprintf(w, "  percentiles: %s\n", percentileMethod)
	fmt.Fprintf(w, "  stddev     : %s\n", stddevMethod)
}

// Comparison is the JSON shape emitted by `compare`.
type Comparison struct {
	Tool          string  `json:"tool"`
	ThresholdPct  float64 `json:"threshold_pct"`
	MedianPct     float64 `json:"median_change_pct"`
	P95Pct        float64 `json:"p95_change_pct"`
	MedianDeltaNs int64   `json:"median_delta_ns"`
	P95DeltaNs    int64   `json:"p95_delta_ns"`
	WorstPct      float64 `json:"worst_change_pct"`
	PooledSDNs    float64 `json:"pooled_stddev_ns"`
	EffectSize    float64 `json:"effect_size"`
	Significance  string  `json:"significance"`
	VerdictMetric string  `json:"verdict_metric"`
	Regression    bool    `json:"regression"`
	Improvement   bool    `json:"improvement"`
	Status        string  `json:"status"`
	ExitCode      int     `json:"exit_code"`
	SmallSample   string  `json:"small_sample_warning,omitempty"`
	Baseline      *Result `json:"baseline"`
	Current       *Result `json:"current"`
}

func classify(effect float64) string {
	a := math.Abs(effect)
	switch {
	case math.IsNaN(a) || math.IsInf(a, 0):
		return "undetermined (zero spread)"
	case a < 1:
		return "within run-to-run noise (|effect| < 1 pooled stddev)"
	case a < 3:
		return "moderate signal (1 <= |effect| < 3 pooled stddev)"
	default:
		return "clear signal (|effect| >= 3 pooled stddev)"
	}
}

func compareResults(base, cur *Result, threshold float64) *Comparison {
	medPct := pctChange(float64(base.Stats.MedianNs), float64(cur.Stats.MedianNs))
	p95Pct := pctChange(float64(base.Stats.P95Ns), float64(cur.Stats.P95Ns))
	worst := math.Max(medPct, p95Pct)

	psd := pooledStddev(base.Stats.StddevNs, base.Stats.Count, cur.Stats.StddevNs, cur.Stats.Count)
	effect := math.NaN()
	if psd > 0 {
		effect = (float64(cur.Stats.MedianNs) - float64(base.Stats.MedianNs)) / psd
	}

	c := &Comparison{
		Tool:          toolName,
		ThresholdPct:  threshold,
		MedianPct:     medPct,
		P95Pct:        p95Pct,
		MedianDeltaNs: cur.Stats.MedianNs - base.Stats.MedianNs,
		P95DeltaNs:    cur.Stats.P95Ns - base.Stats.P95Ns,
		WorstPct:      worst,
		PooledSDNs:    psd,
		EffectSize:    effect,
		Significance:  classify(effect),
		Baseline:      base,
		Current:       cur,
	}
	c.VerdictMetric = "median"
	if p95Pct > medPct {
		c.VerdictMetric = "p95"
	}
	if degenerateTail(base.Stats.Count, 95) || degenerateTail(cur.Stats.Count, 95) {
		c.SmallSample = fmt.Sprintf(
			"p95 collapses onto the maximum at these sample counts (baseline n=%d, current n=%d), so it is decided by one outlier run; use --runs 20 or more for a meaningful p95",
			base.Stats.Count, cur.Stats.Count)
	}
	c.Regression = worst > threshold
	c.Improvement = medPct < 0
	if c.Regression {
		c.Status = "FAIL"
		c.ExitCode = exitRegressed
	} else {
		c.Status = "PASS"
		c.ExitCode = exitOK
	}
	return c
}

func printComparison(w io.Writer, c *Comparison) {
	base, cur := c.Baseline, c.Current
	fmt.Fprintf(w, "SystemPulse - regression comparison\n")
	fmt.Fprintf(w, "  command  : %s\n", quoteCommand(cur.Command))
	label := base.Label
	if label == "" {
		label = "(none)"
	}
	fmt.Fprintf(w, "  baseline : label=%s  recorded=%s  n=%d  cmd=%s\n",
		label, base.CreatedUTC, base.Stats.Count, quoteCommand(base.Command))
	fmt.Fprintf(w, "  current  : n=%d  failures=%d  timeouts=%d\n", cur.Stats.Count, cur.Failures, cur.Timeouts)
	if quoteCommand(base.Command) != quoteCommand(cur.Command) {
		fmt.Fprintf(w, "  note     : baseline command differs from the command measured now\n")
	}
	fmt.Fprintf(w, "\n")
	fmt.Fprintf(w, "  %-8s %14s %14s %12s\n", "metric", "baseline", "current", "change")
	fmt.Fprintf(w, "  %-8s %14s %14s %12s\n", "median", msi(base.Stats.MedianNs), msi(cur.Stats.MedianNs), signed(c.MedianPct))
	fmt.Fprintf(w, "  %-8s %14s %14s %12s\n", "p95", msi(base.Stats.P95Ns), msi(cur.Stats.P95Ns), signed(c.P95Pct))
	fmt.Fprintf(w, "  %-8s %14s %14s %12s\n", "mean", ms(base.Stats.MeanNs), ms(cur.Stats.MeanNs), signed(pctChange(base.Stats.MeanNs, cur.Stats.MeanNs)))
	fmt.Fprintf(w, "\n")
	fmt.Fprintf(w, "  spread check: pooled stddev %s, median shift %s\n", ms(c.PooledSDNs), ms(float64(c.MedianDeltaNs)))
	if math.IsNaN(c.EffectSize) {
		fmt.Fprintf(w, "  effect size : n/a  -> %s\n", c.Significance)
	} else {
		fmt.Fprintf(w, "  effect size : %.2f pooled stddev  -> %s\n", c.EffectSize, c.Significance)
	}
	fmt.Fprintf(w, "  threshold   : %.2f%% (applied to the worse of median and p95 change)\n", c.ThresholdPct)
	if c.SmallSample != "" {
		fmt.Fprintf(w, "  WARNING     : %s\n", c.SmallSample)
	}
	fmt.Fprintf(w, "\n")
	if c.Regression {
		fmt.Fprintf(w, "  RESULT: FAIL - %s regressed %s, exceeding the %.2f%% threshold\n",
			c.VerdictMetric, signed(c.WorstPct), c.ThresholdPct)
		return
	}
	if c.MedianPct < 0 {
		fmt.Fprintf(w, "  RESULT: PASS - median improved by %s\n", signed(c.MedianPct))
		return
	}
	fmt.Fprintf(w, "  RESULT: PASS - worst change %s is within the %.2f%% threshold\n", signed(c.WorstPct), 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 - command latency profiler (Techlosoft PC Performance Console)

USAGE
  systempulse bench   --runs N [--warmup N] [--label NAME] [--save FILE]
                      [--timeout DUR] [--json] -- <command> [args...]
  systempulse compare --baseline FILE --runs N [--threshold-pct P]
                      [--warmup N] [--timeout DUR] [--json] -- <command> [args...]
  systempulse show    --baseline FILE [--json]
  systempulse help | -h | --help

COMMANDS
  bench     Time <command> over N runs and report the full distribution.
  compare   Re-measure <command> now and diff it against a saved baseline.
            Exits %d when the regression exceeds --threshold-pct.
  show      Print a previously saved baseline.

FLAGS
  --runs N            Measured runs (default 30, must be >= 1).
  --warmup N          Discarded runs executed first (default 3).
  --label NAME        Free-text label stored in the baseline.
  --save FILE         Write the bench result as a baseline JSON file.
  --baseline FILE     Baseline JSON to read.
  --threshold-pct P   Regression threshold in percent (default 10).
  --timeout DUR       Per-run wall-clock limit, e.g. 2s, 500ms.
                      Default 0 = no limit (a hanging command hangs the tool).
  --json              Machine-readable output, including raw per-run samples.

EVERYTHING AFTER -- IS THE COMMAND UNDER TEST.

EXIT CODES
  %d  success / no regression
  %d  usage or runtime error
  %d  regression detected above threshold

METHOD
  percentiles: %s
  stddev     : %s
`, toolName, toolVersion, exitRegressed, exitOK, exitError, exitRegressed, percentileMethod, stddevMethod)
}

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

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

// splitAtDoubleDash separates the flag section from the command under test.
func splitAtDoubleDash(args []string) (head, tail []string, found bool) {
	for i, a := range args {
		if a == "--" {
			return args[:i], args[i+1:], true
		}
	}
	return args, nil, false
}

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{
	"runs":          true,
	"warmup":        true,
	"label":         true,
	"save":          true,
	"baseline":      true,
	"threshold-pct": true,
	"timeout":       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)
	}

	head, tail, hasDD := splitAtDoubleDash(args)
	for _, a := range head {
		if isHelp(a) {
			usage(os.Stdout)
			os.Exit(exitOK)
		}
	}
	if len(head) == 0 {
		failUsage("missing subcommand")
	}

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

	switch sub {
	case "bench":
		cmdBench(rest, tail, hasDD)
	case "compare":
		cmdCompare(rest, tail, hasDD)
	case "show":
		cmdShow(rest)
	default:
		failUsage("unknown subcommand %q", sub)
	}
}

// resolveCommand picks the command under test from either the post-`--` tail
// or the leftover positional arguments.
func resolveCommand(fs *flag.FlagSet, tail []string, hasDD bool) []string {
	if hasDD {
		return tail
	}
	return fs.Args()
}

func cmdBench(rest, tail []string, hasDD bool) {
	fs := newFlagSet("bench")
	runs := fs.Int("runs", 30, "")
	warmup := fs.Int("warmup", 3, "")
	label := fs.String("label", "", "")
	save := fs.String("save", "", "")
	timeout := fs.Duration("timeout", 0, "")
	asJSON := fs.Bool("json", false, "")
	if err := fs.Parse(rest); err != nil {
		failUsage("bench: %v", err)
	}

	argv := resolveCommand(fs, tail, hasDD)
	if len(argv) == 0 {
		failUsage("bench: no command given (put the command after --)")
	}
	if *runs < 1 {
		failUsage("bench: --runs must be >= 1 (got %d)", *runs)
	}
	if *warmup < 0 {
		failUsage("bench: --warmup must be >= 0 (got %d)", *warmup)
	}
	if *timeout < 0 {
		failUsage("bench: --timeout must be >= 0 (got %s)", *timeout)
	}

	res, err := profile(argv, *runs, *warmup, *timeout, *label)
	if err != nil {
		fail("%v", err)
	}

	if *save != "" {
		if err := saveBaseline(*save, res); err != nil {
			fail("cannot save baseline: %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  baseline saved to %s\n", *save)
	}
}

func cmdCompare(rest, tail []string, hasDD bool) {
	fs := newFlagSet("compare")
	baseline := fs.String("baseline", "", "")
	runs := fs.Int("runs", 30, "")
	warmup := fs.Int("warmup", 3, "")
	threshold := fs.Float64("threshold-pct", 10, "")
	timeout := fs.Duration("timeout", 0, "")
	asJSON := fs.Bool("json", false, "")
	if err := fs.Parse(rest); err != nil {
		failUsage("compare: %v", err)
	}

	if *baseline == "" {
		failUsage("compare: --baseline is required")
	}
	if *runs < 1 {
		failUsage("compare: --runs must be >= 1 (got %d)", *runs)
	}
	if *warmup < 0 {
		failUsage("compare: --warmup must be >= 0 (got %d)", *warmup)
	}

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

	argv := resolveCommand(fs, tail, hasDD)
	if len(argv) == 0 {
		argv = base.Command
	}
	if len(argv) == 0 {
		failUsage("compare: no command given and the baseline records none")
	}

	cur, err := profile(argv, *runs, *warmup, *timeout, base.Label)
	if err != nil {
		fail("%v", err)
	}

	c := compareResults(base, cur, *threshold)
	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")
	baseline := fs.String("baseline", "", "")
	asJSON := fs.Bool("json", false, "")
	if err := fs.Parse(rest); err != nil {
		failUsage("show: %v", err)
	}
	path := *baseline
	if path == "" && fs.NArg() > 0 {
		path = fs.Arg(0)
	}
	if path == "" {
		failUsage("show: --baseline is required")
	}

	r, err := loadBaseline(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()))
	}
}
