#!/usr/bin/env bash
set -Eeuo pipefail
umask 077

BASE="${ROUTER_OPS_BASE:-/opt/router-ops}"
ROOT="${BUNDLE_ROOT:?}"
RUN_DIR="${ROUTER_DETACHED_RUN_DIR:?}"
CHILD_RESULT="${ROUTER_DETACHED_CHILD_RESULT:?}"
INCOMING="${ROUTER_STEP_INCOMING:-/home/ops/incoming}"
SELFTEST="${ROUTER_EXACT_WORKER_SELFTEST:-false}"
STEP=STEP_050M07R20QZ_R01_VM121_AGENT_VM100_LIFECYCLE_CUTOVER
TARGET_SHA="5e34fecbd2adfbc7634ec9700f265163a6b988f20e34e3bb738ed6baf68944c5"

CURRENT="$RUN_DIR/workflow-current.txt"
EVENTS="$RUN_DIR/workflow-events.tsv"
EVIDENCE="$RUN_DIR/evidence"
REPORT="$RUN_DIR/report"
PAYLOAD="$RUN_DIR/public-payload"
FINAL_ENV="$RUN_DIR/final-publication.env"
TARGET_DIR="$RUN_DIR/target"
mkdir -p "$EVIDENCE" "$REPORT" "$PAYLOAD" "$INCOMING"
chmod 700 "$EVIDENCE" "$REPORT" "$PAYLOAD"

source "$BASE/lib/router-step-workflow.sh"
router_step_workflow_init "$CURRENT" "$EVENTS" preflight backup apply postcheck source_publish report_publish archive

CORE_CHANGE_COMPLETE=false
ROLLBACK_STATUS=not_required
BACKUP_READY=false
STOP_DETAIL=unknown
STOP_HANDLED=false
LATEST_ARCHIVE=not_created
LATEST_ARCHIVE_SHA256=unknown
VM100_PUBLIC_URL=unknown
VM121_PUBLIC_URL=unknown
STEP_REPORT_URL=unknown
PRE_RUNTIME_COUNT=unknown
PRE_DESIRED_COUNT=unknown
PRE_REGISTRY_COUNT=unknown
POST_RUNTIME_COUNT=unknown
POST_DESIRED_COUNT=unknown
POST_REGISTRY_COUNT=unknown
STALE_REGISTRY_PRUNED=unknown
PENDING_JOB_COUNT=unknown
VM100_BACKUP="/root/r20qz-p13-${ROUTER_DETACHED_RUN_ID:-run}"
VM121_BACKUP="/root/r20qz-p13-${ROUTER_DETACHED_RUN_ID:-run}"

write_child() {
  local result="$1"
  local rc="$2"
  local tmp="${CHILD_RESULT}.tmp.$$"
  {
    echo RESULT="$result"
    echo WORKER_RC="$rc"
    echo CORE_CHANGE_COMPLETE="$CORE_CHANGE_COMPLETE"
    echo ROLLBACK_STATUS="$ROLLBACK_STATUS"
    echo STOP_DETAIL="$STOP_DETAIL"
    echo PRE_RUNTIME_COUNT="$PRE_RUNTIME_COUNT"
    echo PRE_DESIRED_COUNT="$PRE_DESIRED_COUNT"
    echo PRE_REGISTRY_COUNT="$PRE_REGISTRY_COUNT"
    echo POST_RUNTIME_COUNT="$POST_RUNTIME_COUNT"
    echo POST_DESIRED_COUNT="$POST_DESIRED_COUNT"
    echo POST_REGISTRY_COUNT="$POST_REGISTRY_COUNT"
    echo STALE_REGISTRY_PRUNED="$STALE_REGISTRY_PRUNED"
    echo VM100_PUBLIC_URL="$VM100_PUBLIC_URL"
    echo VM121_PUBLIC_URL="$VM121_PUBLIC_URL"
    echo STEP_REPORT_URL="$STEP_REPORT_URL"
    echo LATEST_ARCHIVE="$LATEST_ARCHIVE"
    echo LATEST_ARCHIVE_SHA256="$LATEST_ARCHIVE_SHA256"
  } > "$tmp"
  chmod 600 "$tmp"
  mv "$tmp" "$CHILD_RESULT"
}

