#!/usr/bin/env bash
set -euo pipefail

SERVICE_TEMPLATE="${NOVIJ_STORAGE_SLOT_SERVICE_TEMPLATE:-novij-storage@%s.service}"
LEGACY_SERVICE="${NOVIJ_STORAGE_LEGACY_SERVICE:-novij-storage.service}"
BASE_CONFIG="${NOVIJ_STORAGE_BASE_CONFIG:-/etc/novij/storage/ntp-storage.conf}"
SLOTS_DIR="${NOVIJ_STORAGE_SLOTS_DIR:-/etc/novij/storage/slots}"
ACTIVE_SLOT_FILE="${NOVIJ_STORAGE_ACTIVE_SLOT_FILE:-/etc/novij/storage/active-slot}"
NGINX_UPSTREAM_FILE="${NOVIJ_STORAGE_NGINX_UPSTREAM_FILE:-/etc/nginx/novij-storage/active-ws-upstream.conf}"
STATUS_FILE="${NOVIJ_STORAGE_PACKAGE_UPGRADE_STATUS_FILE:-/var/lib/novij/storage/package-upgrade-status.json}"
LOCK_FILE="${NOVIJ_STORAGE_SWITCH_LOCK_FILE:-/run/lock/novij-storage/zero-downtime-switch.lock}"
LOCK_DIR="${NOVIJ_STORAGE_SWITCH_LOCK_DIR:-${LOCK_FILE}.d}"
LOCK_PID_FILE="$LOCK_DIR/pid"
WRITER_LOCK_PATH="${NOVIJ_STORAGE_WRITER_LOCK_PATH:-}"
BLUE_WS_PORT="${NOVIJ_STORAGE_BLUE_WS_PORT:-38991}"
BLUE_HEALTH_PORT="${NOVIJ_STORAGE_BLUE_HEALTH_PORT:-38992}"
GREEN_WS_PORT="${NOVIJ_STORAGE_GREEN_WS_PORT:-38994}"
GREEN_HEALTH_PORT="${NOVIJ_STORAGE_GREEN_HEALTH_PORT:-38995}"
DRAIN_TIMEOUT_SECONDS="${NOVIJ_STORAGE_DRAIN_TIMEOUT_SECONDS:-30}"
STARTUP_TIMEOUT_SECONDS="${NOVIJ_STORAGE_STARTUP_TIMEOUT_SECONDS:-300}"
SOAK_STOP_OLD="${NOVIJ_STORAGE_STOP_OLD_SLOT_AFTER_SWITCH:-0}"
SWITCH_OLD=""
SWITCH_NEW=""
SWITCH_OLD_HEALTH=""
SWITCH_NEW_HEALTH=""
SWITCH_OLD_SERVICE=""
SWITCH_NEW_SERVICE=""
SWITCH_DRAIN_STARTED=0
SWITCH_PROMOTED=0
SWITCH_COMMITTED=0
SWITCH_LEGACY_OLD=0
SWITCH_NEEDS_ROLLBACK=0
SWITCH_NEW_STARTED=0
SWITCH_LOCK_ACQUIRED=0

log() {
  printf '[novij-storage-switch] %s\n' "$*"
}

cleanup_switch_lock() {
  if [ "$SWITCH_LOCK_ACQUIRED" = "1" ]; then
    rm -rf "$LOCK_DIR" >/dev/null 2>&1 || true
  fi
}

acquire_switch_lock() {
  local pid now stale_after lock_age
  mkdir -p "$(dirname "$LOCK_FILE")"
  exec 9>"$LOCK_FILE"
  if ! flock -n 9; then
    log "another switch is active"
    exit 0
  fi

  stale_after="${NOVIJ_STORAGE_SWITCH_LOCK_STALE_SECONDS:-1800}"
  while ! mkdir "$LOCK_DIR" 2>/dev/null; do
    pid="$(cat "$LOCK_PID_FILE" 2>/dev/null || true)"
    if [ -n "$pid" ] && [ "$pid" != "$$" ] && kill -0 "$pid" 2>/dev/null; then
      log "another switch is active: pid=$pid"
      exit 0
    fi
    now="$(date +%s)"
    lock_age=0
    if [ -d "$LOCK_DIR" ]; then
      lock_age=$((now - $(stat -c %Y "$LOCK_DIR" 2>/dev/null || printf '%s' "$now")))
    fi
    if [ "$lock_age" -lt "$stale_after" ]; then
      log "another switch lock exists; treating as active"
      exit 0
    fi
    log "removing stale switch lock dir: $LOCK_DIR"
    rm -rf "$LOCK_DIR" >/dev/null 2>&1 || true
  done
  SWITCH_LOCK_ACQUIRED=1
  printf '%s\n' "$$" >"$LOCK_PID_FILE"
  trap cleanup_switch_lock EXIT
}

