"""Per-group / per-verb `--help` — rendered from the verb grammar itself.

The whole point: an agent must never have to PROBE how to pass an arg. So help is
generated from the SAME tables `to_op` dispatches through (`verbs.POS`,
`verbs.PRIMARY_JSON`, `verbs.SHORT`, the coercion sets) — it cannot drift from what
the CLI actually does. The one thing the client can't know is the server's
authoritative required/optional list, so every body points at
`.runner/docs/commands.md` (run `runner docs pull`) and the `r op <name>` raw form.

`dispatch_help(cmd, rest)` returns rendered text, or None when the command isn't one
we document (so `__main__` falls through to normal handling — no regression).
"""
from . import verbs

PROG = "r"  # the docs lead with `r`; `runner` / `acsetra` are aliases of the same CLI

# --- friendly plane groups: plane -> [(subverb, op_name), …] ------------------
GROUPS = {
    "set": [("create", "set.create"), ("put", "set.put"),
            ("declare_schema", "set.declare_schema"), ("fetch_url", "set.fetch_url"),
            ("remove_row", "set.remove_row"), ("drop", "set.drop"),
            ("list", "set.list"), ("describe", "set.describe"),
            ("rows", "set.rows"), ("validate", "set.validate")],
    "css": [("init", "css.init"), ("set_class", "css.set_class"),
            ("set_rule", "css.set_rule"), ("clear_rule", "css.clear_rule"),
            ("attach", "css.attach"), ("detach", "css.detach"),
            ("list_classes", "css.list_classes"), ("list_rules", "css.list_rules"),
            ("list_attachments", "css.list_attachments"), ("describe", "css.describe"),
            ("resolve", "css.resolve"), ("validate", "css.validate")],
    "asset": [("init", "asset.init"), ("add", "asset.add"), ("attach", "asset.attach"),
              ("detach", "asset.detach"), ("list", "asset.list"),
              ("list_attachments", "asset.list_attachments"), ("describe", "asset.describe"),
              ("resolve", "asset.resolve"), ("validate", "asset.validate"),
              ("check", "asset.check")],
    "behavior": [("init", "behavior.init"), ("add", "behavior.add"),
                 ("set_enabled", "behavior.set_enabled"), ("remove", "behavior.remove"),
                 ("attach", "behavior.attach"),
                 ("set_attachment_enabled", "behavior.set_attachment_enabled"),
                 ("detach", "behavior.detach"), ("list", "behavior.list"),
                 ("list_attachments", "behavior.list_attachments"),
                 ("describe", "behavior.describe"), ("validate", "behavior.validate")],
    "component": [("init", "component.init"), ("set", "component.set"),
                  ("remove", "component.remove"), ("set_route", "component.set_route"),
                  ("remove_route", "component.remove_route"), ("list", "component.list"),
                  ("list_routes", "component.list_routes"), ("describe", "component.describe"),
                  ("validate", "component.validate")],
    "head": [("init", "head.init"), ("set", "head.set"), ("remove", "head.remove"),
             ("list", "head.list"), ("validate", "head.validate")],
}

# Friendly subverb aliases the dispatcher accepts (mirrors verbs.to_op).
GROUP_ALIASES = {"css": {"class": "set_class", "rule": "set_rule"}}

