#!/usr/bin/env python3
"""Whole-host capacity budget for the VR1 nested deployment (D-121 / R-3 / Model B).

Replaces the lost, never-committed `scratchpad/optc-calc.py` (review R3-F06) with a
committed, harnessed, read-only calculator -- the sibling of `dc-dc-ceph-disk-budget.sh`.
It SUMS the RAM / vCPU / disk allocation of every VR1 layer (both DCs' node fleets, the
site containment VMs, Office1, the edges) and compares it to the MEASURED host budget,
reporting the binding resource and a FIT / NO-FIT verdict.

Mutates nothing. Every number is an operator-supplied or ruled input -- no live host is
queried and nothing is inferred. Defaults encode the ruled layout (Option C = 3 control +
2 compute + 4 storage/DC, D-121 R-3) under D-123 **Model B** (the DC node fleet lives INSIDE
the site containment VM), but every value is overridable so the tool outlives the ruling.

RAM is the binding resource in a nested sim (vCPU overcommits; disk is thin-provisioned),
so RAM fit is the gate; vCPU/disk are reported as ratios, not hard ceilings.

Usage:
    python3 dc-dc-whole-host-budget.py [--model A|B] [--dcs N]
        [--control N,VCPU,MEM_GIB,DISK_GIB] [--compute ...] [--storage ...]
        [--containment-overhead-mem-gib G] [--containment-overhead-vcpu V]
        [--office1 VCPU,MEM_GIB,DISK_GIB] [--edge-count N] [--edge MEM_GIB,VCPU]
        [--host VCPU,MEM_GIB,DISK_GIB]

Exit: 0 = FIT (RAM within budget), 1 = NO-FIT (RAM over budget), 2 = usage error.
"""
import argparse
import sys


def triple(s, n=3):
    parts = s.split(",")
    if len(parts) != n:
        raise argparse.ArgumentTypeError(f"expected {n} comma-separated numbers, got '{s}'")
    return [float(p) for p in parts]