write_status() {
  local status="$1"
  local slot="$2"
  local message="$3"
  mkdir -p "$(dirname "$STATUS_FILE")"
  STATUS="$status" SLOT="$slot" MESSAGE="$message" node - <<'NODE' >"$STATUS_FILE.tmp"
const payload = {
  status: process.env.STATUS,
  active_slot: process.env.SLOT,
  message: process.env.MESSAGE,
  checked_at: new Date().toISOString()
};
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
NODE
  mv "$STATUS_FILE.tmp" "$STATUS_FILE"
}

slot_ws_port() {
  case "$1" in
    blue) printf '%s' "$BLUE_WS_PORT" ;;
    green) printf '%s' "$GREEN_WS_PORT" ;;
    *) return 1 ;;
  esac
}

slot_health_port() {
  case "$1" in
    blue) printf '%s' "$BLUE_HEALTH_PORT" ;;
    green) printf '%s' "$GREEN_HEALTH_PORT" ;;
    *) return 1 ;;
  esac
}

service_name() {
  printf "$SERVICE_TEMPLATE" "$1"
}

curl_json() {
  curl -fsS --max-time 5 "$@"
}

json_field() {
  local field="$1"
  node -e '
const fs = require("node:fs");
const field = process.argv[1];
const input = fs.readFileSync(0, "utf8");
const data = input ? JSON.parse(input) : {};
const value = field.split(".").reduce((acc, key) => acc && acc[key], data);
if (value !== undefined && value !== null) process.stdout.write(String(value));
' "$field"
}

yaml_scalar() {
  local key="$1"
  awk -v key="$key" '
    $1 == key ":" {
      $1 = "";
      sub(/^[[:space:]]+/, "");
      gsub(/^["'\'']|["'\'']$/, "");
      print;
      exit
    }
  ' "$BASE_CONFIG"
}

infer_writer_lock_path() {
  local configured data_dir
  configured="$(yaml_scalar writer_lock_path || true)"
  if [ -n "$configured" ]; then
    printf '%s' "$configured"
    return 0
  fi
  data_dir="$(yaml_scalar data_dir || true)"
  if [ -z "$data_dir" ]; then
    data_dir="/var/lib/ntp-storage/data"
  fi
  printf '%s/runtime/active-writer.lock' "${data_dir%/}"
}

runtime_field() {
  local health_port="$1"
  local field="$2"
  curl_json "http://127.0.0.1:${health_port}/version" 2>/dev/null | json_field "$field" 2>/dev/null || true
}

runtime_owner_slot() {
  local candidate health_port slot role writer_locked
  for candidate in blue green; do
    health_port="$(slot_health_port "$candidate")"
    slot="$(runtime_field "$health_port" slot)"
    role="$(runtime_field "$health_port" slot_role)"
    writer_locked="$(runtime_field "$health_port" writer_locked)"
    if [ "$slot" = "$candidate" ] && [ "$role" = "active" ] && [ "$writer_locked" = "true" ]; then
      printf '%s' "$candidate"
      return 0
    fi
  done
  return 1
}

legacy_runtime_owns_lock() {
  local slot role writer_locked
  slot="$(runtime_field "$BLUE_HEALTH_PORT" slot)"
  role="$(runtime_field "$BLUE_HEALTH_PORT" slot_role)"
  writer_locked="$(runtime_field "$BLUE_HEALTH_PORT" writer_locked)"
  [ "$slot" = "single" ] && [ "$role" = "active" ] && [ "$writer_locked" = "true" ]
}

