#!/usr/bin/env bash
# Idempotent backend hot-fix orchestrator with OP2 synchronization, one PM2
# restart, local/public smoke tests, and automatic source rollback.

set -Eeuo pipefail
umask 077

PROJECT_DIR="${PROJECT_DIR:-/home/ubuntu/starpost-backend}"
STAGE_DIR="${HOTFIX_STAGE_DIR:-/home/ubuntu/starpost-backend-hotfix}"
PM2_APP="${PM2_APP:-starpost-api}"
BACKUP_BASE="${HOTFIX_BACKUP_DIR:-$PROJECT_DIR/hotfix-backups}"
LOCK_FILE="/tmp/starpost-backend-hotfix.lock"
RELEASES_DIR="${RELEASES_DIR:-$PROJECT_DIR/releases}"
CURRENT_RELEASE="$RELEASES_DIR/current-release.json"
RUN_LOG="$RELEASES_DIR/deployment-runs.jsonl"
MODE="hotfix"
EDIT_TARGET=""
TEMPLATE_VERSION_MODE=0
NEW_TEMPLATE_VERSION=""
PM2_RESTARTED=0
PM2_RESTART_TRIGGER="none"

case "${1:-}" in
  "") [[ "$#" -eq 0 ]] || exit 2 ;;
  --sync-op2)
    [[ "$#" -eq 1 ]] || { echo "ERROR: --sync-op2 takes no additional arguments" >&2; exit 2; }
    MODE="sync-op2"
    ;;
  --verify)
    [[ "$#" -eq 1 ]] || { echo "ERROR: --verify takes no additional arguments" >&2; exit 2; }
    MODE="verify"
    ;;
  --stage-edit)
    [[ "$#" -eq 2 ]] || {
      echo "Usage: bash scripts/deploy-backend-hotfix.sh --stage-edit <project-relative-js-file>" >&2
      echo "This copies the live source into hotfix staging and edits only the staged copy." >&2
      exit 2
    }
    EDIT_TARGET="${2#./}"
    ;;
  --url-version|--template-version)
    [[ "$#" -eq 1 ]] || { echo "ERROR: --url-version takes no additional arguments" >&2; exit 2; }
    TEMPLATE_VERSION_MODE=1
    ;;
  -h|--help)
    printf 'Usage: bash scripts/deploy-backend-hotfix.sh [\033[1;32m%s\033[0m]\n' '--verify | --sync-op2 | --url-version | --stage-edit <project-relative-js-file>'
    echo "Normal: place changed files under $STAGE_DIR/src/ and run without options."
    echo "Optional --stage-edit: copies from the live path and edits only the staged copy."
    exit 0
    ;;
  *) echo "Usage: bash scripts/deploy-backend-hotfix.sh [--verify | --sync-op2 | --url-version | --stage-edit <project-relative-js-file>]" >&2; exit 2 ;;
esac

GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'; BOLD='\033[1m'; NC='\033[0m'
ok()   { printf "${GREEN}   ✓ %s${NC}\n" "$1"; }
skip() { printf "${YELLOW}   ↷ %s${NC}\n" "$1"; }
warn() { printf "${YELLOW}   ⚠ %s${NC}\n" "$1"; }
die()  { printf "${RED}ERROR: %s${NC}\n" "$1" >&2; exit 1; }

highlight_value() {
  printf "${GREEN}%s${NC}" "$1"
}

print_result_value() {
  printf '  %s: ' "$1"
  highlight_value "$2"
  printf '\n'
}

print_final_result_heading() {
  printf "${GREEN}${BOLD}============================================================${NC}\n"
  printf "${BOLD} FINAL RESULT: ${NC}"
  printf "${GREEN}${BOLD}%s${NC}" "$1"
  printf '\n'
  printf "${GREEN}${BOLD}============================================================${NC}\n"
}

read_env() {
  local name="$1" value=""
  if [[ -f "$PROJECT_DIR/.env" ]]; then
    value="$(sed -n "s/^${name}=//p" "$PROJECT_DIR/.env" | tail -n 1)"
    value="${value%\"}"; value="${value#\"}"
    value="${value%\'}"; value="${value#\'}"
  fi
  printf '%s' "$value"
}

read_template_url_version() {
  sed -nE 's|.*templates/image/.*[?]v=([A-Za-z0-9._-]+).*|\1|p' \
    "$PROJECT_DIR/src/routes/config.js" | head -n 1
}

run_complete_runtime_smoke_tests() {
  local app_url="${APP_URL:-$(read_env APP_URL)}"
  app_url="${app_url%/}"
  [[ -n "$app_url" ]] || die "APP_URL is missing"
  bash scripts/internal/smoke-test-backend-public-assets.sh http://127.0.0.1:3000
  bash scripts/internal/smoke-test-backend-public-assets.sh "$app_url"
}

read_pm2_status() {
  local pm2_json
  pm2_json="$(pm2 jlist 2>/dev/null || true)"
  PM2_JSON="$pm2_json" PM2_APP_NAME="$PM2_APP" node -e '
    try {
      const apps = JSON.parse(process.env.PM2_JSON || "[]");
      const app = apps.find(item => item.name === process.env.PM2_APP_NAME);
      process.stdout.write(app?.pm2_env?.status || "unknown");
    } catch {
      process.stdout.write("unknown");
    }
  '
}

print_pm2_summary() {
  local status
  status="$(read_pm2_status)"
  printf '  PM2: '
  highlight_value "$status"
  if [[ "$PM2_RESTARTED" -eq 1 ]]; then
    printf ' | restart: '
    highlight_value "yes"
    printf ' | cmd: '
    printf 'pm2 restart %s --update-env --time' "$PM2_APP"
    printf ' | via: '
    highlight_value "$PM2_RESTART_TRIGGER"
  else
    printf ' | restart: '
    highlight_value "no"
    printf ' | cmd: none'
    printf ' | via: '
    highlight_value "none"
  fi
  printf '\n'
}

print_no_change_result() {
  local message="$1"
  echo
  print_final_result_heading "$message"
  print_result_value "Active OP2 version" "$(read_env ACTIVE_POSTCARD_DESIGN_VERSION)"
  print_result_value "Template URL version" "$(read_template_url_version)"
  print_pm2_summary
}

[[ "$(id -u)" -ne 0 ]] || die "Run as ubuntu, not root."
cd "$PROJECT_DIR"
exec 9>"$LOCK_FILE"
flock -n 9 || die "Another backend hot-fix deployment is running."

for command in node pm2 curl flock find cmp; do
  command -v "$command" >/dev/null 2>&1 || die "$command is required."
done
[[ -f scripts/internal/deploy-postcard-design.sh ]] || die "Missing internal postcard deployment helper"
[[ -f scripts/internal/smoke-test-backend-public-assets.sh ]] || die "Missing internal backend smoke test"

if [[ "$MODE" == "verify" ]]; then
  echo
  printf "${BOLD}StarPost backend compatibility verification${NC}\n"
  print_result_value "PM2 restart" "no"
  print_result_value "Source changes" "no"
  echo
  run_complete_runtime_smoke_tests
  echo
  print_final_result_heading "VERIFICATION PASSED"
  print_result_value "Active OP2 version" "$(read_env ACTIVE_POSTCARD_DESIGN_VERSION)"
  print_result_value "Template URL version" "$(read_template_url_version)"
  print_pm2_summary
  if [[ -f "$CURRENT_RELEASE" ]]; then
    printf '  Current release: %s\n' "$CURRENT_RELEASE"
  fi
  exit 0
fi

if [[ "$TEMPLATE_VERSION_MODE" -eq 1 ]]; then
  TEMPLATE_CONFIG="src/routes/config.js"
  LIVE_TEMPLATE_CONFIG="$PROJECT_DIR/$TEMPLATE_CONFIG"
  STAGED_TEMPLATE_CONFIG="$STAGE_DIR/$TEMPLATE_CONFIG"
  [[ -f "$LIVE_TEMPLATE_CONFIG" ]] || die "Live backend file does not exist: $TEMPLATE_CONFIG"

  if [[ -d "$STAGE_DIR/src" ]] \
    && [[ -n "$(find "$STAGE_DIR/src" -type f -print -quit)" ]]; then
    die "Hotfix staging is not empty; deploy or clear it before changing the template URL version"
  fi

  mapfile -t TEMPLATE_VERSIONS < <(
    sed -nE 's|.*templates/image/.*[?]v=([A-Za-z0-9._-]+).*|\1|p' "$LIVE_TEMPLATE_CONFIG" | sort -u
  )
  [[ "${#TEMPLATE_VERSIONS[@]}" -eq 1 ]] \
    || die "Expected exactly one template URL version in $TEMPLATE_CONFIG"
  CURRENT_TEMPLATE_VERSION="${TEMPLATE_VERSIONS[0]}"

  if [[ "$CURRENT_TEMPLATE_VERSION" =~ ^(.*-)([0-9]+)$ ]]; then
    DEFAULT_TEMPLATE_VERSION="${BASH_REMATCH[1]}$((10#${BASH_REMATCH[2]} + 1))"
  else
    DEFAULT_TEMPLATE_VERSION="$(date -u +%Y%m%d-%H%M%S)"
  fi

  echo
  printf "${BOLD}Template image URL version${NC}\n"
  echo "  Current: $CURRENT_TEMPLATE_VERSION"
  read -r -p "  New [$DEFAULT_TEMPLATE_VERSION]: " NEW_TEMPLATE_VERSION
  NEW_TEMPLATE_VERSION="${NEW_TEMPLATE_VERSION:-$DEFAULT_TEMPLATE_VERSION}"
  [[ "$NEW_TEMPLATE_VERSION" =~ ^[A-Za-z0-9._-]+$ ]] \
    || die "Template URL version may contain only letters, numbers, dot, underscore and hyphen"
  [[ "$NEW_TEMPLATE_VERSION" != "$CURRENT_TEMPLATE_VERSION" ]] \
    || {
      skip "Template URL version is unchanged"
      print_no_change_result "URL VERSION UNCHANGED"
      exit 0
    }

  mkdir -p "$(dirname "$STAGED_TEMPLATE_CONFIG")"
  cp "$LIVE_TEMPLATE_CONFIG" "$STAGED_TEMPLATE_CONFIG"
  CURRENT_TEMPLATE_VERSION="$CURRENT_TEMPLATE_VERSION" \
  NEW_TEMPLATE_VERSION="$NEW_TEMPLATE_VERSION" \
  node - "$STAGED_TEMPLATE_CONFIG" <<'NODE'
const fs = require('fs');
const path = process.argv[2];
const current = process.env.CURRENT_TEMPLATE_VERSION;
const next = process.env.NEW_TEMPLATE_VERSION;
const source = fs.readFileSync(path, 'utf8');
const needle = `?v=${current}`;
const matches = source.split(needle).length - 1;
if (matches !== 1) throw new Error(`Expected one ${needle} occurrence; found ${matches}`);
fs.writeFileSync(path, source.replace(needle, `?v=${next}`));
NODE
  chmod 0644 "$STAGED_TEMPLATE_CONFIG"
  node --check "$STAGED_TEMPLATE_CONFIG"
  ok "Template URL version staged: $CURRENT_TEMPLATE_VERSION -> $NEW_TEMPLATE_VERSION"
fi

if [[ -n "$EDIT_TARGET" ]]; then
  [[ "$EDIT_TARGET" =~ ^src/[A-Za-z0-9._/-]+\.(js|mjs|cjs)$ ]] \
    || die "--stage-edit accepts one backend JavaScript file under src/: $EDIT_TARGET"

  LIVE_EDIT_SOURCE="$PROJECT_DIR/$EDIT_TARGET"
  STAGED_EDIT_TARGET="$STAGE_DIR/$EDIT_TARGET"
  [[ -f "$LIVE_EDIT_SOURCE" ]] || die "Live backend file does not exist: $EDIT_TARGET"

  if [[ -d "$STAGE_DIR/src" ]] \
    && [[ -n "$(find "$STAGE_DIR/src" -type f -print -quit)" ]]; then
    die "Hotfix staging is not empty; deploy or clear it before using --stage-edit"
  fi

  EDITOR_COMMAND="${HOTFIX_EDITOR:-${EDITOR:-}}"
  if [[ -z "$EDITOR_COMMAND" ]]; then
    if command -v nano >/dev/null 2>&1; then EDITOR_COMMAND="nano"; else EDITOR_COMMAND="vi"; fi
  fi
  command -v "$EDITOR_COMMAND" >/dev/null 2>&1 \
    || die "Editor not found: $EDITOR_COMMAND (set HOTFIX_EDITOR to an executable name)"

  mkdir -p "$(dirname "$STAGED_EDIT_TARGET")"
  cp "$LIVE_EDIT_SOURCE" "$STAGED_EDIT_TARGET"
  chmod 0644 "$STAGED_EDIT_TARGET"

  echo
  printf "${BOLD}Managed backend hotfix editor${NC}\n"
  echo "  Live file remains untouched: $EDIT_TARGET"
  echo "  Editing staged copy:        $STAGED_EDIT_TARGET"
  echo
  "$EDITOR_COMMAND" "$STAGED_EDIT_TARGET"
  node --check "$STAGED_EDIT_TARGET"

  if cmp -s "$LIVE_EDIT_SOURCE" "$STAGED_EDIT_TARGET"; then
    rm -f "$STAGED_EDIT_TARGET"
    find "$STAGE_DIR/src" -depth -type d -empty ! -path "$STAGE_DIR/src" -delete
    skip "No changes saved; live backend remains untouched"
    print_no_change_result "STAGED EDIT UNCHANGED"
    exit 0
  fi

  ok "Changed staged copy ready: $EDIT_TARGET"
fi

echo
printf "${BOLD}StarPost backend deployment${NC}\n"
if [[ "$TEMPLATE_VERSION_MODE" -eq 1 ]]; then
  echo "  Mode: url-version"
else
  echo "  Mode: $MODE"
fi
[[ "$MODE" == "hotfix" ]] && echo "  Stage: $STAGE_DIR"
[[ -n "$EDIT_TARGET" ]] && echo "  Managed edit: $EDIT_TARGET"
echo

if [[ "$TEMPLATE_VERSION_MODE" -eq 1 ]]; then
  NOTE="template-url-$NEW_TEMPLATE_VERSION"
  echo "  Deploy note: $NOTE"
else
  read -r -p "Deploy note, e.g. config-hotfix, op2-layout: " NOTE
fi
NOTE="$(printf '%s' "$NOTE" | tr '[:upper:]' '[:lower:]' \
  | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-+/-/g')"
[[ -n "$NOTE" ]] || die "Deployment note is required."

STAMP="$(date +%Y%m%d-%H%M%S)"
VERSION="${STAMP}-${NOTE}"
BACKUP_DIR="$BACKUP_BASE/$VERSION"
MANIFEST="$BACKUP_DIR/manifest.tsv"
PREVIOUS_OP2_VERSION="$(read_env ACTIVE_POSTCARD_DESIGN_VERSION)"
SOURCE_CHANGED=0
SOURCE_DEPLOYED=0
ROLLBACK_ARMED=0
COMPLETE=0
RUN_RECORDED=0
RUN_OUTCOME="failed"
OP2_CHANGED=-1
STAGED_FILES=()
CHANGED_FILES=()

if [[ "$MODE" == "hotfix" ]]; then
  [[ -d "$STAGE_DIR/src" ]] || die "No staged source directory: $STAGE_DIR/src"
  mapfile -d '' STAGED_FILES < <(find "$STAGE_DIR/src" -type f -print0 | sort -z)
  [[ "${#STAGED_FILES[@]}" -gt 0 ]] || die "No files staged under $STAGE_DIR/src"

  echo "Source comparison:"
  for staged_file in "${STAGED_FILES[@]}"; do
    relative="${staged_file#"$STAGE_DIR/"}"
    target="$PROJECT_DIR/$relative"
    [[ "$relative" =~ ^src/[A-Za-z0-9._/-]+\.(js|mjs|cjs)$ ]] \
      || die "Only backend JavaScript under src/ may be staged: $relative"
    node --check "$staged_file"
    if [[ -f "$target" ]] && cmp -s "$staged_file" "$target"; then
      skip "$relative already matches; skipped"
    else
      printf '   + %s\n' "$relative"
      CHANGED_FILES+=("$staged_file")
    fi
  done
  [[ "${#CHANGED_FILES[@]}" -gt 0 ]] && SOURCE_CHANGED=1 \
    || skip "No backend source differences"
fi

if [[ "$MODE" == "sync-op2" ]]; then
  CONFIRM_PROMPT="Compare OP2 and deploy only if its content changed? [y/N] "
else
  CONFIRM_PROMPT="Deploy changed source files and synchronize OP2 if needed? [y/N] "
fi
read -r -p "$CONFIRM_PROMPT" CONFIRM
case "$CONFIRM" in y|Y|yes|YES) ;; *) die "Deployment cancelled." ;; esac

