// ThermalFlow is a small, honest "fleet performance trend" prototype.
//
// It is the Team/SMB-IT sibling of CorePilot: where CorePilot runs ONE CPU
// benchmark and prints a one-shot report with nothing remembered between
// runs, ThermalFlow's whole job is trend history. It runs the same
// single-thread vs. multi-thread CPU benchmark CorePilot uses, but appends
// every run's result to a persistent local JSON-lines history file and then
// answers "how has this machine's performance changed over time" - flagging
// regressions and improvements against historical baselines. That is the
// genuinely different, IT-fleet-monitoring mechanism this tier is meant to
// sell: repeated measurement plus trend detection, not a fancier one-shot
// benchmark. See README.txt and ../plan.md for the full product plan and
// roadmap (real temperatures, fan curves, CPU affinity, and power profiles
// all need OS-privileged, vendor-specific hardware APIs a portable,
// dependency-free Go CLI cannot reach).
package main

import (
	"bufio"
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"runtime"
	"strings"
	"sync"
	"time"
)

// regressionThresholdPct is how far ops/sec has to move, in either
// direction, before ThermalFlow calls it a REGRESSION or IMPROVEMENT rather
// than STABLE noise. Real benchmark runs on the same machine naturally jitter
// a few percent run to run (other processes, thermal state, scheduler
// noise), so 10% is chosen as a threshold comfortably above typical run-to-
// run jitter while still catching real-world regressions (a throttling CPU,
// a background process eating cores, a bad driver update) promptly.
const regressionThresholdPct = 10.0

// reorderFlags moves all flag tokens (and their values, for flags listed in
// valueFlags) before any positional arguments, working around the stdlib
// flag package's behavior of stopping parsing at the first positional arg.
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 usage() {
	fmt.Fprint(os.Stderr, `ThermalFlow - persistent CPU benchmark trend history + regression detection

Usage:
  thermalflow run   --history FILE [--duration 500ms] [--json]
  thermalflow trend --history FILE [--last N] [--json]
  thermalflow watch --history FILE --interval 1h [--duration 500ms] [--once] [--json]

Commands:
  run      Run one CPU benchmark round, append the result to the history
           file (creating it if needed), and compare it against the
           immediately PREVIOUS run in that history (if any).
  trend    Read-only. Print a table of past runs plus min/max/average
           ops/sec across the shown window, and compare the most recent
           run against the AVERAGE of all prior runs in the full history.
  watch    Convenience wrapper: calls the same logic as "run" repeatedly,
           sleeping --interval between rounds. For users who want
           ThermalFlow to schedule itself instead of being invoked
           externally by cron / Task Scheduler.

Flags:
  --history FILE   Path to the JSON-lines history file (required for all
                   three commands). One JSON object per line, append-only.
  --duration dur   How long to run each benchmark phase (single-thread,
                   then multi-thread) for "run" and "watch". Go duration
                   syntax, e.g. 500ms, 2s, 1m. (default 500ms)
  --interval dur   How long "watch" sleeps between rounds. (default 1h)
  --once           "watch" only: run a single round then exit, instead of
                   looping forever. Equivalent to "run", kept for
                   consistency/testing with the other polling-style tools
                   in this suite.
  --last N         "trend" only: show only the N most recent runs. 0 (the
                   default) or a value >= the number of runs shows all of
                   them. Min/max/average stats are computed over the shown
                   window; the most-recent-vs-average comparison always
                   uses the FULL history, not just the shown window.
  --json           Emit structured JSON instead of a text report.
  -h, --help       Show this help.

Regression threshold: a run's ops/sec more than 10%% slower than the
comparison baseline is flagged REGRESSION, more than 10%% faster is
IMPROVEMENT, otherwise STABLE. 10%% is comfortably above typical run-to-run
benchmark jitter on the same machine while still catching real regressions.

Notes:
  - The CPU benchmark methodology (trial-division primality workload, single
    goroutine vs. NumCPU goroutines, same interleaved stride pattern in both
    phases so per-operation cost is comparable) matches the sibling CorePilot
    tool's tested implementation.
  - Real temperatures, fan curves, CPU affinity pinning, and per-app power
    profiles are on the roadmap; see ../plan.md. They need OS-privileged,
    vendor-specific hardware APIs not available to a portable, dependency-
    free Go CLI.
`)
}

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.Exit(1)
	}

	switch args[0] {
	case "-h", "--help", "help":
		usage()
		os.Exit(0)
	case "run":
		cmdRun(args[1:])
	case "trend":
		cmdTrend(args[1:])
	case "watch":
		cmdWatch(args[1:])
	default:
		fmt.Fprintf(os.Stderr, "thermalflow: unknown command %q\n\n", args[0])
		usage()
		os.Exit(1)
	}
}

