#!/usr/bin/env bash
# vectoris-gw — operación del Vectoris Gateway en Docker Compose (G6, Ubuntu). Ejecutar como root.
#
#   vectoris-gw install DIST.tar.gz --sha256 HEX   primera instalación desde una distribución ya descargada
#   vectoris-gw fetch VERSIÓN SHA256               descarga la distribución de JFrog y la verifica (no la activa)
#   vectoris-gw init [ADMIN]                       Gateway NUEVO (sólo si no hay base): crea base, clave y administrador
#   vectoris-gw restore RESPALDO.tar.gz --yes      restaura un respaldo o MIGRA el de la Mac (misma identidad)
#   vectoris-gw configure                          ajusta gateway.conf para el contenedor (puertos, proxy, URL para PCs)
#   vectoris-gw check [VERSIÓN]                    verificación previa (configuración, clave, huella, base) sin modificar
#   vectoris-gw start | stop | restart | status | logs [N] | fingerprint | releases
#   vectoris-gw backup                             respaldo consistente en caliente (VACUUM INTO) en VGW_BACKUPS
#   vectoris-gw upgrade VERSIÓN SHA256 [--yes]     respaldo → nueva versión → salud; si falla, vuelve sola a la anterior
#   vectoris-gw rollback [--yes]                   vuelve a GATEWAY_PREVIOUS_VERSION (con el respaldo previo si hace falta)
#   vectoris-gw urls                               G6.1: dominio, alias, dirección para PCs y migración anunciada
#   vectoris-gw set-domain DOMINIO [--keep-old] [--migrate] [--agent-url URL] [--yes]
#                                                  G6.1: otro dominio público (certificado nuevo), sin recompilar ni tocar
#                                                  la identidad; si falla, vuelve solo al anterior
#   vectoris-gw server-address [SERVIDOR [HOST:PUERTO | --clear] [--priority AUTO|REMOTE_FIRST]]
#                                                  G6.1: dirección para clientes de cada USB Server (p. ej. SRV-01
#                                                  usb.dock-ia.com:3241); las PCs la reciben sin reinstalar nada
#
# Configuración: /opt/vectoris-gateway/.env (VGW_HOME cambia la raíz; sólo pruebas). Nunca borra datos: lo reemplazado
# queda al lado (data.before-restore-*, data.failed-*). Nunca crea una identidad nueva salvo 'init' sin base.
set -euo pipefail
umask 077

VGW_HOME=${VGW_HOME:-/opt/vectoris-gateway}
ENV_FILE=$VGW_HOME/.env
PROJECT=${VGW_PROJECT:-vectoris-gateway}
GW_UID=10001
CHOWN=${VGW_CHOWN:-yes}               # no = pruebas sin root (Docker Desktop)
HEALTH_TIMEOUT=${VGW_HEALTH_TIMEOUT:-150}
GROUP=com/vectoris/vectoris-gateway

say() { printf '%s\n' "$*"; }
step() { printf '\n== %s\n' "$*"; }
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
ts() { date +%Y%m%d-%H%M%S; }
sha256() { if command -v sha256sum >/dev/null; then sha256sum "$1" | cut -d' ' -f1; else shasum -a 256 "$1" | cut -d' ' -f1; fi; }
need_root() { [ "$CHOWN" = no ] || [ "$(id -u)" = 0 ] || die "ejecutar como root (sudo vectoris-gw $*)"; }
own() { [ "$CHOWN" = no ] || chown -R "$GW_UID:$GW_UID" "$@"; }

# ------------------------------------------------------------------ .env (CLAVE=VALOR; sin ejecutarlo como shell)
envget() {
  [ -f "$ENV_FILE" ] || return 0
  awk -v k="$1" 'index($0, k "=") == 1 { v = substr($0, length(k) + 2) } END { gsub(/^"|"$/, "", v); print v }' "$ENV_FILE"
}
envset() {
  local tmp="$ENV_FILE.tmp.$$"
  awk -v k="$1" -v v="$2" 'BEGIN { done = 0 } index($0, k "=") == 1 { print k "=" v; done = 1; next } { print } END { if (!done) print k "=" v }' "$ENV_FILE" > "$tmp"
  chmod 600 "$tmp"; mv "$tmp" "$ENV_FILE"
}
load_env() {
  [ -f "$ENV_FILE" ] || die "falta $ENV_FILE (vectoris-gw install …)"
  V=$(envget GATEWAY_VERSION); PREV=$(envget GATEWAY_PREVIOUS_VERSION); FP=$(envget GATEWAY_FINGERPRINT)
  DATA=$(envget VGW_DATA); BACKUPS=$(envget VGW_BACKUPS); KEEP=$(envget BACKUP_KEEP)
  [ -n "$V" ] || die "GATEWAY_VERSION vacío en $ENV_FILE"
  [ -n "$DATA" ] && [ -n "$BACKUPS" ] || die "VGW_DATA y VGW_BACKUPS son obligatorios en $ENV_FILE"
  [ "$(envget VGW_ROOT)" = "$VGW_HOME" ] || die "VGW_ROOT en $ENV_FILE ($(envget VGW_ROOT)) no es $VGW_HOME"
  KEEP=${KEEP:-30}
}
require_fp() {
  [ -n "$FP" ] || die "GATEWAY_FINGERPRINT vacío en $ENV_FILE: es la huella del Gateway que se migra (vectoris-gw fingerprint)"
  [[ "$FP" =~ ^[0-9A-Z]{4}(-[0-9A-Z]{4}){7}$ ]] || die "GATEWAY_FINGERPRINT con formato inválido: $FP"
}