mkdir -p "$BACKUP_DIR"
: > "$MANIFEST"
mkdir -p "$RELEASES_DIR"

backup_target() {
  local relative="$1" target="$PROJECT_DIR/$1"
  if awk -F '\t' -v path="$relative" '$2 == path { found=1 } END { exit(found ? 0 : 1) }' "$MANIFEST"; then
    return 0
  fi
  mkdir -p "$BACKUP_DIR/$(dirname "$relative")"
  if [[ -e "$target" ]]; then
    printf 'existing\t%s\n' "$relative" >> "$MANIFEST"
    cp -a "$target" "$BACKUP_DIR/$relative"
  else
    printf 'new\t%s\n' "$relative" >> "$MANIFEST"
  fi
}

write_run_record() {
  local outcome="$1" release_id="${2:-}" active_op2 template_url_version
  active_op2="$(read_env ACTIVE_POSTCARD_DESIGN_VERSION)"
  template_url_version="$(read_template_url_version)"
  RUN_ID="$VERSION" RELEASE_ID="$release_id" NOTE="$NOTE" OUTCOME="$outcome" \
  SOURCE_CHANGED="$SOURCE_CHANGED" OP2_CHANGED="$OP2_CHANGED" ACTIVE_OP2="$active_op2" \
  TEMPLATE_URL_VERSION="$template_url_version" BACKUP_DIR_VALUE="$BACKUP_DIR" \
  node - "$RUN_LOG" <<'NODE'
const fs = require('fs');
const record = {
  runId: process.env.RUN_ID,
  releaseId: process.env.RELEASE_ID || null,
  note: process.env.NOTE,
  finishedAt: new Date().toISOString(),
  outcome: process.env.OUTCOME,
  components: {
    backend: { changed: process.env.SOURCE_CHANGED === '1' },
    op2: {
      changed: process.env.OP2_CHANGED === '1' ? true : process.env.OP2_CHANGED === '0' ? false : null,
      version: process.env.ACTIVE_OP2 || null
    },
    templates: { urlVersion: process.env.TEMPLATE_URL_VERSION || null }
  },
  backupDir: process.env.BACKUP_DIR_VALUE
};
fs.appendFileSync(process.argv[2], JSON.stringify(record) + '\n');
NODE
}

