"""``mgf-secrets`` CLI — P1 verbs (list/show/doctor/probe) + P2 value verbs.

P2 adds the value-handling surface (SPEC §4): ``show --value`` (bare value, TTY-
refused), ``add``/``set`` (value via file/stdin/hidden prompt — NEVER argv), ``exec``
(env-injected child), ``materialize`` (decrypt the store into the bare-value cache),
``export-offline`` (founder bundle), and ``history`` (git audit trail).

**Interface rules, tested not aspirational (SPEC §4):** no verb prints a value except
``show --value`` and ``exec`` injection; value input is never accepted on argv; every
non-value verb has a ``--json`` twin; exit codes are 0 clean · 1 findings · 2 usage/
credential/load error (``exec`` passes the child's code through). The redaction
property test drives this whole surface with planted canaries (SPEC §6.2).
"""

from __future__ import annotations

import argparse
import getpass
import io
import json
import subprocess
import sys
import tarfile
from datetime import UTC, datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any

from . import __version__
from . import materialize as mtz
from . import retire as retire_mod
from . import store as store_mod
from . import sync as sync_mod
from .context import Context, build_context
from .doctor import ALL_SURFACES, DEFAULT_SURFACES, Doctor
from .errors import EXIT_ERROR, EXIT_FINDINGS, EXIT_OK, CredentialError, SecretsError, UsageError
from .probes import run_probe
from .registry import Registry, RegistryError, default_paths, load

if TYPE_CHECKING:
    from collections.abc import Callable, Sequence

    from .model import Entry

#: Verbs that operate purely on the store and need no registry checkout — so `exec`
#: is a valid `$(cat my_stuff/…)` replacement even outside the federation root.
_STORE_ONLY_VERBS = frozenset({"exec", "history", "export-offline"})


def entry_to_json(e: Entry) -> dict[str, Any]:
    """Metadata-only serialization of an entry (never a value)."""
    return {
        "id": e.id,
        "family": e.family,
        "kind": e.kind,
        "state": e.state,
        "purpose": e.purpose,
        "surfaces": [{"type": s.type, **s.data} for s in e.surfaces],
        "consumers": list(e.consumers),
        "policy": {"allowed_repos": e.policy.allowed_repos, "blast_radius": e.policy.blast_radius},
        "rotation": dict(e.rotation),
        "verify": dict(e.verify),
        "recovery": e.recovery,
    }


def _require_registry(reg: Registry | None) -> Registry:
    if reg is None:  # pragma: no cover - guarded by main()'s verb routing
        raise UsageError("this verb needs the registry but none was loaded")
    return reg


# --------------------------------------------------------------------------- #
# P1 verbs.
# --------------------------------------------------------------------------- #


def _cmd_list(args: argparse.Namespace, reg: Registry | None, _ctx: Context | None) -> int:
    reg = _require_registry(reg)
    rows = reg.select(family=args.family, kind=args.kind, state=args.state)
    if args.json:
        print(json.dumps([entry_to_json(e) for e in rows], indent=2, sort_keys=True))
        return EXIT_OK
    if not rows:
        print("(no matching registry entries)")
        return EXIT_OK
    width = max(len(e.id) for e in rows)
    print(f"{'ID'.ljust(width)}  FAMILY  STATE      KIND               SURFACES  LAST-ROTATED")
    for e in rows:
        last = str(e.rotation.get("last", "-"))
        print(
            f"{e.id.ljust(width)}  {e.family:<6}  {e.state:<9}  {e.kind:<17}  "
            f"{len(e.surfaces):<8}  {last}"
        )
    print(f"\n{len(rows)} entr{'y' if len(rows) == 1 else 'ies'}.")
    return EXIT_OK


def _cmd_show(args: argparse.Namespace, reg: Registry | None, _ctx: Context | None) -> int:
    if args.value:
        return _show_value(args)
    reg = _require_registry(reg)
    entry = reg.get(args.id)
    if entry is None:
        raise UsageError(f"unknown id {args.id!r} — run `mgf-secrets list` to see ids")
    if args.json:
        print(json.dumps(entry_to_json(entry), indent=2, sort_keys=True))
        return EXIT_OK
    print(f"id:        {entry.id}")
    print(f"family:    {entry.family}")
    print(f"kind:      {entry.kind}")
    print(f"state:     {entry.state}")
    print(f"purpose:   {entry.purpose}")
    print(
        f"policy:    allowed_repos={entry.policy.allowed_repos!r} "
        f"blast_radius={entry.policy.blast_radius!r}"
    )
    print(f"rotation:  {dict(entry.rotation)}")
    print(f"verify:    {dict(entry.verify)}")
    print(f"recovery:  {entry.recovery}")
    print(f"consumers: {list(entry.consumers)}")
    print("surfaces:")
    for s in entry.surfaces:
        print(f"  - {s.type}: {s.data}")
    return EXIT_OK


