#!/usr/bin/env bash
# scripts/opentofu-validate.sh [--static-only] [tofu-dir]
#
# Syntax/schema/semantic gate for opentofu/ (VR1 IaC, D-103). Read-only: runs the
# static semantic guards (S1, S2) + `tofu fmt -check` + `tofu init -backend=false`
# + `tofu validate` against the root module + S3, which validates EVERY module
# STANDALONE (root-only validation never parses uncalled modules -- that blind spot
# hid two broken modules until 2026-07-13; see S3's comment). Does NOT plan or apply -- no provider
# connection, no libvirt reach required. This is the harness opentofu/README.md
# tells you to run before trusting anything under opentofu/, since it was
# authored in a session with no tofu binary available to self-check.
#
# The S* guards catch libvirt-domain defects that `tofu validate` structurally
# CANNOT see (optional attributes whose ABSENCE is the bug). Both were paid for in
# blood by the 2026-07-12 Office1 OPNsense boot incident:
#   S1  memory without memory_unit  -> 1024x too little RAM  (guest triple-faults)
#   S2  domain without ACPI enabled -> FreeBSD panics; Linux loses clean shutdown
#
# --static-only runs ONLY the S* guards and exits (no tofu binary, no network
# needed). That is how the harness exercises them cheaply.
#
# Exit: 0 clean | 1 S*/fmt/init/validate failed | 2 tofu binary not found.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

MODE="full"
if [ "${1:-}" = "--static-only" ] || [ "${1:-}" = "--check-memory-unit" ]; then
  MODE="static" # --check-memory-unit kept as a back-compat alias
  shift
fi
TOFU_DIR="${1:-$HERE/../opentofu}"

if [ ! -d "$TOFU_DIR" ]; then
  echo "FAIL: no such directory: $TOFU_DIR"
  exit 1
fi

# S1: every libvirt_domain that sets `memory` MUST also set `memory_unit`.
#
# `tofu validate` CANNOT catch this: memory_unit is an optional attribute, so
# omitting it is schema-valid. But on dmacvicar/libvirt >=0.9 a bare `memory` is
# interpreted in libvirt's DEFAULT unit (KiB), not MiB (the 0.8-era meaning), so
# `memory = 2048` silently yields a 2 MiB guest that triple-faults in its
# bootloader. That cost a full session on 2026-07-12 (Office1 OPNsense edge; see
# docs/archive/incident-20260712-opnsense-edge-boot-triplefault.md). This is the guard
# that stops the next VM module from reintroducing it.
#
# Block-scan is safe because `tofu fmt -check` (below) enforces canonical layout:
# top-level resource braces at column 0, attributes at two-space indent. Run
# order puts S1 first so it also works with no tofu binary present.
s1_check() {
  local out
  out="$(find "$TOFU_DIR" -name '*.tf' -not -path '*/.terraform/*' -print0 2>/dev/null \
    | xargs -0 -r awk '
        /^resource "libvirt_domain"/ { inblk=1; mem=0; unit=0; rline=FNR; next }
        inblk && /^  memory[[:space:]]*=/      { mem=1 }
        inblk && /^  memory_unit[[:space:]]*=/ { unit=1 }
        inblk && /^}/ {
          if (mem && !unit)
            printf "  [FAIL] %s:%d sets memory with no memory_unit -- guest gets KiB, not MiB (1024x too little RAM)\n", FILENAME, rline
          inblk=0
        }
      ')"
  if [ -n "$out" ]; then
    echo "$out"
    echo '  FIX: add  memory_unit = "MiB"  to each domain listed above.'
    return 1
  fi
  echo "  [PASS] every libvirt_domain that sets memory also sets memory_unit"
  return 0
}

# S2: every libvirt_domain MUST enable ACPI (`features = { acpi = true }`).
#
# Also invisible to `tofu validate` (the whole `features` block is optional). With no
# features block libvirt renders the machine `acpi=off`. A FreeBSD/OPNsense guest then
# PANICS at interrupt init ("running without device atpic requires a local APIC") and
# spins in ddb at 100% CPU while looking "running". A Linux guest boots but loses clean
# ACPI shutdown -- which breaks MAAS power control on the node VMs. Cost: the second half
# of the 2026-07-12 boot incident, found only once the S1 fix let the kernel get far
# enough to reach interrupt init.
s2_check() {
  local out
  out="$(find "$TOFU_DIR" -name '*.tf' -not -path '*/.terraform/*' -print0 2>/dev/null \
    | xargs -0 -r awk '
        /^resource "libvirt_domain"/ { inblk=1; acpi=0; rline=FNR; next }
        inblk && /^[[:space:]]*acpi[[:space:]]*=[[:space:]]*true/ { acpi=1 }
        inblk && /^}/ {
          if (!acpi)
            printf "  [FAIL] %s:%d libvirt_domain does not enable ACPI -- libvirt renders acpi=off; FreeBSD guests panic, Linux guests lose clean shutdown\n", FILENAME, rline
          inblk=0
        }
      ')"
  if [ -n "$out" ]; then
    echo "$out"
    echo '  FIX: add  features = { acpi = true, apic = {} }  to each domain listed above.'
    return 1
  fi
  echo "  [PASS] every libvirt_domain enables ACPI"
  return 0
}

