#!/usr/bin/env bash
# cutover-lib — relocate a module from its legacy checkout to its AI-OS target.
# Six fail-closed phases with a single rollback path; sourced by modules/*/cutover.sh.
set -euo pipefail

CUTOVER_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
CUTOVER_STATE_ROOT="${HOME}/.local/state/overdeck"
CUTOVER_UNIT_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/systemd/user"
CUTOVER_ENGINE_MARKERS=(HARNESS_TRANSCRIPT_PATH HARNESS_DELIVERY_ID HARNESS_WORKSPACE_LEASE_TOKEN)
CUTOVER_FUSER_CHUNK=500

CUTOVER_MODULE=""
CUTOVER_OLD_PATH=""
CUTOVER_TARGET=""
CUTOVER_MANIFEST=""
CUTOVER_BACKUP_DIR=""
CUTOVER_ARCHIVE_PATH=""
CUTOVER_STATE_FILE=""
CUTOVER_LINKED_STATE_DIRS=()

cutover_log() { printf 'cutover: %s\n' "$*"; }
cutover_die() { printf 'cutover: ERROR: %s\n' "$*" >&2; exit 1; }
cutover_fail() { printf 'cutover: ERROR: %s\n' "$*" >&2; return 1; }

cutover_refuse_engine() {
  local marker
  for marker in "${CUTOVER_ENGINE_MARKERS[@]}"; do
    if [[ -n "${!marker:-}" ]]; then
      cutover_die "refusing to run under the harness engine ($marker is set) — run cutover from the main session"
    fi
  done
}

cutover_require_tools() {
  local tool
  for tool in jq rsync fuser systemctl find awk; do
    command -v "$tool" >/dev/null 2>&1 || cutover_die "required tool missing: $tool"
  done
}

cutover_manifest_query() { jq -r "$1" "$CUTOVER_MANIFEST"; }
cutover_install_mode() { cutover_manifest_query '.install.mode // "in-place"'; }
cutover_health_rel() { cutover_manifest_query '.health // empty'; }
cutover_state_dirs() { cutover_manifest_query '.state_dirs[]? // empty'; }
cutover_services() { cutover_manifest_query '.services[]? // empty'; }

cutover_load_manifest() { # $1=module
  CUTOVER_MANIFEST="$CUTOVER_ROOT/modules/$1/deck.module.json"
  [[ -f "$CUTOVER_MANIFEST" ]] || cutover_die "module manifest missing: $CUTOVER_MANIFEST"
}

cutover_already_cut() {
  [[ -L "$CUTOVER_OLD_PATH" ]] || return 1
  [[ -e "$CUTOVER_TARGET" ]] || return 1
  [[ "$(readlink -f "$CUTOVER_OLD_PATH")" == "$(readlink -f "$CUTOVER_TARGET")" ]]
}

cutover_dir_fingerprint() { # $1=dir
  local dir="$1" count bytes
  count=$(find "$dir" -type f -printf '.' | wc -c)
  bytes=$(find "$dir" -type f -printf '%s\n' | awk '{ s += $1 } END { print s + 0 }')
  printf '%s/%s' "$count" "$bytes"
}

cutover_state_file_path() { printf '%s/cutover/%s.json' "$CUTOVER_STATE_ROOT" "$CUTOVER_MODULE"; }

cutover_write_state_file() { # $1=status $2=reason
  local status="$1" reason="${2:-}" linked
  linked=$(printf '%s\n' "${CUTOVER_LINKED_STATE_DIRS[@]}" \
    | jq -R -s 'split("\n") | map(select(length > 0) | split("\t") | {dir: .[0], prior: .[1]})')
  mkdir -p "$CUTOVER_STATE_ROOT/cutover"
  CUTOVER_STATE_FILE=$(cutover_state_file_path)
  jq -n \
    --arg module "$CUTOVER_MODULE" \
    --arg old_path "$CUTOVER_OLD_PATH" \
    --arg target "$CUTOVER_TARGET" \
    --arg backup_dir "$CUTOVER_BACKUP_DIR" \
    --arg archive_path "$CUTOVER_ARCHIVE_PATH" \
    --arg status "$status" \
    --arg reason "$reason" \
    --argjson linked_state_dirs "$linked" \
    '{module: $module, old_path: $old_path, target: $target,
      backup_dir: $backup_dir, archive_path: $archive_path,
      status: $status, reason: $reason, linked_state_dirs: $linked_state_dirs}' \
    >"$CUTOVER_STATE_FILE"
}

