"""Build a mermaid call-graph from tool-inventory markdown files.

Edges come from each tool's "Calls INTO this repo" section (intra-repo imports)
and "Calls OUT" subprocess invocations of sibling `bin/*.py` scripts.
"""
from __future__ import annotations

import re
from pathlib import Path

INV = Path(__file__).resolve().parent.parent / "docs" / "tools"

BIN_SCRIPTS = {
    "weekly_auditor", "sync_all", "secret_rotator", "memory_maintenance",
    "pg_sync", "chroma_sync_cli", "gen_tool_inventory", "migrate_memory",
    "embed_server", "embed_server_gpu", "install_schedules",
    "test_debug_agent", "test_mcp_proxy", "test_memory_bridge",
    "bench_locomo", "bench_longmemeval", "bench_memory",
    "cli_knowledge", "cli_kb_browse", "memory_doctor", "migrate_flat_memory",
    "pg_setup", "setup_secret", "embed_agent_instructions",
    "inventory_graph", "metadata_filler",
}

# Stdlib module names that sometimes appear in the "Calls INTO this repo"
# section because an agent misclassified an import. Drop them silently.
STDLIB_FALSE_POSITIVES = {
    "pathlib", "argparse", "asyncio", "logging", "subprocess", "sqlite3",
    "json", "os", "sys", "re", "typing", "datetime", "collections",
    "functools", "itertools", "hashlib", "uuid", "tempfile", "shutil",
    "ast", "io", "socket", "struct", "time",
}


def parse_section(text: str, heading: str) -> str:
    m = re.search(rf"## {re.escape(heading)}[^\n]*\n(.*?)(?=\n## |\Z)", text, re.S)
    return m.group(1) if m else ""


def main() -> None:
    edges: list[tuple[str, str, str]] = []
    tools: list[str] = []

    for md in sorted(INV.glob("*.md")):
        if md.name in {"INDEX.md", "CALL_GRAPH.md"}:
            continue
        tool = md.stem
        tools.append(tool)
        txt = md.read_text(encoding="utf-8", errors="replace")

        intra = parse_section(txt, "Calls INTO this repo")
        for line in intra.splitlines():
            stripped = line.strip()
            if not stripped.startswith("-"):
                continue
            # Strip leading "- " and any leading markdown emphasis/backticks
            head = re.sub(r"^[-*]\s+", "", stripped)
            head = re.sub(r"^\*\*", "", head)
            # Take the first module-looking token: backticked OR bare identifier
            mm = re.match(r"[`*]*([a-zA-Z_][a-zA-Z0-9_.]*)", head)
            if not mm:
                continue
            top = mm.group(1).split(".")[0]
            # Filter junk words that sometimes lead bullets
            if top.lower() in {"none", "the", "this", "uses", "imports", "via", "bin"}:
                continue
            if top in STDLIB_FALSE_POSITIVES:
                continue
            # "memory" alone is a directory prefix ("memory.knowledge_helpers"),
            # not a module. Keep submodule name instead.
            if top == "memory":
                sub = re.match(r"memory\.([a-zA-Z_][a-zA-Z0-9_]*)", head)
                if sub:
                    top = sub.group(1)
                else:
                    continue
            if top and top != tool:
                edges.append((tool, top, "import"))

        out = parse_section(txt, "Calls OUT (external side-channels)")
        for mm in re.finditer(r"bin[/\\]([a-zA-Z0-9_]+)\.py", out):
            tgt = mm.group(1)
            if tgt != tool:
                edges.append((tool, tgt, "subprocess"))
        for mm in re.finditer(r"`([a-zA-Z0-9_]+)\.py", out):
            tgt = mm.group(1)
            if tgt in BIN_SCRIPTS and tgt != tool:
                edges.append((tool, tgt, "subprocess"))

    seen: set[tuple[str, str, str]] = set()
    uniq: list[tuple[str, str, str]] = []
    for e in edges:
        if e in seen:
            continue
        seen.add(e)
        uniq.append(e)

    # Classify nodes
    test_nodes = {t for t in tools if t.startswith("test_")}
    bench_nodes = {t for t in tools if t.startswith("bench_")}
    cli_nodes = {t for t in tools if t.startswith("cli_") or t in {"setup_secret", "memory_doctor", "migrate_memory", "migrate_flat_memory", "install_schedules"}}
    sync_nodes = {t for t in tools if "sync" in t or t in {"pg_setup", "embed_server", "embed_server_gpu", "weekly_auditor", "secret_rotator", "embed_agent_instructions"}}

    all_nodes: set[str] = set(tools)
    for s, t, _ in uniq:
        all_nodes.add(s)
        all_nodes.add(t)

    lines: list[str] = []
    lines.append("# Tool call graph")
    lines.append("")
    lines.append("_Generated by `python scripts/inventory_graph.py` from `docs/tools/*.md`._")
    lines.append("")
    lines.append("```mermaid")
    lines.append("graph LR")
    lines.append("  classDef cli fill:#dbeafe,stroke:#1e3a8a,color:#000")
    lines.append("  classDef lib fill:#fef3c7,stroke:#78350f,color:#000")
    lines.append("  classDef test fill:#fce7f3,stroke:#9f1239,color:#000")
    lines.append("  classDef sync fill:#dcfce7,stroke:#14532d,color:#000")
    lines.append("")
    for src, dst, kind in uniq:
        arrow = "-->" if kind == "import" else "-.->|exec|"
        lines.append(f"  {src} {arrow} {dst}")
    lines.append("")
    for n in sorted(all_nodes):
        if n in test_nodes or n in bench_nodes:
            lines.append(f"  class {n} test")
        elif n in cli_nodes:
            lines.append(f"  class {n} cli")
        elif n in sync_nodes:
            lines.append(f"  class {n} sync")
        else:
            lines.append(f"  class {n} lib")
    lines.append("```")
    lines.append("")
    lines.append(f"**Stats:** {len(tools)} tools, {len(uniq)} edges, {len(all_nodes)} total nodes.")

    # ---------------- Interpretive notes (derived from edges) ----------------
    tool_set = set(tools)
    lib_targets = sorted({d for _, d, _ in uniq if d not in tool_set})
    exec_edges = [(s, d) for s, d, k in uniq if k == "subprocess"]
    exec_callers = sorted({s for s, _ in exec_edges})
    has_edge = {s for s, _, _ in uniq} | {d for _, d, _ in uniq if d in tool_set}
    orphans = sorted(t for t in tools if t not in has_edge)

    lines.append("")
    lines.append("## Notes")
    lines.append("")
    lines.append("- Solid arrows = Python import; dotted `exec` = subprocess launch.")
    if lib_targets:
        lines.append(
            "- Library modules (imported but not themselves tools): "
            + ", ".join(f"`{m}`" for m in lib_targets)
            + "."
        )
    for s in exec_callers:
        targets = sorted({d for src, d in exec_edges if src == s})
        lines.append(
            f"- `{s}` execs other tools: "
            + ", ".join(f"`{t}`" for t in targets)
            + "."
        )
    if orphans:
        lines.append(
            "- Orphans (no edges to or from other tools in this graph): "
            + ", ".join(f"`{o}`" for o in orphans)
            + ". Either stdlib-only or they shell out without naming a sibling `bin/*.py`."
        )

    output = "\n".join(lines) + "\n"
    print(output)
    out_path = INV / "CALL_GRAPH.md"
    out_path.write_text(output, encoding="utf-8")
    print(f"# wrote {out_path.relative_to(INV.parent.parent)}")


if __name__ == "__main__":
    main()