# ------------------------------------------------------------------ versiones (releases/VERSIÓN, sólo lectura)
valid_version() { [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?$ ]] || die "versión inválida: $1 (exacta, p. ej. 0.7.0-g6; nunca «latest»)"; }
rel() { printf '%s/releases/%s' "$VGW_HOME" "$1"; }
have_release() { [ -f "$(rel "$1")/app/vectoris-gateway.jar" ] && [ -f "$(rel "$1")/deploy/compose.yaml" ]; }
image_of() { awk '/^ *image: *eclipse-temurin/ { print $2; exit }' "$(rel "$1")/deploy/compose.yaml"; }

# Distribución: un solo directorio vectoris-gateway-VERSIÓN/ con VERSION, app/ y deploy/; nada fuera de él.
unpack() { # unpack DIST SHA256_ESPERADO → imprime la versión
  local dist=$1 want=$2 got v top dir list
  [[ "$want" =~ ^[0-9a-f]{64}$ ]] || die "SHA-256 esperado inválido (64 hexadecimales en minúscula)"
  got=$(sha256 "$dist"); [ "$got" = "$want" ] || die "SHA-256 de $dist NO coincide: $got (esperado $want). No se instala."
  # Con pipefail, «tar | grep -q» puede invertir el resultado (SIGPIPE a tar): se lista primero y se analiza después.
  list=$(tar -tzf "$dist") || die "$dist no es un .tar.gz válido"
  top=$(printf '%s\n' "$list" | awk -F/ '{ print $1 }' | sort -u)
  [ "$(printf '%s\n' "$top" | wc -l | tr -d ' ')" = 1 ] && [[ "$top" =~ ^vectoris-gateway-[0-9A-Za-z.-]+$ ]] || die "distribución con estructura inesperada"
  ! printf '%s\n' "$list" | grep -E '(^/|(^|/)\.\.(/|$))' > /dev/null || die "distribución con rutas inseguras"
  v=${top#vectoris-gateway-}; valid_version "$v"
  dir=$(rel "$v")
  if [ -d "$dir" ]; then
    [ "$(cat "$dir/.dist-sha256" 2>/dev/null)" = "$got" ] || die "ya existe $dir con otro contenido: no se reemplaza"
    printf '%s' "$v"; return
  fi
  mkdir -p "$VGW_HOME/releases"; local tmp; tmp=$(mktemp -d "$VGW_HOME/releases/.tmp.XXXXXX")
  tar -xzf "$dist" -C "$tmp" --no-same-owner
  [ "$(cat "$tmp/$top/VERSION")" = "$v" ] || { rm -rf "$tmp"; die "VERSION interna distinta de $v"; }
  printf '%s\n' "$got" > "$tmp/$top/.dist-sha256"
  chmod -R a+rX,go-w "$tmp/$top"; chmod 755 "$tmp/$top"
  mv "$tmp/$top" "$dir"; rmdir "$tmp"
  printf '%s' "$v"
}

# ------------------------------------------------------------------ Docker
dc() { # dc VERSIÓN args…
  docker compose -p "$PROJECT" --env-file "$ENV_FILE" -f "$(rel "$1")/deploy/compose.yaml" "${@:2}"
}
gwid() { dc "$V" ps -a -q gateway 2>/dev/null | awk 'NR == 1'; }
running() { local id; id=$(gwid); [ -n "$id" ] && [ "$(docker inspect -f '{{.State.Running}}' "$id")" = true ]; }

# Comando del Gateway en un contenedor descartable: sin red, sin privilegios, uid del Gateway, datos indicados.
jrun() { # jrun VERSIÓN DIR_DATOS [-it] args…
  local v=$1 d=$2; shift 2; local tty=()
  [ "${1:-}" = -it ] && { tty=(-it); shift; }
  have_release "$v" || die "no está la versión $v (vectoris-gw fetch $v SHA256)"
  docker run --rm ${tty[@]+"${tty[@]}"} --network none --user "$GW_UID:$GW_UID" --read-only --tmpfs /tmp:rw,exec,nosuid,nodev,size=64m \
    --cap-drop ALL --security-opt no-new-privileges -e VECTORIS_GATEWAY_FINGERPRINT="$FP" ${VGW_EXTRA_MOUNT:+-v "$VGW_EXTRA_MOUNT"} \
    -v "$(rel "$v")/app:/app:ro" -v "$d:/data" "$(image_of "$v")" \
    java -Djava.io.tmpdir=/tmp -cp '/app/vectoris-gateway.jar:/app/lib/*' com.vectoris.gateway.Main "$@"
}

# Las operaciones de G6 (check, backup) existen desde 0.7.0-g6: upgrade/rollback sólo entre versiones que las tienen.
supports_ops() { local h; h=$(jrun "$1" "$DATA" --help 2>/dev/null) || true; [[ "$h" == *"backup --data"* ]]; }

wait_healthy() {
  local id s i
  for ((i = 0; i < HEALTH_TIMEOUT; i += 3)); do
    id=$(gwid)
    if [ -n "$id" ]; then
      s=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}/{{.State.Status}}' "$id")
      case "$s" in
        healthy/running) say "Gateway saludable ($((i))s)"; return 0 ;;
        */exited|*/dead) say "el contenedor del Gateway terminó"; docker logs --tail 15 "$id" 2>&1 | sed 's/^/   | /'; return 1 ;;
      esac
    fi
    sleep 3
  done
  say "el Gateway no quedó saludable en ${HEALTH_TIMEOUT}s"; [ -n "${id:-}" ] && docker logs --tail 15 "$id" 2>&1 | sed 's/^/   | /'
  return 1
}