write_current_release() {
  local active_op2 template_url_version temporary
  active_op2="$(read_env ACTIVE_POSTCARD_DESIGN_VERSION)"
  template_url_version="$(read_template_url_version)"
  temporary="$CURRENT_RELEASE.tmp.$$"
  RELEASE_ID="$VERSION" NOTE="$NOTE" SOURCE_CHANGED="$SOURCE_CHANGED" \
  OP2_CHANGED="$OP2_CHANGED" ACTIVE_OP2="$active_op2" \
  TEMPLATE_URL_VERSION="$template_url_version" BACKUP_DIR_VALUE="$BACKUP_DIR" \
  node - "$temporary" <<'NODE'
const fs = require('fs');
const release = {
  releaseId: process.env.RELEASE_ID,
  note: process.env.NOTE,
  deployedAt: new Date().toISOString(),
  components: {
    backend: { changed: process.env.SOURCE_CHANGED === '1' },
    op2: {
      changed: process.env.OP2_CHANGED === '1',
      version: process.env.ACTIVE_OP2 || null
    },
    templates: { urlVersion: process.env.TEMPLATE_URL_VERSION || null }
  },
  verification: { localSmokeTest: 'passed', publicSmokeTest: 'passed' },
  backupDir: process.env.BACKUP_DIR_VALUE
};
fs.writeFileSync(process.argv[2], JSON.stringify(release, null, 2) + '\n');
NODE
  mv -f "$temporary" "$CURRENT_RELEASE"
}