# --- per-op annotation: one-line desc + the client-known optional flags --------
# kw mirrors the server OPS table (prod_api.py); it is a HINT — commands.md is
# authoritative. `body` marks ops that take a `-b <code|->` body.
OPS_DOC = {
    "set.create": {"desc": "create a value set (mint your own plane/registry)",
                   "kw": ["label", "kind", "schema"]},
    "set.put": {"desc": "write/merge a row (--json is the row meta; MERGES unless --replace)",
                "kw": ["label", "ordinal", "enabled", "replace"]},
    "set.declare_schema": {"desc": "declare/replace a set's row-meta JSON-Schema contract", "kw": []},
    "set.fetch_url": {"desc": "import remote TEXT into a row (SSRF-guarded https GET)",
                      "kw": ["key", "label", "max_bytes", "allow_hosts", "replace", "ordinal"]},
    "set.remove_row": {"desc": "delete one row from a set", "kw": []},
    "set.drop": {"desc": "drop a whole value set", "kw": ["force"]},
    "set.list": {"desc": "list value sets at the scope", "kw": []},
    "set.describe": {"desc": "show a set's schema + metadata", "kw": []},
    "set.rows": {"desc": "list a set's rows", "kw": []},
    "set.validate": {"desc": "re-check every set (grammar, bounds, declared schema)", "kw": []},

    "css.init": {"desc": "bootstrap the css plane for a scope", "kw": []},
    "css.set_class": {"desc": "create/define a css class (identity)",
                      "kw": ["label", "description", "enabled", "ordinal"]},
    "css.set_rule": {"desc": "set a class's declarations for a variant (--json is the decls)",
                     "kw": ["ordinal"]},
    "css.clear_rule": {"desc": "clear a class's rule (one variant or all)", "kw": ["variant"]},
    "css.attach": {"desc": "attach a class to a surface (component:/instance:/part:/raw:/page:)",
                   "kw": ["label", "ordinal"]},
    "css.detach": {"desc": "remove a css attachment", "kw": []},
    "css.list_classes": {"desc": "list css classes", "kw": []},
    "css.list_rules": {"desc": "list css rules", "kw": ["class_code"]},
    "css.list_attachments": {"desc": "list css attachments", "kw": []},
    "css.describe": {"desc": "show a class's rules + attachments", "kw": []},
    "css.resolve": {"desc": "show the resolved cascade for a page", "kw": ["app"]},
    "css.validate": {"desc": "re-check every class/rule/attachment", "kw": []},

    "asset.init": {"desc": "bootstrap the asset plane", "kw": []},
    "asset.add": {"desc": "register a media asset (image/svg/icon/video/iframe)",
                  "kw": ["url", "name", "alt", "provider", "svg", "fit", "controls",
                         "muted", "autoplay", "poster", "ordinal", "storage", "probe"]},
    "asset.attach": {"desc": "fill a named slot on a target with an asset",
                     "kw": ["page", "component", "raw", "type_", "part", "app", "ordinal"]},
    "asset.detach": {"desc": "remove an asset attachment", "kw": []},
    "asset.list": {"desc": "list assets", "kw": []},
    "asset.list_attachments": {"desc": "list asset attachments", "kw": []},
    "asset.describe": {"desc": "show an asset + its attachments", "kw": []},
    "asset.resolve": {"desc": "show resolved asset slots for a page", "kw": ["app"]},
    "asset.validate": {"desc": "re-check every asset/attachment (shape only)", "kw": []},
    "asset.check": {"desc": "probe registered asset URLs (HEAD) for liveness",
                    "kw": ["code", "allow_hosts"]},

    "behavior.init": {"desc": "bootstrap the behavior plane", "kw": []},
    "behavior.add": {"desc": "add a browser ability (mount(ctx) JS body via -b -)",
                     "kw": ["label", "config_schema", "enabled", "ordinal"], "body": True},
    "behavior.set_enabled": {"desc": "enable/disable a behavior definition", "kw": []},
    "behavior.remove": {"desc": "remove a behavior", "kw": []},
    "behavior.attach": {"desc": "mount a behavior on a target (--json is the config)",
                        "kw": ["label", "enabled", "ordinal"]},
    "behavior.set_attachment_enabled": {"desc": "enable/disable one placed mount", "kw": []},
    "behavior.detach": {"desc": "remove a behavior attachment", "kw": []},
    "behavior.list": {"desc": "list behaviors", "kw": []},
    "behavior.list_attachments": {"desc": "list behavior attachments", "kw": []},
    "behavior.describe": {"desc": "show a behavior + its mounts", "kw": []},
    "behavior.validate": {"desc": "re-check every behavior/attachment", "kw": []},

    "component.init": {"desc": "bootstrap the component+route plane", "kw": []},
    "component.set": {"desc": "author a component's node tree (--json is the tree)",
                      "kw": ["parts", "label", "ordinal"]},
    "component.remove": {"desc": "remove a component", "kw": []},
    "component.set_route": {"desc": "mount a component on a page (--json is route props)",
                            "kw": ["instance", "label", "ordinal"]},
    "component.remove_route": {"desc": "remove a route", "kw": []},
    "component.list": {"desc": "list components", "kw": []},
    "component.list_routes": {"desc": "list routes (pages)", "kw": []},
    "component.describe": {"desc": "show a component's refs + mounting routes", "kw": []},
    "component.validate": {"desc": "re-check every tree + ref", "kw": []},

    "head.init": {"desc": "bootstrap document_head (title/viewport/favicon)", "kw": []},
    "head.set": {"desc": "set/merge a <head> entry (--json is its attrs)",
                 "kw": ["tag", "text", "enabled", "ordinal", "label"]},
    "head.remove": {"desc": "remove a head entry", "kw": []},
    "head.list": {"desc": "list head entries in order", "kw": []},
    "head.validate": {"desc": "re-check every head entry", "kw": []},
}

