#!/usr/bin/env python3
"""
fix-api-bindings.py - surgical fix for the api-bindings phantom-endpoint defect.

Two text edits (line-based: does NOT round-trip YAML, so anchors, aliases,
comments and formatting are all preserved):

  1. Shrink the &api-bindings anchor from 9 keys to the two that actually carry
     meaning:
         "":     metal
         public: provider
     The seven dropped keys (admin/internal/shared-db/amqp/certificates/cluster/
     ha) all mapped to 'metal', which is exactly what the "" default already
     provides - and naming an endpoint a charm lacks is a hard `juju deploy`
     error. Dropping them un-breaks keystone, ceph-radosgw and
     openstack-dashboard, which each lack one or more of those endpoints, while
     being functionally identical for the charms that have them.

  2. Repoint `vault` from *api-bindings to *internal-bindings. Vault has no
     `public` endpoint, so even the 2-key anchor would error on it; it is an
     internal-only service and belongs on the metal-default anchor.

Usage:   python3 fix-api-bindings.py [path-to-bundle.yaml]   (default ./bundle.yaml)

Aborts WITHOUT writing if the bundle is not in the expected pre-fix shape
(already fixed, or structure changed). Creates a timestamped .bak, prints a
unified diff, and - if PyYAML is importable - verifies the resolved bindings.
"""
import sys, os, re, shutil, difflib, datetime

PATH = sys.argv[1] if len(sys.argv) > 1 else "bundle.yaml"
DROP = {"admin", "internal", "shared-db", "amqp", "certificates", "cluster", "ha"}
KEEP = {'""', "public"}


def abort(msg):
    sys.stderr.write("ABORT (no changes written): %s\n" % msg)
    sys.exit(1)


def indent_of(line):
    return len(line) - len(line.lstrip())


if not os.path.isfile(PATH):
    abort("file not found: %s (run from the repo root, or pass the path)" % PATH)

with open(PATH, "r", newline="") as fh:     # newline="" -> keep original EOLs
    orig = fh.readlines()
lines = list(orig)

# ---------- Edit 1: shrink &api-bindings ----------
anchor = None
for i, ln in enumerate(lines):
    if re.match(r'^api-bindings:\s*&api-bindings$', ln.strip()):
        anchor = i
        break
if anchor is None:
    abort("could not locate the 'api-bindings: &api-bindings' anchor line")

a_indent = indent_of(lines[anchor])
j = anchor + 1
kept, dropped = [], []
while j < len(lines):
    raw = lines[j]
    s = raw.strip()
    if s == "" or indent_of(raw) <= a_indent:
        break
    key = s.split(":", 1)[0].strip()
    if key in DROP:
        dropped.append(key)
    else:
        kept.append(raw)
    j += 1
block_end = j

kept_keys = {l.strip().split(":", 1)[0].strip() for l in kept}
if kept_keys != KEEP or set(dropped) != DROP:
    abort("api-bindings block not in expected pre-fix shape "
          "(kept=%s dropped=%s) - already fixed or bundle changed; inspect by hand"
          % (sorted(kept_keys), sorted(dropped)))

lines = lines[:anchor + 1] + kept + lines[block_end:]

# ---------- Edit 2: vault -> *internal-bindings ----------
vault = None
for i, ln in enumerate(lines):
    if ln.strip() == "vault:" and indent_of(ln) == 2:
        vault = i
        break
if vault is None:
    abort("could not locate the '  vault:' application block")

fixed_vault = False
k = vault + 1
while k < len(lines):
    raw = lines[k]
    if raw.strip() and indent_of(raw) <= 2:
        break
    if re.match(r'^\s*bindings:\s*\*api-bindings\b', raw):
        lines[k] = raw.replace("*api-bindings", "*internal-bindings", 1)
        fixed_vault = True
        break
    k += 1
if not fixed_vault:
    abort("did not find vault's 'bindings: *api-bindings' line "
          "(already on internal-bindings, or block changed)")

# ---------- Collateral-damage guard ----------
if len(orig) - len(lines) != 7:
    abort("unexpected line-count delta %d (expected exactly -7); aborting"
          % (len(orig) - len(lines)))

# ---------- Backup + write ----------
ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
bak = "%s.bak-%s" % (PATH, ts)
shutil.copy2(PATH, bak)
with open(PATH, "w", newline="") as fh:
    fh.writelines(lines)

print("Backup written: %s" % bak)
print("=== unified diff ===")
sys.stdout.writelines(
    difflib.unified_diff(orig, lines,
                         fromfile="bundle.yaml (before)",
                         tofile="bundle.yaml (after)"))
print("")

# ---------- Verify (best-effort; authoritative re-check is on the jumphost) ----------
try:
    import yaml
except Exception:
    print("NOTE: PyYAML not importable here - semantic verification skipped.")
    print("      Re-verify on the jumphost after pull (it has PyYAML).")
    sys.exit(0)

with open(PATH) as fh:
    doc = yaml.safe_load(fh)
apps = doc["applications"]
MIN = {"": "metal", "public": "provider"}
INT = {"": "metal"}
on_min = ["keystone", "ceph-radosgw", "openstack-dashboard", "octavia",
          "glance", "nova-cloud-controller", "placement", "neutron-api",
          "cinder", "barbican", "magnum"]

print("=== verification ===")
print("YAML parses: PASS")
ok = True
for a in on_min:
    got = apps.get(a, {}).get("bindings")
    p = (got == MIN)
    ok = ok and p
    print("  %-22s -> minimal api-bindings : %s"
          % (a, "PASS" if p else "FAIL %r" % (got,)))
gv = apps.get("vault", {}).get("bindings")
pv = (gv == INT)
ok = ok and pv
print("  %-22s -> internal-bindings     : %s"
      % ("vault", "PASS" if pv else "FAIL %r" % (gv,)))
print("")
print("RESULT:", "ALL CHECKS PASS"
      if ok else "FAILURES - revert with:  cp %s %s" % (bak, PATH))
sys.exit(0 if ok else 2)