cutover_assert_no_partial_state() {
  local state_file status
  state_file=$(cutover_state_file_path)
  [[ -f "$state_file" ]] || return 0
  status=$(jq -r '.status // "in-progress"' "$state_file")
  if [[ "$status" == "complete" ]]; then return 0; fi
  local detail
  detail=$(jq -r '"archive=\(.archive_path) backup=\(.backup_dir) old_path=\(.old_path) reason=\(.reason)"' \
    "$state_file")
  cutover_die "$CUTOVER_MODULE is in state '$status' from an earlier run — $detail; inspect those paths, re-run with --rollback once they are consistent, or delete $state_file to override"
}

cutover_unit_name() { basename "$1"; }

cutover_phase1_preflight() {
  cutover_log "phase 1: preflight"
  local sweep="$CUTOVER_ROOT/tools/ref-sweep.sh"
  local inventory="$CUTOVER_ROOT/tools/ref-inventory.json"
  [[ -f "$sweep" ]] || cutover_fail "phase 1: ref-sweep missing: $sweep" || return 1
  [[ -f "$inventory" ]] || cutover_fail "phase 1: ref inventory missing: $inventory" || return 1

  local scratch swept
  scratch=$(mktemp "${TMPDIR:-/tmp}/cutover-ref-inventory.XXXXXX") \
    || { cutover_fail "phase 1: cannot create scratch inventory"; return 1; }
  swept=$(REF_INVENTORY="$scratch" bash "$sweep") || {
    rm -f "$scratch"
    cutover_fail "phase 1: ref-sweep failed"
    return 1
  }
  rm -f "$scratch"
  if ! diff <(jq -S . "$inventory") <(printf '%s\n' "$swept" | jq -S .) >/dev/null; then
    cutover_fail "phase 1: ref inventory is stale — re-run tools/ref-sweep.sh and commit the result"
    return 1
  fi

  if [[ "$(cutover_install_mode)" == "artifact" ]]; then
    [[ -e "$CUTOVER_TARGET" ]] || cutover_fail "phase 1: artifact missing at $CUTOVER_TARGET — run deckctl install $CUTOVER_MODULE" || return 1
    cutover_run_health "phase 1" || return 1
  fi

  mkdir -p "$CUTOVER_STATE_ROOT/backups" \
    || { cutover_fail "phase 1: cannot create $CUTOVER_STATE_ROOT/backups"; return 1; }
  CUTOVER_BACKUP_DIR=$(mktemp -d "$CUTOVER_STATE_ROOT/backups/$(date -u +%Y%m%dT%H%M%SZ)-$CUTOVER_MODULE.XXXXXX") \
    || { cutover_fail "phase 1: cannot create backup directory"; return 1; }
  mkdir -p "$CUTOVER_BACKUP_DIR/state" "$CUTOVER_BACKUP_DIR/units" \
    || { cutover_fail "phase 1: cannot create backup directory $CUTOVER_BACKUP_DIR"; return 1; }

  local dir
  while IFS= read -r dir; do
    [[ -n "$dir" ]] || continue
    [[ -d "$CUTOVER_OLD_PATH/$dir" ]] || continue
    mkdir -p "$CUTOVER_BACKUP_DIR/state/$dir" \
      || { cutover_fail "phase 1: cannot create backup subdirectory for $dir"; return 1; }
    rsync -aH "$CUTOVER_OLD_PATH/$dir/" "$CUTOVER_BACKUP_DIR/state/$dir/" \
      || { cutover_fail "phase 1: state backup failed for $dir"; return 1; }
  done < <(cutover_state_dirs)

  local svc unit installed
  while IFS= read -r svc; do
    [[ -n "$svc" ]] || continue
    unit=$(cutover_unit_name "$svc")
    installed="$CUTOVER_UNIT_DIR/$unit"
    [[ -e "$installed" || -L "$installed" ]] || continue
    cp -a "$installed" "$CUTOVER_BACKUP_DIR/units/$unit" \
      || { cutover_fail "phase 1: unit backup failed for $unit"; return 1; }
  done < <(cutover_services)

  if [[ "$CUTOVER_MODULE" == "harness" && -d "${HOME}/.harness" ]]; then
    find "${HOME}/.harness" -mindepth 1 -maxdepth 3 -printf '%y %p\n' \
      >"$CUTOVER_BACKUP_DIR/harness-home-inventory.txt" \
      || { cutover_fail "phase 1: HARNESS_HOME inventory failed"; return 1; }
  fi

  cutover_write_state_file in-progress \
    || { cutover_fail "phase 1: cannot record cutover state"; return 1; }
  cutover_log "phase 1: backup at $CUTOVER_BACKUP_DIR"
}