# --- non-plane / session verbs ------------------------------------------------
# usage is shown verbatim; extra is appended as-is (e.g. the worker tier matrix).
VERBS_DOC = {
    # session / client-only
    "signin": {"usage": "signin [--api URL] [--no-browser]", "desc": "device-flow sign-in; caches a token"},
    "whoami": {"usage": "whoami", "desc": "show the signed-in user + workspace"},
    "workspaces": {"usage": "workspaces [--use SLUG]", "desc": "list workspaces; --use switches the active one"},
    "use": {"usage": "use <slug>", "desc": "set the active workspace for this project"},
    "logout": {"usage": "logout", "desc": "forget the cached token"},
    "usage": {"usage": "usage [--days N]", "desc": "metered usage vs your plan caps"},
    "tokens": {"usage": "tokens [new [--name N] | list | revoke <id>]", "desc": "manage API tokens"},
    "docs": {"usage": "docs pull [-S scope]", "desc": "refresh CLAUDE.md + .runner/docs/* (this app's context)"},
    "dev": {"usage": "dev <app> [--no-browser]", "desc": "open your app's hosted surface in the browser"},
    "checkout": {"usage": "checkout <app> [-o dir] [--force]",
                 "desc": "materialize a scope subtree as local files in .tmp/ (read-only; grep/edit, write back with the typed verbs)"},
    # authoring (non-plane)
    "app": {"usage": "app create <name> [--label L]  |  app list", "desc": "create / list app scopes"},
    "apps": {"usage": "apps", "desc": "list app scopes (alias of `app list`)"},
    "auth": {"usage": "auth <app> [--off]  |  auth list [scope]",
             "desc": "gate an app behind pen.acsetra.com sign-in — signed-in visitors are "
                     "enrolled as members on first visit; --off lifts the gate (no secrets, "
                     "reuses the main-site realm)"},
    "ls": {"usage": "ls [-S scope]", "desc": "list value sets"},
    "inspect": {"usage": "inspect [<set>] [-S scope]", "desc": "read a set's rows (alias: i); no set => list"},
    "read": {"usage": "read <code> [-S scope]", "desc": "read a value set (me_* = your own per-user zone)"},
    "show": {"usage": "show <pipeline>", "desc": "reconstruct a pipeline from its rows"},
    "run": {"usage": "run <pipeline> -i '{json}'", "desc": "test-run a pipeline with input"},
    "runs": {"usage": "runs <launch_id>", "desc": "poll a launch's status/output"},
    "compile": {"usage": "compile <app>", "desc": "link the bundle + cross-plane diagnostics + content hash"},
    "doctor": {"usage": "doctor <app>", "desc": "full static gate over the subtree: validate every plane + link refs + compile code (non-zero exit = broken)"},
    "pipe": {"usage": "pipe <code> -S <scope> [-w code:label] [--rate N] [--timeout N] [--keep]\n"
                      "       pipe show <code>  |  pipe list  |  pipe remove <code>",
             "desc": "create/upsert a pipeline envelope (resets workers unless --keep)"},
    "pipes": {"usage": "pipes", "desc": "list pipelines (alias of `pipe list`)"},
    "w": {"usage": "w <pipeline> <tier> [flags]  [-w code:label] [--at N] [-S scope]",
          "desc": "add a worker to a pipeline (alias: work)",
          "extra": [
              "Tiers:",
              "  snippet   -b <code|->                       DB-isolated Python (sets output / output={'rows':[…]})",
              "  internal  -H/--handler NAME                 a vetted internal handler",
              "  httpx     -u/--url URL [-X METHOD]          call a service; keys injected server-side",
              "            [-k/--keys a,b] [-f/--body-from K]",
              "  read      --set CODE [--mode latest]        resolve a value set into the run data",
              "            [--key K] [--as NAME] [--read-scope S]",
              "  llm       [--provider openai] [--model M]   one model call (keys injected server-side)",
              "            [--system-from K] [--user-from ask] [-k/--keys a,b]",
          ]},
    "op": {"usage": "op <name> [--json '{full args bag}'] [--flag value] [-S scope]",
           "desc": "call any op directly. Here --json IS the full args bag (never a single arg)."},
}

