#!/usr/bin/env python3
"""MCP-tool inventory generator. Reads the source-of-truth catalog at
`scripts/mcp_server/consumer_tool_catalog.json` and the handler
registry at `scripts/mcp_server/tools.py`, emits
`docs/contracts/mcp-tool-inventory.md` with every tool cited by
`<file>:<line>`. README's MCP-tool count line links here; the bare
claim is banned.

Contract: step-11 Phase 5 Step 3
(agents/roadmaps/step-11-ruflo-parity.md).

Modes:
  --check    exit non-zero if the generated inventory drifts from
             the on-disk file (CI gate).
  --write    regenerate the inventory file in-place (default).
"""
from __future__ import annotations
import argparse, json, re, sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent.parent
CATALOG = ROOT / "src/scripts/mcp_server/consumer_tool_catalog.json"
TOOLS_PY = ROOT / "src/scripts/mcp_server/tools.py"
OUT = ROOT / "docs/contracts/mcp-tool-inventory.md"

# Match `"<name>": BuiltinTool(` in the ALLOWLIST dict.
HANDLER_RE = re.compile(r'^\s*"([a-z_]+)"\s*:\s*BuiltinTool\(')
# Match `"name": "<name>",` in the catalog json (for catalog citations).
CATALOG_NAME_RE = re.compile(r'^\s*"name"\s*:\s*"([a-z_]+)"\s*,?\s*$')


def _index_handlers() -> dict[str, int]:
    out: dict[str, int] = {}
    for i, line in enumerate(TOOLS_PY.read_text(encoding="utf-8").splitlines(), 1):
        m = HANDLER_RE.match(line)
        if m:
            out[m.group(1)] = i
    return out


def _index_catalog_lines() -> dict[str, int]:
    out: dict[str, int] = {}
    for i, line in enumerate(CATALOG.read_text(encoding="utf-8").splitlines(), 1):
        m = CATALOG_NAME_RE.match(line)
        if m and m.group(1) not in out:
            out[m.group(1)] = i
    return out


def _render(catalog: dict, handlers: dict[str, int], cat_lines: dict[str, int]) -> str:
    tools = catalog["tools"]
    total = len(tools)
    by_transport: dict[str, int] = {}
    by_side_effect: dict[str, int] = {}
    for t in tools:
        for tr in t["implemented_on"]:
            by_transport[tr] = by_transport.get(tr, 0) + 1
        by_side_effect[t["side_effect"]] = by_side_effect.get(t["side_effect"], 0) + 1
    stub_count = sum(1 for t in tools if not t["implemented_on"])
    transport_summary = ", ".join(f"{k}={v}" for k, v in sorted(by_transport.items())) or "none"
    side_effect_summary = ", ".join(f"{k}={v}" for k, v in sorted(by_side_effect.items()))

    lines: list[str] = []
    lines.append("---")
    lines.append("stability: beta")
    lines.append("keep-beta-until: 2026-08-14")
    lines.append("---")
    lines.append("")
    lines.append("# MCP tool inventory")
    lines.append("")
    lines.append("> Generated by [`scripts/audit_mcp_tools.py`](../../scripts/audit_mcp_tools.py)")
    lines.append("> from the source-of-truth catalog")
    lines.append("> [`scripts/mcp_server/consumer_tool_catalog.json`](../../scripts/mcp_server/consumer_tool_catalog.json).")
    lines.append("> Do **not** hand-edit; rerun `python3 scripts/audit_mcp_tools.py --write`.")
    lines.append(">")
    lines.append("> Step-11 Phase 5 Step 3 (`step-11-ruflo-parity.md`).")
    lines.append("")
    lines.append("## Summary")
    lines.append("")
    lines.append(f"- **Total tools:** {total}")
    lines.append(f"- **By transport:** {transport_summary}")
    lines.append(f"- **By side-effect:** {side_effect_summary}")
    lines.append(f"- **Discovery-only stubs (no implementation):** {stub_count}")
    lines.append("")
    lines.append("## Tools")
    lines.append("")
    lines.append("| Tool | Side-effect | Transports | Catalog | Handler |")
    lines.append("|---|---|---|---|---|")
    for t in tools:
        name = t["name"]
        side = t["side_effect"]
        transports = ", ".join(t["implemented_on"]) if t["implemented_on"] else "_(stub)_"
        cat_line = cat_lines.get(name)
        cat_cite = (
            f"[`consumer_tool_catalog.json:{cat_line}`](../../scripts/mcp_server/consumer_tool_catalog.json#L{cat_line})"
            if cat_line else "_missing_"
        )
        h_line = handlers.get(name)
        h_cite = (
            f"[`tools.py:{h_line}`](../../scripts/mcp_server/tools.py#L{h_line})"
            if h_line else "_stub-only_"
        )
        lines.append(f"| `{name}` | `{side}` | {transports} | {cat_cite} | {h_cite} |")
    lines.append("")
    lines.append("## Glossary")
    lines.append("")
    lines.append("- **Side-effect** — `ro` (read-only) · `fs-write` (filesystem write) · `shell` (spawns processes).")
    lines.append("- **Transports** — `stdio` (`scripts/mcp_server/`) · `worker` (`internal/workers/mcp/`). A tool may live on both.")
    lines.append("- **Stub** — catalog-listed for discovery; returns the `not_implemented` envelope from")
    lines.append("  [`mcp-tool-stub-envelope.md`](mcp-tool-stub-envelope.md) until promoted.")
    lines.append("")
    return "\n".join(lines) + "\n"


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__)
    g = ap.add_mutually_exclusive_group()
    g.add_argument("--check", action="store_true", help="Drift gate: exit 1 if file is stale.")
    g.add_argument("--write", action="store_true", help="Regenerate the inventory file.")
    ap.add_argument("--quiet", action="store_true")
    args = ap.parse_args()

    catalog = json.loads(CATALOG.read_text(encoding="utf-8"))
    handlers = _index_handlers()
    cat_lines = _index_catalog_lines()
    rendered = _render(catalog, handlers, cat_lines)

    if args.check:
        on_disk = OUT.read_text(encoding="utf-8") if OUT.exists() else ""
        if on_disk != rendered:
            print(f"❌ {OUT.relative_to(ROOT)} drifted from generator.", file=sys.stderr)
            print("   Run: python3 scripts/audit_mcp_tools.py --write", file=sys.stderr)
            return 1
        if not args.quiet:
            print(f"BASELINE: {OUT.relative_to(ROOT)} is in sync · {len(catalog['tools'])} tool(s)")
        return 0

    OUT.parent.mkdir(parents=True, exist_ok=True)
    OUT.write_text(rendered, encoding="utf-8")
    if not args.quiet:
        print(f"✅ wrote {OUT.relative_to(ROOT)} · {len(catalog['tools'])} tool(s)")
    return 0


if __name__ == "__main__":
    sys.exit(main())
