#!/usr/bin/env bash
# Posly Print Agent updater.
# Supports macOS and Linux.
# Replaces application files while preserving venue credentials, pairing,
# printer-config.json and service registration.

set -euo pipefail

say()  { printf "\033[1;36m[posly]\033[0m %s\n" "$*"; }
warn() { printf "\033[1;33m[posly]\033[0m %s\n" "$*"; }
fail() {
  LAST_ERROR="$*"
  printf "\033[1;31m[posly]\033[0m %s\n" "$*" >&2
  exit 1
}

AGENT_DIR="${POSLY_AGENT_DIR:-$HOME/posly-print-agent}"
INSTALL_BASE="${POSLY_INSTALL_BASE:-https://install.posly.xyz}"
AGENT_VERSION="${POSLY_AGENT_VERSION:-0.6.45}"
AGENT_VERSION="${AGENT_VERSION#v}"
LABEL="${POSLY_LAUNCHD_LABEL:-com.posly.print-agent}"
AUTO_MODE="${POSLY_AUTO_UPDATE:-0}"
UNAME="$(uname -s)"
ATTEMPT_ID="${POSLY_UPDATE_ATTEMPT_ID:-$(date +%s)-$$}"
STATE_PATH="$AGENT_DIR/auto-update-state.json"
CURRENT_VERSION="unknown"
LAST_ERROR=""
POST_STOP=0
UPDATE_COMMITTED=0
ROLLBACK_RUNNING=0
TMP_DIR=""
PRESERVE_RECOVERY_FILES=0
LAUNCHD_DOMAIN="${POSLY_LAUNCHD_DOMAIN:-gui/$(id -u)}"
PLIST="${POSLY_LAUNCHD_PLIST:-$HOME/Library/LaunchAgents/$LABEL.plist}"
LOG_DIR="${POSLY_AGENT_LOG_DIR:-$HOME/Library/Logs/posly-print-agent}"
HEARTBEAT_TIMEOUT_SECONDS="${POSLY_HEARTBEAT_TIMEOUT_SECONDS:-120}"
SERVICE_AUTO_UPDATE="${POSLY_SERVICE_AUTO_UPDATE:-1}"
LEGACY_NODE_PAIRING_ACTIVE=0
LEGACY_ACTIVE_PRINTER_PATH=""
LEGACY_PAIRING_SHA256=""
LEGACY_ACTIVE_PRINTER_SHA256=""
LEGACY_ACTIVE_PRINTER_STATION_COUNT=0
LEGACY_PAIRING_FINGERPRINT=""
LEGACY_ACTIVE_PRINTER_FINGERPRINT=""
MANAGED_PAIRING_INITIAL_FINGERPRINT=""
MANAGED_PRINTER_INITIAL_FINGERPRINT=""
MIGRATED_PAIRING_FINGERPRINT=""
MIGRATED_PRINTER_FINGERPRINT=""
PAIRING_MIGRATION_STATE="none"
PRINTER_MIGRATION_STATE="none"
PAIRING_MIGRATION_BACKUP=""
PRINTER_MIGRATION_BACKUP=""
PAIRING_MIGRATION_NEXT="$AGENT_DIR/.posly-agent.json.update.$$"
PRINTER_MIGRATION_NEXT="$AGENT_DIR/.printer-config.json.update.$$"
RECOVERY_DIR=""
RECOVERY_DIR_CREATED=0
SUPPORT_PATHS="install printer-profiles printer-config.example.json .env.example README.md"
SUPPORT_BACKUP_ROOT=""
SUPPORT_BACKUPS_READY=0
SUPPORT_PATHS_MUTATED=0
UPDATE_LOCK_DIR="$AGENT_DIR/.auto-update.lock.d"
UPDATE_LOCK_OWNER="$UPDATE_LOCK_DIR/owner"
UPDATE_LOCK_TOKEN="$ATTEMPT_ID:$$"
UPDATE_LOCK_ACQUIRED=0

case "$ATTEMPT_ID" in
  ''|*[!A-Za-z0-9._-]*) fail "The update attempt id contains unsafe characters." ;;
esac
RECOVERY_DIR="$AGENT_DIR/.update-recovery-$ATTEMPT_ID"

case "$HEARTBEAT_TIMEOUT_SECONDS" in
  ''|*[!0-9]*) fail "The heartbeat timeout must be a positive number of seconds." ;;
  0) fail "The heartbeat timeout must be a positive number of seconds." ;;
esac
case "$SERVICE_AUTO_UPDATE" in
  0|1) ;;
  *) fail "The service automatic-update policy must be 0 or 1." ;;
esac

case "$UNAME" in
  Darwin|Linux) ;;
  *) fail "This updater supports macOS and Linux only. On Windows use update.ps1." ;;
esac

[ -f "$AGENT_DIR/dist/index.js" ] || fail "No print agent found at $AGENT_DIR. Use the full installer from install.posly.xyz instead."
[ -f "$AGENT_DIR/package.json" ] || fail "The existing agent is missing package.json. Use the full installer from Posly admin."

discover_node_bin() {
  local candidate=""
  local agent_command=""
  local active_node=""
  local active_exec=""
  local explicit_node="${POSLY_NODE_BIN:-}"
  local explicit_exec=""

  # The v0.6.25 pairing belongs to the Node binary running the agent. Resolve
  # that process before any explicit or discovered Node can select stale files.
  case "${POSLY_AGENT_PID:-}" in
    '') ;;
    *[!0-9]*) fail "POSLY_AGENT_PID is not a valid process id. The existing agent was not changed." ;;
    *)
      agent_command="$(/bin/ps -p "$POSLY_AGENT_PID" -o command= 2>/dev/null || true)"
      [ -n "$agent_command" ] \
        || fail "The initiating print-agent process is no longer running. The existing agent was not changed."
      printf '%s\n' "$agent_command" | grep -Fq "$AGENT_DIR/dist/index.js" \
        || fail "POSLY_AGENT_PID does not identify this print agent. The existing agent was not changed."
      active_node="${agent_command%% *}"
      active_node="${active_node#\"}"
      active_node="${active_node%\"}"
      [ -n "$active_node" ] && [ -x "$active_node" ] \
        || fail "The initiating print agent does not expose an executable Node runtime. The existing agent was not changed."
      active_exec="$("$active_node" -p 'process.execPath' 2>/dev/null || true)"
      [ -n "$active_exec" ] && [ -x "$active_exec" ] \
        || fail "The initiating print agent Node runtime could not be verified. The existing agent was not changed."

      if [ -n "$explicit_node" ]; then
        [ -x "$explicit_node" ] \
          || fail "POSLY_NODE_BIN is not executable. The existing agent was not changed."
        explicit_exec="$("$explicit_node" -p 'process.execPath' 2>/dev/null || true)"
        [ -n "$explicit_exec" ] && [ -x "$explicit_exec" ] \
          || fail "POSLY_NODE_BIN could not be verified. The existing agent was not changed."
        [ "$explicit_exec" = "$active_exec" ] \
          || fail "POSLY_NODE_BIN conflicts with the initiating print-agent runtime. The existing agent was not changed."
      fi

      NODE_BIN="$active_exec"
      return 0
      ;;
  esac

  if [ -n "$explicit_node" ]; then
    [ -x "$explicit_node" ] \
      || fail "POSLY_NODE_BIN is not executable. The existing agent was not changed."
    candidate="$("$explicit_node" -p 'process.execPath' 2>/dev/null || true)"
    [ -n "$candidate" ] && [ -x "$candidate" ] \
      || fail "POSLY_NODE_BIN could not be verified. The existing agent was not changed."
    NODE_BIN="$candidate"
    return 0
  fi

  if [ "$UNAME" = "Darwin" ] && [ -f "$PLIST" ] && [ -x /usr/libexec/PlistBuddy ]; then
    candidate="$(/usr/libexec/PlistBuddy -c 'Print :ProgramArguments:0' "$PLIST" 2>/dev/null || true)"
    if [ -n "$candidate" ] && [ -x "$candidate" ]; then
      NODE_BIN="$("$candidate" -p 'process.execPath' 2>/dev/null || true)"
      [ -n "$NODE_BIN" ] && [ -x "$NODE_BIN" ] || return 1
      return 0
    fi
  fi

  candidate="$(command -v node 2>/dev/null || true)"
  if [ -n "$candidate" ] && [ -x "$candidate" ]; then
    NODE_BIN="$("$candidate" -p 'process.execPath' 2>/dev/null || true)"
    [ -n "$NODE_BIN" ] && [ -x "$NODE_BIN" ] || return 1
    return 0
  fi

  for candidate in \
    "$HOME"/Library/pnpm/nodejs/*/bin/node \
    "$HOME"/.nvm/versions/node/*/bin/node \
    /opt/homebrew/bin/node \
    /usr/local/bin/node \
    /usr/bin/node; do
    if [ -x "$candidate" ]; then
      NODE_BIN="$("$candidate" -p 'process.execPath' 2>/dev/null || true)"
      [ -n "$NODE_BIN" ] && [ -x "$NODE_BIN" ] || continue
      return 0
    fi
  done
  return 1
}

