// Command driverrollback takes content-addressed, deduplicated snapshots of a
// driver-store directory tree and restores them again, quarantining rather
// than deleting anything it displaces.
package main

import (
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"
)

const appName = "driverrollback"
const version = "1.0.0"

// ---------------------------------------------------------------------------
// Shared Techlosoft UX helpers (verbatim across the tool line)
// ---------------------------------------------------------------------------

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
	neg := ""
	if n < 0 {
		neg, n = "-", -n
	}
	if n < unit {
		return fmt.Sprintf("%s%d B", neg, n)
	}
	div, exp := int64(unit), 0
	for x := n / unit; x >= unit; x /= unit {
		div *= unit
		exp++
	}
	return fmt.Sprintf("%s%.1f %ciB", neg, float64(n)/float64(div), "KMGTPE"[exp])
}

// ---------------------------------------------------------------------------
// Usage
// ---------------------------------------------------------------------------

const usageText = `%s - snapshot and rollback for a driver store (Techlosoft Device Reliability Center)

USAGE
  %s snapshot --source <dir> [--store <dir>] [--name <label>] [--ledger <file>] [--json]
  %s list     [--store <dir>] [--json]
  %s diff     <snapA> <snapB> [--store <dir>] [--json]
  %s restore  <snap> --target <dir> [--apply] [--quarantine <dir>]
              [--store <dir>] [--ledger <file>] [--json]
  %s verify   <snap> [--store <dir>] [--json]
  %s help | -h | --help

COMMANDS
  snapshot   Walk the source tree, SHA-256 every regular file, copy each unique
             blob into the object store exactly once, and write a manifest.
             Blobs already present are not copied again, so a second snapshot
             of unchanged content adds zero bytes.
  list       Show every snapshot in the store with its timestamp, file count,
             apparent size, and the bytes it actually added after dedup.
  diff       Compare two snapshots: added, removed, changed, unchanged, and the
             byte delta, sorted by path.
  restore    Compute the minimal set of operations that turns the target tree
             back into the snapshot state and print the plan. THIS IS A DRY RUN.
             Nothing is written unless you pass --apply.
  verify     Re-hash every object the snapshot references and report any that
             is missing or corrupt.

FLAGS
  --source <dir>      Directory tree to snapshot.
  --store <dir>       Object store and manifest directory. Default: .store
  --target <dir>      Directory tree restore operates on.
  --name <label>      Free-text label recorded in the snapshot manifest.
  --apply             Actually perform the restore. Without it, restore is a
                      dry run and does not touch a single byte of the target.
  --quarantine <dir>  Where displaced files are MOVED under --apply. Default is
                      a timestamped folder beside the target:
                      <parent>/<target>.quarantine/<UTC timestamp>/
  --ledger <file>     Append-only JSON-lines audit ledger written by every
                      mutating run. Default: <store>/ledger.jsonl
  --json              Machine-readable JSON output (all five subcommands).

SAFETY
  Nothing is ever unlinked. A file restore would overwrite or remove is first
  MOVED into the quarantine directory, and only then is the snapshot version
  written. Every restored file is written to a .part temp file, renamed into
  place, then re-hashed against the manifest; a mismatch is a hard error.

EXAMPLES
  %s snapshot --source ./driverstore --store ./.store --name pre-update
  %s list --store ./.store
  %s diff snap-20260811T090000Z snap-20260811T120000Z --store ./.store
  %s restore snap-20260811T090000Z --target ./driverstore --store ./.store
  %s restore snap-20260811T090000Z --target ./driverstore --store ./.store --apply
  %s verify snap-20260811T090000Z --store ./.store --json

Flags may appear before or after positional arguments.
`

func usageTo(w *os.File) {
	fmt.Fprintf(w, usageText,
		appName, appName, appName, appName, appName, appName, appName,
		appName, appName, appName, appName, appName, appName)
}

func usage() { usageTo(os.Stdout) }

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

func usageErr(format string, args ...any) {
	fmt.Fprintf(os.Stderr, "%s: %s\n\n", appName, fmt.Sprintf(format, args...))
	usageTo(os.Stderr)
	os.Exit(1)
}

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

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
		}
		usageTo(os.Stderr)
		os.Exit(1)
	}
	switch args[0] {
	case "help", "-h", "--help":
		usage()
		os.Exit(0)
	}
	cmd := args[0]
	rest := args[1:]
	for _, a := range rest {
		if a == "-h" || a == "--help" || a == "help" {
			usage()
			os.Exit(0)
		}
	}
	switch cmd {
	case "snapshot":
		cmdSnapshot(rest)
	case "list":
		cmdList(rest)
	case "diff":
		cmdDiff(rest)
	case "restore":
		cmdRestore(rest)
	case "verify":
		cmdVerify(rest)
	default:
		usageErr("unknown command %q", cmd)
	}
}