def _show_value(args: argparse.Namespace) -> int:
    """``show <id> --value`` — the ONE metadata verb that may surface a value.

    Refuses a TTY (values off screens/transcripts) unless ``--tty-ok``; never with
    ``--json``; single value only. Writes raw bytes to stdout for ``$(…)``/pipes.
    """
    if args.json:
        raise UsageError("`show --value` emits a bare value, not JSON — drop --json")
    stdout = sys.stdout
    is_tty = bool(getattr(stdout, "isatty", lambda: False)())
    if is_tty and not args.tty_ok:
        raise UsageError(
            "refusing to print a secret value to a TTY (it would land on your screen "
            "and in the transcript) — pipe it, or pass --tty-ok to override"
        )
    root = store_mod.store_root(args.store, fed_root=args.root)
    value = store_mod.store_get(args.id, root=root, identity_path=args.identity)
    buf = getattr(stdout, "buffer", None)
    if buf is not None:
        buf.write(value)
        buf.flush()
    else:  # a text stream (tests capture via StringIO) — decode losslessly.
        stdout.write(value.decode("utf-8", "surrogateescape"))
        stdout.flush()
    return EXIT_OK


def _cmd_doctor(args: argparse.Namespace, reg: Registry | None, ctx: Context | None) -> int:
    reg = _require_registry(reg)
    surfaces = set(args.surface) if args.surface else set(DEFAULT_SURFACES)
    if ctx is None:
        ctx = build_context(surfaces, root=args.root, best_effort=False)
    doctor = Doctor(
        reg, fed_root=ctx.fed_root, woodpecker=ctx.woodpecker, forge=ctx.forge, hosts=ctx.hosts
    )
    report = doctor.run(sorted(surfaces))
    if args.json:
        print(json.dumps(report.to_json(), indent=2, sort_keys=True))
        return report.exit_code()
    findings = report.sorted_findings()
    unverifiable = report.sorted_unverifiable()
    print(f"doctor — surfaces: {', '.join(report.surfaces)}")
    if not findings and not unverifiable:
        print("clean — declared == deployed == consumed across the checked surfaces.")
    elif findings:
        current = None
        for f in findings:
            if f.severity != current:
                current = f.severity
                print(f"\n[{f.severity.upper()}]")
            print(f"  {f.check} {f.id}: {f.finding}")
        print(f"\n{len(findings)} finding(s).")
    if unverifiable:
        print("\n[UNVERIFIABLE] (could not check — distinct from clean; exit 1)")
        for u in unverifiable:
            print(f"  {u.check} {u.target}: {u.reason}")
        print(f"\n{len(unverifiable)} unverifiable.")
    if report.skipped:
        print("\nskipped:")
        for s in report.skipped:
            print(f"  - {s}")
    return report.exit_code()


def _cmd_probe(args: argparse.Namespace, reg: Registry | None, ctx: Context | None) -> int:
    reg = _require_registry(reg)
    entry = reg.get(args.id)
    if entry is None:
        raise UsageError(f"unknown id {args.id!r}")
    if ctx is None:
        ctx = build_context(set(ALL_SURFACES), root=args.root, best_effort=True)
    result = run_probe(entry, ctx)
    if args.json:
        print(json.dumps(result.to_json(), indent=2, sort_keys=True))
    else:
        print(f"{result.status.upper():<8} {result.id} ({result.kind}): {result.message}")
        if result.evidence:
            print(f"  evidence: {result.evidence}")
    return result.exit_code()


# --------------------------------------------------------------------------- #
# P2 value verbs.
# --------------------------------------------------------------------------- #