NODE_BIN=""
discover_node_bin \
  || fail "Node.js is not available. Install Node 20 or newer, then retry."
[ -n "$NODE_BIN" ] && [ -x "$NODE_BIN" ] || fail "Node.js is not available. Install Node 20 or newer, then retry."
NODE_DIR="$(cd "$(dirname "$NODE_BIN")" && pwd)"
LEGACY_PAIRING_PATH="$NODE_DIR/posly-agent.json"
NPM_BIN_FOR_SERVICE="${POSLY_NPM_BIN:-}"
if [ -z "$NPM_BIN_FOR_SERVICE" ] && [ -x "$NODE_DIR/npm" ]; then
  NPM_BIN_FOR_SERVICE="$NODE_DIR/npm"
fi
if [ -z "$NPM_BIN_FOR_SERVICE" ]; then
  NPM_BIN_FOR_SERVICE="$(command -v npm 2>/dev/null || true)"
fi

validate_pairing_file() {
  local pairing_path="$1"
  "$NODE_BIN" - "$pairing_path" <<'NODE' >/dev/null 2>&1
const fs = require("fs");
const config = JSON.parse(fs.readFileSync(process.argv[2], "utf8").replace(/^\uFEFF/, ""));
if (!config || typeof config !== "object") process.exit(1);
if (typeof config.apiUrl !== "string" || !/^https?:$/.test(new URL(config.apiUrl).protocol)) process.exit(1);
if (typeof config.tenantId !== "string" || config.tenantId.length === 0) process.exit(1);
if (typeof config.agentToken !== "string" || config.agentToken.length === 0) process.exit(1);
NODE
}

pairing_identities_match() {
  local legacy_path="$1"
  local managed_path="$2"
  "$NODE_BIN" - "$legacy_path" "$managed_path" <<'NODE' >/dev/null 2>&1
const fs = require("fs");
const read = (path) => JSON.parse(fs.readFileSync(path, "utf8").replace(/^\uFEFF/, ""));
const legacy = read(process.argv[2]);
const managed = read(process.argv[3]);
for (const field of ["apiUrl", "tenantId", "agentToken"]) {
  if (String(legacy[field] ?? "") !== String(managed[field] ?? "")) process.exit(1);
}
NODE
}

file_sha256() {
  local path="$1"
  "$NODE_BIN" - "$path" <<'NODE'
const crypto = require("crypto");
const fs = require("fs");
process.stdout.write(crypto.createHash("sha256").update(fs.readFileSync(process.argv[2])).digest("hex"));
NODE
}

path_fingerprint() {
  local path="$1"

  [ ! -L "$path" ] || return 1
  if [ -f "$path" ]; then
    printf 'file:%s\n' "$(file_sha256 "$path")"
    return 0
  fi
  if [ ! -e "$path" ]; then
    printf '%s\n' "missing"
    return 0
  fi
  return 1
}

printer_config_summary() {
  local printer_path="$1"
  "$NODE_BIN" - "$printer_path" <<'NODE'
const crypto = require("crypto");
const fs = require("fs");
const bytes = fs.readFileSync(process.argv[2]);
const raw = bytes.toString("utf8").replace(/^\uFEFF/, "");
const parsed = JSON.parse(raw);
let stations;
if (Array.isArray(parsed)) {
  stations = parsed;
} else if (parsed && typeof parsed === "object" && Array.isArray(parsed.stations)) {
  stations = parsed.stations;
} else {
  process.exit(1);
}
const stationIds = [];
for (const station of stations) {
  if (!station || typeof station !== "object" || typeof station.stationId !== "string" || station.stationId.trim().length === 0) {
    process.exit(1);
  }
  stationIds.push(station.stationId);
}
if (new Set(stationIds).size !== stationIds.length) process.exit(1);
const digest = crypto.createHash("sha256").update(bytes).digest("hex");
process.stdout.write(`${stationIds.length}:${digest}`);
NODE
}

# v0.6.25 on macOS stored pair-code credentials beside the Node binary.
# Detect that exact runtime so the replacement service keeps the active pairing.
if [ -f "$LEGACY_PAIRING_PATH" ] && \
   grep -Fq 'dirname(process.execPath)' "$AGENT_DIR/dist/config.js" 2>/dev/null; then
  validate_pairing_file "$LEGACY_PAIRING_PATH" \
    || fail "The legacy macOS pairing file is invalid. The existing agent was not changed."
  LEGACY_NODE_PAIRING_ACTIVE=1
  LEGACY_ACTIVE_PRINTER_PATH="$(POSLY_LEGACY_NODE_DIR="$NODE_DIR" POSLY_AGENT_DIR="$AGENT_DIR" "$NODE_BIN" - "$LEGACY_PAIRING_PATH" <<'NODE'
const fs = require("fs");
const path = require("path");
const config = JSON.parse(fs.readFileSync(process.argv[2], "utf8").replace(/^\uFEFF/, ""));
const configured = typeof config.printerConfigPath === "string" && config.printerConfigPath.length > 0
  ? config.printerConfigPath
  : path.join(process.env.POSLY_LEGACY_NODE_DIR, "printer-config.json");
process.stdout.write(path.isAbsolute(configured)
  ? configured
  : path.resolve(process.env.POSLY_AGENT_DIR, configured));