// ---------------------------------------------------------------------------
// Flag plumbing
// ---------------------------------------------------------------------------

var valueFlags = map[string]bool{
	"source": true, "s": true,
	"store": true, "S": true,
	"target": true, "t": true,
	"quarantine": true, "q": true,
	"ledger": true, "l": true,
	"name": true, "n": true,
}

const defaultStore = ".store"

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

func addStoreFlag(fs *flag.FlagSet) *string {
	p := fs.String("store", defaultStore, "object store directory")
	fs.StringVar(p, "S", defaultStore, "shorthand for --store")
	return p
}

func mustOpenStore(path string, needExisting bool) *Store {
	s, err := openStore(path)
	if err != nil {
		fail("%v", err)
	}
	if needExisting {
		if err := s.mustExist(); err != nil {
			if errors.Is(err, errNoStore) {
				fmt.Fprintf(os.Stderr, "%s: no store at %s\n", appName, s.Root)
				fmt.Fprintf(os.Stderr, "Take a snapshot first, for example:\n")
				fmt.Fprintf(os.Stderr, "  %s snapshot --source <dir> --store %s\n", appName, path)
				os.Exit(1)
			}
			fail("%v", err)
		}
	}
	return s
}

func ledgerPath(flagValue string, s *Store) string {
	if flagValue != "" {
		return flagValue
	}
	return filepath.Join(s.Root, "ledger.jsonl")
}

func emitJSON(v any) {
	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	if err := enc.Encode(v); err != nil {
		fail("%v", err)
	}
}

// ---------------------------------------------------------------------------
// snapshot
// ---------------------------------------------------------------------------

func cmdSnapshot(argv []string) {
	fs := newFlagSet("snapshot")
	source := fs.String("source", "", "directory tree to snapshot")
	fs.StringVar(source, "s", "", "shorthand for --source")
	storePath := addStoreFlag(fs)
	name := fs.String("name", "", "free-text label for this snapshot")
	fs.StringVar(name, "n", "", "shorthand for --name")
	ledger := fs.String("ledger", "", "audit ledger (JSON lines)")
	fs.StringVar(ledger, "l", "", "shorthand for --ledger")
	asJSON := fs.Bool("json", false, "JSON output")
	if err := fs.Parse(reorderFlags(argv, valueFlags)); err != nil {
		os.Exit(1)
	}
	if *source == "" && fs.NArg() > 0 {
		*source = fs.Arg(0)
	}
	if *source == "" {
		usageErr("snapshot needs --source <dir>")
	}
	if fs.NArg() > 1 {
		usageErr("snapshot takes at most one positional argument, got %d", fs.NArg())
	}

	s := mustOpenStore(*storePath, false)
	now := time.Now().UTC()
	m, audit, err := takeSnapshot(s, *source, *name, now)
	if err != nil {
		fail("%v", err)
	}

	lp := ledgerPath(*ledger, s)
	rec := AuditRecord{
		TS: now, Tool: appName + " " + version, Op: "snapshot",
		Snapshot: m.ID, Store: s.Root, Source: m.Source, Result: "ok",
		Counts: map[string]int{
			"files": m.FileCount, "unique_objects": m.UniqueObjects,
			"added_objects": m.AddedObjects, "skipped": m.SkippedCount,
		},
		Bytes: m.AddedBytes, Files: audit,
	}
	if err := appendLedger(lp, rec); err != nil {
		fail("%v", err)
	}

	if *asJSON {
		emitJSON(map[string]any{
			"snapshot":       m,
			"store":          s.Root,
			"ledger":         lp,
			"apparent_human": humanBytes(m.ApparentBytes),
			"added_human":    humanBytes(m.AddedBytes),
			"deduped_bytes":  m.ApparentBytes - m.AddedBytes,
		})
		return
	}
	fmt.Printf("snapshot %s\n", m.ID)
	if m.Name != "" {
		fmt.Printf("  name        : %s\n", m.Name)
	}
	fmt.Printf("  created     : %s\n", m.Created.Format(time.RFC3339))
	fmt.Printf("  source      : %s\n", m.Source)
	fmt.Printf("  store       : %s\n", s.Root)
	fmt.Printf("  files       : %d in %d dirs\n", m.FileCount, m.DirCount)
	fmt.Printf("  apparent    : %s (%d bytes)\n", humanBytes(m.ApparentBytes), m.ApparentBytes)
	fmt.Printf("  unique blobs: %d (%s)\n", m.UniqueObjects, humanBytes(m.UniqueBytes))
	fmt.Printf("  added       : %d objects, %s (%d bytes)\n", m.AddedObjects, humanBytes(m.AddedBytes), m.AddedBytes)
	fmt.Printf("  deduped     : %s not copied again\n", humanBytes(m.ApparentBytes-m.AddedBytes))
	if m.SkippedCount > 0 {
		fmt.Printf("  skipped     : %d non-regular entries (symlinks, devices, sockets)\n", m.SkippedCount)
	}
	fmt.Printf("  ledger      : %s\n", lp)
}