archive_latest() {
  local log="$EVIDENCE/archive.log"
  router_step_archive_latest_python "$BASE" "$INCOMING" "$(date -u +%Y%m%d-%H%M%S)" | tee "$log"
  LATEST_ARCHIVE="$(awk -F= '$1=="LATEST_ARCHIVE"{print substr($0,index($0,"=")+1)}' "$log" | tail -n1)"
  LATEST_ARCHIVE_SHA256="$(awk -F= '$1=="LATEST_ARCHIVE_SHA256"{print $2}' "$log" | tail -n1)"
}

vm100() {
  ssh -T -o BatchMode=yes -o ConnectTimeout=20 pve-mgts \
    "ssh -T -i /root/.ssh/pve_to_openwrt_mgts_ed25519 -o BatchMode=yes -o ConnectTimeout=20 root@10.71.100.1 '$1'"
}

vm121() {
  ssh -T -o BatchMode=yes -o ConnectTimeout=20 vm121 "$1"
}

vm100_put() {
  local src="$1"
  local dst="$2"
  local mode="$3"
  ssh -T -o BatchMode=yes -o ConnectTimeout=20 pve-mgts \
    "ssh -T -i /root/.ssh/pve_to_openwrt_mgts_ed25519 -o BatchMode=yes -o ConnectTimeout=20 root@10.71.100.1 'umask 077; cat > ${dst}.r20qz-new; chmod $mode ${dst}.r20qz-new; mv ${dst}.r20qz-new $dst'" < "$src"
}

vm121_put() {
  local src="$1"
  local dst="$2"
  local mode="$3"
  ssh -T -o BatchMode=yes -o ConnectTimeout=20 vm121 \
    "umask 077; cat > ${dst}.r20qz-new; chmod $mode ${dst}.r20qz-new; mv ${dst}.r20qz-new $dst" < "$src"
}

rollback_now() {
  [ "$BACKUP_READY" = true ] || return 0
  local ok=true
  set +e
  vm121 "systemctl stop wg-access-agent.service wg-access-trigger-watch.service wg-access-reconcile.timer >/dev/null 2>&1 || true; cp '$VM121_BACKUP/wg_access_agent.py' /opt/wg-access/agent/wg_access_agent.py; chmod 755 /opt/wg-access/agent/wg_access_agent.py; cp '$VM121_BACKUP/agent.env.example' /opt/wg-access/agent/agent.env.example; chmod 600 /opt/wg-access/agent/agent.env.example" || ok=false
  vm100 "cp '$VM100_BACKUP/router-wgpay-peer-lifecycle.sh' /usr/local/sbin/router-wgpay-peer-lifecycle.sh; chmod 755 /usr/local/sbin/router-wgpay-peer-lifecycle.sh; rm -rf /etc/router-wgpay-peer-state; cp -a '$VM100_BACKUP/peer-state' /etc/router-wgpay-peer-state; /usr/local/sbin/router-wgpay-canary-apply.sh start >/dev/null 2>&1; /usr/local/sbin/router-egress-activity-apply.sh >/dev/null 2>&1" || ok=false
  vm121 "systemctl start wg-access-trigger-watch.service >/dev/null 2>&1 || true; systemctl start wg-access-reconcile.timer >/dev/null 2>&1 || true" || ok=false
  set -e
  if [[ "$ok" == true ]]; then ROLLBACK_STATUS=PASS; else ROLLBACK_STATUS=FAILED; fi
}

stop_handler() {
  local rc="$1"
  local line="$2"
  local cmd="$3"
  trap - ERR
  [[ "$STOP_HANDLED" == false ]] || exit "$rc"
  STOP_HANDLED=true
  if [[ "$CORE_CHANGE_COMPLETE" != true ]]; then
    rollback_now
  else
    ROLLBACK_STATUS=not_required
  fi
  {
    echo RESULT=STOP_R20QZ_VM121_AGENT_VM100_LIFECYCLE_CUTOVER
    echo STOP_RC="$rc"
    echo STOP_LINE="$line"
    printf 'STOP_COMMAND=%q\n' "$cmd"
    echo CORE_CHANGE_COMPLETE="$CORE_CHANGE_COMPLETE"
    echo ROLLBACK_STATUS="$ROLLBACK_STATUS"
    echo STOP_DETAIL="$STOP_DETAIL"
  } | tee "$REPORT/stop.txt" "$EVIDENCE/stop.txt"
  set +e
  archive_latest >/dev/null 2>&1
  set -e
  write_child STOP_R20QZ_VM121_AGENT_VM100_LIFECYCLE_CUTOVER "$rc"
  exit "$rc"
}
trap 'rc=$?; stop_handler "$rc" "$LINENO" "$BASH_COMMAND"' ERR