# verb aliases -> canonical key in VERBS_DOC
VERB_ALIASES = {"i": "inspect", "work": "w", "login": "signin", "signout": "logout", "co": "checkout"}

_REV_SHORT = {long: short for short, long in verbs.SHORT.items()}


def _flag_name(name):
    return name.replace("_", "-")


def _alias(name):
    s = _REV_SHORT.get(name)
    return f"-{s}/--{_flag_name(name)}" if s else f"--{_flag_name(name)}"


def _vtype(name):
    if name in verbs.BOOL_ARGS:
        return "flag"
    if name in verbs.JSON_ARGS:
        return "json"
    if name in verbs.INT_ARGS:
        return "int"
    if name in verbs.FLOAT_ARGS:
        return "float"
    return "str"


def _usage_flag(name):
    t = _vtype(name)
    base = f"--{_flag_name(name)}"
    if t == "flag":
        return f"[{base}]"
    if t == "json":
        return f"[{base} '{{…}}']"
    return f"[{base} {'N' if t in ('int', 'float') else 'V'}]"


def render_op(op):
    pos = verbs.POS.get(op, [])
    prim = verbs.PRIMARY_JSON.get(op)
    doc = OPS_DOC.get(op, {})
    kw = doc.get("kw", [])
    body = doc.get("body", False)
    group, sub = op.split(".", 1)

    parts = [f"{PROG} {group} {sub}"] + [f"<{p}>" for p in pos]
    if prim:
        parts.append(f"--json '{{{prim}}}'")
    if body:
        parts.append("-b <code|->")
    parts += [_usage_flag(k) for k in kw]
    parts.append("[-S scope]")

    out = ["Usage:", "  " + " ".join(parts), ""]
    if doc.get("desc"):
        out += [doc["desc"], ""]
    if pos:
        out.append("Positional order:")
        out += [f"  {i}. {p}" for i, p in enumerate(pos, 1)]
        out.append("")
    if prim:
        out += [f"--json  → sets ONLY the `{prim}` arg (this op's payload), NOT the whole args bag.",
                f"        Other args come from positionals/flags. For the full bag: {PROG} op {op} --json '{{…}}'", ""]
    else:
        out += ["--json  → merges its keys into the args bag.", ""]
    if body:
        out += ["-b      body from an inline string, or '-' to read stdin (never escape code into JSON).", ""]
    if kw:
        out.append("Flags:")
        out += [f"  {_alias(k):<22} {_vtype(k)}" for k in kw]
        out.append("")
    out.append(f"Raw op: {PROG} op {op} …   ·   authoritative args: .runner/docs/commands.md")
    return "\n".join(out)