// ---------------------------------------------------------------------------
// list
// ---------------------------------------------------------------------------

func cmdList(argv []string) {
	fs := newFlagSet("list")
	storePath := addStoreFlag(fs)
	asJSON := fs.Bool("json", false, "JSON output")
	if err := fs.Parse(reorderFlags(argv, valueFlags)); err != nil {
		os.Exit(1)
	}
	if fs.NArg() > 0 {
		usageErr("list takes no positional arguments, got %q", fs.Arg(0))
	}
	s := mustOpenStore(*storePath, true)
	ms, err := s.listManifests()
	if err != nil {
		fail("%v", err)
	}

	var totalAdded, totalApparent int64
	rows := make([]map[string]any, 0, len(ms))
	for _, m := range ms {
		totalAdded += m.AddedBytes
		totalApparent += m.ApparentBytes
		rows = append(rows, map[string]any{
			"id": m.ID, "name": m.Name, "created": m.Created,
			"files": m.FileCount, "apparent_bytes": m.ApparentBytes,
			"apparent_human": humanBytes(m.ApparentBytes),
			"added_bytes":    m.AddedBytes, "added_human": humanBytes(m.AddedBytes),
			"added_objects": m.AddedObjects, "unique_objects": m.UniqueObjects,
			"source": m.Source,
		})
	}
	if *asJSON {
		emitJSON(map[string]any{
			"store": s.Root, "count": len(ms), "snapshots": rows,
			"store_bytes_added": totalAdded, "apparent_bytes_total": totalApparent,
			"deduped_bytes": totalApparent - totalAdded,
		})
		return
	}
	fmt.Printf("store     : %s\n", s.Root)
	fmt.Printf("snapshots : %d\n\n", len(ms))
	if len(ms) == 0 {
		fmt.Println("(no snapshots yet)")
		return
	}
	fmt.Printf("%-24s  %-20s  %7s  %12s  %12s  %s\n", "ID", "CREATED", "FILES", "APPARENT", "ADDED", "NAME")
	for _, m := range ms {
		fmt.Printf("%-24s  %-20s  %7d  %12s  %12s  %s\n",
			m.ID, m.Created.Format("2006-01-02 15:04:05"), m.FileCount,
			humanBytes(m.ApparentBytes), humanBytes(m.AddedBytes), m.Name)
	}
	fmt.Println()
	fmt.Printf("apparent across all snapshots : %s (%d bytes)\n", humanBytes(totalApparent), totalApparent)
	fmt.Printf("actually added to the store   : %s (%d bytes)\n", humanBytes(totalAdded), totalAdded)
	fmt.Printf("saved by deduplication        : %s (%d bytes)\n", humanBytes(totalApparent-totalAdded), totalApparent-totalAdded)
}

// ---------------------------------------------------------------------------
// diff
// ---------------------------------------------------------------------------