router_step_phase_begin preflight
"$ROOT/scripts/target-apply.sh" "$TARGET_SHA" "$TARGET_DIR" > "$EVIDENCE/target-extract.txt"

if [[ "$SELFTEST" == true ]]; then
  cat > "$EVIDENCE/vm100-preflight.txt" <<'EOF'
HEAD=bc1ca33b10ae2a2977f984fddf9f6dc75ca2b559
TREE=aea04fbe9c40af01a638c7a2b12a841ce6fd5d0b
STATUS_COUNT=0
initialized=true
registry_valid=true
profile_count=2
wireguard_count=2
amneziawg_count=0
selector_registry_consistent=true
runtime_pair=10.253.1.14	AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
registry_pair=10.253.1.13	BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
registry_pair=10.253.1.14	AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
RESULT=PASS_VM100_PREFLIGHT
EOF
  cat > "$EVIDENCE/vm121-preflight.txt" <<'EOF'
HEAD=93e0e5a31f0900a498b77d22e6a7f2ca5fd7fee0
TREE=b6fb72951eedc96decf445b71e7d9d6e77ec5ee3
STATUS_COUNT=0
desired_pair=10.253.1.14	AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
pending_job_count=0
RESULT=PASS_VM121_PREFLIGHT
EOF
else
  vm100 'G=/root/.vm100-source.git; echo HEAD=$(git --git-dir="$G" rev-parse HEAD); echo TREE=$(git --git-dir="$G" rev-parse HEAD^{tree}); echo STATUS_COUNT=$(git --git-dir="$G" --work-tree=/ status --porcelain --untracked-files=no | wc -l | tr -d " "); /usr/local/sbin/router-wgpay-peer-lifecycle.sh --status; wg show wg_paid allowed-ips 2>/dev/null | while read -r key ips; do oldifs=$IFS; IFS=,; for ip in $ips; do ip=${ip%/32}; echo "runtime_pair=$ip	$key"; done; IFS=$oldifs; done; while IFS="$(printf "\\t")" read -r profile protocol iface key ip normal active generation created updated; do case "$profile" in ""|\#*) continue;; esac; [ "$protocol" = wireguard ] && echo "registry_pair=$ip	$key"; done < /etc/router-wgpay-peer-state/registry.tsv; echo RESULT=PASS_VM100_PREFLIGHT' > "$EVIDENCE/vm100-preflight.txt" 2>&1

  vm121 'G=/root/.vm121-source.git; echo HEAD=$(git --git-dir="$G" rev-parse HEAD); echo TREE=$(git --git-dir="$G" rev-parse HEAD^{tree}); echo STATUS_COUNT=$(git --git-dir="$G" --work-tree=/ status --porcelain --untracked-files=no | wc -l | tr -d " "); grep -F "wg set " /opt/wg-access/agent/wg_access_agent.py >/dev/null && echo LEGACY_DIRECT_WG_WRITE_PRESENT=true || echo LEGACY_DIRECT_WG_WRITE_PRESENT=false' > "$EVIDENCE/vm121-preflight.txt" 2>&1

  ssh -T -o BatchMode=yes -o ConnectTimeout=20 vm121 'docker exec -i wgaccess-postgres sh -lc '\''exec psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -At'\''' >> "$EVIDENCE/vm121-preflight.txt" 2>> "$EVIDENCE/vm121-preflight.txt" <<'SQL'
SELECT 'desired_pair=' || tunnel_ip || E'\t' || public_key
FROM peers
WHERE enabled IS TRUE
ORDER BY tunnel_ip;
SELECT 'pending_job_count=' || count(*)
FROM provisioning_jobs
WHERE status IN ('pending','running');
SQL
  echo RESULT=PASS_VM121_PREFLIGHT >> "$EVIDENCE/vm121-preflight.txt"
fi

grep -Fqx 'HEAD=bc1ca33b10ae2a2977f984fddf9f6dc75ca2b559' "$EVIDENCE/vm100-preflight.txt" || { STOP_DETAIL=vm100_head; false; }
grep -Fqx 'TREE=aea04fbe9c40af01a638c7a2b12a841ce6fd5d0b' "$EVIDENCE/vm100-preflight.txt" || { STOP_DETAIL=vm100_tree; false; }
grep -Fqx 'STATUS_COUNT=0' "$EVIDENCE/vm100-preflight.txt" || { STOP_DETAIL=vm100_dirty; false; }
grep -Fqx 'initialized=true' "$EVIDENCE/vm100-preflight.txt" || { STOP_DETAIL=vm100_lifecycle_uninitialized; false; }
grep -Fqx 'registry_valid=true' "$EVIDENCE/vm100-preflight.txt" || { STOP_DETAIL=vm100_registry_invalid; false; }
grep -Fqx 'selector_registry_consistent=true' "$EVIDENCE/vm100-preflight.txt" || { STOP_DETAIL=vm100_selector_registry_inconsistent; false; }