// ---------------------------------------------------------------------
// CPU benchmark (same methodology as the sibling CorePilot tool)
// ---------------------------------------------------------------------

// isPrime reports whether n is prime using simple trial division. This is
// the CPU-bound workload used by the benchmark: cheap enough per-check to
// run millions of times per second, expensive enough (once n grows) to
// exercise real integer arithmetic rather than being optimized away.
func isPrime(n uint64) bool {
	if n < 2 {
		return false
	}
	if n%2 == 0 {
		return n == 2
	}
	if n%3 == 0 {
		return n == 3
	}
	for i := uint64(5); i*i <= n; i += 6 {
		if n%i == 0 || n%(i+2) == 0 {
			return false
		}
	}
	return true
}

// runWorkload runs trial-division primality checks starting at "start" and
// advancing by "stride" each step, for the given duration, on the calling
// goroutine. It returns the total number of primality checks performed.
//
// A goroutine that calls this with start=base+idx and stride=numWorkers
// checks a distinct interleaved slice of the same number range as every
// other worker (0,1,2,3,... split round-robin) - no shared counters, no
// locks, no contention, and critically no worker racing ahead into much
// larger (and therefore much more expensive to trial-divide) numbers than
// the others. That keeps the per-check cost comparable between the
// single-thread and multi-thread phases so their ops/sec numbers are an
// apples-to-apples comparison.
func runWorkload(start, stride uint64, duration time.Duration) uint64 {
	n := start
	var ops uint64
	deadline := time.Now().Add(duration)
	const checkMask = uint64(2047) // check the clock every 2048 iterations
	for {
		isPrime(n)
		n += stride
		ops++
		if ops&checkMask == 0 && time.Now().After(deadline) {
			break
		}
	}
	return ops
}

// historyEntry is one line of the history JSON-lines file: the result of a
// single benchmark round.
type historyEntry struct {
	TimestampUTC          string  `json:"timestamp_utc"`
	NumCPU                int     `json:"num_cpu"`
	SingleThreadOpsPerSec float64 `json:"single_thread_ops_per_sec"`
	MultiThreadOpsPerSec  float64 `json:"multi_thread_ops_per_sec"`
	ScalingFactor         float64 `json:"scaling_factor"`
}

// runBenchmarkOnce runs the two-phase benchmark (single-thread, then
// NumCPU-parallel multi-thread) for "duration" per phase and returns the
// resulting history entry. Both phases walk the same odd-number stride
// pattern over the same base range - only the parallelism differs - so the
// per-operation cost is comparable and the resulting ops/sec numbers are an
// apples-to-apples comparison, exactly as CorePilot's benchmark does.
func runBenchmarkOnce(duration time.Duration, quiet bool) historyEntry {
	numCPU := runtime.NumCPU()

	if !quiet {
		fmt.Println("Running single-thread phase...")
	}

	// Both phases check the same starting number range, restricted to odd
	// numbers only, so the per-check cost (trial division up to sqrt(n)) is
	// comparable between them - only the parallelism differs.
	const workloadBase = uint64(1_000_003) // odd

	singleOps := runWorkload(workloadBase, 2, duration)
	singleOpsPerSec := float64(singleOps) / duration.Seconds()

	if !quiet {
		fmt.Printf("Running multi-thread phase (%d workers)...\n", numCPU)
	}

	// Multi-thread phase: numCPU goroutines round-robin the SAME odd number
	// range the single-thread phase checks (worker i checks
	// workloadBase+2i, workloadBase+2i+2*numCPU, ...), so each worker's own
	// state is entirely independent - no shared counters, no locks, no
	// contention - while covering equivalent-cost numbers to the
	// single-thread phase.
	results := make([]uint64, numCPU)
	var wg sync.WaitGroup
	for i := 0; i < numCPU; i++ {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			start := workloadBase + uint64(idx)*2
			results[idx] = runWorkload(start, uint64(numCPU)*2, duration)
		}(i)
	}
	wg.Wait()

	var multiOps uint64
	for _, r := range results {
		multiOps += r
	}
	multiOpsPerSec := float64(multiOps) / duration.Seconds()

	scaling := 0.0
	if singleOpsPerSec > 0 {
		scaling = multiOpsPerSec / singleOpsPerSec
	}

	return historyEntry{
		TimestampUTC:          time.Now().UTC().Format(time.RFC3339),
		NumCPU:                numCPU,
		SingleThreadOpsPerSec: singleOpsPerSec,
		MultiThreadOpsPerSec:  multiOpsPerSec,
		ScalingFactor:         scaling,
	}
}

