#!/usr/bin/env python3
"""
Generate knowledge/wiki/infrastructure/topology.md (Mermaid views) and per-entity context
cards from inventory.yaml.

Views:
  1. Compute & ingress — hypervisors → guests → services → public URLs
  2. Storage — mounts and pools per guest

Context cards (oikos/cards/<name>.md): one compact (~30-line) file per
host and service — identity, ontology edges, safe actions + risk class,
doc pointer, recent ledger history. This is the token-efficiency layer:
an agent orienting on an entity reads one card instead of several
search_docs/get_page round-trips.

Run from the repo root:
    python3 oikos/gen-topology.py            # writes topology.md + cards/
    python3 oikos/gen-topology.py --check    # exit 1 if output would change

Wired into the same regeneration path as mcp/build_host_files.py so the
diagrams and cards never drift from inventory. Edges follow
oikos/ontology.yaml (hosts, provides, routes-to, mounts, stores-on).
"""

from __future__ import annotations

import argparse
import sys
from pathlib import Path

try:
    import yaml
except ImportError:  # pragma: no cover
    print("PyYAML is required: pip install pyyaml", file=sys.stderr)
    sys.exit(2)

REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO))
from oikos import gen_topology_lib as lib  # noqa: E402
from oikos import ledger as oikos_ledger  # noqa: E402
from oikos import policy as oikos_policy  # noqa: E402
from oikos import relations as oikos_relations  # noqa: E402

INVENTORY = REPO / "inventory.yaml"
OUTPUT = REPO / "knowledge" / "wiki" / "infrastructure" / "topology.md"
CARDS_DIR = REPO / "oikos" / "cards"

BANNER = (
    "<!-- Generated by oikos/gen-topology.py from inventory.yaml. -->\n"
    "<!-- Do NOT edit by hand - your changes will be overwritten. -->\n"
)

# View/graph logic lives in oikos/gen_topology_lib.py (importable — this
# file's hyphenated name can't be). Re-exported here so existing call
# sites in this module don't need a rename.
node_id = lib.node_id
guest_label = lib.guest_label
compute_view = lib.compute_view
storage_view = lib.storage_view
archaeology_table = lib.archaeology_table


def _host_card(name: str, entry: dict, inv: dict) -> str:
    lines = [f"# {name} (host:{name})\n"]
    tag = "LXC" if entry.get("kind") == "lxc" else "VM" if entry.get("kind") == "vm" else entry.get("kind", "")
    pve = entry.get("pve_id")
    lines.append(f"- kind: {entry.get('kind', '?')}" + (f" ({tag} {pve})" if pve else ""))
    lines.append(f"- state: {entry.get('state', 'active')}")
    if entry.get("host"):
        lines.append(f"- runs-on: host:{entry['host']}")
    if entry.get("role"):
        lines.append(f"- role: {entry['role']}")
    addr = entry.get("lan_ip", "")
    mesh = entry.get("mesh", {})
    mesh_bits = []
    for m, v in mesh.items():
        if isinstance(v, dict) and (v.get("ip") or v.get("fqdn")):
            mesh_bits.append(f"{m}:{v.get('fqdn') or v.get('ip')}")
    if addr or mesh_bits:
        lines.append(f"- address: {addr}" + (f" (mesh: {', '.join(mesh_bits)})" if mesh_bits else ""))
    if entry.get("mounts"):
        lines.append(f"- mounts: {', '.join(entry['mounts'])}")
    doc = None
    if entry.get("kind") == "lxc" and pve:
        cand = REPO / "knowledge" / "wiki" / "containers" / f"{pve}-{name}.md"
        if cand.exists():
            doc = str(cand.relative_to(REPO))
    elif entry.get("kind") == "vm" and pve:
        cand = REPO / "knowledge" / "wiki" / "vms" / f"{pve}-{name}.md"
        if cand.exists():
            doc = str(cand.relative_to(REPO))
    elif entry.get("kind") == "proxmox-host":
        cand = REPO / "knowledge" / "wiki" / "hosts" / f"{name}.md"
        if cand.exists():
            doc = str(cand.relative_to(REPO))
    if doc:
        lines.append(f"- doc: {doc}")
    if entry.get("age_pubkey"):
        lines.append("- secrets: enrolled (age key present)")

    rel = oikos_relations.relations(f"host:{name}", inv)
    lines.append("\n## Blast radius")
    lines.append(f"- impacts: {', '.join(rel['impacts']) or '(none)'}")
    lines.append(f"- affected by: {', '.join(rel['affected_by']) or '(none)'}")
    if rel["blast_radius"]:
        lines.append(f"- full blast radius: {', '.join(rel['blast_radius'])}")

    lines.append("\n## Safe actions")
    lines.append("- see the services this host runs for action-level risk classes")

    hist = oikos_ledger.history(f"host:{name}", limit=5)
    lines.append("\n## Recent changes")
    if hist:
        for h in hist:
            lines.append(f"- {h.get('ts', '?')} {h.get('action', '?')} ({h.get('risk', '?')}) — {h.get('result', '?')}")
    else:
        lines.append("- (none yet)")
    return "\n".join(lines) + "\n"