grep -Fqx 'HEAD=93e0e5a31f0900a498b77d22e6a7f2ca5fd7fee0' "$EVIDENCE/vm121-preflight.txt" || { STOP_DETAIL=vm121_head; false; }
grep -Fqx 'TREE=b6fb72951eedc96decf445b71e7d9d6e77ec5ee3' "$EVIDENCE/vm121-preflight.txt" || { STOP_DETAIL=vm121_tree; false; }
grep -Fqx 'STATUS_COUNT=0' "$EVIDENCE/vm121-preflight.txt" || { STOP_DETAIL=vm121_dirty; false; }
if [[ "$SELFTEST" != true ]]; then
  grep -Fqx 'LEGACY_DIRECT_WG_WRITE_PRESENT=true' "$EVIDENCE/vm121-preflight.txt" || { STOP_DETAIL=vm121_legacy_agent_shape; false; }
fi
PENDING_JOB_COUNT="$(awk -F= '$1=="pending_job_count"{print $2}' "$EVIDENCE/vm121-preflight.txt" | tail -n1)"
[[ "$PENDING_JOB_COUNT" == 0 ]] || { STOP_DETAIL=pending_jobs_present; false; }

grep '^runtime_pair=' "$EVIDENCE/vm100-preflight.txt" | cut -d= -f2- | LC_ALL=C sort -u > "$EVIDENCE/pre-runtime.set"
grep '^registry_pair=' "$EVIDENCE/vm100-preflight.txt" | cut -d= -f2- | LC_ALL=C sort -u > "$EVIDENCE/pre-registry.set"
grep '^desired_pair=' "$EVIDENCE/vm121-preflight.txt" | cut -d= -f2- | LC_ALL=C sort -u > "$EVIDENCE/pre-desired.set"
PRE_RUNTIME_COUNT="$(wc -l < "$EVIDENCE/pre-runtime.set" | tr -d ' ')"
PRE_REGISTRY_COUNT="$(wc -l < "$EVIDENCE/pre-registry.set" | tr -d ' ')"
PRE_DESIRED_COUNT="$(wc -l < "$EVIDENCE/pre-desired.set" | tr -d ' ')"
cmp -s "$EVIDENCE/pre-runtime.set" "$EVIDENCE/pre-desired.set" || { STOP_DETAIL=runtime_desired_not_aligned_at_cutover; false; }
comm -23 "$EVIDENCE/pre-desired.set" "$EVIDENCE/pre-registry.set" > "$EVIDENCE/pre-desired-missing-from-registry.set"
[[ ! -s "$EVIDENCE/pre-desired-missing-from-registry.set" ]] || { STOP_DETAIL=desired_missing_from_registry; false; }
comm -23 "$EVIDENCE/pre-registry.set" "$EVIDENCE/pre-desired.set" > "$EVIDENCE/pre-stale-registry.set"
STALE_REGISTRY_PRUNED="$(wc -l < "$EVIDENCE/pre-stale-registry.set" | tr -d ' ')"
router_step_phase_complete preflight runtime_equals_vm121_desired_and_registry_only_contains_safe_stale_extras

router_step_phase_begin backup
if [[ "$SELFTEST" == true ]]; then
  BACKUP_READY=true
  echo RESULT=PASS_R20QZ_BACKUP_SELFTEST > "$EVIDENCE/backup.txt"
else
  vm100 "rm -rf '$VM100_BACKUP'; mkdir -p '$VM100_BACKUP'; chmod 700 '$VM100_BACKUP'; cp /usr/local/sbin/router-wgpay-peer-lifecycle.sh '$VM100_BACKUP/router-wgpay-peer-lifecycle.sh'; cp -a /etc/router-wgpay-peer-state '$VM100_BACKUP/peer-state'" > "$EVIDENCE/vm100-backup.txt" 2>&1
  vm121 "rm -rf '$VM121_BACKUP'; mkdir -p '$VM121_BACKUP'; chmod 700 '$VM121_BACKUP'; cp /opt/wg-access/agent/wg_access_agent.py '$VM121_BACKUP/wg_access_agent.py'; cp /opt/wg-access/agent/agent.env.example '$VM121_BACKUP/agent.env.example'" > "$EVIDENCE/vm121-backup.txt" 2>&1
  BACKUP_READY=true