def main():
    ap = argparse.ArgumentParser(description=__doc__,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
    # Node roles per DC: count, vCPU, mem GiB, disk GiB  (ruled Option C / R-3 defaults)
    ap.add_argument("--control", type=lambda s: triple(s, 4), default=[3, 16, 64, 150],
                    help="control nodes/DC: count,vcpu,mem_gib,disk_gib (default 3,16,64,150)")
    ap.add_argument("--compute", type=lambda s: triple(s, 4), default=[2, 12, 48, 100],
                    help="compute nodes/DC: count,vcpu,mem_gib,disk_gib (default 2,12,48,100)")
    ap.add_argument("--storage", type=lambda s: triple(s, 4), default=[4, 8, 24, 550],
                    help="storage nodes/DC: count,vcpu,mem_gib,disk_gib (default 4,8,24,550 -- R-3)")
    ap.add_argument("--dcs", type=int, default=2, help="number of DCs (default 2)")
    ap.add_argument("--model", choices=["A", "B"], default="B",
                    help="D-123 model: A=nodes vcloud-level (+ small rack); B=nodes inside containment VM (default B)")
    # Containment VM overhead (Model B: the VM's own OS + MAAS rack + LXD, ON TOP of the node RAM it holds).
    ap.add_argument("--containment-overhead-mem-gib", type=float, default=16.0,
                    help="per-DC containment-VM overhead RAM GiB, Model B (default 16)")
    ap.add_argument("--containment-overhead-vcpu", type=float, default=4.0,
                    help="per-DC containment-VM overhead vCPU, Model B (default 4)")
    # Model A rack headend (small, holds no nodes) -- D-124 original 4/8/80.
    ap.add_argument("--rack", type=lambda s: triple(s, 3), default=[4, 8, 80],
                    help="Model A rack headend/DC: vcpu,mem_gib,disk_gib (default 4,8,80)")
    # Office1 (voffice1) + edges.
    ap.add_argument("--office1", type=lambda s: triple(s, 3), default=[16, 32, 600],
                    help="Office1 voffice1: vcpu,mem_gib,disk_gib (default 16,32,600)")
    ap.add_argument("--edge-count", type=int, default=3, help="OPNsense edges total (office1 + per DC; default 3)")
    ap.add_argument("--edge", type=lambda s: triple(s, 2), default=[2, 2],
                    help="per-edge: mem_gib,vcpu (default 2,2)")
    # Measured host budget.
    ap.add_argument("--host", type=lambda s: triple(s, 3), default=[256, 1024, 10240],
                    help="MEASURED host budget: vcpu,mem_gib,disk_gib (default 256,1024,10240)")
    args = ap.parse_args()

    def role_sum(role):
        cnt, vcpu, mem, disk = role
        return cnt * vcpu, cnt * mem, cnt * disk

    # Per-DC node fleet.
    dc_vcpu = dc_mem = dc_disk = 0.0
    for role in (args.control, args.compute, args.storage):
        v, m, d = role_sum(role)
        dc_vcpu += v; dc_mem += m; dc_disk += d

    lines = []
    lines.append(f"Per-DC node fleet: {dc_vcpu:.0f} vCPU / {dc_mem:.0f} GiB / {dc_disk:.0f} GiB")

    tot_vcpu = tot_mem = tot_disk = 0.0
    # Both DCs' nodes.
    tot_vcpu += args.dcs * dc_vcpu
    tot_mem += args.dcs * dc_mem
    tot_disk += args.dcs * dc_disk

    if args.model == "B":
        # Nodes live INSIDE the containment VM; add per-DC containment overhead on top.
        tot_vcpu += args.dcs * args.containment_overhead_vcpu
        tot_mem += args.dcs * args.containment_overhead_mem_gib
        per_dc_vm = dc_mem + args.containment_overhead_mem_gib
        lines.append(f"Model B: each containment VM (vvr1-dcN) holds one DC's fleet -> ~{per_dc_vm:.0f} GiB RAM each")
        lines.append(f"  + containment overhead {args.dcs} x ({args.containment_overhead_vcpu:.0f} vCPU / "
                     f"{args.containment_overhead_mem_gib:.0f} GiB)")
    else:
        # Model A: nodes at vcloud level, plus a small rack headend per DC.
        rv, rm, rd = args.rack
        tot_vcpu += args.dcs * rv; tot_mem += args.dcs * rm; tot_disk += args.dcs * rd
        lines.append(f"Model A: nodes vcloud-level + a {rv:.0f}/{rm:.0f}/{rd:.0f} rack headend x{args.dcs}")

    # Office1 + edges (both models).
    ov, om, od = args.office1
    tot_vcpu += ov; tot_mem += om; tot_disk += od
    em, ev = args.edge
    tot_vcpu += args.edge_count * ev
    tot_mem += args.edge_count * em
    lines.append(f"Office1 voffice1: {ov:.0f}/{om:.0f}/{od:.0f}; edges: {args.edge_count} x ({ev:.0f} vCPU / {em:.0f} GiB)")

    hv, hm, hd = args.host
    print("=== dc-dc-whole-host-budget: VR1 nested capacity (D-121 / R-3 / Model {}) ===".format(args.model))
    for ln in lines:
        print("  " + ln)
    print()
    print(f"  TOTAL allocation : {tot_vcpu:.0f} vCPU / {tot_mem:.0f} GiB / {tot_disk:.0f} GiB")
    print(f"  MEASURED host    : {hv:.0f} vCPU / {hm:.0f} GiB / {hd:.0f} GiB")
    print(f"  RAM   : {tot_mem:.0f}/{hm:.0f} GiB = {100*tot_mem/hm:.0f}%   <- BINDING resource")
    print(f"  vCPU  : {tot_vcpu:.0f}/{hv:.0f} = {100*tot_vcpu/hv:.0f}%   (overcommits in a sim; not a hard ceiling)")
    print(f"  disk  : {tot_disk:.0f}/{hd:.0f} GiB = {100*tot_disk/hd:.0f}%   (thin-provisioned)")
    print()

    ram_fit = tot_mem <= hm
    if ram_fit:
        print(f"VERDICT: FIT -- RAM within budget ({hm - tot_mem:.0f} GiB headroom).")
        print(f"WHOLE-HOST: model={args.model} ram={tot_mem:.0f}/{hm:.0f}GiB verdict=FIT "
              f"headroom={hm - tot_mem:.0f}GiB vcpu={tot_vcpu:.0f}/{hv:.0f} disk={tot_disk:.0f}/{hd:.0f}GiB")
        return 0
    else:
        print(f"VERDICT: NO-FIT -- RAM OVER budget by {tot_mem - hm:.0f} GiB. Reduce per-node RAM or node count.")
        print(f"WHOLE-HOST: model={args.model} ram={tot_mem:.0f}/{hm:.0f}GiB verdict=NO-FIT over={tot_mem - hm:.0f}GiB")
        return 1


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