rollback() {
  local status=$?
  if [[ "$COMPLETE" -eq 0 && "$ROLLBACK_ARMED" -eq 1 ]]; then
    printf "\n${YELLOW}${BOLD}Deployment failed; restoring backend source...${NC}\n" >&2
    while IFS=$'\t' read -r state relative; do
      [[ -n "$relative" ]] || continue
      target="$PROJECT_DIR/$relative"
      if [[ "$state" == "existing" ]]; then
        mkdir -p "$(dirname "$target")"
        cp -a "$BACKUP_DIR/$relative" "$target"
      else
        rm -f -- "$target"
      fi
    done < "$MANIFEST"
    if [[ "$OP2_CHANGED" -eq 1 ]]; then
      PREVIOUS_OP2_VERSION="$PREVIOUS_OP2_VERSION" node - "$PROJECT_DIR/.env" <<'NODE' || true
const fs = require('fs');
const path = process.argv[2];
const previous = process.env.PREVIOUS_OP2_VERSION;
let text = fs.existsSync(path) ? fs.readFileSync(path, 'utf8') : '';
const line = `ACTIVE_POSTCARD_DESIGN_VERSION=${previous}`;
if (/^ACTIVE_POSTCARD_DESIGN_VERSION=.*$/m.test(text)) {
  text = text.replace(/^ACTIVE_POSTCARD_DESIGN_VERSION=.*$/m, line);
} else {
  text = text.replace(/\s*$/, '\n') + line + '\n';
}
fs.writeFileSync(path, text);
NODE
    fi
    pm2 restart "$PM2_APP" --update-env --time >/dev/null 2>&1 || true
    RUN_OUTCOME="failed-rolled-back"
    warn "Source restored from $BACKUP_DIR"
  fi
  if [[ "$RUN_RECORDED" -eq 0 ]]; then
    write_run_record "$RUN_OUTCOME" "" || true
    RUN_RECORDED=1
  fi
  return "$status"
}
trap rollback EXIT