fi
router_step_phase_complete backup affected_sources_and_dynamic_registry_backed_up

router_step_phase_begin apply
if [[ "$SELFTEST" == true ]]; then
  echo RESULT=PASS_R20QZ_APPLY_SELFTEST > "$EVIDENCE/apply.txt"
else
  vm121 'systemctl stop wg-access-trigger-watch.service wg-access-reconcile.timer wg-access-agent.service >/dev/null 2>&1 || true' > "$EVIDENCE/vm121-stop-services.txt" 2>&1
  vm100_put "$TARGET_DIR/vm100/usr/local/sbin/router-wgpay-peer-lifecycle.sh" /usr/local/sbin/router-wgpay-peer-lifecycle.sh 755
  vm100 'sh -n /usr/local/sbin/router-wgpay-peer-lifecycle.sh; /usr/local/sbin/router-wgpay-peer-lifecycle.sh --status' > "$EVIDENCE/vm100-after-install.txt" 2>&1
  grep -Fqx 'RESULT=PASS_PEER_LIFECYCLE_STATUS' "$EVIDENCE/vm100-after-install.txt" || { STOP_DETAIL=vm100_lifecycle_install_verify; false; }

  vm121_put "$TARGET_DIR/vm121/opt/wg-access/agent/wg_access_agent.py" /opt/wg-access/agent/wg_access_agent.py 755
  vm121_put "$TARGET_DIR/vm121/opt/wg-access/agent/agent.env.example" /opt/wg-access/agent/agent.env.example 600
  vm121 'python3 - <<'\''PY'\''
from pathlib import Path
p=Path("/opt/wg-access/agent/wg_access_agent.py")
compile(p.read_text(encoding="utf-8"), str(p), "exec")
print("RESULT=PASS_VM121_AGENT_COMPILE")
PY
! grep -F "wg set " /opt/wg-access/agent/wg_access_agent.py
! grep -F "REMOTE_WG_INTERFACE" /opt/wg-access/agent/wg_access_agent.py' > "$EVIDENCE/vm121-after-install.txt" 2>&1
  grep -Fqx 'RESULT=PASS_VM121_AGENT_COMPILE' "$EVIDENCE/vm121-after-install.txt" || { STOP_DETAIL=vm121_agent_install_verify; false; }

  vm121 'systemctl start wg-access-agent.service; systemctl show wg-access-agent.service -p Result -p ExecMainStatus --no-pager' > "$EVIDENCE/first-lifecycle-reconcile.txt" 2>&1
  grep -Fqx 'Result=success' "$EVIDENCE/first-lifecycle-reconcile.txt" || { STOP_DETAIL=first_lifecycle_reconcile_failed; false; }
  grep -Fqx 'ExecMainStatus=0' "$EVIDENCE/first-lifecycle-reconcile.txt" || { STOP_DETAIL=first_lifecycle_reconcile_rc; false; }
  vm121 'systemctl start wg-access-trigger-watch.service; systemctl start wg-access-reconcile.timer' > "$EVIDENCE/vm121-services-restored.txt" 2>&1
fi
router_step_phase_complete apply vm121_agent_cut_over_to_vm100_lifecycle_without_selector_fields

router_step_phase_begin postcheck
if [[ "$SELFTEST" == true ]]; then
  cat > "$EVIDENCE/vm100-postcheck.txt" <<'EOF'
initialized=true
registry_valid=true
profile_count=1
wireguard_count=1
amneziawg_count=0
selector_registry_consistent=true
runtime_pair=10.253.1.14	AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
registry_pair=10.253.1.14	AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
selector_count=1
canonical_count=1
RESULT=PASS_R20QZ_VM100_POSTCHECK
EOF
  cat > "$EVIDENCE/vm121-postcheck.txt" <<'EOF'