NODE
)"
  [ -f "$LEGACY_ACTIVE_PRINTER_PATH" ] \
    || fail "The active legacy macOS printer configuration is missing. The existing agent was not changed."
  LEGACY_PRINTER_SUMMARY="$(printer_config_summary "$LEGACY_ACTIVE_PRINTER_PATH")" \
    || fail "The active legacy macOS printer configuration or its station IDs are invalid. The existing agent was not changed."
  LEGACY_ACTIVE_PRINTER_STATION_COUNT="${LEGACY_PRINTER_SUMMARY%%:*}"
  LEGACY_ACTIVE_PRINTER_SHA256="${LEGACY_PRINTER_SUMMARY#*:}"
  LEGACY_PAIRING_SHA256="$(file_sha256 "$LEGACY_PAIRING_PATH")" \
    || fail "The active legacy macOS pairing file could not be fingerprinted. The existing agent was not changed."
  LEGACY_PAIRING_FINGERPRINT="$(path_fingerprint "$LEGACY_PAIRING_PATH")" \
    || fail "The active legacy macOS pairing path is not a regular file. The existing agent was not changed."
  LEGACY_ACTIVE_PRINTER_FINGERPRINT="$(path_fingerprint "$LEGACY_ACTIVE_PRINTER_PATH")" \
    || fail "The active legacy macOS printer path is not a regular file. The existing agent was not changed."
  MANAGED_PAIRING_INITIAL_FINGERPRINT="$(path_fingerprint "$AGENT_DIR/posly-agent.json")" \
    || fail "The managed pairing path is not a regular file. The existing agent was not changed."
  MANAGED_PRINTER_INITIAL_FINGERPRINT="$(path_fingerprint "$AGENT_DIR/printer-config.json")" \
    || fail "The managed printer path is not a regular file. The existing agent was not changed."

  if [ -f "$AGENT_DIR/posly-agent.json" ]; then
    validate_pairing_file "$AGENT_DIR/posly-agent.json" \
      || fail "The existing agent-directory pairing file is invalid. The existing agent was not changed."
    pairing_identities_match "$LEGACY_PAIRING_PATH" "$AGENT_DIR/posly-agent.json" \
      || fail "The existing agent-directory pairing conflicts with the active legacy venue identity. The existing agent was not changed."
  fi

  if [ -f "$AGENT_DIR/printer-config.json" ]; then
    MANAGED_PRINTER_SUMMARY="$(printer_config_summary "$AGENT_DIR/printer-config.json")" \
      || fail "The existing agent-directory printer configuration is invalid. The existing agent was not changed."
    MANAGED_PRINTER_STATION_COUNT="${MANAGED_PRINTER_SUMMARY%%:*}"
    if [ "$MANAGED_PRINTER_STATION_COUNT" -gt 0 ] && \
       ! cmp -s "$LEGACY_ACTIVE_PRINTER_PATH" "$AGENT_DIR/printer-config.json"; then
      fail "The existing agent-directory printer config conflicts with the active legacy printer config. The existing agent was not changed."
    fi
  fi
  say "validated active legacy macOS printer config with $LEGACY_ACTIVE_PRINTER_STATION_COUNT station(s)"
fi

if [ ! -f "$AGENT_DIR/posly-agent.json" ] && \
   [ ! -f "$AGENT_DIR/.env" ] && \
   [ "$LEGACY_NODE_PAIRING_ACTIVE" != "1" ]; then
  fail "The agent has no saved venue pairing. Use the full installer from Posly admin."
fi

migrate_legacy_node_pairing() {
  local current_printer_summary=""

  [ "$LEGACY_NODE_PAIRING_ACTIVE" = "1" ] || return 0
  ensure_recovery_dir || return 1

  validate_pairing_file "$LEGACY_PAIRING_PATH" || return 1
  [ "$(path_fingerprint "$LEGACY_PAIRING_PATH")" = "$LEGACY_PAIRING_FINGERPRINT" ] || return 1
  current_printer_summary="$(printer_config_summary "$LEGACY_ACTIVE_PRINTER_PATH")" || return 1
  [ "${current_printer_summary#*:}" = "$LEGACY_ACTIVE_PRINTER_SHA256" ] || return 1
  [ "$(path_fingerprint "$LEGACY_ACTIVE_PRINTER_PATH")" = "$LEGACY_ACTIVE_PRINTER_FINGERPRINT" ] || return 1
  [ "$(path_fingerprint "$AGENT_DIR/posly-agent.json")" = "$MANAGED_PAIRING_INITIAL_FINGERPRINT" ] || return 1
  [ "$(path_fingerprint "$AGENT_DIR/printer-config.json")" = "$MANAGED_PRINTER_INITIAL_FINGERPRINT" ] || return 1

  PAIRING_MIGRATION_BACKUP="$RECOVERY_DIR/posly-agent.json.before-migration"
  if [ -f "$AGENT_DIR/posly-agent.json" ]; then
    cp -p "$AGENT_DIR/posly-agent.json" "$PAIRING_MIGRATION_BACKUP" || return 1
    [ "$(path_fingerprint "$PAIRING_MIGRATION_BACKUP")" = "$MANAGED_PAIRING_INITIAL_FINGERPRINT" ] || return 1
    PAIRING_MIGRATION_STATE="replaced"
  else
    PAIRING_MIGRATION_STATE="created"
  fi
  printf '%s\n' "$PAIRING_MIGRATION_STATE" > "$RECOVERY_DIR/posly-agent.state" || return 1
  POSLY_PAIRING_SOURCE="$LEGACY_PAIRING_PATH" \
  POSLY_PAIRING_DESTINATION="$PAIRING_MIGRATION_NEXT" \
  "$NODE_BIN" <<'NODE' || return 1
const fs = require("fs");
const config = JSON.parse(fs.readFileSync(process.env.POSLY_PAIRING_SOURCE, "utf8").replace(/^\uFEFF/, ""));
delete config.printerConfigPath;
fs.writeFileSync(process.env.POSLY_PAIRING_DESTINATION, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
NODE
  chmod 600 "$PAIRING_MIGRATION_NEXT" || return 1
  MIGRATED_PAIRING_FINGERPRINT="$(path_fingerprint "$PAIRING_MIGRATION_NEXT")" || return 1
  [ "$(path_fingerprint "$AGENT_DIR/posly-agent.json")" = "$MANAGED_PAIRING_INITIAL_FINGERPRINT" ] || return 1
  mv "$PAIRING_MIGRATION_NEXT" "$AGENT_DIR/posly-agent.json" || return 1
  [ "$(path_fingerprint "$AGENT_DIR/posly-agent.json")" = "$MIGRATED_PAIRING_FINGERPRINT" ] || return 1

  PRINTER_MIGRATION_BACKUP="$RECOVERY_DIR/printer-config.json.before-migration"
  if [ -f "$AGENT_DIR/printer-config.json" ]; then
    cp -p "$AGENT_DIR/printer-config.json" "$PRINTER_MIGRATION_BACKUP" || return 1
    [ "$(path_fingerprint "$PRINTER_MIGRATION_BACKUP")" = "$MANAGED_PRINTER_INITIAL_FINGERPRINT" ] || return 1
    PRINTER_MIGRATION_STATE="replaced"
  else
    PRINTER_MIGRATION_STATE="created"
  fi
  printf '%s\n' "$PRINTER_MIGRATION_STATE" > "$RECOVERY_DIR/printer-config.state" || return 1
  cp -p "$LEGACY_ACTIVE_PRINTER_PATH" "$PRINTER_MIGRATION_NEXT" || return 1
  MIGRATED_PRINTER_FINGERPRINT="$(path_fingerprint "$PRINTER_MIGRATION_NEXT")" || return 1
  [ "$(path_fingerprint "$AGENT_DIR/printer-config.json")" = "$MANAGED_PRINTER_INITIAL_FINGERPRINT" ] || return 1
  mv "$PRINTER_MIGRATION_NEXT" "$AGENT_DIR/printer-config.json" || return 1
  [ "$(path_fingerprint "$AGENT_DIR/printer-config.json")" = "$MIGRATED_PRINTER_FINGERPRINT" ] || return 1
  [ "$MIGRATED_PRINTER_FINGERPRINT" = "$LEGACY_ACTIVE_PRINTER_FINGERPRINT" ] || return 1

  say "migrated the active legacy macOS pairing and printer config into the agent directory"
}

ensure_recovery_dir() {
  if [ "$RECOVERY_DIR_CREATED" = "1" ]; then
    [ -d "$RECOVERY_DIR" ] && [ ! -L "$RECOVERY_DIR" ]
    return
  fi
  [ ! -e "$RECOVERY_DIR" ] || return 1
  mkdir "$RECOVERY_DIR" || return 1
  RECOVERY_DIR_CREATED=1
  chmod 700 "$RECOVERY_DIR" || return 1
}

backup_support_paths() {
  local path=""
  local source=""
  local backup=""
  local state_path=""
  local state=""

  ensure_recovery_dir || return 1
  SUPPORT_BACKUP_ROOT="$RECOVERY_DIR/support-paths"
  mkdir "$SUPPORT_BACKUP_ROOT" || return 1
  chmod 700 "$SUPPORT_BACKUP_ROOT" || return 1

  for path in $SUPPORT_PATHS; do
    source="$AGENT_DIR/$path"
    backup="$SUPPORT_BACKUP_ROOT/$path"
    state_path="$SUPPORT_BACKUP_ROOT/$path.state"
    [ ! -L "$source" ] || return 1
    if [ -d "$source" ]; then
      state="directory"
      cp -Rp "$source" "$backup" || return 1
    elif [ -f "$source" ]; then
      state="file"
      cp -p "$source" "$backup" || return 1
    elif [ ! -e "$source" ]; then
      state="missing"
    else
      return 1
    fi
    printf '%s\n' "$state" > "$state_path" || return 1
  done
  SUPPORT_BACKUPS_READY=1
}

restore_support_paths() {
  local path=""
  local destination=""
  local backup=""
  local state_path=""
  local state=""
  local restore_next=""
  local failed=0

  [ "$SUPPORT_PATHS_MUTATED" = "1" ] || return 0
  [ "$SUPPORT_BACKUPS_READY" = "1" ] || return 1
  [ -d "$SUPPORT_BACKUP_ROOT" ] && [ ! -L "$SUPPORT_BACKUP_ROOT" ] || return 1

  for path in $SUPPORT_PATHS; do
    destination="$AGENT_DIR/$path"
    backup="$SUPPORT_BACKUP_ROOT/$path"
    state_path="$SUPPORT_BACKUP_ROOT/$path.state"
    restore_next="$AGENT_DIR/.support-restore-${path}.$$"
    state="$(sed -n '1p' "$state_path" 2>/dev/null || true)"
    rm -rf "$restore_next" || failed=1
    case "$state" in
      missing)
        rm -rf "$destination" || failed=1
        ;;
      file)
        [ -f "$backup" ] && [ ! -L "$backup" ] || {
          failed=1
          continue
        }
        cp -p "$backup" "$restore_next" \
          && rm -rf "$destination" \
          && mv "$restore_next" "$destination" \
          || failed=1
        ;;
      directory)
        [ -d "$backup" ] && [ ! -L "$backup" ] || {
          failed=1
          continue
        }
        cp -Rp "$backup" "$restore_next" \
          && rm -rf "$destination" \
          && mv "$restore_next" "$destination" \
          || failed=1
        ;;
      *) failed=1 ;;
    esac
  done
  if [ "$failed" = "0" ]; then
    SUPPORT_PATHS_MUTATED=0
    return 0
  fi
  PRESERVE_RECOVERY_FILES=1
  return 1
}