func cmdDiff(argv []string) {
	fs := newFlagSet("diff")
	storePath := addStoreFlag(fs)
	asJSON := fs.Bool("json", false, "JSON output")
	all := fs.Bool("all", false, "include unchanged files in the report")
	if err := fs.Parse(reorderFlags(argv, valueFlags)); err != nil {
		os.Exit(1)
	}
	if fs.NArg() != 2 {
		usageErr("diff needs exactly two snapshot ids, got %d", fs.NArg())
	}
	s := mustOpenStore(*storePath, true)
	a, err := s.loadManifest(fs.Arg(0))
	if err != nil {
		fail("%v", err)
	}
	b, err := s.loadManifest(fs.Arg(1))
	if err != nil {
		fail("%v", err)
	}
	r := diffSnapshots(a, b)

	if *asJSON {
		out := *r
		if !*all {
			kept := make([]DiffEntry, 0, len(r.Entries))
			for _, e := range r.Entries {
				if e.Change != "unchanged" {
					kept = append(kept, e)
				}
			}
			out.Entries = kept
		}
		emitJSON(map[string]any{
			"diff":                out,
			"bytes_added_human":   humanBytes(r.BytesAdded),
			"bytes_removed_human": humanBytes(r.BytesRemoved),
			"bytes_delta_human":   humanBytes(r.BytesDelta),
			"includes_unchanged":  *all,
		})
		return
	}

	fmt.Printf("diff %s -> %s\n", a.ID, b.ID)
	fmt.Printf("  from : %s  %d files  %s\n", a.Created.Format(time.RFC3339), a.FileCount, humanBytes(a.ApparentBytes))
	fmt.Printf("  to   : %s  %d files  %s\n", b.Created.Format(time.RFC3339), b.FileCount, humanBytes(b.ApparentBytes))
	fmt.Println()
	for _, e := range r.Entries {
		switch e.Change {
		case "added":
			fmt.Printf("  + %s  (%s)\n", e.Path, humanBytes(e.ToSize))
		case "removed":
			fmt.Printf("  - %s  (%s)\n", e.Path, humanBytes(e.FromSize))
		case "changed":
			fmt.Printf("  ~ %s  (%s -> %s, %s)\n", e.Path,
				humanBytes(e.FromSize), humanBytes(e.ToSize), signedBytes(e.SizeDelta))
			fmt.Printf("      %s -> %s\n", short(e.FromHash), short(e.ToHash))
		case "unchanged":
			if *all {
				fmt.Printf("    %s  (%s, unchanged)\n", e.Path, humanBytes(e.ToSize))
			}
		}
	}
	if r.Added+r.Removed+r.Changed == 0 {
		fmt.Println("  (no differences)")
	}
	fmt.Println()
	fmt.Printf("  added     : %d files, %s\n", r.Added, humanBytes(r.BytesAdded))
	fmt.Printf("  removed   : %d files, %s\n", r.Removed, humanBytes(r.BytesRemoved))
	fmt.Printf("  changed   : %d files\n", r.Changed)
	fmt.Printf("  unchanged : %d files\n", r.Unchanged)
	fmt.Printf("  net size  : %s (%d bytes)\n", signedBytes(r.BytesDelta), r.BytesDelta)
}

func signedBytes(n int64) string {
	if n > 0 {
		return "+" + humanBytes(n)
	}
	return humanBytes(n)
}

func short(hash string) string {
	if len(hash) <= 12 {
		return hash
	}
	return hash[:12]
}

// ---------------------------------------------------------------------------
// restore
// ---------------------------------------------------------------------------