desired_pair=10.253.1.14	AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
pending_job_count=0
agent_state_secret_keys=0
legacy_direct_wg_write_present=false
trigger_watch_active=active
reconcile_timer_active=active
RESULT=PASS_R20QZ_VM121_POSTCHECK
EOF
else
  vm100 '/usr/local/sbin/router-wgpay-peer-lifecycle.sh --status; wg show wg_paid allowed-ips 2>/dev/null | while read -r key ips; do oldifs=$IFS; IFS=,; for ip in $ips; do ip=${ip%/32}; echo "runtime_pair=$ip	$key"; done; IFS=$oldifs; done; while IFS="$(printf "\\t")" read -r profile protocol iface key ip normal active generation created updated; do case "$profile" in ""|\#*) continue;; esac; [ "$protocol" = wireguard ] && echo "registry_pair=$ip	$key"; done < /etc/router-wgpay-peer-state/registry.tsv; echo selector_count=$(grep -Ev "^[[:space:]]*(#|$)" /etc/router-wgpay-peer-state/peers.conf | wc -l | tr -d " "); echo canonical_count=$(grep -Ev "^[[:space:]]*(#|$)" /etc/router-wgpay-peer-state/canonical.conf | wc -l | tr -d " "); nft list table inet wgpay_dscp_canary >/dev/null; nft list table inet router_egress_activity >/dev/null; echo RESULT=PASS_R20QZ_VM100_POSTCHECK' > "$EVIDENCE/vm100-postcheck.txt" 2>&1

  ssh -T -o BatchMode=yes -o ConnectTimeout=20 vm121 'docker exec -i wgaccess-postgres sh -lc '\''exec psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -At'\''' > "$EVIDENCE/vm121-postcheck.txt" 2>> "$EVIDENCE/vm121-postcheck.txt" <<'SQL'
SELECT 'desired_pair=' || tunnel_ip || E'\t' || public_key
FROM peers
WHERE enabled IS TRUE
ORDER BY tunnel_ip;
SELECT 'pending_job_count=' || count(*)
FROM provisioning_jobs
WHERE status IN ('pending','running');
SQL
  vm121 'python3 - <<'\''PY'\''
import json
from pathlib import Path
env={}
for raw in Path("/opt/wg-access/agent/agent.env").read_text(encoding="utf-8").splitlines():
    raw=raw.strip()
    if raw and not raw.startswith("#") and "=" in raw:
        k,v=raw.split("=",1); env[k.strip()]=v.strip()
state=Path(env["STATE_FILE"])
data=json.loads(state.read_text(encoding="utf-8")) if state.exists() else {}
forbidden={"preshared_key","private_key","client_config"}
def count_bad(x):
    if isinstance(x,dict):
        return sum(1 for k in x if k in forbidden)+sum(count_bad(v) for v in x.values())
    if isinstance(x,list):
        return sum(count_bad(v) for v in x)
    return 0
print("agent_state_secret_keys="+str(count_bad(data)))
PY
if grep -F "wg set " /opt/wg-access/agent/wg_access_agent.py >/dev/null; then echo legacy_direct_wg_write_present=true; else echo legacy_direct_wg_write_present=false; fi
echo trigger_watch_active=$(systemctl is-active wg-access-trigger-watch.service || true)
echo reconcile_timer_active=$(systemctl is-active wg-access-reconcile.timer || true)
echo RESULT=PASS_R20QZ_VM121_POSTCHECK' >> "$EVIDENCE/vm121-postcheck.txt" 2>> "$EVIDENCE/vm121-postcheck.txt"
fi

grep -Fqx 'registry_valid=true' "$EVIDENCE/vm100-postcheck.txt" || { STOP_DETAIL=post_registry_invalid; false; }
grep -Fqx 'selector_registry_consistent=true' "$EVIDENCE/vm100-postcheck.txt" || { STOP_DETAIL=post_selector_registry_inconsistent; false; }
grep -Fqx 'amneziawg_count=0' "$EVIDENCE/vm100-postcheck.txt" || { STOP_DETAIL=post_awg_count; false; }
grep -Fqx 'agent_state_secret_keys=0' "$EVIDENCE/vm121-postcheck.txt" || { STOP_DETAIL=agent_state_secret_residue; false; }
grep -Fqx 'legacy_direct_wg_write_present=false' "$EVIDENCE/vm121-postcheck.txt" || { STOP_DETAIL=legacy_direct_wg_write_still_present; false; }
grep -Fqx 'trigger_watch_active=active' "$EVIDENCE/vm121-postcheck.txt" || { STOP_DETAIL=trigger_watch_not_active; false; }
grep -Fqx 'reconcile_timer_active=active' "$EVIDENCE/vm121-postcheck.txt" || { STOP_DETAIL=reconcile_timer_not_active; false; }
[[ "$(awk -F= '$1=="pending_job_count"{print $2}' "$EVIDENCE/vm121-postcheck.txt" | tail -n1)" == 0 ]] || { STOP_DETAIL=post_pending_jobs; false; }