restore_pairing_migration() {
  local failed=0

  rm -f "$PAIRING_MIGRATION_NEXT" "$PRINTER_MIGRATION_NEXT" || failed=1
  case "$PAIRING_MIGRATION_STATE" in
    replaced)
      cp -p "$PAIRING_MIGRATION_BACKUP" "$PAIRING_MIGRATION_NEXT" \
        && mv "$PAIRING_MIGRATION_NEXT" "$AGENT_DIR/posly-agent.json" \
        || failed=1
      ;;
    created)
      rm -f "$AGENT_DIR/posly-agent.json" || failed=1
      ;;
  esac
  case "$PRINTER_MIGRATION_STATE" in
    replaced)
      cp -p "$PRINTER_MIGRATION_BACKUP" "$PRINTER_MIGRATION_NEXT" \
        && mv "$PRINTER_MIGRATION_NEXT" "$AGENT_DIR/printer-config.json" \
        || failed=1
      ;;
    created)
      rm -f "$AGENT_DIR/printer-config.json" || failed=1
      ;;
  esac
  if [ "$failed" = "0" ]; then
    PAIRING_MIGRATION_STATE="none"
    PRINTER_MIGRATION_STATE="none"
    return 0
  fi
  PRESERVE_RECOVERY_FILES=1
  return 1
}

remove_recovery_dir() {
  [ "$RECOVERY_DIR_CREATED" = "1" ] || return 0
  [ -e "$RECOVERY_DIR" ] || return 0
  [ "$PRESERVE_RECOVERY_FILES" = "0" ] || return 1
  rm -rf "$RECOVERY_DIR" || return 1
  RECOVERY_DIR_CREATED=0
}

verify_legacy_migration_before_stop() {
  local current_printer_summary=""

  [ "$LEGACY_NODE_PAIRING_ACTIVE" = "1" ] || return 0
  validate_pairing_file "$LEGACY_PAIRING_PATH" || return 1
  [ "$(path_fingerprint "$LEGACY_PAIRING_PATH")" = "$LEGACY_PAIRING_FINGERPRINT" ] || return 1
  pairing_identities_match "$LEGACY_PAIRING_PATH" "$AGENT_DIR/posly-agent.json" || return 1
  current_printer_summary="$(printer_config_summary "$LEGACY_ACTIVE_PRINTER_PATH")" || return 1
  [ "${current_printer_summary#*:}" = "$LEGACY_ACTIVE_PRINTER_SHA256" ] || return 1
  [ "$(path_fingerprint "$LEGACY_ACTIVE_PRINTER_PATH")" = "$LEGACY_ACTIVE_PRINTER_FINGERPRINT" ] || return 1
  [ "$(path_fingerprint "$AGENT_DIR/posly-agent.json")" = "$MIGRATED_PAIRING_FINGERPRINT" ] || return 1
  [ "$(path_fingerprint "$AGENT_DIR/printer-config.json")" = "$MIGRATED_PRINTER_FINGERPRINT" ]
}