preflight() { # preflight VERSIÓN DIR_DATOS: configuración completa + verificación del Gateway (sólo lectura)
  require_fp
  [ -n "$(envget ACME_EMAIL)" ] || die "ACME_EMAIL vacío en $ENV_FILE (obligatorio para Let's Encrypt)"
  [ -n "$(envget GATEWAY_DOMAIN)" ] || die "GATEWAY_DOMAIN vacío en $ENV_FILE"
  have_release "$1" || die "no está la versión $1"
  [ -f "$2/gateway.db" ] || die "no hay base en $2 (vectoris-gw restore RESPALDO o vectoris-gw init)"
  say "Verificación con $1 de $2:"
  jrun "$1" "$2" check --data /data --expect "$FP" | sed 's/^/   /'
  [ "${PIPESTATUS[0]}" = 0 ]
}

start_version() { # start_version VERSIÓN (ya en .env): levanta y espera salud
  # El contenedor del Gateway se RECREA siempre: tras restaurar o volver atrás, VGW_DATA es otro directorio con el mismo
  # nombre y un contenedor reutilizado podría seguir montando el anterior (comprobado en Docker Desktop).
  dc "$1" up -d --remove-orphans --force-recreate gateway && dc "$1" up -d --remove-orphans && wait_healthy
}

# ------------------------------------------------------------------ gateway.conf para el contenedor
configure_conf() { # configure_conf DIR_DATOS
  local f=$1/gateway.conf proxy url tmp
  proxy=$(envget VGW_PROXY_IP); url=$(envget AGENT_GATEWAY_URL); local mig; mig=$(envget GATEWAY_MIGRATE_URL)
  [ -n "$proxy" ] || die "VGW_PROXY_IP vacío en $ENV_FILE"
  [ -f "$f" ] && cp -p "$f" "$f.bak-$(ts)"
  tmp="$f.tmp.$$"
  { [ -f "$f" ] && cat "$f"; true; } | awk -v proxy="$proxy/32" -v url="$url" -v mig="$mig" '
    BEGIN { set["API_LISTEN"] = "0.0.0.0:3250"; set["CONSOLE_LISTEN"] = "0.0.0.0:3251"; set["CONSOLE_CONTAINER"] = "yes"; set["TRUSTED_PROXIES"] = proxy
            if (url != "") set["AGENT_GATEWAY_URL"] = url
            set["GATEWAY_MIGRATE_URL"] = mig }
    { split($0, kv, "="); k = kv[1]; gsub(/ /, "", k) }
    (k in set) && $0 !~ /^[ \t]*#/ { if (!(k in seen)) print k "=" set[k]; seen[k] = 1; next }
    { print }
    END { for (k in set) if (!(k in seen)) print k "=" set[k] }' > "$tmp"
  chmod 600 "$tmp"; mv "$tmp" "$f"; own "$f"
}