if [[ "$SOURCE_CHANGED" -eq 1 ]]; then
  backup_target "src/services/default-ink.js"
  backup_target "src/pages/postcard-back.js"
  for staged_file in "${CHANGED_FILES[@]}"; do
    relative="${staged_file#"$STAGE_DIR/"}"
    target="$PROJECT_DIR/$relative"
    backup_target "$relative"
    mkdir -p "$(dirname "$target")"
    temporary="$(mktemp "$(dirname "$target")/.hotfix.XXXXXX")"
    cp "$staged_file" "$temporary"
    chmod 0644 "$temporary"
    mv -f "$temporary" "$target"
    node --check "$target"
  done
  SOURCE_DEPLOYED=1
  ROLLBACK_ARMED=1
  ok "Changed backend source installed"
fi

# OP2 synchronization can update these source fallbacks before bundle install.
backup_target "src/services/default-ink.js"
backup_target "src/pages/postcard-back.js"
ROLLBACK_ARMED=1

set +e
DESIGN_VERSION="$VERSION" bash scripts/internal/deploy-postcard-design.sh
OP2_RESULT=$?
set -e

case "$OP2_RESULT" in
  0)
    OP2_CHANGED=1
    PM2_RESTARTED=1
    if [[ "$MODE" == "sync-op2" ]]; then
      PM2_RESTART_TRIGGER="--sync-op2"
    elif [[ "$TEMPLATE_VERSION_MODE" -eq 1 ]]; then
      PM2_RESTART_TRIGGER="--url-version"
    elif [[ -n "$EDIT_TARGET" ]]; then
      PM2_RESTART_TRIGGER="--stage-edit"
    else
      PM2_RESTART_TRIGGER="default deploy"
    fi
    ok "OP2 synchronized, backend restarted and smoke-tested"
    ;;
  3) OP2_CHANGED=0; skip "OP2 already matches; synchronization skipped" ;;
  *) exit "$OP2_RESULT" ;;