write_state() {
  local phase="$1"
  local error="${2:-}"
  POSLY_UPDATE_STATE_PATH="$STATE_PATH" \
  POSLY_UPDATE_ATTEMPT_ID="$ATTEMPT_ID" \
  POSLY_UPDATE_FROM_VERSION="$CURRENT_VERSION" \
  POSLY_UPDATE_TO_VERSION="$AGENT_VERSION" \
  POSLY_UPDATE_PHASE="$phase" \
  POSLY_UPDATE_INSTALL_DIR="$AGENT_DIR" \
  POSLY_UPDATE_PID="$$" \
  POSLY_UPDATE_ERROR="$error" \
  "$NODE_BIN" <<'NODE' >/dev/null 2>&1
const fs = require("fs");
const state = {
  attemptId: process.env.POSLY_UPDATE_ATTEMPT_ID,
  fromVersion: process.env.POSLY_UPDATE_FROM_VERSION,
  toVersion: process.env.POSLY_UPDATE_TO_VERSION,
  phase: process.env.POSLY_UPDATE_PHASE,
  startedAt: new Date().toISOString(),
  updatedAt: new Date().toISOString(),
  installDir: process.env.POSLY_UPDATE_INSTALL_DIR,
  pid: Number(process.env.POSLY_UPDATE_PID || 0),
};
try {
  const existing = JSON.parse(fs.readFileSync(process.env.POSLY_UPDATE_STATE_PATH, "utf8"));
  if (existing && existing.attemptId === state.attemptId && existing.startedAt) {
    state.startedAt = existing.startedAt;
  }
} catch {}
if (process.env.POSLY_UPDATE_ERROR) state.error = process.env.POSLY_UPDATE_ERROR;
fs.writeFileSync(process.env.POSLY_UPDATE_STATE_PATH, JSON.stringify(state, null, 2));
NODE
}

write_state_best_effort() {
  local phase="$1"
  local error="${2:-}"
  if ! write_state "$phase" "$error"; then
    warn "could not record update state phase $phase"
  fi
}

xml_escape() {
  local value="$1"
  value="${value//&/&amp;}"
  value="${value//</&lt;}"
  value="${value//>/&gt;}"
  value="${value//\"/&quot;}"
  printf '%s' "$value"
}

macos_plist_matches_agent() {
  local plist_label=""
  local plist_node=""
  local plist_entry=""
  local plist_agent_dir=""
  local plist_install_base=""
  local plist_auto_update=""

  [ -f "$PLIST" ] || return 1
  /usr/bin/plutil -lint "$PLIST" >/dev/null 2>&1 || return 1
  [ -x /usr/libexec/PlistBuddy ] || return 1
  plist_label="$(/usr/libexec/PlistBuddy -c 'Print :Label' "$PLIST" 2>/dev/null || true)"
  plist_node="$(/usr/libexec/PlistBuddy -c 'Print :ProgramArguments:0' "$PLIST" 2>/dev/null || true)"
  plist_entry="$(/usr/libexec/PlistBuddy -c 'Print :ProgramArguments:1' "$PLIST" 2>/dev/null || true)"
  plist_agent_dir="$(/usr/libexec/PlistBuddy -c 'Print :EnvironmentVariables:POSLY_AGENT_DIR' "$PLIST" 2>/dev/null || true)"
  plist_install_base="$(/usr/libexec/PlistBuddy -c 'Print :EnvironmentVariables:POSLY_INSTALL_BASE' "$PLIST" 2>/dev/null || true)"
  plist_auto_update="$(/usr/libexec/PlistBuddy -c 'Print :EnvironmentVariables:POSLY_AUTO_UPDATE' "$PLIST" 2>/dev/null || true)"
  [ "$plist_label" = "$LABEL" ] \
    && [ -x "$plist_node" ] \
    && [ "$plist_node" = "$NODE_BIN" ] \
    && [ "$plist_entry" = "$AGENT_DIR/dist/index.js" ] \
    && [ "$plist_agent_dir" = "$AGENT_DIR" ] \
    && [ "$plist_install_base" = "$INSTALL_BASE" ] \
    && [ "$plist_auto_update" = "$SERVICE_AUTO_UPDATE" ]
}

write_macos_plist() {
  local plist_tmp=""
  local label_xml=""
  local node_xml=""
  local entry_xml=""
  local agent_xml=""
  local log_xml=""
  local node_dir_xml=""
  local npm_xml=""
  local install_base_xml=""
  local auto_update_xml=""

  mkdir -p "$(dirname "$PLIST")" "$LOG_DIR" || return 1
  plist_tmp="$PLIST.next.$$"
  label_xml="$(xml_escape "$LABEL")"
  node_xml="$(xml_escape "$NODE_BIN")"
  entry_xml="$(xml_escape "$AGENT_DIR/dist/index.js")"
  agent_xml="$(xml_escape "$AGENT_DIR")"
  log_xml="$(xml_escape "$LOG_DIR")"
  node_dir_xml="$(xml_escape "$NODE_DIR")"
  npm_xml="$(xml_escape "$NPM_BIN_FOR_SERVICE")"
  install_base_xml="$(xml_escape "$INSTALL_BASE")"
  auto_update_xml="$(xml_escape "$SERVICE_AUTO_UPDATE")"

  cat > "$plist_tmp" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>$label_xml</string>
  <key>ProgramArguments</key>
  <array>
    <string>$node_xml</string>
    <string>$entry_xml</string>
  </array>
  <key>WorkingDirectory</key>
  <string>$agent_xml</string>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>
  <key>ThrottleInterval</key>
  <integer>30</integer>
  <key>StandardOutPath</key>
  <string>$log_xml/agent.log</string>
  <key>StandardErrorPath</key>
  <string>$log_xml/agent.err</string>
  <key>EnvironmentVariables</key>
  <dict>
    <key>HOME</key>
    <string>$(xml_escape "$HOME")</string>
    <key>POSLY_AGENT_DIR</key>
    <string>$agent_xml</string>
    <key>POSLY_NODE_BIN</key>
    <string>$node_xml</string>
    <key>POSLY_NPM_BIN</key>
    <string>$npm_xml</string>
    <key>POSLY_INSTALL_BASE</key>
    <string>$install_base_xml</string>
    <key>POSLY_AUTO_UPDATE</key>
    <string>$auto_update_xml</string>
    <key>POSLY_LAUNCHD_LABEL</key>
    <string>$label_xml</string>
    <key>POSLY_LAUNCHD_PLIST</key>
    <string>$(xml_escape "$PLIST")</string>
    <key>PATH</key>
    <string>$node_dir_xml:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
  </dict>
</dict>
</plist>
EOF
  /usr/bin/plutil -lint "$plist_tmp" >/dev/null 2>&1 || {
    rm -f "$plist_tmp"
    return 1
  }
  chmod 600 "$plist_tmp" || {
    rm -f "$plist_tmp"
    return 1
  }
  mv "$plist_tmp" "$PLIST"
}

ensure_macos_service_definition() {
  if macos_plist_matches_agent; then
    return 0
  fi
  say "repairing the macOS LaunchAgent registration"
  write_macos_plist || return 1
  macos_plist_matches_agent
}

macos_service_loaded() {
  /bin/launchctl print "$LAUNCHD_DOMAIN/$LABEL" >/dev/null 2>&1
}

macos_service_running() {
  /bin/launchctl print "$LAUNCHD_DOMAIN/$LABEL" 2>/dev/null | grep -q 'state = running'
}

macos_service_pid() {
  /bin/launchctl print "$LAUNCHD_DOMAIN/$LABEL" 2>/dev/null \
    | awk '/pid = / { print $3; exit }'
}

wait_for_macos_service_running() {
  local deadline=$((SECONDS + 30))
  while [ "$SECONDS" -lt "$deadline" ]; do
    if macos_service_running; then
      return 0
    fi
    sleep 1
  done
  return 1
}

wait_for_macos_service_unloaded() {
  local deadline=$((SECONDS + 15))
  while [ "$SECONDS" -lt "$deadline" ]; do
    if ! macos_service_loaded; then
      return 0
    fi
    sleep 1
  done
  return 1
}