# ------------------------------------------------------------------ respaldo
backup_to() { # backup_to VERSIÓN → imprime la ruta del .tar.gz
  local v=$1 t stage out
  t=$(ts); mkdir -p "$BACKUPS"; chmod 700 "$BACKUPS"
  stage=$(mktemp -d "$BACKUPS/.stage.XXXXXX"); own "$stage"
  VGW_EXTRA_MOUNT="$stage:/out" jrun "$v" "$DATA" backup --data /data --out /out/data >&2 || { rm -rf "$stage"; return 1; }
  out="$BACKUPS/gateway-$t-$v.tar.gz"
  tar -C "$stage" -czf "$out" data; chmod 600 "$out"; rm -rf "$stage"
  sha256 "$out" > "$out.sha256"; chmod 600 "$out.sha256"
  [[ $'\n'"$(tar -tzf "$out")"$'\n' == *$'\n'data/gateway.key$'\n'* ]] || return 1
  # Retención: los KEEP más nuevos (nunca el recién hecho).
  ls -1t "$BACKUPS"/gateway-*.tar.gz 2>/dev/null | tail -n +"$((KEEP + 1))" | while read -r old; do rm -f "$old" "$old.sha256"; done
  printf '%s' "$out"
}

# Restaura un respaldo (formato G6 o el de run-on-macos.sh: ambos con data/) en DIR_DESTINO (no debe existir).
extract_backup() { # extract_backup ARCHIVO DIR_DESTINO
  local f=$1 dst=$2 stage list
  [ -f "$f" ] || die "no existe $f"
  if [ -f "$f.sha256" ]; then [ "$(sha256 "$f")" = "$(cut -d' ' -f1 < "$f.sha256")" ] || die "$f no coincide con $f.sha256"; fi
  list=$(tar -tzf "$f") || die "$f no es un .tar.gz válido"
  ! printf '%s\n' "$list" | grep -vE '^(\./)?data/?([A-Za-z0-9._-]+)?$' > /dev/null || die "$f contiene rutas fuera de data/: no es un respaldo del Gateway"
  ! printf '%s\n' "$list" | grep -E '(^|/)\.\.(/|$)' > /dev/null || die "$f contiene rutas inseguras"
  stage=$(mktemp -d "$(dirname "$dst")/.restore.XXXXXX")
  local topt=()
  case "$(tar --version 2>/dev/null)" in *GNU*) topt=(--warning=no-unknown-keyword) ;; esac   # pax de macOS (xattr)
  tar -xzf "$f" -C "$stage" --no-same-owner ${topt[@]+"${topt[@]}"} || { rm -rf "$stage"; die "no se pudo extraer $f"; }
  find "$stage/data" -name '._*' -type f -delete                       # metadatos de macOS
  [ -f "$stage/data/gateway.db" ] && [ -f "$stage/data/gateway.key" ] || { rm -rf "$stage"; die "$f no tiene data/gateway.db y data/gateway.key"; }
  chmod 700 "$stage/data"; find "$stage/data" -type f -exec chmod 600 {} +
  own "$stage/data"; mv "$stage/data" "$dst"; rmdir "$stage"
}

# Se mueve el CONTENIDO, no el directorio: VGW_DATA conserva su identidad y el montaje del contenedor nunca queda
# apuntando a un directorio renombrado (en Docker Desktop un contenedor recreado siguió escribiendo en el anterior).
swap_in() { # swap_in DIR_NUEVO ETIQUETA → el contenido de DATA pasa a DATA.ETIQUETA-ts (imprime su ruta) y el de DIR_NUEVO a DATA
  local aside; aside="$DATA.$2-$(ts)"
  mkdir -p "$DATA" "$aside"; chmod 700 "$DATA" "$aside"
  find "$DATA" -mindepth 1 -maxdepth 1 -exec mv {} "$aside"/ \;
  find "$1" -mindepth 1 -maxdepth 1 -exec mv {} "$DATA"/ \;
  rmdir "$1"; own "$DATA"; printf '%s' "$aside"
}