def render_group(group):
    out = [f"{PROG} {group} — subcommands:", ""]
    for sub, op in GROUPS[group]:
        out.append(f"  {sub:<22} {OPS_DOC.get(op, {}).get('desc', '')}")
    al = GROUP_ALIASES.get(group)
    if al:
        out += ["", "Aliases: " + ", ".join(f"{a} → {c}" for a, c in al.items())]
    out += ["", f"Run `{PROG} {group} <sub> --help` for exact usage of a subcommand."]
    return "\n".join(out)


def render_verb(verb):
    doc = VERBS_DOC[verb]
    out = ["Usage:", "  " + PROG + " " + doc["usage"], ""]
    if doc.get("desc"):
        out += [doc["desc"], ""]
    if doc.get("extra"):
        out += doc["extra"] + [""]
    out.append(f"More: `{PROG} help` · grammar: .runner/docs/commands.md")
    return "\n".join(out).rstrip()


def render_top():
    out = [f"{PROG} — author and run hosted Runner apps.  (also installed as `runner`, `acsetra`)", ""]
    out += ["Session",
            "  r signin / whoami / workspaces / use <slug> / logout / usage / tokens",
            "  r docs pull            refresh CLAUDE.md + .runner/docs/*",
            "  r dev <app>            open your app's hosted surface in the browser",
            "  r checkout <app>       materialize the app as local files in .tmp/",
            ""]
    out += ["Authoring (planes — run `r <group> --help` for subcommands)",
            "  r app create <name> ; r apps",
            "  r auth <app>           require pen.acsetra.com sign-in to use an app",
            "  r set | css | asset | behavior | component | head  <subverb> …",
            "  r pipe <code> -S <scope> -w c:l ; r w <code> <tier> …",
            "  r ls | inspect <set> | read <code> | show <pipe>",
            "  r run <pipe> -i '{…}' ; r runs <id> ; r doctor <app> ; r compile <app>",
            "  r op <name> --json '{…}'   call any op directly",
            ""]
    out += ["Help",
            "  r <group> --help              list a plane's subcommands",
            "  r <group> <subverb> --help    exact usage, positional order, the --json rule",
            "  r <verb> --help               e.g. `r w --help`, `r op --help`",
            ""]
    out += ["Env: ACSETRA_API_BASE, ACSETRA_TOKEN, ACSETRA_WORKSPACE, ACSETRA_SCOPE"]
    return "\n".join(out)


def _first_token(rest):
    for t in rest:
        if not t.startswith("-"):
            return t
    return None


def dispatch_help(cmd, rest):
    """Return rendered help text, or None if `cmd` isn't one we document."""
    cmd = cmd.replace("-", "_")
    cmd = VERB_ALIASES.get(cmd, cmd)

    if cmd == "op":
        name = _first_token(rest)
        if name and name in OPS_DOC:
            return render_op(name)
        if name and "." in name:
            return render_op(name)  # best-effort for any dotted op (e.g. pipe.add_worker)
        return render_verb("op")

    if cmd == "help":
        nxt = _first_token(rest)
        return dispatch_help(nxt, rest[1:]) if nxt else render_top()

    if cmd in GROUPS:
        sub = _first_token(rest)
        if sub:
            sub = sub.replace("-", "_")
            sub = GROUP_ALIASES.get(cmd, {}).get(sub, sub)
            op = f"{cmd}.{sub}"
            if op in OPS_DOC or any(o == op for _, o in GROUPS[cmd]):
                return render_op(op)
        return render_group(cmd)

    if cmd in VERBS_DOC:
        return render_verb(cmd)

    return None