def _read_value_input(args: argparse.Namespace, entry_id: str) -> bytes:
    """Value from ``--value-file PATH`` / ``--value-file -`` (stdin) / hidden prompt.

    NEVER from argv (``--value X`` is trapped by the parser). AG-18: the value is
    returned as bytes and never logged.
    """
    if getattr(args, "value", None) is not None:  # the argv trap
        raise UsageError(
            "a secret value must never be passed on argv (it lands in ps/shell history) — "
            "use --value-file PATH, --value-file - (stdin), or the hidden prompt"
        )
    src = args.value_file
    if src == "-":
        data = (
            sys.stdin.buffer.read() if hasattr(sys.stdin, "buffer") else sys.stdin.read().encode()
        )
        return data[:-1] if data.endswith(b"\n") else data
    if src is not None:
        path = Path(src)
        if not path.is_file():
            raise UsageError(f"--value-file {src!r} not found")
        return path.read_bytes()
    # Hidden interactive prompt (no echo). getpass strips the trailing newline.
    entered = getpass.getpass(f"value for {entry_id} (hidden): ")
    return entered.encode("utf-8")


def _cmd_add_set(args: argparse.Namespace, reg: Registry | None, _ctx: Context | None) -> int:
    reg = _require_registry(reg)
    entry = reg.get(args.id)
    if entry is None:
        raise UsageError(
            f"unknown id {args.id!r} — declare it in SECRETS.toml first, then `add`. "
            "Scaffold: [<id>] with family/kind/purpose/surfaces([{type='store'},…])/"
            "policy/rotation/verify/recovery/state (see the kind template in an existing entry)"
        )
    if entry.state == "held":
        raise UsageError(
            f"{args.id!r} is state=held — the founder's hold forbids storing a value for it; "
            "un-hold deliberately in the registry first (never by arming)"
        )
    root = store_mod.store_root(args.store, fed_root=args.root)
    exists = store_mod.cipher_path(root, args.id).is_file()
    verb = args.verb
    if verb == "add" and exists:
        raise UsageError(f"{args.id!r} already has a stored value — use `set` to update/rotate it")
    if verb == "set" and not exists:
        raise UsageError(f"{args.id!r} has no stored value yet — use `add` to create it")

    value = _read_value_input(args, args.id)
    recipients = store_mod.load_recipients(root)
    stamped = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
    row = store_mod.store_put(
        args.id,
        value,
        root=root,
        encrypted_at=stamped,
        recipients=recipients,
        family=entry.family,
        kind=entry.kind,
    )
    # Report metadata only — never the value.
    summary = {
        "id": args.id,
        "action": "stored" if verb == "add" else "updated",
        "file": row["file"],
        "sha256": row["sha256"],
        "recipients": len(recipients),
        "encrypted_at": row["encrypted_at"],
    }
    if args.json:
        print(json.dumps(summary, indent=2, sort_keys=True))
    else:
        print(
            f"{summary['action']} {args.id} → {row['file']} "
            f"(encrypted to {len(recipients)} recipients; sha256 {row['sha256'][:12]}…)"
        )
    return EXIT_OK


def _parse_with_spec(spec: str) -> tuple[str, str]:
    """``id`` or ``id:ENV_VAR`` → (id, env_var). Default env var = ``ID`` (uppercased)."""
    if ":" in spec:
        entry_id, env = spec.split(":", 1)
        if not entry_id or not env:
            raise UsageError(f"--with {spec!r} malformed — want id or id:ENV_VAR")
        return entry_id, env
    return spec, spec.upper().replace("-", "_")


def _cmd_exec(args: argparse.Namespace, _reg: Registry | None, _ctx: Context | None) -> int:
    """Run a child with store values injected as env vars — nothing on disk/argv/log."""
    cmd = list(args.cmd)
    if cmd and cmd[0] == "--":
        cmd = cmd[1:]
    if not cmd:
        raise UsageError("exec needs a command after `--`: mgf-secrets exec --with ID -- CMD …")
    if not args.with_:
        raise UsageError("exec needs at least one --with ID[:ENV_VAR]")
    root = store_mod.store_root(args.store, fed_root=args.root)
    import os

    child_env = dict(os.environ)
    for spec in args.with_:
        entry_id, env_var = _parse_with_spec(spec)
        value = store_mod.store_get(entry_id, root=root, identity_path=args.identity)
        # Inject as a str env var (values only in the child's environment).
        child_env[env_var] = value.decode("utf-8", "surrogateescape")
    try:
        completed = subprocess.run(cmd, env=child_env, check=False)  # noqa: S603 - declared argv, no shell
    except FileNotFoundError:
        raise UsageError(f"exec: command not found: {cmd[0]!r}") from None
    return completed.returncode