# ------------------------------------------------------------------ comandos
cmd=${1:-}; [ $# -gt 0 ] && shift
case "$cmd" in
install)
  need_root install
  dist=${1:-}; [ -f "$dist" ] || die "uso: vectoris-gw install DIST.tar.gz --sha256 HEX"
  [ "${2:-}" = --sha256 ] && [ -n "${3:-}" ] || die "falta --sha256 HEX (del manifiesto de la versión publicada)"
  mkdir -p "$VGW_HOME"; chmod 755 "$VGW_HOME"
  v=$(unpack "$dist" "$3"); say "Versión $v en $(rel "$v") (SHA-256 verificado)"
  if [ ! -f "$ENV_FILE" ]; then
    awk -v v="$v" -v root="$VGW_HOME" 'index($0, "GATEWAY_VERSION=") == 1 { print "GATEWAY_VERSION=" v; next }
      index($0, "VGW_ROOT=") == 1 { print "VGW_ROOT=" root; next } { print }' "$(rel "$v")/deploy/.env.example" > "$ENV_FILE"
    chmod 600 "$ENV_FILE"; say "Creado $ENV_FILE: completar ACME_EMAIL y revisar GATEWAY_FINGERPRINT / GATEWAY_DOMAIN."
  else say "$ENV_FILE ya existe: no se modifica (para usar $v: vectoris-gw upgrade $v $3)."; fi
  load_env
  mkdir -p "$(dirname "$DATA")" "$BACKUPS" "$VGW_HOME/bin"; chmod 700 "$BACKUPS"
  if [ ! -e "$DATA" ]; then mkdir -p "$DATA"; chmod 700 "$DATA"; own "$DATA"; fi
  install -m 755 "$(rel "$v")/deploy/vectoris-gw" "$VGW_HOME/bin/vectoris-gw"
  [ "$CHOWN" = yes ] && ln -sf "$VGW_HOME/bin/vectoris-gw" /usr/local/sbin/vectoris-gw
  say "Instalado. Siguiente paso: completar $ENV_FILE y luego 'vectoris-gw restore RESPALDO.tar.gz --yes' (migración) o 'vectoris-gw init'." ;;

fetch)
  need_root fetch; load_env
  v=${1:-}; want=${2:-}; valid_version "$v"; [ -n "$want" ] || die "uso: vectoris-gw fetch VERSIÓN SHA256"
  if have_release "$v"; then [ "$(cat "$(rel "$v")/.dist-sha256")" = "$want" ] || die "la versión $v instalada tiene otro SHA-256"; say "La versión $v ya está (SHA-256 verificado)."; exit 0; fi
  url="$(envget JFROG_REPO_URL)/$GROUP/$v/vectoris-gateway-$v-dist.tar.gz"
  case "$url" in https://*) ;; http://*|file://*) [ "${VGW_ALLOW_INSECURE_REPO:-}" = 1 ] || die "el repositorio debe ser https://" ;; *) die "JFROG_REPO_URL inválida" ;; esac
  auth=(); n=$(envget JFROG_NETRC); [ -n "$n" ] && [ -f "$n" ] && auth=(--netrc-file "$n")
  mkdir -p "$VGW_HOME/downloads"; f="$VGW_HOME/downloads/vectoris-gateway-$v-dist.tar.gz"
  say "Descargando $url"
  curl -fsSL --retry 3 ${auth[@]+"${auth[@]}"} -o "$f.part" "$url" || die "no se pudo descargar $url"
  mv "$f.part" "$f"
  if curl -fsSL ${auth[@]+"${auth[@]}"} -o "$f.sha256" "$url.sha256" 2>/dev/null; then
    [ "$(tr -d ' \n' < "$f.sha256" | cut -c1-64)" = "$want" ] || die "el .sha256 publicado en JFrog no coincide con el esperado"
    say "SHA-256 publicado en JFrog: coincide"
  fi
  v2=$(unpack "$f" "$want"); [ "$v2" = "$v" ] || die "la distribución es de la versión $v2, no $v"
  say "Versión $v lista en $(rel "$v") (no activada; activar con: vectoris-gw upgrade $v $want)" ;;

init)
  need_root init; load_env
  [ -f "$DATA/gateway.db" ] && die "ya hay una base en $DATA: init NO se ejecuta sobre un Gateway existente (usar restore para migrar)"
  [ -n "$FP" ] && die "GATEWAY_FINGERPRINT ya está definido ($FP): para conservar esa identidad usar 'restore' con su respaldo, no init"
  mkdir -p "$DATA"; chmod 700 "$DATA"; own "$DATA"
  FP="" jrun "$V" "$DATA" -it init --data /data --admin "${1:-admin}"
  configure_conf "$DATA"
  fp=$(FP="" jrun "$V" "$DATA" fingerprint --data /data)
  envset GATEWAY_FINGERPRINT "$fp"
  say "Gateway NUEVO con huella $fp (guardada en $ENV_FILE). Iniciar: vectoris-gw start" ;;

configure)
  need_root configure; load_env; [ -d "$DATA" ] || die "no existe $DATA"
  configure_conf "$DATA"; say "gateway.conf ajustado (copia previa gateway.conf.bak-*):"; grep -v '^#' "$DATA/gateway.conf" | sed 's/^/   /'
  running && say "Aplicar: vectoris-gw restart" || true ;;