stop_agent_pid() {
  local pid="$1"
  local command_line=""
  local deadline=0

  case "$pid" in
    ''|*[!0-9]*) return 0 ;;
  esac
  kill -0 "$pid" 2>/dev/null || return 0
  command_line="$(/bin/ps -p "$pid" -o command= 2>/dev/null || true)"
  printf '%s\n' "$command_line" | grep -Fq "$AGENT_DIR/dist/index.js" || return 1
  kill "$pid" 2>/dev/null || return 1
  deadline=$((SECONDS + 10))
  while [ "$SECONDS" -lt "$deadline" ]; do
    kill -0 "$pid" 2>/dev/null || return 0
    sleep 1
  done
  kill -9 "$pid" 2>/dev/null || true
  sleep 1
  ! kill -0 "$pid" 2>/dev/null
}

stop_legacy_agent_pid() {
  stop_agent_pid "${POSLY_AGENT_PID:-}"
}

exact_agent_pids() {
  local entry="$AGENT_DIR/dist/index.js"
  local pid=""
  local command=""
  /bin/ps -axo pid=,command= 2>/dev/null \
    | while read -r pid command; do
        case "$pid" in
          ''|*[!0-9]*) continue ;;
        esac
        [ "$pid" != "$$" ] || continue
        case "$command" in
          *"$entry"*) printf '%s\n' "$pid" ;;
        esac
      done
}

stop_all_exact_agent_processes() {
  local deadline=$((SECONDS + 15))
  local pids=""
  local pid=""

  while [ "$SECONDS" -lt "$deadline" ]; do
    pids="$(exact_agent_pids)"
    [ -n "$pids" ] || return 0
    say "stopping print-agent process ids: $(printf '%s' "$pids" | tr '\n' ' ')"
    for pid in $pids; do
      kill "$pid" 2>/dev/null || true
    done
    sleep 1
  done

  pids="$(exact_agent_pids)"
  for pid in $pids; do
    kill -9 "$pid" 2>/dev/null || true
  done
  sleep 1
  [ -z "$(exact_agent_pids)" ]
}

stop_macos_agent() {
  local launchd_pid=""

  if macos_service_loaded; then
    launchd_pid="$(macos_service_pid || true)"
    /bin/launchctl bootout "$LAUNCHD_DOMAIN/$LABEL" || return 1
    wait_for_macos_service_unloaded || return 1
    stop_agent_pid "$launchd_pid" || return 1
  fi
  # A broken legacy install can have both a loaded LaunchAgent and separate
  # foreground processes. The updater is scoped to this exact install path, so
  # stop every daemon from that install before starting one replacement.
  stop_all_exact_agent_processes || return 1
}

hold_linux_auto_updates() {
  local user_config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
  local hold_dir="$user_config_home/systemd/user/posly-print-agent.service.d"
  local hold_path="$hold_dir/90-posly-update-hold.conf"
  local hold_next="$hold_path.next.$$"

  if [ -e "$hold_path" ] && [ ! -f "$hold_path" ]; then
    return 1
  fi
  mkdir -p "$hold_dir" || return 1
  cat > "$hold_next" <<'EOF' || return 1
[Service]
Environment=POSLY_AUTO_UPDATE=0
EOF
  chmod 600 "$hold_next" || {
    rm -f "$hold_next"
    return 1
  }
  mv "$hold_next" "$hold_path" || return 1
  systemctl --user daemon-reload || return 1
}

restart_agent() {
  local service_was_loaded=0
  case "$UNAME" in
    Darwin)
      ensure_macos_service_definition || return 1
      if macos_service_loaded; then
        service_was_loaded=1
      else
        if ! /bin/launchctl bootstrap "$LAUNCHD_DOMAIN" "$PLIST"; then
          sleep 2
          macos_service_loaded || /bin/launchctl bootstrap "$LAUNCHD_DOMAIN" "$PLIST" || return 1
        fi
      fi
      if [ "${POSLY_LAUNCHD_SKIP_ENABLE:-0}" != "1" ]; then
        /bin/launchctl enable "$LAUNCHD_DOMAIN/$LABEL" || return 1
      fi
      if [ "$service_was_loaded" = "1" ]; then
        /bin/launchctl kickstart -k "$LAUNCHD_DOMAIN/$LABEL" || return 1
      fi
      wait_for_macos_service_running || return 1
      ;;
    Linux)
      if systemctl --user cat posly-print-agent.service >/dev/null 2>&1; then
        systemctl --user daemon-reload
        if [ "$AUTO_MODE" = "1" ]; then
          systemctl --user restart posly-print-agent.service
        else
          systemctl --user enable --now posly-print-agent.service
        fi
      else
        return 2
      fi
      ;;
  esac
}

restore_previous_files() {
  if [ -d "$AGENT_DIR/dist.previous" ]; then
    rm -rf "$AGENT_DIR/dist" || return 1
    mv "$AGENT_DIR/dist.previous" "$AGENT_DIR/dist" || return 1
  fi
  if [ -f "$AGENT_DIR/package.json.previous" ]; then
    rm -f "$AGENT_DIR/package.json" || return 1
    mv "$AGENT_DIR/package.json.previous" "$AGENT_DIR/package.json" || return 1
  fi
  rm -rf "$AGENT_DIR/dist.next" "$AGENT_DIR/package.json.next" || return 1
  [ -f "$AGENT_DIR/dist/index.js" ] || return 1
  [ -f "$AGENT_DIR/package.json" ] || return 1
  [ "$("$NODE_BIN" -p "require('$AGENT_DIR/package.json').version" 2>/dev/null || true)" = "$CURRENT_VERSION" ]
}

rollback_and_restart() {
  local reason="$1"
  local rollback_error=""
  local migration_restore_error=""

  [ "$ROLLBACK_RUNNING" = "0" ] || return 1
  ROLLBACK_RUNNING=1
  write_state_best_effort "rollback_started" "$reason"
  if [ "$UNAME" = "Darwin" ] && macos_service_loaded; then
    if ! /bin/launchctl bootout "$LAUNCHD_DOMAIN/$LABEL"; then
      rollback_error="could not stop the failed macOS service"
    elif ! wait_for_macos_service_unloaded; then
      rollback_error="the failed macOS service did not unload"
    fi
  elif [ "$UNAME" = "Linux" ]; then
    systemctl --user stop posly-print-agent.service >/dev/null 2>&1 || true
  fi
  if [ -z "$rollback_error" ] && ! stop_all_exact_agent_processes; then
    rollback_error="could not stop all failed processes for the selected install"
  fi
  if [ -z "$rollback_error" ] && ! restore_previous_files; then
    rollback_error="could not restore the previous agent files"
  fi
  if [ -z "$rollback_error" ] && ! restore_pairing_migration; then
    migration_restore_error="could not restore the pre-update pairing files"
    PRESERVE_RECOVERY_FILES=1
  fi
  if [ -z "$rollback_error" ] && ! restore_support_paths; then
    rollback_error="could not restore the pre-update support files"
    PRESERVE_RECOVERY_FILES=1
  fi
  if [ -z "$rollback_error" ] && [ "$UNAME" = "Darwin" ]; then
    SERVICE_AUTO_UPDATE=0
    if ! write_macos_plist; then
      rollback_error="could not hold automatic updates on the restored macOS service"
    fi
  fi
  if [ -z "$rollback_error" ] && [ "$UNAME" = "Linux" ]; then
    if ! hold_linux_auto_updates; then
      rollback_error="could not hold automatic updates on the restored Linux service"
    fi
  fi
  if [ -z "$rollback_error" ] && ! restart_agent; then
    rollback_error="the restored agent service did not start"
  fi
  if [ -n "$migration_restore_error" ]; then
    if [ -n "$rollback_error" ]; then
      rollback_error="$rollback_error; $migration_restore_error"
    else
      rollback_error="$migration_restore_error"
    fi
  fi
  if [ -n "$rollback_error" ]; then
    write_state_best_effort "failed" "$reason; rollback failed: $rollback_error"
    warn "rollback failed: $rollback_error"
    return 1
  fi
  write_state_best_effort "failed" \
    "$reason; previous version restarted locally; production heartbeat was not verified by updater"
  if ! remove_recovery_dir; then
    warn "could not remove the completed update recovery directory"
  fi
  warn "previous agent files restored and restarted locally; production heartbeat was not verified by updater"
  return 0
}