write_slot_config() {
  local slot="$1"
  local role="$2"
  local ws_port health_port dir tmp
  ws_port="$(slot_ws_port "$slot")"
  health_port="$(slot_health_port "$slot")"
  dir="$SLOTS_DIR/$slot"
  tmp="$dir/ntp-storage.conf.tmp"
  mkdir -p "$dir"
  awk -v ws="$ws_port" -v hp="$health_port" '
    BEGIN { wrote_ws = 0; wrote_hp = 0; skip_runtime = 0 }
    function is_top_key(line) {
      return line ~ /^[A-Za-z0-9_-]+:[[:space:]]*/
    }
    skip_runtime && is_top_key($0) {
      skip_runtime = 0
    }
    skip_runtime {
      next
    }
    /^runtime:[[:space:]]*$/ {
      skip_runtime = 1
      next
    }
    /^[[:space:]]*ws_port:[[:space:]]*/ {
      print "ws_port: " ws; wrote_ws = 1; next
    }
    /^[[:space:]]*health_port:[[:space:]]*/ {
      print "health_port: " hp; wrote_hp = 1; next
    }
    { print }
    END {
      if (!wrote_ws) print "ws_port: " ws
      if (!wrote_hp) print "health_port: " hp
    }
  ' "$BASE_CONFIG" >"$tmp"
  cat >>"$tmp" <<EOF

runtime:
  slot: "$slot"
  slot_role: "$role"
  writer_lock_path: "$WRITER_LOCK_PATH"
EOF
  mv "$tmp" "$dir/ntp-storage.conf"
  write_slot_env "$slot" "$role" "$dir"
}

write_slot_env() {
  local slot="$1"
  local role="$2"
  local dir="$3"
  local tmp
  tmp="$dir/storage.env.tmp"
  if [ -f /etc/novij/storage/storage.env ]; then
    awk '
      /^[[:space:]]*NOVIJ_STORAGE_SLOT[[:space:]]*=/ { next }
      /^[[:space:]]*NOVIJ_STORAGE_SLOT_ROLE[[:space:]]*=/ { next }
      /^[[:space:]]*NOVIJ_STORAGE_CONFIG[[:space:]]*=/ { next }
      /^[[:space:]]*NOVIJ_STORAGE_WRITER_LOCK_PATH[[:space:]]*=/ { next }
      { print }
    ' /etc/novij/storage/storage.env >"$tmp"
  else
    : >"$tmp"
  fi
  {
    printf 'NOVIJ_STORAGE_SLOT=%s\n' "$slot"
    printf 'NOVIJ_STORAGE_SLOT_ROLE=%s\n' "$role"
    printf 'NOVIJ_STORAGE_CONFIG=%s/ntp-storage.conf\n' "$dir"
    printf 'NOVIJ_STORAGE_WRITER_LOCK_PATH=%s\n' "$WRITER_LOCK_PATH"
  } >>"$tmp"
  mv "$tmp" "$dir/storage.env"
  chmod 0640 "$dir/storage.env" 2>/dev/null || true
}

slot_runtime_config_digest() {
  local slot="$1"
  local slot_dir="$SLOTS_DIR/$slot"
  {
    for path in "$slot_dir/ntp-storage.conf" "$slot_dir/storage.env"; do
      if [ -f "$path" ]; then
        printf 'file %s\n' "${path##*/}"
        sha256sum "$path"
      else
        printf 'missing %s\n' "${path##*/}"
      fi
    done
  } | sha256sum | awk '{print $1}'
}

write_nginx_upstream() {
  local slot="$1"
  local ws_port
  ws_port="$(slot_ws_port "$slot")"
  mkdir -p "$(dirname "$NGINX_UPSTREAM_FILE")"
  printf 'server 127.0.0.1:%s;\n' "$ws_port" >"$NGINX_UPSTREAM_FILE.tmp"
  mv "$NGINX_UPSTREAM_FILE.tmp" "$NGINX_UPSTREAM_FILE"
}

reload_nginx() {
  if command -v nginx >/dev/null 2>&1; then
    nginx -t
    systemctl reload nginx || true
  fi
}

nginx_supports_dynamic_ws_upstream() {
  local dump
  if ! command -v nginx >/dev/null 2>&1; then
    return 1
  fi
  dump="$(nginx -T 2>/dev/null || true)"
  if [ -z "$dump" ]; then
    return 1
  fi
  [[ "$dump" == *"$NGINX_UPSTREAM_FILE"* ]] &&
    grep -Eq 'proxy_pass[[:space:]]+http://ntp_storage_ws;' <<<"$dump"
}