// ---------------------------------------------------------------------
// History file I/O
// ---------------------------------------------------------------------

// readHistory reads all entries from a JSON-lines history file. A missing
// file is not an error - it returns a nil slice, which callers treat as "no
// history yet".
func readHistory(path string) ([]historyEntry, error) {
	f, err := os.Open(path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, err
	}
	defer f.Close()

	var entries []historyEntry
	scanner := bufio.NewScanner(f)
	scanner.Buffer(make([]byte, 64*1024), 1024*1024)
	lineNum := 0
	for scanner.Scan() {
		lineNum++
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}
		var e historyEntry
		if err := json.Unmarshal([]byte(line), &e); err != nil {
			return nil, fmt.Errorf("history file %s line %d: invalid JSON: %w", path, lineNum, err)
		}
		entries = append(entries, e)
	}
	if err := scanner.Err(); err != nil {
		return nil, err
	}
	return entries, nil
}

// appendHistory appends a single entry as one JSON line to the history
// file, creating the file (and any necessary content) if it doesn't exist.
func appendHistory(path string, e historyEntry) error {
	f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	defer f.Close()

	b, err := json.Marshal(e)
	if err != nil {
		return err
	}
	b = append(b, '\n')
	_, err = f.Write(b)
	return err
}

// ---------------------------------------------------------------------
// Comparison / regression detection
// ---------------------------------------------------------------------

// pctChange returns the percent change from oldVal to newVal. Positive
// means newVal is larger (faster, for ops/sec); negative means smaller
// (slower).
func pctChange(oldVal, newVal float64) float64 {
	if oldVal == 0 {
		return 0
	}
	return (newVal - oldVal) / oldVal * 100
}

// classify turns a percent change into REGRESSION / IMPROVEMENT / STABLE
// using regressionThresholdPct. pct is expected to be "new vs. baseline":
// negative means slower than baseline, positive means faster.
func classify(pct float64) string {
	switch {
	case pct <= -regressionThresholdPct:
		return "REGRESSION"
	case pct >= regressionThresholdPct:
		return "IMPROVEMENT"
	default:
		return "STABLE"
	}
}

// combineStatus rolls up per-metric statuses into one overall status,
// treating a regression on EITHER metric as the overall status (bad news
// takes priority for fleet-monitoring purposes), then an improvement on
// either metric (with no regression), otherwise stable.
func combineStatus(a, b string) string {
	if a == "REGRESSION" || b == "REGRESSION" {
		return "REGRESSION"
	}
	if a == "IMPROVEMENT" || b == "IMPROVEMENT" {
		return "IMPROVEMENT"
	}
	return "STABLE"
}

// comparison is a generic "new run vs. some baseline" result, used both for
// "run"'s vs-previous-run comparison and "trend"'s vs-historical-average
// comparison.
type comparison struct {
	HasBaseline           bool    `json:"has_baseline"`
	BaselineDescription   string  `json:"baseline_description,omitempty"`
	BaselineRunCount      int     `json:"baseline_run_count"`
	SingleThreadPctChange float64 `json:"single_thread_pct_change"`
	MultiThreadPctChange  float64 `json:"multi_thread_pct_change"`
	SingleThreadStatus    string  `json:"single_thread_status"`
	MultiThreadStatus     string  `json:"multi_thread_status"`
	OverallStatus         string  `json:"overall_status"`
}

