#!/usr/bin/env python3
"""
fix-bundle-haclusters.py - BUNDLEFIX-003

Add `options: { cluster_count: 1 }` to the 10 *active* testcloud haclusters so
the committed bundle matches the running model (we already set this at runtime
via `juju config`). Single-unit principals on the testcloud cannot form the
default 3-peer cluster; cluster_count=1 lets a 1-node cluster form and bring up
the (reachable, public->provider) VIP. Roosevelt's separate 3-unit bundle keeps
the default.

Text/line based - never round-trips YAML, so anchors/comments/formatting are
preserved. Only touches the named, *uncommented* hacluster lines; the commented
v2-deferred ones (vault-hacluster, ceph-radosgw-hacluster, designate-hacluster)
are left untouched. Idempotent: skips a line that already has cluster_count, and
aborts cleanly if nothing needs changing.

Usage: python3 fix-bundle-haclusters.py [path-to-bundle.yaml]   (default ./bundle.yaml)
"""
import sys, os, re, shutil, difflib, datetime

PATH = sys.argv[1] if len(sys.argv) > 1 else "bundle.yaml"
HACLUSTERS = ["keystone", "glance", "nova-cloud-controller", "neutron-api",
              "cinder", "octavia", "barbican", "magnum", "placement",
              "openstack-dashboard"]
INSERT_AFTER = "channel: 2.4/stable }"
INSERT_WITH = "channel: 2.4/stable, options: { cluster_count: 1 } }"


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


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:
    orig = fh.readlines()
lines = list(orig)

changed = []
for name in HACLUSTERS:
    # uncommented inline def line for this hacluster
    pat = re.compile(r'^\s*%s-hacluster:\s*\{\s*charm:\s*hacluster' % re.escape(name))
    hits = [i for i, ln in enumerate(lines)
            if pat.match(ln) and not ln.lstrip().startswith("#")]
    if len(hits) != 1:
        abort("expected exactly 1 uncommented '%s-hacluster' inline def, found %d"
              % (name, len(hits)))
    i = hits[0]
    if "cluster_count" in lines[i]:
        abort("%s-hacluster already has cluster_count - already applied? inspect."
              % name)
    if INSERT_AFTER not in lines[i]:
        abort("%s-hacluster line not in expected inline shape: %r"
              % (name, lines[i].strip()))
    lines[i] = lines[i].replace(INSERT_AFTER, INSERT_WITH, 1)
    changed.append(name)

if len(changed) != len(HACLUSTERS):
    abort("only changed %d of %d haclusters" % (len(changed), len(HACLUSTERS)))

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("")

try:
    import yaml
except Exception:
    print("NOTE: PyYAML not importable - semantic verification skipped; re-verify on jumphost.")
    sys.exit(0)

apps = yaml.safe_load(open(PATH))["applications"]
print("=== verification ===")
print("YAML parses: PASS")
ok = True
for name in HACLUSTERS:
    a = apps.get("%s-hacluster" % name, {})
    cc = (a.get("options") or {}).get("cluster_count")
    p = (cc == 1)
    ok &= p
    print("  %-30s cluster_count==1 : %s" % (name + "-hacluster", "PASS" if p else "FAIL (%r)" % cc))
# deferred ones must NOT have appeared
for absent in ("vault-hacluster", "ceph-radosgw-hacluster", "designate-hacluster"):
    p = absent not in apps
    ok &= p
    print("  %-30s stays absent     : %s" % (absent, "PASS" if p else "FAIL"))
print("\nRESULT:", "ALL CHECKS PASS"
      if ok else "FAILURES - revert: cp %s %s" % (bak, PATH))
sys.exit(0 if ok else 2)