esac

if [[ "$SOURCE_CHANGED" -eq 1 && "$OP2_CHANGED" -eq 0 ]]; then
  pm2 restart "$PM2_APP" --update-env --time >/dev/null
  PM2_RESTARTED=1
  if [[ "$TEMPLATE_VERSION_MODE" -eq 1 ]]; then
    PM2_RESTART_TRIGGER="--url-version"
  elif [[ -n "$EDIT_TARGET" ]]; then
    PM2_RESTART_TRIGGER="--stage-edit"
  else
    PM2_RESTART_TRIGGER="default deploy"
  fi
  ok "PM2 restarted: $PM2_APP"
  ready=0
  for _attempt in {1..30}; do
    if curl -fsS http://127.0.0.1:3000/api/config >/dev/null 2>&1; then ready=1; break; fi
    sleep 1
  done
  [[ "$ready" -eq 1 ]] || die "Backend did not become ready after PM2 restart"
  run_complete_runtime_smoke_tests
  ok "Source hot fix restarted and passed local/public smoke tests"
fi

if [[ "$SOURCE_CHANGED" -eq 0 && "$OP2_CHANGED" -eq 0 ]]; then
  ROLLBACK_ARMED=0
  run_complete_runtime_smoke_tests
  ok "Current OP2 and template release passed local/public smoke tests"
  skip "No source or OP2 differences; nothing deployed"
  RUN_OUTCOME="skipped"
  write_run_record "$RUN_OUTCOME" ""
else
  RUN_OUTCOME="deployed"
  write_current_release
  write_run_record "$RUN_OUTCOME" "$VERSION"
fi

COMPLETE=1
RUN_RECORDED=1

if [[ "$MODE" == "hotfix" ]]; then
  mv "$STAGE_DIR/src" "$BACKUP_DIR/staged-src"
  find "$BACKUP_DIR/staged-src" -type d -exec chmod 0700 {} +
  find "$BACKUP_DIR/staged-src" -type f -exec chmod 0600 {} +
  mkdir -p "$STAGE_DIR/src"
fi

echo
if [[ "$RUN_OUTCOME" == "deployed" ]]; then
  if [[ "$TEMPLATE_VERSION_MODE" -eq 1 ]]; then
    FINAL_RESULT_MESSAGE="URL VERSION DEPLOYED SUCCESSFULLY"
  elif [[ "$MODE" == "sync-op2" ]]; then
    FINAL_RESULT_MESSAGE="OP2 SYNCHRONIZED SUCCESSFULLY"
  elif [[ -n "$EDIT_TARGET" ]]; then
    FINAL_RESULT_MESSAGE="STAGED EDIT DEPLOYED SUCCESSFULLY"
  else
    FINAL_RESULT_MESSAGE="BACKEND DEPLOYED SUCCESSFULLY"
  fi
elif [[ "$MODE" == "sync-op2" ]]; then
  FINAL_RESULT_MESSAGE="OP2 CHECK PASSED — NO CHANGES"
else
  FINAL_RESULT_MESSAGE="DEPLOYMENT CHECK PASSED — NO CHANGES"
fi
print_final_result_heading "$FINAL_RESULT_MESSAGE"
print_result_value "Run ID" "$VERSION"
print_result_value "Source changed" "$SOURCE_CHANGED"
print_result_value "OP2 changed" "$OP2_CHANGED"
print_result_value "Active OP2 version" "$(read_env ACTIVE_POSTCARD_DESIGN_VERSION)"
print_result_value "Template URL version" "$(read_template_url_version)"
print_pm2_summary
printf '  Backup/audit: %s/' "${BACKUP_DIR%/*}"
highlight_value "${BACKUP_DIR##*/}"
printf '\n'
if [[ "$RUN_OUTCOME" == "deployed" ]]; then
  print_result_value "Release ID" "$VERSION"
  printf '  Current release: %s\n' "$CURRENT_RELEASE"
else
  print_result_value "Current release" "unchanged"
fi