cutover_stop_units() {
  local svc
  while IFS= read -r svc; do
    [[ -n "$svc" ]] || continue
    systemctl --user stop "$(cutover_unit_name "$svc")" >/dev/null 2>&1 || true
  done < <(cutover_services)
}

cutover_start_units() {
  local svc unit
  while IFS= read -r svc; do
    [[ -n "$svc" ]] || continue
    unit=$(cutover_unit_name "$svc")
    systemctl --user enable "$unit" >/dev/null 2>&1 || true
    systemctl --user start "$unit" || return 1
  done < <(cutover_services)
}

cutover_assert_units_inactive() {
  local svc unit
  while IFS= read -r svc; do
    [[ -n "$svc" ]] || continue
    unit=$(cutover_unit_name "$svc")
    if systemctl --user is-active --quiet "$unit"; then
      cutover_fail "phase 2: unit still active after stop: $unit"
      return 1
    fi
  done < <(cutover_services)
}

cutover_assert_no_processes() {
  local canonical proc pid link target
  canonical=$(readlink -f "$CUTOVER_OLD_PATH")
  for proc in /proc/[0-9]*; do
    pid=${proc#/proc/}
    [[ "$pid" == "$$" ]] && continue
    for link in cwd exe root; do
      target=$(readlink "$proc/$link" 2>/dev/null) || continue
      if [[ "$target" == "$canonical" || "$target" == "$canonical/"* ]]; then
        cutover_fail "phase 2: pid $pid still lives in $canonical ($link -> $target) — stop it or leave the directory"
        return 1
      fi
    done
  done
}

cutover_assert_no_open_handles() {
  local dir path
  local -a files=()
  while IFS= read -r dir; do
    [[ -n "$dir" ]] || continue
    [[ -d "$CUTOVER_OLD_PATH/$dir" ]] || continue
    while IFS= read -r -d '' path; do
      files+=("$path")
    done < <(find "$CUTOVER_OLD_PATH/$dir" -type f -print0)
  done < <(cutover_state_dirs)
  ((${#files[@]} > 0)) || return 0

  local i=0
  local -a chunk
  while ((i < ${#files[@]})); do
    chunk=("${files[@]:i:CUTOVER_FUSER_CHUNK}")
    if fuser -s "${chunk[@]}" 2>/dev/null; then
      for path in "${chunk[@]}"; do
        if fuser -s "$path" 2>/dev/null; then
          cutover_fail "phase 2: open handle on $path — stop whatever holds it"
          return 1
        fi
      done
      cutover_fail "phase 2: open handle under $CUTOVER_OLD_PATH state dirs"
      return 1
    fi
    i=$((i + CUTOVER_FUSER_CHUNK))
  done
}

cutover_phase2_stop() {
  cutover_log "phase 2: stop units and assert quiescence"
  cutover_stop_units
  cutover_assert_units_inactive || return 1
  cutover_assert_no_processes || return 1
  cutover_assert_no_open_handles || return 1
}

cutover_migrate_state_dir() { # $1=state dir
  local dir="$1" src dst src_fp dst_fp
  src="$CUTOVER_OLD_PATH/$dir"
  dst="$CUTOVER_STATE_ROOT/$CUTOVER_MODULE/$dir"
  [[ -d "$src" ]] || return 0
  src_fp=$(cutover_dir_fingerprint "$src")
  mkdir -p "$dst" || { cutover_fail "phase 3: cannot create $dst"; return 1; }
  rsync -aH "$src/" "$dst/" || { cutover_fail "phase 3: rsync failed for $dir"; return 1; }
  dst_fp=$(cutover_dir_fingerprint "$dst")
  if [[ "$src_fp" != "$dst_fp" ]]; then
    cutover_fail "phase 3: state parity mismatch for $dir (source count/bytes $src_fp, destination $dst_fp)"
    return 1
  fi
}

cutover_link_state_dirs() {
  if [[ "$(cutover_install_mode)" == "artifact" ]]; then return 0; fi
  local dir link dst prior linked=0
  while IFS= read -r dir; do
    [[ -n "$dir" ]] || continue
    dst="$CUTOVER_STATE_ROOT/$CUTOVER_MODULE/$dir"
    link="$CUTOVER_TARGET/$dir"
    mkdir -p "$dst" || { cutover_fail "phase 3: cannot create $dst"; return 1; }
    if [[ -L "$link" ]]; then
      if [[ "$(readlink -f "$link")" != "$(readlink -f "$dst")" ]]; then
        cutover_fail "phase 3: $link already links to $(readlink "$link") — expected $dst"
        return 1
      fi
      continue
    elif [[ -d "$link" ]]; then
      rmdir "$link" 2>/dev/null || {
        cutover_fail "phase 3: $link is a non-empty directory — move it aside, then re-run"
        return 1
      }
      prior="empty-dir"
    elif [[ -e "$link" ]]; then
      cutover_fail "phase 3: $link exists and is not a directory — move it aside, then re-run"
      return 1
    else
      prior="absent"
    fi
    ln -s "$dst" "$link" || { cutover_fail "phase 3: cannot link $link -> $dst"; return 1; }
    CUTOVER_LINKED_STATE_DIRS+=("$dir"$'\t'"$prior")
    linked=1
  done < <(cutover_state_dirs)
  if ((linked)); then
    cutover_write_state_file in-progress \
      || { cutover_fail "phase 3: cannot record linked state dirs"; return 1; }
  fi
}

cutover_unlink_state_dirs() {
  local entry dir prior link rc=0
  for entry in "${CUTOVER_LINKED_STATE_DIRS[@]}"; do
    dir="${entry%%$'\t'*}"
    prior="${entry#*$'\t'}"
    link="$CUTOVER_TARGET/$dir"
    [[ -L "$link" ]] || continue
    if ! rm -f "$link"; then rc=1; continue; fi
    if [[ "$prior" == "empty-dir" ]]; then
      mkdir -p "$link" || rc=1
    fi
  done
  return "$rc"
}

cutover_phase3_state() {
  cutover_log "phase 3: state migration"
  local dir
  while IFS= read -r dir; do
    [[ -n "$dir" ]] || continue
    cutover_migrate_state_dir "$dir" || return 1
  done < <(cutover_state_dirs)
  cutover_link_state_dirs || return 1
  if [[ "$CUTOVER_MODULE" == "harness" && -d "${HOME}/.harness" ]]; then
    cutover_log "phase 3: HARNESS_HOME ${HOME}/.harness inventoried, left untouched"
  fi
}

cutover_phase4_worktrees() {
  [[ "$CUTOVER_MODULE" == "harness" ]] || return 0
  cutover_log "phase 4: worktree teardown"
  local reaper="${HOME}/.claude/bin/wt-reaper.sh"
  command -v git >/dev/null 2>&1 || { cutover_fail "phase 4: git not available"; return 1; }
  [[ -x "$reaper" ]] || { cutover_fail "phase 4: worktree reaper missing: $reaper"; return 1; }
  "$reaper" --apply --root "$CUTOVER_OLD_PATH" >/dev/null \
    || { cutover_fail "phase 4: worktree reaper failed"; return 1; }
  git -C "$CUTOVER_OLD_PATH" worktree prune \
    || { cutover_fail "phase 4: git worktree prune failed"; return 1; }
  local registered
  registered=$(git -C "$CUTOVER_OLD_PATH" worktree list --porcelain | grep -c '^worktree ' || true)
  if ((registered > 1)); then
    cutover_fail "phase 4: $((registered - 1)) registered worktree(s) remain under $CUTOVER_OLD_PATH"
    return 1
  fi
}

cutover_link_units() { # $1=source root
  local src_root="$1" svc unit src
  mkdir -p "$CUTOVER_UNIT_DIR"
  while IFS= read -r svc; do
    [[ -n "$svc" ]] || continue
    unit=$(cutover_unit_name "$svc")
    src="$src_root/$svc"
    [[ -f "$src" ]] || { cutover_fail "phase 5: unit file missing: $src"; return 1; }
    ln -sfn "$src" "$CUTOVER_UNIT_DIR/$unit" \
      || { cutover_fail "phase 5: cannot link unit $unit"; return 1; }
  done < <(cutover_services)
}

cutover_run_health() { # $1=phase label
  local phase="$1" health_rel root
  health_rel=$(cutover_health_rel)
  [[ -n "$health_rel" ]] || { cutover_fail "$phase: manifest declares no health script"; return 1; }
  if [[ "$(cutover_install_mode)" == "artifact" ]]; then
    root="$CUTOVER_TARGET"
  else
    root="$CUTOVER_ROOT/modules/$CUTOVER_MODULE"
  fi
  if ! (cd "$root" && DECKCTL_ARTIFACT_ROOT="$root" "./${health_rel#./}"); then
    cutover_fail "$phase: health check failed at $root"
    return 1
  fi
}

cutover_phase5_swap() {
  cutover_log "phase 5: archive checkout and link target"
  CUTOVER_ARCHIVE_PATH="${HOME}/Projects/archive/${CUTOVER_MODULE}-pre-ai-os"
  [[ -d "$CUTOVER_OLD_PATH" && ! -L "$CUTOVER_OLD_PATH" ]] \
    || { cutover_fail "phase 5: old checkout is not a plain directory: $CUTOVER_OLD_PATH"; return 1; }
  [[ ! -e "$CUTOVER_ARCHIVE_PATH" ]] \
    || { cutover_fail "phase 5: archive path already exists: $CUTOVER_ARCHIVE_PATH"; return 1; }
  mkdir -p "$(dirname "$CUTOVER_ARCHIVE_PATH")"
  cutover_write_state_file in-progress
  mv "$CUTOVER_OLD_PATH" "$CUTOVER_ARCHIVE_PATH" \
    || { cutover_fail "phase 5: cannot archive $CUTOVER_OLD_PATH"; return 1; }
  ln -s "$CUTOVER_TARGET" "$CUTOVER_OLD_PATH" \
    || { cutover_fail "phase 5: cannot symlink $CUTOVER_OLD_PATH -> $CUTOVER_TARGET"; return 1; }
  cutover_link_units "$CUTOVER_TARGET" || return 1
  systemctl --user daemon-reload || { cutover_fail "phase 5: daemon-reload failed"; return 1; }
  cutover_start_units || { cutover_fail "phase 5: unit start failed"; return 1; }
  cutover_run_health "phase 5" || return 1
}

cutover_phase6_assert_refs() {
  cutover_log "phase 6: ref-sweep assertion"
  bash "$CUTOVER_ROOT/tools/ref-sweep.sh" --assert \
    || { cutover_fail "phase 6: inventoried references do not resolve"; return 1; }
}

cutover_restore() { # $1=reason
  local reason="$1" incomplete=0 dir svc unit backup installed
  cutover_log "restore: $reason"
  cutover_stop_units
  cutover_unlink_state_dirs || incomplete=1

  if [[ -L "$CUTOVER_OLD_PATH" ]]; then
    rm -f "$CUTOVER_OLD_PATH" || incomplete=1
  fi
  if [[ -n "$CUTOVER_ARCHIVE_PATH" && -d "$CUTOVER_ARCHIVE_PATH" ]]; then
    if [[ -e "$CUTOVER_OLD_PATH" ]]; then
      incomplete=1
    else
      mv "$CUTOVER_ARCHIVE_PATH" "$CUTOVER_OLD_PATH" || incomplete=1
    fi
  fi

  if [[ -n "$CUTOVER_BACKUP_DIR" && -d "$CUTOVER_BACKUP_DIR/state" && -d "$CUTOVER_OLD_PATH" ]]; then
    while IFS= read -r dir; do
      [[ -n "$dir" ]] || continue
      [[ -d "$CUTOVER_BACKUP_DIR/state/$dir" ]] || continue
      mkdir -p "$CUTOVER_OLD_PATH/$dir" || { incomplete=1; continue; }
      rsync -aH --delete "$CUTOVER_BACKUP_DIR/state/$dir/" "$CUTOVER_OLD_PATH/$dir/" || incomplete=1
    done < <(cutover_state_dirs)
  fi

  if [[ -n "$CUTOVER_BACKUP_DIR" && -d "$CUTOVER_BACKUP_DIR/units" ]]; then
    while IFS= read -r svc; do
      [[ -n "$svc" ]] || continue
      unit=$(cutover_unit_name "$svc")
      backup="$CUTOVER_BACKUP_DIR/units/$unit"
      installed="$CUTOVER_UNIT_DIR/$unit"
      rm -f "$installed" || incomplete=1
      if [[ -e "$backup" || -L "$backup" ]]; then
        cp -a "$backup" "$installed" || incomplete=1
      fi
    done < <(cutover_services)
  fi

  systemctl --user daemon-reload || incomplete=1
  cutover_start_units || incomplete=1

  if ((incomplete)); then
    cutover_write_state_file rollback-incomplete "$reason"
    printf 'cutover: ERROR: ROLLBACK INCOMPLETE — archive=%s backup=%s old_path=%s (recorded in %s)\n' \
      "$CUTOVER_ARCHIVE_PATH" "$CUTOVER_BACKUP_DIR" "$CUTOVER_OLD_PATH" "$(cutover_state_file_path)" >&2
    return 1
  fi
  rm -f "$(cutover_state_file_path)"
  cutover_log "restore: complete"
}

cutover_rollback_from_state() { # $1=module
  local module="$1" state_file="$CUTOVER_STATE_ROOT/cutover/$1.json"
  cutover_refuse_engine
  cutover_require_tools
  cutover_load_manifest "$module"
  [[ -f "$state_file" ]] || cutover_die "no cutover state recorded for '$module' ($state_file missing)"
  CUTOVER_MODULE="$module"
  CUTOVER_OLD_PATH=$(jq -r '.old_path' "$state_file")
  CUTOVER_TARGET=$(jq -r '.target' "$state_file")
  CUTOVER_BACKUP_DIR=$(jq -r '.backup_dir' "$state_file")
  CUTOVER_ARCHIVE_PATH=$(jq -r '.archive_path' "$state_file")
  mapfile -t CUTOVER_LINKED_STATE_DIRS \
    < <(jq -r '.linked_state_dirs[]? | "\(.dir)\t\(.prior)"' "$state_file")
  cutover_restore "standalone rollback of $module" || exit 1
}

cutover_run() { # $1=module $2=old-path $3=target [--rollback]
  [[ $# -ge 3 ]] || cutover_die "usage: cutover_run <module> <old-path> <target> [--rollback]"
  local module="$1" old_path="$2" target="$3"
  shift 3

  if [[ "${1:-}" == "--rollback" ]]; then
    shift
    [[ $# -eq 0 ]] || cutover_die "unexpected argument after --rollback: $1"
    cutover_rollback_from_state "$module"
    return 0
  fi
  [[ $# -eq 0 ]] || cutover_die "unexpected argument: $1"

  CUTOVER_MODULE="$module"
  CUTOVER_OLD_PATH="$old_path"
  CUTOVER_TARGET="$target"
  CUTOVER_BACKUP_DIR=""
  CUTOVER_ARCHIVE_PATH=""
  CUTOVER_LINKED_STATE_DIRS=()

  cutover_refuse_engine
  cutover_require_tools
  cutover_load_manifest "$module"
  cutover_assert_no_partial_state

  if cutover_already_cut; then
    cutover_log "$module already cut over ($CUTOVER_OLD_PATH -> $CUTOVER_TARGET); nothing to do"
    return 0
  fi

  [[ -d "$CUTOVER_OLD_PATH" ]] || cutover_die "phase 0: legacy checkout missing: $CUTOVER_OLD_PATH"
  [[ -e "$CUTOVER_TARGET" ]] || cutover_die "phase 0: cutover target missing: $CUTOVER_TARGET"

  local entry label fn
  for entry in \
    "1:cutover_phase1_preflight" \
    "2:cutover_phase2_stop" \
    "3:cutover_phase3_state" \
    "4:cutover_phase4_worktrees" \
    "5:cutover_phase5_swap" \
    "6:cutover_phase6_assert_refs"; do
    label="phase ${entry%%:*}"
    fn="${entry#*:}"
    if ! "$fn"; then
      cutover_restore "rollback after $label failure" || exit 1
      cutover_die "cutover aborted in $label; system restored to its pre-cutover state"
    fi
  done

  cutover_write_state_file complete
  cutover_log "$module cutover complete: $CUTOVER_OLD_PATH -> $CUTOVER_TARGET"
}