func compareAgainst(desc string, baselineRunCount int, baselineSingle, baselineMulti float64, current historyEntry) comparison {
	singlePct := pctChange(baselineSingle, current.SingleThreadOpsPerSec)
	multiPct := pctChange(baselineMulti, current.MultiThreadOpsPerSec)
	singleStatus := classify(singlePct)
	multiStatus := classify(multiPct)
	return comparison{
		HasBaseline:           true,
		BaselineDescription:   desc,
		BaselineRunCount:      baselineRunCount,
		SingleThreadPctChange: singlePct,
		MultiThreadPctChange:  multiPct,
		SingleThreadStatus:    singleStatus,
		MultiThreadStatus:     multiStatus,
		OverallStatus:         combineStatus(singleStatus, multiStatus),
	}
}

func printComparison(label string, c comparison) {
	if !c.HasBaseline {
		fmt.Printf("%s: no baseline available.\n", label)
		return
	}
	fmt.Printf("%s (%s):\n", label, c.BaselineDescription)
	fmt.Printf("  Single-thread change: %+.1f%%  [%s]\n", c.SingleThreadPctChange, c.SingleThreadStatus)
	fmt.Printf("  Multi-thread change:  %+.1f%%  [%s]\n", c.MultiThreadPctChange, c.MultiThreadStatus)
	fmt.Printf("  Overall:              %s\n", c.OverallStatus)
}

// ---------------------------------------------------------------------
// "run" command
// ---------------------------------------------------------------------

type runOutput struct {
	HistoryFile        string       `json:"history_file"`
	TotalRunsInHistory int          `json:"total_runs_in_history"`
	Entry              historyEntry `json:"entry"`
	VsPreviousRun      comparison   `json:"vs_previous_run"`
}

func cmdRun(args []string) {
	for _, a := range args {
		if a == "-h" || a == "--help" || a == "help" {
			usage()
			os.Exit(0)
		}
	}

	valueFlags := map[string]bool{"history": true, "duration": true}
	args = reorderFlags(args, valueFlags)

	fs := flag.NewFlagSet("run", flag.ExitOnError)
	fs.Usage = usage
	historyPath := fs.String("history", "", "path to the JSON-lines history file (required)")
	duration := fs.Duration("duration", 500*time.Millisecond, "duration to run each benchmark phase for")
	jsonOut := fs.Bool("json", false, "emit structured JSON output")
	if err := fs.Parse(args); err != nil {
		os.Exit(1)
	}

	if *historyPath == "" {
		fmt.Fprintln(os.Stderr, "thermalflow: --history is required")
		usage()
		os.Exit(1)
	}
	if *duration <= 0 {
		fmt.Fprintln(os.Stderr, "thermalflow: --duration must be positive")
		os.Exit(1)
	}

	out, err := doRunRound(*historyPath, *duration, *jsonOut)
	if err != nil {
		fmt.Fprintf(os.Stderr, "thermalflow: %v\n", err)
		os.Exit(1)
	}

	if *jsonOut {
		enc := json.NewEncoder(os.Stdout)
		enc.SetIndent("", "  ")
		if err := enc.Encode(out); err != nil {
			fmt.Fprintf(os.Stderr, "thermalflow: failed to encode JSON: %v\n", err)
			os.Exit(1)
		}
	}
}