grep '^runtime_pair=' "$EVIDENCE/vm100-postcheck.txt" | cut -d= -f2- | LC_ALL=C sort -u > "$EVIDENCE/post-runtime.set"
grep '^registry_pair=' "$EVIDENCE/vm100-postcheck.txt" | cut -d= -f2- | LC_ALL=C sort -u > "$EVIDENCE/post-registry.set"
grep '^desired_pair=' "$EVIDENCE/vm121-postcheck.txt" | cut -d= -f2- | LC_ALL=C sort -u > "$EVIDENCE/post-desired.set"
POST_RUNTIME_COUNT="$(wc -l < "$EVIDENCE/post-runtime.set" | tr -d ' ')"
POST_REGISTRY_COUNT="$(wc -l < "$EVIDENCE/post-registry.set" | tr -d ' ')"
POST_DESIRED_COUNT="$(wc -l < "$EVIDENCE/post-desired.set" | tr -d ' ')"
cmp -s "$EVIDENCE/post-runtime.set" "$EVIDENCE/post-desired.set" || { STOP_DETAIL=post_runtime_desired_mismatch; false; }
cmp -s "$EVIDENCE/post-registry.set" "$EVIDENCE/post-desired.set" || { STOP_DETAIL=post_registry_desired_mismatch; false; }
[[ "$(awk -F= '$1=="selector_count"{print $2}' "$EVIDENCE/vm100-postcheck.txt" | tail -n1)" == "$POST_DESIRED_COUNT" ]] || { STOP_DETAIL=post_selector_count; false; }
[[ "$(awk -F= '$1=="canonical_count"{print $2}' "$EVIDENCE/vm100-postcheck.txt" | tail -n1)" == "$POST_DESIRED_COUNT" ]] || { STOP_DETAIL=post_canonical_count; false; }

CORE_CHANGE_COMPLETE=true
BACKUP_READY=false
ROLLBACK_STATUS=not_required
router_step_phase_complete postcheck vm121_desired_vm100_registry_runtime_and_selector_membership_aligned

router_step_phase_begin source_publish
if [[ "$SELFTEST" == true ]]; then
  cat > "$EVIDENCE/vm100-machine-close.txt" <<'EOF'
RESULT=PASS_ROUTER_MACHINE_CLOSE
COMMIT=1111111111111111111111111111111111111111
TREE=2222222222222222222222222222222222222222
GIT_FILE_COUNT=33
VM100_PUBLIC_URL=selftest://vm100-source/
EOF
  cat > "$EVIDENCE/vm121-machine-close.txt" <<'EOF'
RESULT=PASS_ROUTER_MACHINE_CLOSE
COMMIT=3333333333333333333333333333333333333333
TREE=4444444444444444444444444444444444444444
GIT_FILE_COUNT=41
VM121_PUBLIC_URL=selftest://vm121-source/
EOF
  VM100_PUBLIC_URL=selftest://vm100-source/
  VM121_PUBLIC_URL=selftest://vm121-source/
else
  "$BASE/bin/router-machine-close-safe" vm100 'STEP_050M07R20QZ_R01: lifecycle ensure for VM121 agent ownership cutover' > "$EVIDENCE/vm100-machine-close.txt" 2>&1
  grep -Fqx 'RESULT=PASS_ROUTER_MACHINE_CLOSE' "$EVIDENCE/vm100-machine-close.txt" || { STOP_DETAIL=vm100_machine_close; false; }
  VM100_PUBLIC_URL="$(awk -F= '$1=="VM100_PUBLIC_URL"{print substr($0,index($0,"=")+1)}' "$EVIDENCE/vm100-machine-close.txt" | tail -n1)"

  "$BASE/bin/router-machine-close-safe" vm121 'STEP_050M07R20QZ_R01: agent cutover to VM100 lifecycle without selector ownership' > "$EVIDENCE/vm121-machine-close.txt" 2>&1
  grep -Fqx 'RESULT=PASS_ROUTER_MACHINE_CLOSE' "$EVIDENCE/vm121-machine-close.txt" || { STOP_DETAIL=vm121_machine_close; false; }
  VM121_PUBLIC_URL="$(awk -F= '$1=="VM121_PUBLIC_URL"{print substr($0,index($0,"=")+1)}' "$EVIDENCE/vm121-machine-close.txt" | tail -n1)"