storage_edge_enabled() {
  [ -s /etc/novij/storage/protocol-edge.env ] &&
    command -v novij-storage-edge-backend >/dev/null 2>&1 &&
    systemctl cat novij-storage-edge.service >/dev/null 2>&1
}

refresh_storage_routing() {
  local slot="$1"
  local refreshed=0
  if nginx_supports_dynamic_ws_upstream; then
    write_nginx_upstream "$slot"
    reload_nginx
    refreshed=1
  fi
  if storage_edge_enabled; then
    systemctl restart novij-storage-edge.service
    refreshed=1
  fi
  [ "$refreshed" = "1" ]
}

rollback_after_error() {
  local line="$1"
  if [ "$SWITCH_COMMITTED" = "1" ]; then
    return 0
  fi
  if [ "$SWITCH_DRAIN_STARTED" != "1" ] && [ "$SWITCH_NEEDS_ROLLBACK" != "1" ]; then
    if [ "$SWITCH_NEW_STARTED" = "1" ] && [ -n "$SWITCH_NEW_SERVICE" ]; then
      log "error before drain at line $line; stopping candidate slot $SWITCH_NEW_SERVICE"
      systemctl stop "$SWITCH_NEW_SERVICE" || true
    fi
    systemctl restart novij-storage-status-sidecar.service >/dev/null 2>&1 || true
    write_status "blocked" "${SWITCH_OLD:-}" "switch error before drain at line $line"
    return 0
  fi
  if [ -z "$SWITCH_OLD" ]; then
    return 0
  fi
  log "error after draining $SWITCH_OLD at line $line; rolling back active slot"
  write_slot_config "$SWITCH_OLD" "active" || true
  [ -n "$SWITCH_NEW" ] && write_slot_config "$SWITCH_NEW" "standby" || true
  mkdir -p "$(dirname "$ACTIVE_SLOT_FILE")"
  printf '%s\n' "$SWITCH_OLD" >"$ACTIVE_SLOT_FILE"
  if [ "$SWITCH_NEW_STARTED" = "1" ] && [ -n "$SWITCH_NEW_SERVICE" ]; then
    systemctl stop "$SWITCH_NEW_SERVICE" || true
  fi
  if [ -n "$SWITCH_OLD_SERVICE" ]; then
    systemctl restart "$SWITCH_OLD_SERVICE" || true
  fi
  if [ -n "$SWITCH_OLD_HEALTH" ]; then
    curl_json -X POST "http://127.0.0.1:${SWITCH_OLD_HEALTH}/internal/drain/cancel" >/dev/null || true
    wait_http_ok "http://127.0.0.1:${SWITCH_OLD_HEALTH}/readyz" 15 || true
  fi
  refresh_storage_routing "$SWITCH_OLD" || true
  if [ "$SWITCH_LEGACY_OLD" = "1" ]; then
    systemctl disable "$(service_name blue)" "$(service_name green)" >/dev/null 2>&1 || true
  fi
  systemctl restart novij-storage-status-sidecar.service >/dev/null 2>&1 || true
  write_status "blocked" "$SWITCH_OLD" "rollback after switch error at line $line"
}

wait_http_ok() {
  local url="$1"
  local timeout="${2:-20}"
  local deadline=$((SECONDS + timeout))
  until curl_json "$url" >/dev/null 2>&1; do
    if [ "$SECONDS" -ge "$deadline" ]; then
      return 1
    fi
    sleep 1
  done
}

wait_tcp_ok() {
  local port="$1"
  local timeout="${2:-20}"
  local host
  host="${NOVIJ_STORAGE_WS_PROBE_HOST:-$(yaml_scalar ws_bind || true)}"
  host="${host:-127.0.0.1}"
  local deadline=$((SECONDS + timeout))
  until node -e '
const net = require("node:net");
const host = process.argv[1];
const port = Number(process.argv[2]);
const socket = net.createConnection({ host, port });
const timer = setTimeout(() => {
  socket.destroy();
  process.exit(1);
}, 900);
socket.once("connect", () => {
  clearTimeout(timer);
  socket.end();
  process.exit(0);
});
socket.once("error", () => {
  clearTimeout(timer);
  process.exit(1);
});
' "$host" "$port" >/dev/null 2>&1; do
    if [ "$SECONDS" -ge "$deadline" ]; then
      return 1
    fi
    sleep 1
  done
}

