#!/usr/bin/env python3
"""
Dump the OFFICE1-NETBOX RECORD (the VR1 working IPAM apex) to a reviewable JSON
artifact. READ-ONLY. NOTHING is ever written to NetBox.

WHY A SEPARATE SCRIPT FROM netbox/prod-draft-dump.py (operator-directed 2026-07-25):
one script per SOURCE, so each holds only its own credential/URL and the source can
never be confused. The two NetBoxes are DIFFERENT systems with DIFFERENT roles:

  - prod-draft-dump.py  -> netbox.baldurkeep.com  : the FROZEN v1 REFERENCE (4.5.8,
        bare 40-char v1 token). Takes NO write during VR1 (DOCFIX-195); it is the
        eventual production apex, held read-only.
  - THIS script         -> office1-netbox 10.10.1.10:8000 : the VR1 WORKING APEX /
        "the RECORD" (4.6.4, assembled v2 token nbt_<key>.<plaintext>). This is what
        every VR1 consuming system (OpenTofu, MAAS, the overlays, the Juju bundle)
        actually reads (DOCFIX-195), and what has been EDITED as the dc0/dc1 buildout
        proceeded -- so this is the "current in-cloud NetBox" state.

Use this to capture the CURRENT apex for a design/review session that has no live infra
(e.g. Chat). It is a point-in-time snapshot; re-run to refresh.

TOKEN NEVER TRAVELS: the script reads NETBOX_URL + NETBOX_TOKEN from the environment;
source the record env first (below). It is not embedded and not printed (only the base
URL + per-endpoint counts are printed).

TRAP -- User-Agent (references/platform-traps.md): the baldurkeep upstream 403s the
default Python UA; office1-netbox does not have that WAF, but we send an accepted UA
regardless (harmless) so the same code path works against either 4.5/4.6 apex.

Usage:
    . ~/vr1-office1-creds/vr1-netbox-sandbox.env      # NETBOX_URL=http://10.10.1.10:8000 + v2 NETBOX_TOKEN
    python3 netbox/office1-record-dump.py --out netbox/draft/vr1-office1-current-YYYYMMDD.json
"""

import argparse
import json
import os
import sys
import urllib.error
import urllib.request

UA = "curl/8.5.0"

# Same IPAM object set + dependency order as prod-draft-dump.py, so the two apexes
# produce structurally comparable dumps (a fidelity diff is then meaningful).
ENDPOINTS = [
    ("ipam/rirs", ["name", "slug", "is_private", "description"]),
    ("ipam/roles", ["name", "slug", "weight", "description"]),
    ("dcim/regions", ["name", "slug", "parent", "description"]),
    ("dcim/sites", ["name", "slug", "status", "region", "description"]),
    ("ipam/aggregates", ["prefix", "rir", "description"]),
    ("ipam/prefixes", ["prefix", "status", "role", "scope_type", "scope_id",
                       "description", "is_pool", "mark_utilized"]),
    ("ipam/ip-ranges", ["start_address", "end_address", "status", "role", "description"]),
    ("ipam/ip-addresses", ["address", "status", "role", "dns_name", "description"]),
]


def die(msg):
    print("FAIL: %s" % msg, file=sys.stderr)
    sys.exit(2)


def require_env(name):
    v = os.environ.get(name)
    if not v:
        die("%s is not set. Source the RECORD env first: "
            ". ~/vr1-office1-creds/vr1-netbox-sandbox.env" % name)
    return v


def get_all(base, token, path):
    """Paginate an endpoint fully. Read-only."""
    out = []
    url = "%s/api/%s/?limit=200" % (base, path)
    while url:
        req = urllib.request.Request(url, headers={
            "Authorization": "Token %s" % token,     # v2 wire form: Token nbt_<key>.<plaintext>
            "Accept": "application/json",
            "User-Agent": UA,
        })
        try:
            with urllib.request.urlopen(req, timeout=45) as r:
                data = json.load(r)
        except urllib.error.HTTPError as exc:
            if exc.code in (401, 403):
                die("HTTP %d on %s -- check the RECORD env token is the ASSEMBLED v2 form "
                    "(nbt_<key>.<plaintext>), not the API's bare token field." % (exc.code, path))
            die("HTTP %d on %s: %s" % (exc.code, path, exc.reason))
        out += data["results"]
        url = data.get("next") or None
    return out


def slim(obj, fields):
    """Keep only what a downstream fidelity/seed step needs; nested refs -> slug/value.
    IDs are DISCARDED (they are apex-local and would mis-link on the way into another apex)."""
    rec = {}
    for f in fields:
        v = obj.get(f)
        rec[f] = (v.get("slug") or v.get("value")) if isinstance(v, dict) else v
    return rec


def rewrite_prefix_scope(rec, obj, site_id_to_slug, region_id_to_slug):
    """Rewrite a prefix's scope_id (apex-local id) to a slug the reader can re-resolve."""
    sid, st = obj.get("scope_id"), obj.get("scope_type")
    if sid is not None and st == "dcim.site":
        rec["scope_site"] = site_id_to_slug.get(sid)
    elif sid is not None and st == "dcim.region":
        rec["scope_region"] = region_id_to_slug.get(sid)
    rec.pop("scope_id", None)
    return rec


def main():
    ap = argparse.ArgumentParser(description=__doc__.split("\n\n", 1)[0])
    ap.add_argument("--out", required=True, help="Path to write the JSON snapshot")
    args = ap.parse_args()

    base = require_env("NETBOX_URL").rstrip("/")
    token = require_env("NETBOX_TOKEN")

    print("Reading the office1-netbox RECORD from %s (READ-ONLY) ..." % base)
    raw_sites = get_all(base, token, "dcim/sites")
    site_id_to_slug = {s["id"]: s["slug"] for s in raw_sites}
    raw_regions = get_all(base, token, "dcim/regions")
    region_id_to_slug = {r["id"]: r["slug"] for r in raw_regions}

    draft = {"_source": base,
             "_note": "READ-ONLY snapshot of the office1-netbox RECORD (VR1 working apex, "
                      "DOCFIX-195). IDs discarded; refs by slug/prefix."}
    for path, fields in ENDPOINTS:
        rows = raw_sites if path == "dcim/sites" else get_all(base, token, path)
        slimmed = []
        for o in rows:
            rec = slim(o, fields)
            if path == "ipam/prefixes":
                rec = rewrite_prefix_scope(rec, o, site_id_to_slug, region_id_to_slug)
            if path == "ipam/ip-ranges":
                rec["range"] = "%s-%s" % (rec.get("start_address"), rec.get("end_address"))
            slimmed.append(rec)
        draft[path] = slimmed
        print("  %-18s %d" % (path, len(slimmed)))

    os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
    with open(args.out, "w") as fh:
        json.dump(draft, fh, indent=2, sort_keys=True)
        fh.write("\n")
    print("\nWrote %s. NOTHING was written to NetBox." % args.out)
    return 0


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