check)
  load_env; require_fp; jrun "${1:-$V}" "$DATA" check --data /data --expect "$FP" ;;

start)
  need_root start; load_env; preflight "$V" "$DATA" || die "la verificación falló: no se inicia"
  start_version "$V" || die "el Gateway no arrancó bien (vectoris-gw logs)"
  say "Gateway $V en marcha. Consola: ssh -L 3251:127.0.0.1:$(envget CONSOLE_PORT) root@SERVIDOR → http://127.0.0.1:3251" ;;

stop) need_root stop; load_env; dc "$V" stop; say "Detenido (se mantiene detenido aunque el servidor reinicie)." ;;

restart) need_root restart; load_env; preflight "$V" "$DATA" || die "la verificación falló: no se reinicia"; dc "$V" stop gateway; start_version "$V" ;;

status)
  load_env
  say "Versión: $V   (anterior: ${PREV:-ninguna})   huella esperada: ${FP:-SIN DEFINIR}"
  dc "$V" ps -a --format 'table {{.Service}}\t{{.Status}}\t{{.Ports}}' 2>/dev/null || true
  last=$(ls -1t "$BACKUPS"/gateway-*.tar.gz 2>/dev/null | awk 'NR == 1'); say "Último respaldo: ${last:-ninguno}" ;;

logs) load_env; dc "$V" logs --tail "${1:-100}" gateway proxy ;;

fingerprint) load_env; jrun "$V" "$DATA" fingerprint --data /data ${FP:+--expect "$FP"} ;;