echo "== S1 libvirt_domain memory_unit guard =="
if s1_check; then
  s1_rc=0
else
  s1_rc=1
fi

echo "== S2 libvirt_domain ACPI guard =="
if s2_check; then
  s2_rc=0
else
  s2_rc=1
fi

if [ "$MODE" = "static" ]; then
  [ "$s1_rc" -eq 0 ] && [ "$s2_rc" -eq 0 ] && exit 0
  exit 1
fi

if ! command -v tofu >/dev/null 2>&1; then
  echo "FAIL: tofu binary not found on PATH -- install OpenTofu, then re-run this script"
  exit 2
fi

fail=$(( s1_rc | s2_rc ))

echo "== tofu fmt -check -recursive (REPO-TRACKED FILES ONLY) =="
# SCOPED TO REPO CONTENT (fixed 2026-07-30). `tofu fmt -recursive` walks EVERY .tf/.tfvars under
# the tree, including GITIGNORED local files -- per-DC `*.auto.tfvars`, which exist only on the
# host that actually runs the applies. Measured: this gate read ALL GREEN on the jumphost and
# FAILED on the Office1 headend, and the sole flagged file was
# `opentofu/vr1-dc0-substrate/d124-inner.auto.tfvars`, which is gitignored operator input, not
# repo content. So the gate was RED ON THE ONLY HOST THAT DEPLOYS and green where it did not
# matter -- the same host-dependent-verdict class as the LC_ALL manifest sort.
# The repo's formatting standard applies to what the repo carries. A local tfvars is the
# operator's file and is none of this gate's business.
FMT_DRIFT=""
while IFS= read -r _f; do
  [ -n "$_f" ] || continue
  # A gitignored path is not repo content -- skip it, and say so rather than silently.
  if git -C "$HERE/.." check-ignore -q "$_f" 2>/dev/null; then
    echo "  [skip] $_f is gitignored (local input, not repo content)"
    continue
  fi
  FMT_DRIFT="$FMT_DRIFT $_f"
done <<EOF
$(tofu fmt -check -recursive -list "$TOFU_DIR" 2>/dev/null || true)
EOF
if [ -n "${FMT_DRIFT# }" ]; then
  echo "  [FAIL] formatting drift in repo-tracked file(s):${FMT_DRIFT}"
  echo "         run: tofu fmt${FMT_DRIFT}"
  fail=1
else
  echo "  [ok] no formatting drift in repo-tracked files"
fi

echo "== tofu init -backend=false (downloads providers; needs registry network access) =="
if ! ( cd "$TOFU_DIR" && tofu init -backend=false -input=false ); then
  echo "  [FAIL] init failed"
  exit 1
fi

echo "== tofu validate (root module) =="
if ! ( cd "$TOFU_DIR" && tofu validate ); then
  echo "  [FAIL] validate failed"
  fail=1
fi