func cmdRestore(argv []string) {
	fs := newFlagSet("restore")
	storePath := addStoreFlag(fs)
	target := fs.String("target", "", "directory tree to restore into")
	fs.StringVar(target, "t", "", "shorthand for --target")
	quarantine := fs.String("quarantine", "", "where displaced files are moved")
	fs.StringVar(quarantine, "q", "", "shorthand for --quarantine")
	ledger := fs.String("ledger", "", "audit ledger (JSON lines)")
	fs.StringVar(ledger, "l", "", "shorthand for --ledger")
	apply := fs.Bool("apply", false, "actually perform the restore (default: dry run)")
	asJSON := fs.Bool("json", false, "JSON output")
	all := fs.Bool("all", false, "include unchanged (keep) files in the plan")
	if err := fs.Parse(reorderFlags(argv, valueFlags)); err != nil {
		os.Exit(1)
	}
	if fs.NArg() != 1 {
		usageErr("restore needs exactly one snapshot id, got %d", fs.NArg())
	}
	if *target == "" {
		usageErr("restore needs --target <dir>")
	}
	s := mustOpenStore(*storePath, true)
	m, err := s.loadManifest(fs.Arg(0))
	if err != nil {
		fail("%v", err)
	}
	plan, err := planRestore(s, m, *target)
	if err != nil {
		fail("%v", err)
	}

	now := time.Now().UTC()
	qroot := *quarantine
	if qroot == "" {
		qroot = defaultQuarantine(plan.Target, now)
	} else {
		abs, err := filepath.Abs(qroot)
		if err != nil {
			fail("cannot resolve quarantine %q: %v", qroot, err)
		}
		qroot = abs
	}
	if strings.HasPrefix(qroot+string(filepath.Separator), plan.Target+string(filepath.Separator)) {
		fail("quarantine %s is inside the target %s - choose a directory outside the tree", qroot, plan.Target)
	}

	var audit []AuditFile
	var applyErr error
	if *apply {
		audit, applyErr = applyRestore(s, m, plan, qroot)
		lp := ledgerPath(*ledger, s)
		rec := AuditRecord{
			TS: now, Tool: appName + " " + version, Op: "restore",
			Snapshot: m.ID, Store: s.Root, Target: plan.Target, Quarantine: qroot,
			Result: "ok",
			Counts: map[string]int{
				"create": plan.Create, "overwrite": plan.Overwrite,
				"remove": plan.Remove, "keep": plan.Keep, "conflict": plan.Conflict,
			},
			Bytes: plan.BytesWritten, Files: audit,
		}
		if applyErr != nil {
			rec.Result = "error"
			rec.Error = applyErr.Error()
		}
		if lerr := appendLedger(lp, rec); lerr != nil {
			fail("%v", lerr)
		}
		if applyErr != nil {
			fail("%v", applyErr)
		}
		if *asJSON {
			emitJSON(map[string]any{
				"mode": "apply", "plan": filterPlan(plan, *all), "quarantine": qroot,
				"ledger": lp, "outcomes": audit,
				"bytes_written_human": humanBytes(plan.BytesWritten),
			})
			return
		}
		printPlan(plan, m, qroot, true, *all)
		fmt.Printf("\nledger: %s\n", lp)
		return
	}

	if *asJSON {
		emitJSON(map[string]any{
			"mode": "dry-run", "plan": filterPlan(plan, *all),
			"quarantine_would_be": qroot,
			"bytes_written_human": humanBytes(plan.BytesWritten),
			"bytes_moved_human":   humanBytes(plan.BytesMoved),
			"note":                "dry run - nothing was written; pass --apply to perform this restore",
		})
		return
	}
	printPlan(plan, m, qroot, false, *all)
}

func filterPlan(p *RestorePlan, all bool) RestorePlan {
	out := *p
	if all {
		return out
	}
	kept := make([]PlanOp, 0, len(p.Ops))
	for _, o := range p.Ops {
		if o.Op != opKeep {
			kept = append(kept, o)
		}
	}
	out.Ops = kept
	return out
}

func printPlan(p *RestorePlan, m *Manifest, qroot string, applied, all bool) {
	verb := "restore plan (DRY RUN)"
	if applied {
		verb = "restore applied"
	}
	fmt.Printf("%s\n", verb)
	fmt.Printf("  snapshot : %s", p.Snapshot)
	if m.Name != "" {
		fmt.Printf("  (%s)", m.Name)
	}
	fmt.Println()
	fmt.Printf("  taken    : %s\n", m.Created.Format(time.RFC3339))
	fmt.Printf("  target   : %s\n", p.Target)
	fmt.Printf("  store    : %s\n", p.Store)
	if applied {
		fmt.Printf("  quarantine: %s\n", qroot)
	} else if p.Overwrite+p.Remove > 0 {
		fmt.Printf("  quarantine would be: %s\n", qroot)
	}
	fmt.Println()
	for _, o := range p.Ops {
		switch o.Op {
		case opCreate:
			fmt.Printf("  create    %s  (%s)\n", o.Path, humanBytes(o.WantSize))
		case opOverwrite:
			fmt.Printf("  overwrite %s  (%s -> %s)\n", o.Path, humanBytes(o.HaveSize), humanBytes(o.WantSize))
			if o.Detail != "" {
				fmt.Printf("              %s\n", o.Detail)
			}
		case opRemove:
			fmt.Printf("  quarantine %s  (%s, not in snapshot)\n", o.Path, humanBytes(o.HaveSize))
		case opConflict:
			fmt.Printf("  CONFLICT  %s  (%s)\n", o.Path, o.Detail)
		case opKeep:
			if all {
				fmt.Printf("  keep      %s  (%s, already correct)\n", o.Path, humanBytes(o.WantSize))
			}
		}
	}
	if p.Mutations() == 0 && p.Conflict == 0 {
		fmt.Printf("  (target already matches the snapshot - nothing to do)\n")
	}
	fmt.Println()
	fmt.Printf("  create     : %d files, %s to write\n", p.Create, humanBytes(bytesOf(p, opCreate)))
	fmt.Printf("  overwrite  : %d files, %s to write, %s to quarantine\n", p.Overwrite, humanBytes(bytesOf(p, opOverwrite)), humanBytes(quarantineBytes(p, opOverwrite)))
	fmt.Printf("  quarantine : %d files, %s\n", p.Remove, humanBytes(quarantineBytes(p, opRemove)))
	fmt.Printf("  keep       : %d files\n", p.Keep)
	if p.Conflict > 0 {
		fmt.Printf("  conflicts  : %d (restore --apply will refuse until these are resolved)\n", p.Conflict)
	}
	if !applied {
		fmt.Println()
		if p.Mutations() > 0 {
			fmt.Printf("DRY RUN - nothing was written. Re-run with --apply to perform this restore.\n")
		} else {
			fmt.Printf("DRY RUN - nothing to do.\n")
		}
	}
}