def _cmd_materialize(args: argparse.Namespace, reg: Registry | None, _ctx: Context | None) -> int:
    reg = _require_registry(reg)
    from .registry import fed_root as _fed_root

    fed = _fed_root(args.root)
    root = store_mod.store_root(args.store, fed_root=args.root)
    items = mtz.plan(reg, fed_root=fed, root=root, only=args.only)
    if args.apply:
        result = mtz.apply(items, fed_root=fed, root=root, identity_path=args.identity)
        if args.json:
            print(json.dumps(result.to_json(), indent=2, sort_keys=True))
        else:
            print(f"materialize --apply: wrote {len(result.written)} bare-value file(s)")
            for wid in sorted(result.written):
                print(f"  wrote  {wid}")
            for hid in sorted(result.refused_held):
                print(f"  held   {hid} (refused — never materialized)")
            for sid in sorted(result.skipped_no_store):
                print(f"  skip   {sid} (no stored value yet — `add` it first)")
            if result.leftovers:
                print("  leftovers (NOT deleted — prune deliberately):")
                for p in sorted(result.leftovers):
                    print(f"    {p}")
        return EXIT_OK
    # --plan (default): key-free, zero values, zero writes.
    if args.json:
        print(json.dumps([pi.to_json() for pi in items], indent=2, sort_keys=True))
        return EXIT_OK
    pending = [pi for pi in items if pi.pending]
    print(f"materialize --plan: {len(items)} declared local-file target(s), {len(pending)} pending")
    for pi in items:
        print(f"  {pi.status:<8} {pi.id}  →  {pi.target}  ({pi.declared_mode})")
    if not any(pi.store_present for pi in items):
        print("\n(store is empty for these ids — `add` values, then `materialize --apply`)")
    return EXIT_OK