acquire_update_lock() {
  local owner=""
  local owner_pid=""
  local retired=""
  local attempt=0

  while [ "$attempt" -lt 16 ]; do
    attempt=$((attempt + 1))
    if mkdir "$UPDATE_LOCK_DIR" 2>/dev/null; then
      if ! printf '%s\n' "$UPDATE_LOCK_TOKEN" > "$UPDATE_LOCK_OWNER"; then
        rmdir "$UPDATE_LOCK_DIR" 2>/dev/null || true
        fail "Could not persist install-level update lock ownership. The existing agent was not changed."
      fi
      UPDATE_LOCK_ACQUIRED=1
      return 0
    fi

    owner="$(cat "$UPDATE_LOCK_OWNER" 2>/dev/null || true)"
    owner_pid="${owner##*:}"
    case "$owner_pid" in
      ''|*[!0-9]*) ;;
      *)
        if kill -0 "$owner_pid" 2>/dev/null; then
          fail "Another updater already owns this print-agent install. The existing agent was not changed."
        fi
        ;;
    esac

    retired="$AGENT_DIR/.auto-update.lock.retired.$ATTEMPT_ID.$$.$attempt"
    if mv "$UPDATE_LOCK_DIR" "$retired" 2>/dev/null; then
      rm -r "$retired" 2>/dev/null || true
    fi
  done
  fail "Could not acquire the install-level update lock. The existing agent was not changed."
}

release_update_lock() {
  [ "$UPDATE_LOCK_ACQUIRED" = "1" ] || return 0
  if [ "$(cat "$UPDATE_LOCK_OWNER" 2>/dev/null || true)" = "$UPDATE_LOCK_TOKEN" ]; then
    rm -f "$UPDATE_LOCK_OWNER" 2>/dev/null || true
    rmdir "$UPDATE_LOCK_DIR" 2>/dev/null || true
  fi
  UPDATE_LOCK_ACQUIRED=0
}

wait_for_heartbeat_confirmation() {
  local deadline=$((SECONDS + HEARTBEAT_TIMEOUT_SECONDS))
  local phase=""
  while [ "$SECONDS" -lt "$deadline" ]; do
    phase="$(POSLY_UPDATE_STATE_PATH="$STATE_PATH" "$NODE_BIN" <<'NODE' 2>/dev/null || true
try {
  const fs = require("fs");
  const state = JSON.parse(fs.readFileSync(process.env.POSLY_UPDATE_STATE_PATH, "utf8"));
  process.stdout.write(String(state.phase || ""));
} catch {}
NODE
)"
    case "$phase" in
      heartbeat_confirmed) return 0 ;;
      failed) return 1 ;;
    esac
    sleep 2
  done
  return 1
}

TMP_DIR="$(mktemp -d /tmp/posly-print-agent-update.XXXXXX)"
TMP_TAR="$TMP_DIR/agent.tar.gz"
TMP_MANIFEST="$TMP_DIR/print-agent-archives.sha256"
EXTRACT_DIR="$TMP_DIR/extract"
on_exit() {
  local rc=$?
  local reason="${LAST_ERROR:-updater exited with status $rc}"
  trap - EXIT
  set +e
  if [ "$UPDATE_LOCK_ACQUIRED" != "1" ]; then
    [ -z "$TMP_DIR" ] || rm -rf "$TMP_DIR"
    exit "$rc"
  fi
  if [ "$rc" -ne 0 ]; then
    if [ "$POST_STOP" = "1" ] && [ "$UPDATE_COMMITTED" != "1" ]; then
      rollback_and_restart "$reason"
    else
      if ! restore_pairing_migration; then
        reason="$reason; could not restore the pre-update pairing files"
        PRESERVE_RECOVERY_FILES=1
      elif ! remove_recovery_dir; then
        reason="$reason; could not remove the completed update recovery directory"
      fi
      if ! rm -rf "$AGENT_DIR/dist.next" "$AGENT_DIR/package.json.next"; then
        reason="$reason; could not remove staged update files"
      fi
      write_state_best_effort "failed" "$reason"
    fi
  fi
  [ -z "$TMP_DIR" ] || rm -rf "$TMP_DIR"
  release_update_lock
  exit "$rc"
}
trap on_exit EXIT
acquire_update_lock
mkdir -p "$EXTRACT_DIR"

ARCHIVE_NAME="posly-print-agent.tar.gz"
if [ "$AGENT_VERSION" != "latest" ]; then
  ARCHIVE_NAME="posly-print-agent-$AGENT_VERSION.tar.gz"
fi
ARCHIVE_URL="$INSTALL_BASE/$ARCHIVE_NAME?posly-version=$AGENT_VERSION&posly-attempt=$ATTEMPT_ID"
MANIFEST_URL="$INSTALL_BASE/print-agent-archives.sha256?posly-version=$AGENT_VERSION&posly-attempt=$ATTEMPT_ID"

CURRENT_VERSION="$("$NODE_BIN" -p "require('$AGENT_DIR/package.json').version" 2>/dev/null || printf 'unknown')"
write_state "download_started" \
  || fail "Could not record update state before download. The existing agent was not changed."
say "downloading the print agent $AGENT_VERSION"
curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 15 --max-time 120 \
  -H 'Cache-Control: no-cache' -H 'Pragma: no-cache' \
  "$ARCHIVE_URL" -o "$TMP_TAR" \
  || fail "Download failed. Check the internet connection and retry."
ARCHIVE_BYTES="$(wc -c < "$TMP_TAR" | tr -d ' ')"
ARCHIVE_MAGIC="$(od -An -tx1 -N2 "$TMP_TAR" | tr -d ' \n')"
say "  got $ARCHIVE_BYTES bytes with signature $ARCHIVE_MAGIC"
[ "$ARCHIVE_BYTES" -ge 1024 ] \
  || fail "The downloaded update is too small to be a print agent archive."
[ "$ARCHIVE_BYTES" -le 52428800 ] \
  || fail "The downloaded update exceeds the 50 MB safety limit."
[ "$ARCHIVE_MAGIC" = "1f8b" ] \
  || fail "The downloaded update is not a gzip archive."
curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 15 --max-time 60 \
  -H 'Cache-Control: no-cache' -H 'Pragma: no-cache' \
  "$MANIFEST_URL" -o "$TMP_MANIFEST" \
  || fail "The update checksum manifest could not be downloaded."
MANIFEST_MATCHES="$(awk -v expected="$ARCHIVE_NAME" \
  '$2 == expected || $2 == "./" expected { print tolower($1) }' \
  "$TMP_MANIFEST")"