# S3: validate EVERY module standalone, not just the ones root happens to call.
#
# THE BLIND SPOT THIS CLOSES (DOCFIX-194, 2026-07-13): `tofu validate` on the root
# module only reaches modules the root actually INSTANTIATES. Modules that are not
# called -- or are called only from a commented-out block -- are NEVER PARSED. This
# gate printed a green "PASS" on 2026-07-13 while TWO modules were flat-out broken:
#
#   node-vm     libvirt_volume had a top-level `format = {...}` -- not a real
#               attribute (it nests under `target`). Could never have applied.
#               node-vm builds EVERY DC node.
#   netem-link  a destroy-time provisioner referenced var.* -- OpenTofu rejects
#               that at INIT. The module could not even initialize. netem-link is
#               the DR/latency mechanism for Stage 3 and the Stage 6 failover drill.
#
# Both would have detonated at first use, mid-deploy, in a stage where the runbook
# says "fix-forward is usually correct". A gate that reports green over unparsed code
# is worse than no gate: it manufactures false confidence. Hence this loop.
#
# Each module is validated in a TEMP COPY so the gate never writes .terraform/ or
# .terraform.lock.hcl into the repo tree (module-level lock files are noise; only the
# ROOT lock file is tracked, deliberately -- see .gitignore).
echo "== tofu validate (every module standalone -- the root-only blind spot) =="
if [ -d "$TOFU_DIR/modules" ]; then
  for moddir in "$TOFU_DIR"/modules/*/; do
    mod="$(basename "$moddir")"
    tmp="$(mktemp -d)"
    cp -R "$moddir." "$tmp/" 2>/dev/null
    rm -rf "$tmp/.terraform" "$tmp/.terraform.lock.hcl"
    if out="$( cd "$tmp" && tofu init -backend=false -input=false -no-color 2>&1 && tofu validate -no-color 2>&1 )"; then
      echo "  [PASS] modules/$mod"
    else
      echo "  [FAIL] modules/$mod"
      printf '%s\n' "$out" | grep -E "^(Error|  on |    [0-9]+:)" | head -8 | sed 's/^/         /'
      fail=1
    fi
    rm -rf "$tmp"
  done
else
  echo "  [WARN] no modules/ dir under $TOFU_DIR -- nothing to validate standalone"
fi

# EXTRA ROOTS -- the second blind spot, found 2026-07-20. This gate validated the
# OUTER root plus every module, and NOTHING ELSE. But the tree now holds two more
# roots that are applied against live infrastructure:
#   opentofu/vr1-dc0-substrate/  (D-123 Model B inner substrate -- 28 resources)
#   opentofu/vr1-dc0-maas/       (MAAS registration)
# Both were shipped and applied while this gate reported ALL GREEN over them, which
# is the same false-confidence failure the module loop above exists to prevent --
# a gate that is silent about applied code is worse than no gate.
#
# A root is any directory (other than modules/ and the tree root itself) that
# contains a *.tf declaring `terraform {` -- discovered, never hardcoded, so a
# vr1-dc1-* root added later is covered the day it appears rather than the day
# someone remembers to edit this list.
#
# Validated with -backend=false in a TEMP COPY, exactly like the modules: no
# .terraform/ or lock files are written into the repo tree, and no state or
# credentials are touched (validate never contacts a provider's API).
#
# The temp copy is of the WHOLE opentofu/ tree, not the root directory alone:
# these roots reference sibling modules by RELATIVE path (`source = "../modules/..."`),
# so copying just the root yields "Unreadable module directory" -- measured, and the
# reason this loop is shaped differently from the module loop above.
echo "== tofu validate (every EXTRA root -- roots other than the tree root) =="
extra_roots=0
treecopy="$(mktemp -d)"
cp -R "$TOFU_DIR/." "$treecopy/" 2>/dev/null
find "$treecopy" -name '.terraform' -type d -prune -exec rm -rf {} + 2>/dev/null
# local *.auto.tfvars are gitignored operator files that may hold secrets; validate
# does not need them, so they never enter the temp tree.
find "$treecopy" -name '*.auto.tfvars' -o -name '*.auto.tfvars.json' | xargs -r rm -f
while IFS= read -r rootdir; do
  [ -n "$rootdir" ] || continue
  rel="${rootdir#"$TOFU_DIR"/}"
  extra_roots=$((extra_roots+1))
  if out="$( cd "$treecopy/$rel" && tofu init -backend=false -input=false -no-color 2>&1 && tofu validate -no-color 2>&1 )"; then
    echo "  [PASS] $rel"
  else
    echo "  [FAIL] $rel"
    printf '%s\n' "$out" | grep -E "^(Error|  on |    [0-9]+:)" | head -8 | sed 's/^/         /'
    fail=1
  fi
done <<EOF
$(find "$TOFU_DIR" -mindepth 2 -maxdepth 2 -name '*.tf' -not -path '*/modules/*' -not -path '*/.terraform/*' \
    -exec grep -l '^terraform[[:space:]]*{' {} + 2>/dev/null | xargs -r -n1 dirname | sort -u)
EOF
rm -rf "$treecopy"
[ "$extra_roots" -gt 0 ] || echo "  (none found -- only the tree root)"

if [ "$fail" -eq 0 ]; then
  echo "PASS: opentofu-validate ($TOFU_DIR)"
  exit 0
else
  echo "FAIL: opentofu-validate ($TOFU_DIR)"
  exit 1
fi
