#!/usr/bin/env python3
"""
neighbors — navigate the wikilink graph around a note (vaultq verb; M3).

The spider as *retrieval* instead of a cost profile: given a note, return its
graph neighborhood — who it links, who links it, N hops out — as paths + types,
so the model picks what to Read instead of walking the graph by reading.

Direction: `out` follows outgoing links, `in` follows backlinks, `both` the
union. Each neighbor reports the hop it was found at, the direction of the edge
that discovered it, and `via` (the note it was discovered from — "" at hop 1).

Read-only; stdlib-only; reuses vaultgraph (M0) — one build_graph pass, then a
light frontmatter read of just the reached notes for their types.

    python -m alexandria_vault.vault_core.query.vaultq neighbors "Acme" --hops 1
    python -m alexandria_vault.vault_core.query.vaultq neighbors "Alex Chen" --dir in
    python -m alexandria_vault.vault_core.query.vaultq neighbors "Acme Pilot" --hops 2 --type person
"""
import os
import sys
import json
import argparse
from pathlib import Path

from .vaultgraph import (find_vault_root, build_graph, split_frontmatter)  # noqa: E402


def _title(rel):
    return os.path.splitext(os.path.basename(rel))[0]


def neighbors(root, seed, hops=1, direction="both", type_=None, limit=50):
    """BFS the neighborhood. Returns {seed, hops, dir, total, neighbors:
    [{rel, title, type, hop, dir, via}]} sorted by (hop, rel). A seed that
    doesn't resolve returns {error, candidates?}."""
    root = Path(root)
    notes, by_name, inbound, _ = build_graph(root)

    seed_rel = seed if seed in notes else by_name.get(seed.casefold())
    if not seed_rel:
        # help the caller: near-miss candidates by substring
        cands = sorted(r for n, r in by_name.items() if seed.casefold() in n)[:5]
        return {"error": f"seed not found: {seed!r}", "candidates": cands}

    found = {}                      # rel -> {hop, dir, via}
    frontier = [seed_rel]
    for depth in range(1, hops + 1):
        nxt = []
        for src in frontier:
            edges = []
            if direction in ("out", "both"):
                edges += [(t, "out") for t in notes[src]["out"]]
            if direction in ("in", "both"):
                edges += [(s, "in") for s in sorted(inbound[src])]
            for rel, d in edges:
                if rel == seed_rel:
                    continue
                if rel not in found:
                    found[rel] = {"hop": depth, "dir": d,
                                  "via": "" if src == seed_rel else _title(src)}
                    nxt.append(rel)
                elif found[rel]["hop"] == depth == 1 and found[rel]["dir"] != d:
                    found[rel]["dir"] = "both"      # linked in both directions
        frontier = nxt

    rows = []
    for rel, info in found.items():
        raw = (root / rel).read_text(encoding="utf-8", errors="replace")
        fm, _ = split_frontmatter(raw)
        t = fm.get("type", "")
        if type_ and t != type_:
            continue
        rows.append({"rel": rel, "title": _title(rel), "type": t, **info})
    rows.sort(key=lambda r: (r["hop"], r["rel"]))
    total = len(rows)
    return {"seed": seed_rel, "hops": hops, "dir": direction,
            "total": total, "neighbors": rows[:limit]}


def print_pretty(out):
    if "error" in out:
        print(f'✗ {out["error"]}')
        for c in out.get("candidates", []):
            print(f"   candidate: {c}")
        return
    shown = len(out["neighbors"])
    print(f'[[{_title(out["seed"])}]] — {out["total"]} neighbor(s) within '
          f'{out["hops"]} hop(s), dir={out["dir"]}'
          + (f" (showing {shown})" if out["total"] > shown else ""))
    for n in out["neighbors"]:
        arrow = {"out": "→", "in": "←", "both": "↔"}[n["dir"]]
        via = f'  (via {n["via"]})' if n["via"] else ""
        print(f'  h{n["hop"]} {arrow}  [[{n["title"]}]]  ({n["type"] or "?"}){via}')


def main():
    ap = argparse.ArgumentParser(description=__doc__,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("seed", help="note name, alias, or rel path")
    ap.add_argument("--hops", type=int, default=1)
    ap.add_argument("--dir", choices=["in", "out", "both"], default="both")
    ap.add_argument("--type", dest="type_")
    ap.add_argument("--limit", type=int, default=50)
    ap.add_argument("--pretty", action="store_true")
    args = ap.parse_args()
    out = neighbors(find_vault_root(), args.seed, hops=args.hops,
                    direction=args.dir, type_=args.type_, limit=args.limit)
    print_pretty(out) if args.pretty else print(json.dumps(out, indent=1))


if __name__ == "__main__":
    main()