// doRunRound performs one full "run" round: read existing history, run the
// benchmark, append the new entry, compute the vs-previous-run comparison,
// and (unless jsonOut, in which case the caller prints the JSON) print a
// text report. It returns the structured result either way so callers (run
// and watch) can also emit JSON.
func doRunRound(historyPath string, duration time.Duration, jsonOut bool) (runOutput, error) {
	prior, err := readHistory(historyPath)
	if err != nil {
		return runOutput{}, fmt.Errorf("reading history: %w", err)
	}

	numCPU := runtime.NumCPU()
	if !jsonOut {
		fmt.Printf("ThermalFlow run - logical cores: %d, phase duration: %s\n\n", numCPU, duration.String())
	}

	entry := runBenchmarkOnce(duration, jsonOut)

	if err := appendHistory(historyPath, entry); err != nil {
		return runOutput{}, fmt.Errorf("appending to history: %w", err)
	}

	var vsPrev comparison
	if len(prior) > 0 {
		prev := prior[len(prior)-1]
		vsPrev = compareAgainst("previous run at "+prev.TimestampUTC, 1, prev.SingleThreadOpsPerSec, prev.MultiThreadOpsPerSec, entry)
	} else {
		vsPrev = comparison{HasBaseline: false}
	}

	out := runOutput{
		HistoryFile:        historyPath,
		TotalRunsInHistory: len(prior) + 1,
		Entry:              entry,
		VsPreviousRun:      vsPrev,
	}

	if !jsonOut {
		fmt.Println()
		fmt.Println("=== ThermalFlow run report ===")
		fmt.Printf("Timestamp (UTC):        %s\n", entry.TimestampUTC)
		fmt.Printf("Logical cores (NumCPU): %d\n", entry.NumCPU)
		fmt.Printf("Phase duration:         %s\n", duration.String())
		fmt.Println()
		fmt.Printf("Single-thread ops/sec:  %.0f\n", entry.SingleThreadOpsPerSec)
		fmt.Printf("Multi-thread ops/sec:   %.0f\n", entry.MultiThreadOpsPerSec)
		fmt.Printf("Scaling factor:         %.2fx\n", entry.ScalingFactor)
		fmt.Println()
		fmt.Printf("History file:           %s (%d run(s) total after this one)\n", historyPath, out.TotalRunsInHistory)
		fmt.Println()
		if vsPrev.HasBaseline {
			printComparison("Comparison vs previous run", vsPrev)
		} else {
			fmt.Println("Comparison vs previous run: no previous run in history - this is the first recorded run.")
		}
		fmt.Println()
		fmt.Printf("(Regression threshold: >%.0f%% slower = REGRESSION, >%.0f%% faster = IMPROVEMENT, else STABLE.)\n", regressionThresholdPct, regressionThresholdPct)
	}

	return out, nil
}

// ---------------------------------------------------------------------
// "trend" command
// ---------------------------------------------------------------------

type trendStats struct {
	WindowSize               int     `json:"window_size"`
	MinSingleThreadOpsPerSec float64 `json:"min_single_thread_ops_per_sec"`
	MaxSingleThreadOpsPerSec float64 `json:"max_single_thread_ops_per_sec"`
	AvgSingleThreadOpsPerSec float64 `json:"avg_single_thread_ops_per_sec"`
	MinMultiThreadOpsPerSec  float64 `json:"min_multi_thread_ops_per_sec"`
	MaxMultiThreadOpsPerSec  float64 `json:"max_multi_thread_ops_per_sec"`
	AvgMultiThreadOpsPerSec  float64 `json:"avg_multi_thread_ops_per_sec"`
}

type trendOutput struct {
	HistoryFile         string         `json:"history_file"`
	TotalRunsInHistory  int            `json:"total_runs_in_history"`
	ShownRuns           []historyEntry `json:"shown_runs"`
	Stats               trendStats     `json:"stats"`
	MostRecentVsHistAvg comparison     `json:"most_recent_vs_historical_average"`
	Message             string         `json:"message,omitempty"`
}