def _cmd_export_offline(
    args: argparse.Namespace, _reg: Registry | None, _ctx: Context | None
) -> int:
    """Bundle the whole store into ONE age file encrypted to the founder key only."""
    out = args.out
    if out in ("-", "/dev/stdout"):
        raise UsageError("export-offline refuses to print the bundle — pass a real --out PATH")
    root = store_mod.store_root(args.store, fed_root=args.root)
    founder = store_mod.founder_recipient(root)
    # Tar the store's committed artifacts in-memory, then age-encrypt to the founder.
    blob = io.BytesIO()
    members = 0
    with tarfile.open(fileobj=blob, mode="w") as tar:
        for name in ("manifest.json", "recipients.txt"):
            p = Path(root) / name
            if p.is_file():
                tar.add(p, arcname=name)
                members += 1
        sdir = store_mod.secrets_dir(root)
        if sdir.is_dir():
            for age_file in sorted(sdir.glob("*.age")):
                tar.add(age_file, arcname=f"secrets/{age_file.name}")
                members += 1
    bundle = store_mod.encrypt(blob.getvalue(), [founder])
    outp = Path(out)
    import os

    fd = os.open(outp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    try:
        os.write(fd, bundle)
    finally:
        os.close(fd)
    summary = {
        "out": str(outp),
        "bytes": len(bundle),
        "members": members,
        "recipient": "founder",
    }
    if args.json:
        print(json.dumps(summary, indent=2, sort_keys=True))
    else:
        print(
            f"export-offline: wrote {len(bundle)} bytes ({members} member(s)) to {outp}, "
            "encrypted to the founder recipient only"
        )
    return EXIT_OK


def _cmd_history(args: argparse.Namespace, _reg: Registry | None, _ctx: Context | None) -> int:
    """Rotation/audit trail for ``<id>.age`` from the store's git log — metadata only."""
    root = store_mod.store_root(args.store, fed_root=args.root)
    rel = f"secrets/{args.id}.age"
    try:
        proc = subprocess.run(  # noqa: S603 - fixed argv, no shell
            ["git", "-C", str(root), "log", "--format=%H%x09%aI%x09%s", "--", rel],  # noqa: S607
            capture_output=True,
            text=True,
            check=False,
        )
    except FileNotFoundError:
        raise CredentialError("git not found — history needs git to read the store log") from None
    if proc.returncode != 0:
        raise CredentialError(
            f"cannot read git history for {rel} in {root} "
            f"(is it a git checkout?): {proc.stderr.strip()}"
        )
    rows = []
    for line in proc.stdout.splitlines():
        parts = line.split("\t", 2)
        if len(parts) == 3:
            rows.append({"commit": parts[0], "date": parts[1], "subject": parts[2]})
    if args.json:
        print(json.dumps({"id": args.id, "file": rel, "history": rows}, indent=2, sort_keys=True))
        return EXIT_OK
    if not rows:
        print(f"no history for {rel} (never committed to the store)")
        return EXIT_OK
    print(f"history — {rel} ({len(rows)} revision(s), newest first):")
    for r in rows:
        print(f"  {r['commit'][:12]}  {r['date']}  {r['subject']}")
    return EXIT_OK


# --------------------------------------------------------------------------- #
# P3 verbs — sync (the reconciler) + retire (state flip).
# --------------------------------------------------------------------------- #


def _default_canary(root: str | None) -> Callable[[], sync_mod.CanaryResult]:
    """The playbook's sooth canary: shell out to ``ping_sooth_ci.py`` if present
    (triggers + polls the sooth pipelines). A real surface-1 apply must run it."""
    from .registry import fed_root as _fed_root

    script = _fed_root(root) / "mgf-standard" / "scripts" / "ping_sooth_ci.py"

    def run() -> sync_mod.CanaryResult:
        if not script.is_file():
            return sync_mod.CanaryResult(False, f"canary script not found at {script}")
        proc = subprocess.run(  # noqa: S603 - fixed argv (sys.executable), no shell
            [sys.executable, str(script)],
            capture_output=True,
            text=True,
            check=False,
        )
        tail = (proc.stdout or proc.stderr).strip().splitlines()[-1:] or [""]
        return sync_mod.CanaryResult(proc.returncode == 0, tail[0])

    return run


def _sync_surfaces(args: argparse.Namespace) -> set[str]:
    return set(args.surface) if args.surface else set(sync_mod.DEFAULT_SYNC_SURFACES)


def _cmd_sync(args: argparse.Namespace, reg: Registry | None, ctx: Context | None) -> int:
    reg = _require_registry(reg)
    surfaces = _sync_surfaces(args)
    if ctx is None:
        ctx = build_context(surfaces, root=args.root, best_effort=False)
    store_root = store_mod.store_root(args.store, fed_root=args.root)
    canary = None if args.skip_canary else _default_canary(args.root)
    rec = sync_mod.Reconciler(
        reg,
        fed_root=ctx.fed_root,
        store_root=store_root,
        identity_path=args.identity,
        woodpecker=ctx.woodpecker,
        forge=ctx.forge,
        hosts=ctx.hosts,
        canary=canary,
    )
    if args.apply:
        report = rec.apply(
            sorted(surfaces), allow_retire=args.allow_retire, skip_canary=args.skip_canary
        )
        if args.json:
            print(json.dumps(report.to_json(), indent=2, sort_keys=True))
        else:
            _print_apply(report)
        if report.failed:
            return EXIT_ERROR
        if report.playbook and report.playbook.canary_ok is False:
            return EXIT_FINDINGS
        return EXIT_OK
    plan = rec.plan(sorted(surfaces))
    if args.json:
        print(json.dumps(plan.to_json(), indent=2, sort_keys=True))
        return EXIT_OK
    _print_plan(plan)
    return EXIT_OK


def _print_plan(plan: sync_mod.SyncPlan) -> None:
    print(f"sync --plan — surfaces: {', '.join(plan.surfaces)}")
    actions = plan.sorted_actions()
    if not actions:
        print("empty — declared == deployed across the checked surfaces (nothing to do).")
    else:
        print(f"\n{len(actions)} action(s) (value-free):")
        for a in actions:
            print(f"  {a.line()}")
            if a.detail:
                print(f"      {a.detail}")
    if plan.notes:
        print("\nnotes:")
        for n in plan.notes:
            print(f"  [{n.kind}] {n.id}: {n.message}")


def _print_apply(report: sync_mod.ApplyReport) -> None:
    print(f"sync --apply — surfaces: {', '.join(report.surfaces)}")
    print(f"applied {len(report.applied)} action(s):")
    for a in report.applied:
        print(f"  {a.line()}")
    for f in report.failed:
        print(f"  FAILED {f['op']} {f['target']}: {f['error']}")
    if report.playbook:
        pb = report.playbook
        print("\ncontract-surface playbook (org secret touched):")
        print(f"  scope audit: {'clean' if pb.audit_clean else 'RESIDUAL DRIFT'}")
        print(f"  reminder:    {pb.reminder}")
        if pb.canary_ran:
            print(f"  sooth canary: {'GREEN' if pb.canary_ok else 'RED'} — {pb.canary_detail}")
        else:
            print(f"  sooth canary: not run — {pb.canary_detail}")


def _cmd_retire(args: argparse.Namespace, reg: Registry | None, _ctx: Context | None) -> int:
    reg = _require_registry(reg)
    from .registry import fed_root as _fed_root

    fed = _fed_root(args.root)
    store_root = store_mod.store_root(args.store, fed_root=args.root)
    if args.apply:
        result = retire_mod.apply_retire(
            reg,
            args.id,
            fed_root=fed,
            toml_paths=default_paths(args.root),
            store_root=store_root,
            allow_retire=args.allow_retire,
        )
        if args.json:
            print(json.dumps(result.to_json(), indent=2, sort_keys=True))
        else:
            print(f"retire --apply: {result.id} {result.from_state} → {result.to_state}")
            print(f"  state flipped: {result.state_flipped} ({result.toml_file})")
            print(f"  store removed: {result.store_removed}")
        return EXIT_OK
    plan = retire_mod.plan_retire(reg, args.id, fed_root=fed, store_root=store_root)
    if args.json:
        print(json.dumps(plan.to_json(), indent=2, sort_keys=True))
        return EXIT_OK
    print(f"retire --plan: {plan.id} {plan.from_state} → {plan.to_state}")
    print(f"  store present: {plan.store_present}")
    if plan.references:
        print(f"  live references: {', '.join(plan.references)}")
    if plan.refused:
        print(f"  REFUSED: {plan.refusal}")
    else:
        print("  ready — `retire <id> --apply --allow-retire` to complete.")
    return EXIT_OK


_VERBS = {
    "list": _cmd_list,
    "show": _cmd_show,
    "doctor": _cmd_doctor,
    "probe": _cmd_probe,
    "add": _cmd_add_set,
    "set": _cmd_add_set,
    "exec": _cmd_exec,
    "materialize": _cmd_materialize,
    "export-offline": _cmd_export_offline,
    "history": _cmd_history,
    "sync": _cmd_sync,
    "retire": _cmd_retire,
}


# --------------------------------------------------------------------------- #
# Parser + entry point.
# --------------------------------------------------------------------------- #


def _add_value_input_args(p: argparse.ArgumentParser) -> None:
    p.add_argument(
        "--value-file",
        metavar="PATH",
        help="read the value from PATH, or '-' for stdin (bare value; NEVER argv)",
    )
    # The argv trap: `--value X` is explicitly recognised so it can be REJECTED with a
    # pointed message (rather than a bare argparse 'unrecognized argument').
    p.add_argument("--value", help=argparse.SUPPRESS)


def _build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(
        prog="mgf-secrets",
        description="Foundation credential registry + doctor + encrypted store (P2).",
    )
    p.add_argument("--version", action="version", version=f"mgf-secrets {__version__}")
    p.add_argument("--root", default=None, help="federation root ($MGF_ROOT / ~/PycharmProjects)")
    p.add_argument(
        "--store",
        default=None,
        help="store checkout ($MGF_SECRETS_STORE / <root>/mgf-secrets-store)",
    )
    sub = p.add_subparsers(dest="verb", required=True)

    lst = sub.add_parser("list", help="registry table (metadata)")
    lst.add_argument("--family")
    lst.add_argument("--kind")
    lst.add_argument("--state")
    lst.add_argument("--json", action="store_true")

    show = sub.add_parser("show", help="full metadata for one entry, or --value for the bare value")
    show.add_argument("id")
    show.add_argument("--json", action="store_true")
    show.add_argument(
        "--value", action="store_true", help="emit the bare value (refuses a TTY without --tty-ok)"
    )
    show.add_argument("--tty-ok", action="store_true", help="allow --value to write to a TTY")
    show.add_argument("--identity", help="decrypting key (default: the operator key)")

    doc = sub.add_parser("doctor", help="declared == deployed == consumed (read-only)")
    doc.add_argument(
        "--surface",
        action="append",
        choices=list(ALL_SURFACES),
        help="restrict/opt-in surfaces (default: woodpecker + local)",
    )
    doc.add_argument("--json", action="store_true")

    prb = sub.add_parser("probe", help="one entry's liveness probe")
    prb.add_argument("id")
    prb.add_argument("--json", action="store_true")

    for name in ("add", "set"):
        ap = sub.add_parser(
            name,
            help=f"{'store a new' if name == 'add' else 'update the'} value (file/stdin/prompt)",
        )
        ap.add_argument("id")
        _add_value_input_args(ap)
        ap.add_argument("--json", action="store_true")

    ex = sub.add_parser("exec", help="run a child with store values injected as env vars")
    ex.add_argument(
        "--with",
        dest="with_",
        action="append",
        metavar="ID[:ENV_VAR]",
        help="inject secret ID as $ENV_VAR (default $ID); repeatable",
    )
    ex.add_argument("--identity", help="decrypting key (default: the operator key)")
    ex.add_argument("cmd", nargs=argparse.REMAINDER, help="-- COMMAND [ARGS…]")

    mz = sub.add_parser("materialize", help="decrypt the store into the bare-value my_stuff/ cache")
    mz.add_argument(
        "--only", action="append", metavar="ID", help="restrict to these ids; repeatable"
    )
    mz.add_argument(
        "--apply", action="store_true", help="write files (default is --plan, key-free)"
    )
    mz.add_argument("--plan", action="store_true", help="show the plan (default; explicit is fine)")
    mz.add_argument("--identity", help="decrypting key for --apply (default: the operator key)")
    mz.add_argument("--json", action="store_true")

    eo = sub.add_parser(
        "export-offline", help="ONE age bundle (founder key) for the password manager"
    )
    eo.add_argument(
        "--out", required=True, metavar="PATH", help="write the bundle here (never stdout)"
    )
    eo.add_argument("--json", action="store_true")

    hi = sub.add_parser("history", help="git rotation/audit trail for <id>.age (metadata only)")
    hi.add_argument("id")
    hi.add_argument("--json", action="store_true")

    sy = sub.add_parser("sync", help="reconcile declared → deployed per surface (plan-first)")
    sy.add_argument(
        "--surface",
        action="append",
        choices=list(sync_mod.ALL_SYNC_SURFACES),
        help="restrict surfaces (default: all — woodpecker/forge/hosts/local)",
    )
    sy.add_argument("--apply", action="store_true", help="execute the plan (default is --plan)")
    sy.add_argument("--plan", action="store_true", help="show the plan (default; explicit is fine)")
    sy.add_argument(
        "--allow-retire",
        action="store_true",
        help="permit removals (P3 sync is forward-only; retirement is the `retire` verb)",
    )
    sy.add_argument(
        "--skip-canary",
        action="store_true",
        help="skip the playbook sooth canary (dry/local only — a real surface-1 apply must run it)",
    )
    sy.add_argument("--identity", help="decrypting key for value/key injection on --apply")
    sy.add_argument("--json", action="store_true")

    rt = sub.add_parser("retire", help="state-flip a credential active→retiring (completion-gated)")
    rt.add_argument("id")
    rt.add_argument("--apply", action="store_true", help="perform the flip + store removal")
    rt.add_argument(
        "--allow-retire", action="store_true", help="required for --apply (completion gate)"
    )
    rt.add_argument("--json", action="store_true")
    return p


def main(
    argv: Sequence[str] | None = None,
    *,
    registry: Registry | None = None,
    context: Context | None = None,
) -> int:
    parser = _build_parser()
    args = parser.parse_args(argv)
    try:
        reg: Registry | None
        if registry is not None:
            reg = registry
        elif args.verb in _STORE_ONLY_VERBS:
            reg = None
        else:
            reg = load(root=args.root)
        return _VERBS[args.verb](args, reg, context)
    except RegistryError as exc:
        print("mgf-secrets: registry failed to validate (load error):", file=sys.stderr)
        for problem in exc.problems:
            print(f"  - {problem}", file=sys.stderr)
        return EXIT_ERROR
    except (CredentialError, UsageError) as exc:
        print(f"mgf-secrets: {exc}", file=sys.stderr)
        return EXIT_ERROR
    except SecretsError as exc:  # pragma: no cover - defensive catch-all
        print(f"mgf-secrets: {exc}", file=sys.stderr)
        return EXIT_ERROR


if __name__ == "__main__":  # pragma: no cover
    raise SystemExit(main())