wait_runtime_slot() {
  local health_port="$1"
  local expected_slot="$2"
  local expected_role="$3"
  local timeout="${4:-20}"
  local deadline slot role
  deadline=$((SECONDS + timeout))
  while [ "$SECONDS" -lt "$deadline" ]; do
    slot="$(runtime_field "$health_port" slot)"
    role="$(runtime_field "$health_port" slot_role)"
    if [ "$slot" = "$expected_slot" ] && [ "$role" = "$expected_role" ]; then
      return 0
    fi
    sleep 1
  done
  log "runtime slot check failed on health port $health_port: expected ${expected_slot}/${expected_role}, got ${slot:-unknown}/${role:-unknown}"
  return 1
}

wait_drain_zero() {
  local slot="$1"
  local health_port deadline body mutations
  health_port="$(slot_health_port "$slot")"
  deadline=$((SECONDS + DRAIN_TIMEOUT_SECONDS))
  while [ "$SECONDS" -lt "$deadline" ]; do
    body="$(curl_json "http://127.0.0.1:${health_port}/internal/inflight" 2>/dev/null || true)"
    mutations="$(printf '%s' "$body" | json_field active_mutations 2>/dev/null || printf '')"
    if [ "${mutations:-0}" = "0" ]; then
      return 0
    fi
    sleep 1
  done
  return 1
}

retire_legacy_service() {
  systemctl disable --now "$LEGACY_SERVICE" >/dev/null 2>&1 || true
}

runtime_version_needs_restart() {
  local installed_version="${1:-}"
  local runtime_version="${2:-}"
  [ -n "$installed_version" ] && [ "$runtime_version" != "$installed_version" ]
}

stop_slot_units_for_legacy_owner() {
  local blue_service green_service
  blue_service="$(service_name blue)"
  green_service="$(service_name green)"
  log "legacy/single runtime owns writer lock; stopping slot units before migration"
  systemctl stop "$blue_service" "$green_service" >/dev/null 2>&1 || true
  systemctl reset-failed "$blue_service" "$green_service" >/dev/null 2>&1 || true
}

active_slot() {
  if [ -s "$ACTIVE_SLOT_FILE" ]; then
    tr -d '[:space:]' <"$ACTIVE_SLOT_FILE"
  else
    printf 'blue'
  fi
}

inactive_slot() {
  case "$1" in
    blue) printf 'green' ;;
    green) printf 'blue' ;;
    *) printf 'green' ;;
  esac
}