def _service_card(name: str, entry: dict, inv: dict) -> str:
    lines = [f"# {name} (service:{name})\n"]
    if entry.get("backend"):
        lines.append(f"- backend: host:{entry['backend']}")
    url = entry.get("url") or entry.get("endpoint")
    if url:
        lines.append(f"- url: {url}")
    if entry.get("doc_page"):
        lines.append(f"- doc: {entry['doc_page']}")
    if entry.get("config_repo"):
        lines.append(f"- config repo: {entry['config_repo']}")
    if entry.get("risk_notes"):
        lines.append(f"- risk notes: {entry['risk_notes']}")

    rel = oikos_relations.relations(f"service:{name}", inv)
    lines.append("\n## Blast radius")
    lines.append(f"- impacts: {', '.join(rel['impacts']) or '(none)'}")
    lines.append(f"- affected by: {', '.join(rel['affected_by']) or '(none)'}")

    lines.append("\n## Safe actions")
    for a in oikos_policy.safe_actions_for_service(name, entry):
        lines.append(f"- {a['action']} — {a['risk']} (approval: {a['approval']})")

    hist = oikos_ledger.history(f"service:{name}", limit=5)
    lines.append("\n## Recent changes")
    if hist:
        for h in hist:
            lines.append(f"- {h.get('ts', '?')} {h.get('action', '?')} ({h.get('risk', '?')}) — {h.get('result', '?')}")
    else:
        lines.append("- (none yet)")
    return "\n".join(lines) + "\n"


def generate_cards(inv: dict) -> dict[Path, str]:
    desired: dict[Path, str] = {}
    for name, entry in inv.get("hosts", {}).items():
        desired[CARDS_DIR / f"host-{name}.md"] = _host_card(name, entry, inv)
    for name, entry in inv.get("services", {}).items():
        if isinstance(entry, dict):
            desired[CARDS_DIR / f"service-{name}.md"] = _service_card(name, entry, inv)
    return desired


def write_cards(inv: dict, check: bool = False) -> int:
    CARDS_DIR.mkdir(parents=True, exist_ok=True)
    desired = generate_cards(inv)
    diff_count = 0
    for path, content in desired.items():
        existing = path.read_text() if path.exists() else ""
        if existing != content:
            diff_count += 1
            if not check:
                path.write_text(content)
    for existing_path in CARDS_DIR.glob("*.md"):
        if existing_path not in desired:
            diff_count += 1
            if not check:
                existing_path.unlink()
    return diff_count


def render(inv: dict) -> str:
    hosts = inv.get("hosts", {})
    services = inv.get("services", {})
    counts = (
        f"{sum(1 for e in hosts.values() if e.get('kind') == 'proxmox-host')} hypervisors, "
        f"{sum(1 for e in hosts.values() if e.get('kind') == 'lxc')} LXCs, "
        f"{sum(1 for e in hosts.values() if e.get('kind') == 'vm')} VMs, "
        f"{sum(1 for e in hosts.values() if e.get('kind') == 'workstation')} workstations, "
        f"{len(services)} services"
    )
    parts = [
        BANNER,
        "# Topology (generated)\n",
        f"Source: [inventory.yaml](../../../inventory.yaml) — {counts}.",
        "Edge semantics: [oikos/ontology.yaml](../../../oikos/ontology.yaml). "
        "Operating model: [OIKOS.md](../../../.agents/OIKOS.md).\n",
        "## Compute & ingress\n",
        "\n".join(compute_view(inv)) + "\n",
        "## Storage (mounts)\n",
        "\n".join(storage_view(inv)) + "\n",
    ]
    arch = archaeology_table(inv)
    if arch:
        parts += ["## Archaeology (destroyed nodes)\n", "\n".join(arch) + "\n"]
    return "\n".join(parts)


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--check", action="store_true",
                        help="exit 1 if output would change (don't write)")
    args = parser.parse_args()

    inv = yaml.safe_load(INVENTORY.read_text())
    content = render(inv)
    existing = OUTPUT.read_text() if OUTPUT.exists() else ""
    topology_changed = existing != content
    card_diffs = write_cards(inv, check=args.check)

    if args.check:
        if topology_changed:
            print(f"{OUTPUT.relative_to(REPO)} would change", file=sys.stderr)
        if card_diffs:
            print(f"{card_diffs} card(s) in oikos/cards/ would change", file=sys.stderr)
        return 1 if (topology_changed or card_diffs) else 0

    if topology_changed:
        OUTPUT.write_text(content)
        print(f"wrote {OUTPUT.relative_to(REPO)}")
    if card_diffs:
        print(f"wrote/updated {card_diffs} card(s) in oikos/cards/")
    return 0


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