[ "$(printf '%s\n' "$MANIFEST_MATCHES" | sed '/^$/d' | wc -l | tr -d ' ')" = "1" ] \
  || fail "The update checksum manifest has no unique entry for $ARCHIVE_NAME."
EXPECTED_SHA256="$(printf '%s\n' "$MANIFEST_MATCHES" | sed '/^$/d')"
[ "${#EXPECTED_SHA256}" -eq 64 ] \
  || fail "The update checksum manifest contains an invalid SHA-256 digest."
case "$EXPECTED_SHA256" in
  *[!0-9a-f]*) fail "The update checksum manifest contains an invalid SHA-256 digest." ;;
esac
ACTUAL_SHA256="$("$NODE_BIN" -e \
  'const fs=require("fs"),crypto=require("crypto");process.stdout.write(crypto.createHash("sha256").update(fs.readFileSync(process.argv[1])).digest("hex"))' \
  "$TMP_TAR")"
[ "$ACTUAL_SHA256" = "$EXPECTED_SHA256" ] \
  || fail "The downloaded update failed SHA-256 verification."
say "  SHA-256 verified"
tar -xzf "$TMP_TAR" --strip-components=1 -C "$EXTRACT_DIR" \
  || fail "The downloaded update could not be extracted."
[ -f "$EXTRACT_DIR/dist/index.js" ] || fail "The downloaded update is missing dist/index.js."
[ -f "$EXTRACT_DIR/package.json" ] || fail "The downloaded update is missing package.json."
DOWNLOADED_VERSION="$("$NODE_BIN" -p "require('$EXTRACT_DIR/package.json').version" 2>/dev/null || true)"
if [ "$AGENT_VERSION" != "latest" ] && [ "$DOWNLOADED_VERSION" != "$AGENT_VERSION" ]; then
  fail "package.json version $DOWNLOADED_VERSION does not match requested version $AGENT_VERSION"
fi
CURRENT_DEPENDENCIES="$("$NODE_BIN" -p "JSON.stringify(require('$AGENT_DIR/package.json').dependencies || {})" 2>/dev/null || true)"
DOWNLOADED_DEPENDENCIES="$("$NODE_BIN" -p "JSON.stringify(require('$EXTRACT_DIR/package.json').dependencies || {})" 2>/dev/null || true)"
DEPENDENCIES_CHANGED=0
if [ "$CURRENT_DEPENDENCIES" != "$DOWNLOADED_DEPENDENCIES" ]; then
  DEPENDENCIES_CHANGED=1
fi
if [ "$AUTO_MODE" = "1" ] && [ "$DEPENDENCIES_CHANGED" = "1" ]; then
  fail "This update changes dependencies and requires the visible manual updater."
fi
NPM_BIN=""
if [ "$DEPENDENCIES_CHANGED" = "1" ]; then
  NPM_BIN="${POSLY_NPM_BIN:-}"
  if [ -z "$NPM_BIN" ] && [ -x "$NODE_DIR/npm" ]; then
    NPM_BIN="$NODE_DIR/npm"
  fi
  if [ -z "$NPM_BIN" ]; then
    NPM_BIN="$(command -v npm || true)"
  fi
  [ -n "$NPM_BIN" ] && [ -x "$NPM_BIN" ] || fail "npm is not available. Install Node 20 or newer, then retry."
fi

# Stage every essential file before interrupting the current process.
say "staging the update"
migrate_legacy_node_pairing \
  || fail "Could not migrate the active legacy macOS pairing safely. The existing agent was not changed."
backup_support_paths \
  || fail "Could not preserve the current support files safely. The existing agent was not changed."
rm -rf "$AGENT_DIR/dist.next" "$AGENT_DIR/dist.previous" \
  "$AGENT_DIR/package.json.next" "$AGENT_DIR/package.json.previous"
cp -R "$EXTRACT_DIR/dist" "$AGENT_DIR/dist.next"
cp "$EXTRACT_DIR/package.json" "$AGENT_DIR/package.json.next"
write_state "staged" \
  || fail "Could not record the staged update safely. The existing agent was not stopped."

# The archive is fully staged before the current service is interrupted.
# macOS always unloads first so every restart follows one bootstrap path.
case "$UNAME" in
  Darwin)
    ensure_macos_service_definition \
      || fail "Could not repair the macOS LaunchAgent registration before update."
    verify_legacy_migration_before_stop \
      || fail "The active legacy macOS pairing or printer configuration changed during staging. The existing agent was not stopped."
    say "stopping the current print agent"
    POST_STOP=1
    stop_macos_agent || fail "Could not stop the current macOS print agent safely."
    ;;
  Linux)
    verify_legacy_migration_before_stop \
      || fail "The active legacy pairing or printer configuration changed during staging. The existing agent was not stopped."
    POST_STOP=1
    if [ "$AUTO_MODE" != "1" ]; then
      say "stopping the current print agent"
      systemctl --user stop posly-print-agent.service \
        || fail "Could not stop the current Linux print agent safely."
    fi
    ;;
esac

say "installing the update"
write_state "activating" \
  || fail "Could not record activation state. The previous agent will be restored."
mv "$AGENT_DIR/dist" "$AGENT_DIR/dist.previous" \
  || fail "Could not preserve the current agent files before activation."
mv "$AGENT_DIR/package.json" "$AGENT_DIR/package.json.previous" \
  || fail "Could not preserve the current package metadata before activation."
mv "$AGENT_DIR/dist.next" "$AGENT_DIR/dist" \
  || fail "Could not activate the downloaded agent files."
mv "$AGENT_DIR/package.json.next" "$AGENT_DIR/package.json" \
  || fail "Could not activate the downloaded package metadata."
SUPPORT_PATHS_MUTATED=1
for path in $SUPPORT_PATHS; do
  if [ -e "$EXTRACT_DIR/$path" ]; then
    if ! rm -rf "$AGENT_DIR/$path" || ! cp -R "$EXTRACT_DIR/$path" "$AGENT_DIR/$path"; then
      warn "Could not refresh optional file: $path"
    fi
  fi
done

if [ "$DEPENDENCIES_CHANGED" = "1" ]; then
  say "refreshing changed dependencies"
  (
    cd "$AGENT_DIR"
    "$NPM_BIN" install --omit=dev --no-audit --no-fund
  ) || fail "Dependency refresh failed. Run the visible updater again."
else
  say "dependencies unchanged; skipping npm install"
fi

say "restarting the print agent"
write_state "restart_requested" \
  || fail "Could not record restart state. The previous agent will be restored."
if ! restart_agent; then
  fail "Update failed because the replacement service did not start."
fi

if [ "$AUTO_MODE" = "1" ]; then
  say "waiting for post-update heartbeat"
  if ! wait_for_heartbeat_confirmation; then
    fail "Update failed because the replacement agent did not confirm a heartbeat."
  fi
  say "post-update heartbeat confirmed"
fi

rm -rf "$AGENT_DIR/dist.previous" "$AGENT_DIR/package.json.previous" \
  || warn "Could not remove the previous update files after confirmation."
UPDATE_COMMITTED=1
if ! remove_recovery_dir; then
  warn "Could not remove the completed update recovery directory."
fi

VERSION="$("$NODE_BIN" -p "require('$AGENT_DIR/package.json').version" 2>/dev/null || printf 'unknown')"
say "update complete: v$VERSION"
say "return to the POS; the update notice should clear within 30 seconds"