main() {
  trap 'rollback_after_error "$LINENO"' ERR
  acquire_switch_lock

  if [ ! -f "$BASE_CONFIG" ]; then
    log "base config missing: $BASE_CONFIG"
    write_status "blocked" "" "base config missing"
    exit 0
  fi

  if [ -z "$WRITER_LOCK_PATH" ]; then
    WRITER_LOCK_PATH="$(infer_writer_lock_path)"
  fi

  local configured_old runtime_owner old new old_ws new_ws old_health new_health new_service old_service legacy_old old_expected_slot old_runtime_slot old_runtime_role old_runtime_drain old_runtime_version installed_version force_restart_old legacy_service_active legacy_runtime_active old_config_digest updated_old_config_digest
  configured_old="$(active_slot)"
  old="$configured_old"
  runtime_owner="$(runtime_owner_slot || true)"
  legacy_runtime_active=0
  if legacy_runtime_owns_lock; then
    legacy_runtime_active=1
  fi
  if [ -n "$runtime_owner" ] && [ "$runtime_owner" != "$configured_old" ]; then
    log "active-slot mismatch: file says $configured_old but runtime owner is $runtime_owner; using live owner"
    old="$runtime_owner"
  elif [ -z "$runtime_owner" ] && [ "$legacy_runtime_active" = "1" ] && [ "$configured_old" != "blue" ]; then
    log "active-slot mismatch: file says $configured_old but legacy single runtime owns blue port; using blue for migration"
    old="blue"
  fi
  new="$(inactive_slot "$old")"
  old_ws="$(slot_ws_port "$old")"
  new_ws="$(slot_ws_port "$new")"
  old_health="$(slot_health_port "$old")"
  new_health="$(slot_health_port "$new")"
  new_service="$(service_name "$new")"
  old_runtime_slot="$(runtime_field "$old_health" slot)"
  old_runtime_role="$(runtime_field "$old_health" slot_role)"
  old_runtime_drain="$(runtime_field "$old_health" drain_state)"
  old_runtime_version="$(runtime_field "$old_health" version)"
  installed_version="$(dpkg-query -W -f='${Version}' novij-storage 2>/dev/null || true)"
  legacy_old=0
  force_restart_old=0
  legacy_service_active=0
  if systemctl is-active --quiet "$LEGACY_SERVICE" >/dev/null 2>&1; then
    legacy_service_active=1
  fi
  if [ "$legacy_runtime_active" = "1" ] ||
     [ "$old_runtime_slot" = "single" ] ||
     { [ "$legacy_service_active" = "1" ] &&
       { [ -z "$old_runtime_slot" ] || [ "$old_runtime_slot" != "$old" ] || [ ! -s "$ACTIVE_SLOT_FILE" ]; }; }; then
    old_service="$LEGACY_SERVICE"
    legacy_old=1
    old_expected_slot="single"
  else
    old_service="$(service_name "$old")"
    old_expected_slot="$old"
  fi
  case "$old_runtime_slot" in
    blue|green)
      if [ "$old_runtime_slot" != "$old" ]; then
        log "active-slot mismatch: file says $old but health port reports runtime slot $old_runtime_slot; repairing canonical $old service"
        systemctl stop "$(service_name "$old_runtime_slot")" >/dev/null 2>&1 || true
        old_service="$(service_name "$old")"
        legacy_old=0
        old_expected_slot="$old"
        force_restart_old=1
        old_runtime_slot=""
        old_runtime_role=""
        old_runtime_drain=""
      fi
      ;;
  esac

  SWITCH_OLD="$old"
  SWITCH_NEW="$new"
  SWITCH_OLD_HEALTH="$old_health"
  SWITCH_NEW_HEALTH="$new_health"
  SWITCH_OLD_SERVICE="$old_service"
  SWITCH_NEW_SERVICE="$new_service"
  SWITCH_LEGACY_OLD="$legacy_old"
  if [ "$old_runtime_role" = "draining" ] || [ "$old_runtime_drain" = "draining" ]; then
    SWITCH_NEEDS_ROLLBACK=1
  fi
  if runtime_version_needs_restart "$installed_version" "$old_runtime_version"; then
    log "active $old runtime version ${old_runtime_version:-unknown} differs from installed package $installed_version; restarting it before validation"
    force_restart_old=1
  fi
  if [ "$legacy_old" != "1" ]; then
    retire_legacy_service
  fi

  log "active=$old inactive=$new old_service=$old_service old_runtime=${old_runtime_slot:-unknown}/${old_runtime_role:-unknown}/${old_runtime_drain:-unknown} writer_lock=$WRITER_LOCK_PATH"
  if ! nginx_supports_dynamic_ws_upstream && ! storage_edge_enabled; then
    log "no dynamic Storage edge is configured for blue/green upstream; refusing switch"
    write_status "blocked" "$old" "no dynamic Storage edge is configured for blue/green upstream"
    exit 1
  fi
  old_config_digest="$(slot_runtime_config_digest "$old")"
  write_slot_config "$old" "active"
  write_slot_config "$new" "standby"
  updated_old_config_digest="$(slot_runtime_config_digest "$old")"
  if [ "$updated_old_config_digest" != "$old_config_digest" ]; then
    log "active $old runtime config changed; restarting it before validation"
    force_restart_old=1
  fi
  mkdir -p "$(dirname "$ACTIVE_SLOT_FILE")"
  printf '%s\n' "$old" >"$ACTIVE_SLOT_FILE"

  if [ "$legacy_old" = "1" ]; then
    stop_slot_units_for_legacy_owner
  fi
  systemctl daemon-reload || true
  if systemctl is-active --quiet "$new_service" >/dev/null 2>&1; then
    log "resetting inactive $new service to standby before active slot check"
    if systemctl restart "$new_service" &&
       wait_runtime_slot "$new_health" "$new" "standby" "$STARTUP_TIMEOUT_SECONDS"; then
      SWITCH_NEW_STARTED=1
    else
      log "inactive $new service was not healthy standby; stopping it before switch"
      systemctl stop "$new_service" >/dev/null 2>&1 || true
      SWITCH_NEW_STARTED=0
    fi
  fi
  old_health_ready=0
  if curl_json "http://127.0.0.1:${old_health}/healthz" >/dev/null 2>&1; then
    old_health_ready=1
  elif systemctl is-active --quiet "$old_service" >/dev/null 2>&1; then
    log "$old_service is active but health port $old_health is not ready; waiting before restart"
    if wait_http_ok "http://127.0.0.1:${old_health}/healthz" "$STARTUP_TIMEOUT_SECONDS"; then
      old_health_ready=1
    fi
  fi
  if [ "$force_restart_old" = "1" ] ||
     [ "$old_runtime_role" = "draining" ] || [ "$old_runtime_drain" = "draining" ] ||
     [ "$old_health_ready" != "1" ]; then
    systemctl restart "$old_service"
  else
    systemctl start "$old_service" || true
  fi
  wait_http_ok "http://127.0.0.1:${old_health}/healthz" "$STARTUP_TIMEOUT_SECONDS"
  wait_runtime_slot "$old_health" "$old_expected_slot" "active" "$STARTUP_TIMEOUT_SECONDS"
  curl_json -X POST "http://127.0.0.1:${old_health}/internal/drain/cancel" >/dev/null || true
  wait_http_ok "http://127.0.0.1:${old_health}/readyz" "$STARTUP_TIMEOUT_SECONDS"
  wait_tcp_ok "$old_ws" "$STARTUP_TIMEOUT_SECONDS"
  refresh_storage_routing "$old"

  systemctl restart "$new_service"
  SWITCH_NEW_STARTED=1

  wait_http_ok "http://127.0.0.1:${new_health}/healthz" "$STARTUP_TIMEOUT_SECONDS"
  wait_runtime_slot "$new_health" "$new" "standby" "$STARTUP_TIMEOUT_SECONDS"
  wait_http_ok "http://127.0.0.1:${new_health}/internal/standby-readyz" "$STARTUP_TIMEOUT_SECONDS"
  curl_json "http://127.0.0.1:${new_health}/version" >/dev/null
  wait_tcp_ok "$new_ws" "$STARTUP_TIMEOUT_SECONDS"

  log "starting drain on $old"
  curl_json -X POST "http://127.0.0.1:${old_health}/internal/drain/start" >/dev/null || true
  SWITCH_DRAIN_STARTED=1
  wait_drain_zero "$old" || {
    log "drain timeout on $old"
    write_status "blocked" "$old" "drain timeout"
    exit 1
  }

  log "promoting $new"
  curl_json -X POST "http://127.0.0.1:${new_health}/internal/promote" >/dev/null
  wait_runtime_slot "$new_health" "$new" "active" 10
  SWITCH_PROMOTED=1
  write_slot_config "$new" "active"
  write_slot_config "$old" "standby"
  printf '%s\n' "$new" >"$ACTIVE_SLOT_FILE"
  refresh_storage_routing "$new"

  wait_http_ok "http://127.0.0.1:${new_health}/readyz" 10
  wait_tcp_ok "$new_ws" 10
  sleep "${NOVIJ_STORAGE_POST_SWITCH_SOAK_SECONDS:-3}"
  wait_http_ok "http://127.0.0.1:${new_health}/readyz" 10
  wait_tcp_ok "$new_ws" 10
  SWITCH_COMMITTED=1
  systemctl enable "$(service_name blue)" "$(service_name green)" >/dev/null 2>&1 || true
  if [ "$SOAK_STOP_OLD" = "1" ]; then
    systemctl stop "$old_service" || true
  fi
  retire_legacy_service
  systemctl restart novij-storage-status-sidecar.service >/dev/null 2>&1 || true
  write_status "upgraded" "$new" "zero-downtime switch OK"
  log "switch OK: $new"
}

if [ "${NOVIJ_STORAGE_ZERO_DOWNTIME_SWITCH_LIB_ONLY:-0}" = "1" ]; then
  return 0 2>/dev/null || exit 0
fi

main "$@"