func cmdTrend(args []string) {
	for _, a := range args {
		if a == "-h" || a == "--help" || a == "help" {
			usage()
			os.Exit(0)
		}
	}

	valueFlags := map[string]bool{"history": true, "last": true}
	args = reorderFlags(args, valueFlags)

	fs := flag.NewFlagSet("trend", flag.ExitOnError)
	fs.Usage = usage
	historyPath := fs.String("history", "", "path to the JSON-lines history file (required)")
	last := fs.Int("last", 0, "show only the N most recent runs (0 = all)")
	jsonOut := fs.Bool("json", false, "emit structured JSON output")
	if err := fs.Parse(args); err != nil {
		os.Exit(1)
	}

	if *historyPath == "" {
		fmt.Fprintln(os.Stderr, "thermalflow: --history is required")
		usage()
		os.Exit(1)
	}

	entries, err := readHistory(*historyPath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "thermalflow: %v\n", err)
		os.Exit(1)
	}

	if len(entries) == 0 {
		msg := fmt.Sprintf("No history yet at %s. Run `thermalflow run --history %s` to get started.", *historyPath, *historyPath)
		if *jsonOut {
			out := trendOutput{HistoryFile: *historyPath, TotalRunsInHistory: 0, Message: msg}
			enc := json.NewEncoder(os.Stdout)
			enc.SetIndent("", "  ")
			enc.Encode(out)
		} else {
			fmt.Println(msg)
		}
		return
	}

	windowSize := len(entries)
	if *last > 0 && *last < len(entries) {
		windowSize = *last
	}
	window := entries[len(entries)-windowSize:]

	stats := trendStats{WindowSize: windowSize}
	stats.MinSingleThreadOpsPerSec = window[0].SingleThreadOpsPerSec
	stats.MaxSingleThreadOpsPerSec = window[0].SingleThreadOpsPerSec
	stats.MinMultiThreadOpsPerSec = window[0].MultiThreadOpsPerSec
	stats.MaxMultiThreadOpsPerSec = window[0].MultiThreadOpsPerSec
	var sumSingle, sumMulti float64
	for _, e := range window {
		if e.SingleThreadOpsPerSec < stats.MinSingleThreadOpsPerSec {
			stats.MinSingleThreadOpsPerSec = e.SingleThreadOpsPerSec
		}
		if e.SingleThreadOpsPerSec > stats.MaxSingleThreadOpsPerSec {
			stats.MaxSingleThreadOpsPerSec = e.SingleThreadOpsPerSec
		}
		if e.MultiThreadOpsPerSec < stats.MinMultiThreadOpsPerSec {
			stats.MinMultiThreadOpsPerSec = e.MultiThreadOpsPerSec
		}
		if e.MultiThreadOpsPerSec > stats.MaxMultiThreadOpsPerSec {
			stats.MaxMultiThreadOpsPerSec = e.MultiThreadOpsPerSec
		}
		sumSingle += e.SingleThreadOpsPerSec
		sumMulti += e.MultiThreadOpsPerSec
	}
	stats.AvgSingleThreadOpsPerSec = sumSingle / float64(len(window))
	stats.AvgMultiThreadOpsPerSec = sumMulti / float64(len(window))

	// Most-recent-vs-average comparison always uses the FULL history (not
	// just the shown --last window): all runs before the most recent one,
	// averaged, as the historical baseline. This is deliberately a
	// different, fuller comparison than "run"'s vs-immediately-previous-run
	// check.
	mostRecent := entries[len(entries)-1]
	prior := entries[:len(entries)-1]
	var vsAvg comparison
	if len(prior) > 0 {
		var priorSumSingle, priorSumMulti float64
		for _, e := range prior {
			priorSumSingle += e.SingleThreadOpsPerSec
			priorSumMulti += e.MultiThreadOpsPerSec
		}
		priorAvgSingle := priorSumSingle / float64(len(prior))
		priorAvgMulti := priorSumMulti / float64(len(prior))
		desc := fmt.Sprintf("average of %d prior run(s)", len(prior))
		vsAvg = compareAgainst(desc, len(prior), priorAvgSingle, priorAvgMulti, mostRecent)
	} else {
		vsAvg = comparison{HasBaseline: false}
	}

	out := trendOutput{
		HistoryFile:         *historyPath,
		TotalRunsInHistory:  len(entries),
		ShownRuns:           window,
		Stats:               stats,
		MostRecentVsHistAvg: vsAvg,
	}

	if *jsonOut {
		enc := json.NewEncoder(os.Stdout)
		enc.SetIndent("", "  ")
		if err := enc.Encode(out); err != nil {
			fmt.Fprintf(os.Stderr, "thermalflow: failed to encode JSON: %v\n", err)
			os.Exit(1)
		}
		return
	}

	fmt.Println("=== ThermalFlow trend report ===")
	fmt.Printf("History file:           %s\n", *historyPath)
	fmt.Printf("Total runs in history:  %d\n", len(entries))
	if windowSize == len(entries) {
		fmt.Printf("Showing:                all %d run(s)\n", windowSize)
	} else {
		fmt.Printf("Showing:                last %d run(s)\n", windowSize)
	}
	fmt.Println()
	fmt.Printf("%-25s %8s %18s %18s %10s\n", "TIMESTAMP (UTC)", "NUM_CPU", "SINGLE OPS/SEC", "MULTI OPS/SEC", "SCALING")
	for _, e := range window {
		fmt.Printf("%-25s %8d %18.0f %18.0f %9.2fx\n", e.TimestampUTC, e.NumCPU, e.SingleThreadOpsPerSec, e.MultiThreadOpsPerSec, e.ScalingFactor)
	}
	fmt.Println()
	fmt.Println("Stats over shown window:")
	fmt.Printf("  Single-thread ops/sec: min %.0f  max %.0f  avg %.0f\n", stats.MinSingleThreadOpsPerSec, stats.MaxSingleThreadOpsPerSec, stats.AvgSingleThreadOpsPerSec)
	fmt.Printf("  Multi-thread ops/sec:  min %.0f  max %.0f  avg %.0f\n", stats.MinMultiThreadOpsPerSec, stats.MaxMultiThreadOpsPerSec, stats.AvgMultiThreadOpsPerSec)
	fmt.Println()
	if vsAvg.HasBaseline {
		printComparison("Most recent run vs. historical average", vsAvg)
	} else {
		fmt.Println("Most recent run vs. historical average: no prior runs to compare against - this is the only run.")
	}
	fmt.Println()
	fmt.Printf("(Regression threshold: >%.0f%% slower = REGRESSION, >%.0f%% faster = IMPROVEMENT, else STABLE.)\n", regressionThresholdPct, regressionThresholdPct)
}