fi
router_step_phase_complete source_publish vm100_and_vm121_machine_git_closed_and_published

cat > "$REPORT/report.txt" <<EOF
RESULT=PASS_R20QZ_VM121_AGENT_VM100_LIFECYCLE_CUTOVER
REVISION=R01
CORE_CHANGE_COMPLETE=true
ROLLBACK_STATUS=not_required
VM100_BASE_COMMIT=bc1ca33b10ae2a2977f984fddf9f6dc75ca2b559
VM121_BASE_COMMIT=93e0e5a31f0900a498b77d22e6a7f2ca5fd7fee0
PRE_RUNTIME_COUNT=$PRE_RUNTIME_COUNT
PRE_DESIRED_COUNT=$PRE_DESIRED_COUNT
PRE_REGISTRY_COUNT=$PRE_REGISTRY_COUNT
STALE_REGISTRY_PRUNED=$STALE_REGISTRY_PRUNED
POST_RUNTIME_COUNT=$POST_RUNTIME_COUNT
POST_DESIRED_COUNT=$POST_DESIRED_COUNT
POST_REGISTRY_COUNT=$POST_REGISTRY_COUNT
VM121_DIRECT_WG_WRITES=false
VM121_SELECTOR_OWNERSHIP=false
SELECTOR_OWNER=VM100
VM100_LIFECYCLE_ENSURE=true
VM121_AGENT_STATE_SECRET_KEYS=0
DATABASE_SCHEMA_CHANGED=false
VM101_CHANGED=false
REBOOT_PERFORMED=false
LONG_RECOVERY_TEST_REPEATED=false
VM100_PUBLIC_URL=$VM100_PUBLIC_URL
VM121_PUBLIC_URL=$VM121_PUBLIC_URL
VM101_PUBLIC_URL=https://reports.secret-studio.ru/latest/20260801-095331_vm101_git_source_ac678d1b3164/
GLOBAL_PLAN_URL=https://reports.secret-studio.ru/latest/20260802-105248_global_project_plan_wg_paid_after_r20qw/
ACCESS_MAP_URL=https://reports.secret-studio.ru/latest/20260715-210236_access_map_canonical_current/
NEXT_ACTION=P1.4 restart/reconcile proof
EOF
cp "$REPORT/report.txt" "$REPORT/verify.txt"
cp "$ROOT/scripts/run-step.sh" "$REPORT/executed-step.sh"
cp "$ROOT/release-info.json" "$REPORT/release-info.json"
cp "$ROOT/manifest.sha256" "$REPORT/bundle-manifest.sha256"
cp "$ROOT/rollback/README.md" "$REPORT/rollback.txt"
cp "$EVIDENCE"/*.txt "$REPORT/" 2>/dev/null || true

router_step_phase_begin report_publish
if [[ "$SELFTEST" == true ]]; then
  DEST="${ROUTER_PUBLIC_ROOT:?}/latest/r20qz-r01-selftest"
  rm -rf "$DEST"
  mkdir -p "$DEST"
  cp -a "$REPORT"/. "$DEST"/
  STEP_REPORT_URL=selftest://r20qz-r01-selftest/
else
  args=()
  while IFS= read -r file; do
    args+=("$file" "$(basename "$file")")
  done < <(find "$REPORT" -maxdepth 1 -type f -print | LC_ALL=C sort)
  router_step_publish_payload step050m07r20qz_r01_vm121_agent_vm100_lifecycle_cutover "$REPORT" "$PAYLOAD" "$CURRENT" "$EVENTS" "$FINAL_ENV" "${args[@]}"
  STEP_REPORT_URL="$(awk -F= '$1=="PUBLIC_URL"{print substr($0,index($0,"=")+1)}' "$FINAL_ENV" | tail -n1)"
fi
router_step_phase_complete report_publish step_report_published

router_step_phase_begin archive
archive_latest
router_step_phase_complete archive latest_created
router_step_workflow_mark_complete

trap - ERR
STOP_DETAIL=none
write_child PASS_R20QZ_VM121_AGENT_VM100_LIFECYCLE_CUTOVER 0
cat "$REPORT/report.txt"
echo STEP_REPORT_URL="$STEP_REPORT_URL"
echo LATEST_ARCHIVE="$LATEST_ARCHIVE"
echo LATEST_ARCHIVE_SHA256="$LATEST_ARCHIVE_SHA256"