func bytesOf(p *RestorePlan, op string) int64 {
	var n int64
	for _, o := range p.Ops {
		if o.Op == op {
			n += o.WantSize
		}
	}
	return n
}

func quarantineBytes(p *RestorePlan, op string) int64 {
	var n int64
	for _, o := range p.Ops {
		if o.Op == op {
			n += o.HaveSize
		}
	}
	return n
}

// ---------------------------------------------------------------------------
// verify
// ---------------------------------------------------------------------------

func cmdVerify(argv []string) {
	fs := newFlagSet("verify")
	storePath := addStoreFlag(fs)
	asJSON := fs.Bool("json", false, "JSON output")
	all := fs.Bool("all", false, "list healthy objects too, not just problems")
	if err := fs.Parse(reorderFlags(argv, valueFlags)); err != nil {
		os.Exit(1)
	}
	if fs.NArg() != 1 {
		usageErr("verify needs exactly one snapshot id, got %d", fs.NArg())
	}
	s := mustOpenStore(*storePath, true)
	m, err := s.loadManifest(fs.Arg(0))
	if err != nil {
		fail("%v", err)
	}
	r := verifySnapshot(s, m)

	if *asJSON {
		out := *r
		if !*all {
			kept := make([]VerifyObject, 0, len(r.Results))
			for _, o := range r.Results {
				if o.Status != "ok" {
					kept = append(kept, o)
				}
			}
			out.Results = kept
		}
		emitJSON(map[string]any{
			"verify": out, "bytes_human": humanBytes(r.Bytes), "includes_ok": *all,
		})
		if !r.Healthy {
			os.Exit(2)
		}
		return
	}

	fmt.Printf("verify %s\n", m.ID)
	fmt.Printf("  store   : %s\n", s.Root)
	fmt.Printf("  files   : %d referencing %d unique objects\n", m.FileCount, r.Objects)
	fmt.Println()
	for _, o := range r.Results {
		if o.Status == "ok" {
			if *all {
				fmt.Printf("  ok       %s  %s  (%d path(s))\n", short(o.Hash), humanBytes(o.Size), len(o.Paths))
			}
			continue
		}
		fmt.Printf("  %-8s %s  %s\n", strings.ToUpper(o.Status), short(o.Hash), o.Detail)
		if o.Got != "" && o.Got != o.Hash {
			fmt.Printf("             stored bytes hash to %s\n", short(o.Got))
		}
		for _, p := range o.Paths {
			fmt.Printf("             used by %s\n", p)
		}
	}
	fmt.Println()
	fmt.Printf("  objects ok : %d\n", r.OK)
	fmt.Printf("  missing    : %d\n", r.Missing)
	fmt.Printf("  corrupt    : %d\n", r.Corrupt)
	fmt.Printf("  unreadable : %d\n", r.Unreadable)
	fmt.Printf("  verified   : %s (%d bytes)\n", humanBytes(r.Bytes), r.Bytes)
	if r.Healthy {
		fmt.Printf("  result     : healthy - every object this snapshot needs is present and intact\n")
		return
	}
	fmt.Printf("  result     : DAMAGED - this snapshot cannot be fully restored\n")
	os.Exit(2)
}