// ---------------------------------------------------------------------
// "watch" command
// ---------------------------------------------------------------------

func cmdWatch(args []string) {
	for _, a := range args {
		if a == "-h" || a == "--help" || a == "help" {
			usage()
			os.Exit(0)
		}
	}

	valueFlags := map[string]bool{"history": true, "duration": true, "interval": true}
	args = reorderFlags(args, valueFlags)

	fs := flag.NewFlagSet("watch", flag.ExitOnError)
	fs.Usage = usage
	historyPath := fs.String("history", "", "path to the JSON-lines history file (required)")
	duration := fs.Duration("duration", 500*time.Millisecond, "duration to run each benchmark phase for")
	interval := fs.Duration("interval", time.Hour, "how long to sleep between rounds")
	once := fs.Bool("once", false, "run a single round then exit")
	jsonOut := fs.Bool("json", false, "emit structured JSON output")
	if err := fs.Parse(args); err != nil {
		os.Exit(1)
	}

	if *historyPath == "" {
		fmt.Fprintln(os.Stderr, "thermalflow: --history is required")
		usage()
		os.Exit(1)
	}
	if *duration <= 0 {
		fmt.Fprintln(os.Stderr, "thermalflow: --duration must be positive")
		os.Exit(1)
	}
	if !*once && *interval <= 0 {
		fmt.Fprintln(os.Stderr, "thermalflow: --interval must be positive (unless --once is given)")
		os.Exit(1)
	}

	round := 0
	for {
		round++
		if !*jsonOut {
			fmt.Printf("--- watch round %d (%s) ---\n", round, time.Now().UTC().Format(time.RFC3339))
		}
		out, err := doRunRound(*historyPath, *duration, *jsonOut)
		if err != nil {
			fmt.Fprintf(os.Stderr, "thermalflow: %v\n", err)
			os.Exit(1)
		}
		if *jsonOut {
			enc := json.NewEncoder(os.Stdout)
			enc.SetIndent("", "  ")
			if err := enc.Encode(out); err != nil {
				fmt.Fprintf(os.Stderr, "thermalflow: failed to encode JSON: %v\n", err)
				os.Exit(1)
			}
		}

		if *once {
			return
		}

		if !*jsonOut {
			fmt.Printf("Sleeping %s until next round...\n\n", interval.String())
		}
		time.Sleep(*interval)
	}
}