releases)
  load_env; for d in "$VGW_HOME"/releases/*/; do [ -d "$d" ] || continue; v=$(basename "$d")
    m=""; [ "$v" = "$V" ] && m=" ← actual"; [ "$v" = "$PREV" ] && m=" ← anterior"; say "$v  sha256 $(cat "$d/.dist-sha256" 2>/dev/null)$m"; done ;;

backup)
  need_root backup; load_env; require_fp
  out=$(backup_to "$V") || die "el respaldo falló"
  say "Respaldo: $out"; say "SHA-256: $(cat "$out.sha256")"; say "Contiene la CLAVE PRIVADA del Gateway: copiarlo fuera del servidor cifrado o por canal seguro." ;;

restore)
  need_root restore; load_env; require_fp
  f=${1:-}; [ "${2:-}" = --yes ] || die "uso: vectoris-gw restore RESPALDO.tar.gz --yes (reemplaza los datos actuales; quedan al lado, no se borran)"
  new="$DATA.incoming-$(ts)"
  step "1/4 Extraer y verificar $f"
  extract_backup "$f" "$new"
  configure_conf "$new"
  if ! preflight "$V" "$new"; then say "Restauración cancelada: los datos actuales no se tocaron (lo extraído queda en $new)."; exit 1; fi
  step "2/4 Detener el Gateway"; dc "$V" stop gateway 2>/dev/null || true
  step "3/4 Reemplazar los datos"; aside=$(swap_in "$new" before-restore); say "Datos anteriores: $aside"
  step "4/4 Iniciar y verificar"
  if start_version "$V"; then say "Restaurado: Gateway $V con la huella $FP."; exit 0; fi
  say "FALLÓ: se vuelve a los datos anteriores"; dc "$V" stop gateway || true
  swap_in "$aside" failed-restore >/dev/null; start_version "$V" || true
  die "la restauración falló; los datos anteriores están de nuevo en $DATA" ;;

upgrade)
  need_root upgrade; load_env; require_fp
  new=${1:-}; want=${2:-}; valid_version "$new"; [ -n "$want" ] || die "uso: vectoris-gw upgrade VERSIÓN SHA256 [--yes]"
  [ "$new" != "$V" ] || die "la versión $new ya está activa"
  have_release "$new" || "$0" fetch "$new" "$want"
  [ "$(cat "$(rel "$new")/.dist-sha256")" = "$want" ] || die "la versión $new instalada tiene otro SHA-256"
  supports_ops "$new" || die "$new no tiene las operaciones de G6 (check/backup): no se administra con vectoris-gw"
  step "1/5 Verificación de la base actual con $new (sólo lectura)"
  preflight "$new" "$DATA" || die "$new no acepta los datos actuales: no se actualiza (nada cambió)"
  [ "${3:-}" = --yes ] || { say "Todo listo. Para actualizar $V → $new (corte de unos segundos): vectoris-gw upgrade $new $want --yes"; exit 0; }
  step "2/5 Detener el Gateway (la base queda quieta para el respaldo)"; dc "$V" stop gateway
  step "3/5 Respaldo previo"
  bk=$(backup_to "$V") || { start_version "$V"; die "el respaldo falló: no se actualiza (el Gateway $V sigue)"; }
  say "Respaldo: $bk"; envset LAST_UPGRADE_BACKUP "$bk"
  step "4/5 Activar $new"
  envset GATEWAY_PREVIOUS_VERSION "$V"; envset GATEWAY_VERSION "$new"
  step "5/5 Iniciar y verificar"
  if start_version "$new"; then say "Actualizado a $new (anterior: $V; respaldo: $bk)."; exit 0; fi
  say "FALLÓ $new: vuelta atrás automática a $V"
  dc "$new" stop gateway || true
  envset GATEWAY_VERSION "$V"; envset GATEWAY_PREVIOUS_VERSION "$PREV"
  restored="$DATA.incoming-$(ts)"
  extract_backup "$bk" "$restored"; failed=$(swap_in "$restored" failed-upgrade)
  start_version "$V" || die "la vuelta atrás tampoco arrancó: revisar 'vectoris-gw logs' (datos de $new en $failed)"
  die "la actualización a $new falló; el Gateway volvió a $V con el respaldo previo (datos de $new en $failed)" ;;

rollback)
  need_root rollback; load_env; require_fp
  [ -n "$PREV" ] || die "no hay versión anterior (GATEWAY_PREVIOUS_VERSION vacío)"
  have_release "$PREV" || die "no está la versión $PREV en $(rel "$PREV")"
  supports_ops "$PREV" || die "$PREV no tiene las operaciones de G6: vuelta atrás manual (docs/g6-gateway-docker.md)"
  step "Vuelta atrás $V → $PREV"
  if jrun "$PREV" "$DATA" check --data /data --expect "$FP" > /dev/null; then
    say "$PREV acepta la base actual: sólo se cambia la versión (sin perder datos)."
    [ "${1:-}" = --yes ] || { say "Confirmar: vectoris-gw rollback --yes"; exit 0; }
    dc "$V" stop gateway; bk=$(backup_to "$V") && say "Respaldo de seguridad: $bk"
    envset GATEWAY_VERSION "$PREV"; envset GATEWAY_PREVIOUS_VERSION "$V"
    start_version "$PREV" || die "$PREV no arrancó (vectoris-gw logs)"; say "Gateway en $PREV."; exit 0
  fi
  bk=$(envget LAST_UPGRADE_BACKUP)
  [ -n "$bk" ] && [ -f "$bk" ] || die "$PREV no acepta la base actual (esquema más nuevo) y no hay respaldo previo a la actualización: usar el procedimiento de vuelta atrás de esquema de la versión (docs/g6-gateway-docker.md)"
  say "$PREV NO acepta la base actual (la actualización cambió el esquema): se restaura el respaldo previo $bk."
  say "SE PIERDEN los cambios hechos desde la actualización (los datos actuales quedan al lado)."
  [ "${1:-}" = --yes ] || { say "Confirmar: vectoris-gw rollback --yes"; exit 0; }
  dc "$V" stop gateway; safety=$(backup_to "$V") && say "Respaldo de los datos actuales: $safety"
  restored="$DATA.incoming-$(ts)"; extract_backup "$bk" "$restored"
  envset GATEWAY_VERSION "$PREV"; envset GATEWAY_PREVIOUS_VERSION "$V"
  aside=$(swap_in "$restored" before-rollback); say "Datos de $V: $aside"
  start_version "$PREV" || die "$PREV no arrancó (vectoris-gw logs)"; say "Gateway en $PREV con el respaldo previo a la actualización." ;;

urls)
  load_env
  say "Huella del Gateway (no cambia con el dominio): ${FP:-SIN DEFINIR}"
  say "Dominio HTTPS (certificado):  $(envget GATEWAY_DOMAIN)"
  a=$(envget GATEWAY_DOMAIN_ALIASES); say "Alias (otros nombres):        ${a:-ninguno}"
  say "Dirección para PCs (código):  $(envget AGENT_GATEWAY_URL)"
  m=$(envget GATEWAY_MIGRATE_URL); say "Migración anunciada:          ${m:-ninguna}"
  say "API HTTP de G5:               $(envget API_HTTP_BIND):$(envget API_HTTP_PORT)"
  [ -f "$DATA/gateway.conf" ] && { say "gateway.conf (en uso tras el último configure/restart):"; grep -E '^(AGENT_GATEWAY_URL|GATEWAY_MIGRATE_URL)=' "$DATA/gateway.conf" | sed 's/^/   /'; }
  true ;;

set-domain)
  need_root set-domain; load_env; require_fp
  dom=${1:-}; [ $# -gt 0 ] && shift
  keep=no; mig=no; yes=no; aurl=""
  while [ $# -gt 0 ]; do case "$1" in --keep-old) keep=yes ;; --migrate) mig=yes ;; --yes) yes=yes ;; --agent-url) aurl=${2:-}; shift ;; *) die "opción desconocida: $1" ;; esac; shift; done
  dom=$(printf '%s' "$dom" | tr 'A-Z' 'a-z'); dom=${dom%.}
  [[ "$dom" =~ ^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]([a-z0-9-]{0,61}[a-z0-9])?$ ]] && [ ${#dom} -le 253 ] \
    || die "uso: vectoris-gw set-domain DOMINIO [--keep-old] [--migrate] [--agent-url URL] [--yes]   (DOMINIO: nombre completo, p. ej. gateway.empresa.com)"
  old=$(envget GATEWAY_DOMAIN); [ "$dom" != "$old" ] || die "el dominio ya es $dom"
  aliases=$(envget GATEWAY_DOMAIN_ALIASES)
  # shellcheck disable=SC2086
  aliases=$( { printf '%s\n' $aliases | grep -vxF "$dom" || true; } | tr '\n' ' '); [ $keep = yes ] && aliases="$aliases $old"
  aliases=$(printf '%s' "$aliases" | xargs)
  cur=$(envget AGENT_GATEWAY_URL)
  if [ -z "$aurl" ]; then   # mismo esquema y puerto; sólo cambia el nombre si era el dominio anterior
    aurl=$(printf '%s' "$cur" | awk -v o="$old" -v n="$dom" '{ p = index($0, "://"); h = substr($0, p + 3); split(h, a, ":"); if (a[1] == o) print substr($0, 1, p + 2) n substr(h, length(a[1]) + 1); else print }')
  fi
  newmig=$(envget GATEWAY_MIGRATE_URL); [ $mig = yes ] && newmig=$aurl
  # un aviso que apuntaba al dominio que se deja no tiene sentido: se quita (salvo --migrate)
  if [ $mig = no ] && [ -n "$newmig" ] && [ "$(printf '%s' "$newmig" | awk '{ p = index($0, "://"); h = substr($0, p + 3); split(h, a, ":"); print a[1] }')" = "$old" ]; then newmig=""; fi
  step "Plan"
  say "  Dominio HTTPS:        $old → $dom (Caddy pide el certificado de $dom: el DNS de $dom tiene que apuntar a este servidor)"
  say "  Alias:                ${aliases:-ninguno}"
  say "  Dirección para PCs:   ${cur:-ninguna} → ${aurl:-ninguna} (códigos de instalación nuevos)"
  say "  Migración anunciada:  ${newmig:-ninguna}$([ $mig = yes ] && echo ' (agentes 0.13.0-g6 y USB Server 0.6.2 se pasan solos cuando allí responda esta huella)')"
  say "  Sin cambios: huella $FP, clave, base, versión $V. Sin recompilar."
  [ $keep = yes ] || say "  AVISO: sin --keep-old, $old deja de tener certificado: las PCs y servidores que lo usan por https dejan de conectar."
  [ $yes = yes ] || { say "SIMULACIÓN: agregar --yes para aplicar."; exit 0; }
  t=$(ts); cp -p "$ENV_FILE" "$ENV_FILE.bak-$t"
  envset GATEWAY_DOMAIN "$dom"; envset GATEWAY_DOMAIN_ALIASES "$aliases"; envset AGENT_GATEWAY_URL "$aurl"; envset GATEWAY_MIGRATE_URL "$newmig"
  undo() { say "FALLÓ: se vuelve a la configuración anterior ($ENV_FILE.bak-$t)"; cp -p "$ENV_FILE.bak-$t" "$ENV_FILE"; configure_conf "$DATA"; dc "$V" stop gateway || true; start_version "$V" || true; }
  configure_conf "$DATA"
  if ! preflight "$V" "$DATA"; then undo; die "la configuración nueva no pasa la verificación: no se cambió nada"; fi
  dc "$V" stop gateway
  if ! start_version "$V"; then undo; die "el Gateway no arrancó con el dominio nuevo: se volvió al anterior ($old)"; fi
  got=$(jrun "$V" "$DATA" fingerprint --data /data --expect "$FP") || { undo; die "la huella cambió (no debería): se volvió al anterior"; }
  say "Dominio $dom activo; huella $got (sin cambios). Copia previa: $ENV_FILE.bak-$t"
  say "Verificar el certificado: curl -sS https://$dom/api/v1/hello -H 'X-Vectoris-Nonce: prueba-1234567890' | head -c 200" ;;

server-address)
  need_root server-address; load_env
  jrun "$V" "$DATA" server-address --data /data "$@" ;;

*) sed -n '2,23p' "$0"; exit 2 ;;
esac
