#!/usr/bin/env bash
# ════════════════════════════════════════════════════════════════════════════
# VexGuard — instalador del agente (Linux y macOS)
#
# Plantilla: la API sustituye los marcadores @@…@@ y la sirve en /install.sh.
# AVISO: mientras la plataforma no publique el binario real del agente (vexguard-agent-<so>-<arq> en
# SHA256SUMS), el agente que se instala es un MARCADOR: se activa con tu licencia, ocupa un seat y mantiene
# el equipo en línea en la consola, pero NO protege el equipo. Cuando el binario esté publicado, este mismo
# instalador instala ese (agente real en Go) sin más cambios.
#
# Uso:
#   curl -fsSL <URL>/install.sh | sudo VEXGUARD_LICENSE_KEY=VXG-xxxx-xxxx-xxxx-xxxx bash
#   curl -fsSL <URL>/install.sh | sudo bash -s -- --uninstall
#
# Variables opcionales:
#   VEXGUARD_DEVICE_CLASS=workstation|server   fuerza el tipo de equipo (por defecto: servidor si la licencia es de
#                                               servidor o el equipo no tiene entorno gráfico; si no, equipo de usuario)
#   VEXGUARD_INSTALL_DIR  (/opt/vexguard)       VEXGUARD_CONF_DIR  (/etc/vexguard)
#   VEXGUARD_NO_SERVICE=1                       no instala ni arranca el servicio
#   VEXGUARD_ALLOW_NON_ROOT=1                   solo para pruebas con directorios propios
#
# Seguridad: la clave de licencia solo se usa para activar. NO se escribe en disco ni en logs;
# en /etc/vexguard queda únicamente el device_token (modo 600). El agente descargado se
# verifica contra /downloads/agent/SHA256SUMS y, si no coincide, se aborta.
# ════════════════════════════════════════════════════════════════════════════
set -euo pipefail
export LC_ALL=C

VEXGUARD_BASE_URL='https://vexguard.cybernetix.es'
# Solo lo rellena el instalador personalizado que se descarga desde la consola; el público va vacío.
EMBEDDED_KEY=''
# Tipo de equipo que le corresponde a esa licencia ('server' si es de servidor). Vacío = se deduce del equipo.
EMBEDDED_CLASS=''

INSTALL_DIR="${VEXGUARD_INSTALL_DIR:-/opt/vexguard}"
CONF_DIR="${VEXGUARD_CONF_DIR:-/etc/vexguard}"
SERVICE_NAME="vexguard-agent"
UNIT_NAME="vexguard-agent.service"
LAUNCHD_LABEL="com.vexguard.agent"
LAUNCHD_DIR="${VEXGUARD_LAUNCHD_DIR:-/Library/LaunchDaemons}"   # (las pruebas lo cambian)
MAC_LOG_DIR="${VEXGUARD_MAC_LOG_DIR:-/Library/Logs/VexGuard}"
LAUNCHD_LABEL_OLD="es.vexguard.agent"   # el del agente de marcador de versiones anteriores
KEY_REGEX='^VXG-[0-9A-HJKMNP-TV-Z]{4}(-[0-9A-HJKMNP-TV-Z]{4}){3}$'

say()  { printf '==> %s\n' "$*"; }
warn() { printf 'Aviso: %s\n' "$*" >&2; }
die()  { printf 'Error: %s\n' "$*" >&2; exit 1; }

WORKDIR=""
cleanup() { if [ -n "$WORKDIR" ] && [ -d "$WORKDIR" ]; then rm -rf "$WORKDIR"; fi; }
trap cleanup EXIT

# Todas las rutas que acaban en un servicio o en un archivo de configuración: solo caracteres seguros.
# (el espacio se admite por «/Library/Application Support/VexGuard» de macOS; todo va siempre entre comillas.)
safe_path() { case "$1" in /*) ;; *) return 1 ;; esac; case "$1" in *[!A-Za-z0-9/._\ -]*) return 1 ;; esac; return 0; }

# En macOS las rutas son las del sistema: binario en /usr/local/vexguard, configuración en /Library/Application Support/VexGuard
# (el estado cuelga de ahí, en state/) y registros en /Library/Logs/VexGuard. VEXGUARD_INSTALL_DIR / VEXGUARD_CONF_DIR mandan.
apply_platform_defaults() {
  if [ "${OS_NAME:-}" = Darwin ]; then
    [ -n "${VEXGUARD_INSTALL_DIR:-}" ] || INSTALL_DIR="/usr/local/vexguard"
    [ -n "${VEXGUARD_CONF_DIR:-}" ] || CONF_DIR="/Library/Application Support/VexGuard"
  fi
  return 0
}

# curl con TLS estricto cuando la web es https.
vcurl() {
  case "$VEXGUARD_BASE_URL" in
    https://*) curl --proto '=https' --tlsv1.2 "$@" ;;
    *) curl "$@" ;;
  esac
}

# >>> compartido con el instalador (se inyecta tal cual en install.sh; necesita die() y OS_NAME)
OS_NAME=$(uname -s 2>/dev/null || echo Linux)

# ───────────────────────── Utilidades ─────────────────────────

# Escapa un texto para meterlo en una cadena JSON: solo ASCII imprimible, sin comillas ni barras sueltas.
json_esc() { printf '%s' "$1" | tr -cd '\040-\176' | sed 's/\\/\\\\/g; s/"/\\"/g'; }

sha256_stdin() {
  if command -v sha256sum >/dev/null 2>&1; then sha256sum | awk '{print $1}'
  elif command -v shasum >/dev/null 2>&1; then shasum -a 256 | awk '{print $1}'
  else openssl dgst -sha256 | awk '{print $NF}'
  fi
}

# ───────────────────────── Huella del equipo ─────────────────────────
# SHA-256 de «vexguard-fp-v1|machine-id|serie de placa|MAC principal» (en minúsculas).
# MISMA lógica que scripts/install/install.sh.tpl: si cambias una, cambia la otra (hay un test que las compara).

fp_machine_id() {
  for f in /etc/machine-id /var/lib/dbus/machine-id; do
    if [ -r "$f" ] && [ -s "$f" ]; then tr -d '[:space:]' < "$f"; return 0; fi
  done
  if [ "$OS_NAME" = Darwin ]; then
    ioreg -rd1 -c IOPlatformExpertDevice 2>/dev/null | awk -F'"' '/IOPlatformUUID/ {print $4; exit}'
    return 0
  fi
  hostname 2>/dev/null || true # último recurso: poco estable
}

fp_board_serial() {
  if [ "$OS_NAME" = Darwin ]; then
    ioreg -rd1 -c IOPlatformExpertDevice 2>/dev/null | awk -F'"' '/IOPlatformSerialNumber/ {print $4; exit}'
  elif [ -r /sys/class/dmi/id/board_serial ]; then
    cat /sys/class/dmi/id/board_serial 2>/dev/null || true
  fi
}

# Descarta los valores de relleno que ponen algunos fabricantes.
fp_clean_serial() {
  s=$(printf '%s' "$1" | tr -d '\r' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
  l=$(printf '%s' "$s" | tr 'A-Z' 'a-z')
  case "$l" in
    ''|none|unknown|n/a|0|'not specified'|'not applicable'|'default string'|'to be filled by o.e.m.'|'system serial number'|'base board serial number'|123456789|1234567890) return 0 ;;
  esac
  printf '%s' "$s"
}

fp_primary_mac() {
  dev=""
  if [ "$OS_NAME" = Darwin ]; then
    dev=$(route -n get default 2>/dev/null | awk '/interface:/ {print $2; exit}')
    if [ -n "$dev" ]; then ifconfig "$dev" 2>/dev/null | awk '/ether/ {print $2; exit}'; fi
    return 0
  fi
  if command -v ip >/dev/null 2>&1; then
    dev=$(ip -o route show default 2>/dev/null | awk '{for (i = 1; i < NF; i++) if ($i == "dev") {print $(i + 1); exit}}')
  fi
  if [ -z "$dev" ] && [ -r /proc/net/route ]; then
    dev=$(awk '$2 == "00000000" {print $1; exit}' /proc/net/route)
  fi
  if [ -n "$dev" ] && [ -r "/sys/class/net/$dev/address" ]; then
    cat "/sys/class/net/$dev/address"
    return 0
  fi
  # Sin ruta por defecto: la primera interfaz física (con dispositivo detrás).
  for p in /sys/class/net/*; do
    n=${p##*/}
    case "$n" in lo|docker*|veth*|br-*|virbr*|tun*|tap*|vnet*|cni*|flannel*|cali*|wg*) continue ;; esac
    [ -e "$p/device" ] || continue
    m=$(cat "$p/address" 2>/dev/null || true)
    case "$m" in ''|00:00:00:00:00:00) continue ;; esac
    printf '%s' "$m"
    return 0
  done
  return 0
}

compute_fingerprint() {
  mid=$(fp_machine_id | tr -d '[:space:]' | tr 'A-Z' 'a-z')
  ser=$(fp_clean_serial "$(fp_board_serial 2>/dev/null || true)" | tr -d '[:space:]' | tr 'A-Z' 'a-z')
  mac=$(fp_primary_mac | tr -d '[:space:]' | tr 'A-Z' 'a-z')
  if [ -z "$mid" ] && [ -z "$mac" ]; then
    die "No se pudo obtener ningún identificador de hardware (machine-id ni MAC) para calcular la huella."
  fi
  printf 'vexguard-fp-v1|%s|%s|%s' "$mid" "$ser" "$mac" | sha256_stdin
}

# ───────────────────────── Datos del equipo ─────────────────────────

detect_os_version() {
  if [ "$OS_NAME" = Darwin ]; then
    printf 'macOS %s' "$(sw_vers -productVersion 2>/dev/null || echo '')"
  elif [ -r /etc/os-release ]; then
    ( . /etc/os-release 2>/dev/null; printf '%s' "${PRETTY_NAME:-Linux}" ) | cut -c1-80
  else
    printf 'Linux %s' "$(uname -r 2>/dev/null)"
  fi
}

detect_arch() {
  case "$(uname -m 2>/dev/null)" in
    x86_64|amd64) echo x64 ;;
    aarch64|arm64) echo arm64 ;;
    *) echo x64 ;;
  esac
}

# Equipo o servidor. VEXGUARD_DEVICE_CLASS lo fuerza (la licencia de servidor solo activa servidores y al revés).
detect_device_class() {
  case "${VEXGUARD_DEVICE_CLASS:-}" in workstation|server) echo "$VEXGUARD_DEVICE_CLASS"; return 0 ;; esac
  if [ "$OS_NAME" = Darwin ]; then echo workstation; return 0; fi
  if command -v systemctl >/dev/null 2>&1 && [ "$(systemctl get-default 2>/dev/null)" = graphical.target ]; then
    echo workstation
  else
    echo server
  fi
}
# <<< compartido

sha256_file() { sha256_stdin < "$1"; }

# Lee un valor de texto / numérico de un JSON en una sola línea (sin jq).
json_str() { sed -n 's/.*"'"$1"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1; }
json_num() { sed -n 's/.*"'"$1"'"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' | head -n 1; }

usage() {
  cat <<'EOF_USAGE'
Instalador del agente de VexGuard (marcador).

  curl -fsSL <URL>/install.sh | sudo VEXGUARD_LICENSE_KEY=VXG-xxxx-xxxx-xxxx-xxxx bash
  ... | sudo bash -s -- --uninstall      desinstala el agente (recuerda liberar el seat en la consola)
EOF_USAGE
}

# ───────────────────────── Comprobaciones previas ─────────────────────────

require_root() {
  if [ "$(id -u)" -ne 0 ] && [ "${VEXGUARD_ALLOW_NON_ROOT:-}" != 1 ]; then
    die "Este instalador debe ejecutarse como root. Prueba: curl -fsSL ${VEXGUARD_BASE_URL}/install.sh | sudo VEXGUARD_LICENSE_KEY=VXG-... bash"
  fi
}

# Mayúsculas, sin espacios, y I/L→1, O→0 (como hace el servidor).
normalize_key() { printf '%s' "$1" | tr -d '[:space:]' | tr 'a-z' 'A-Z' | tr 'ILO' '110'; }

check_license_key() {
  local raw="${VEXGUARD_LICENSE_KEY:-$EMBEDDED_KEY}"
  if [ -z "$raw" ]; then
    die "Falta la clave de licencia. Ejecuta: curl -fsSL ${VEXGUARD_BASE_URL}/install.sh | sudo VEXGUARD_LICENSE_KEY=VXG-xxxx-xxxx-xxxx-xxxx bash"
  fi
  LICENSE_KEY=$(normalize_key "$raw")
  if ! [[ $LICENSE_KEY =~ $KEY_REGEX ]]; then
    die "La clave de licencia no tiene un formato válido (debe ser VXG-xxxx-xxxx-xxxx-xxxx). Cópiala de nuevo desde la consola."
  fi
  # Una licencia de servidor solo activa servidores: el instalador personalizado ya lo sabe.
  if [ -z "${VEXGUARD_DEVICE_CLASS:-}" ] && [ -n "$EMBEDDED_CLASS" ]; then export VEXGUARD_DEVICE_CLASS="$EMBEDDED_CLASS"; fi
}

detect_platform() {
  case "$OS_NAME" in
    Linux) OS_ID=linux ;;
    Darwin) OS_ID=macos ;;
    *) die "Sistema no soportado por este instalador: $OS_NAME. Para Windows usa install.ps1." ;;
  esac
  ARCH=$(detect_arch)
  case "$(uname -m 2>/dev/null)" in
    x86_64|amd64|aarch64|arm64) ;;
    *) die "Arquitectura no soportada: $(uname -m). Se admiten x86_64 y arm64." ;;
  esac
}

PM=""
detect_pm() {
  if command -v apt-get >/dev/null 2>&1; then PM=apt
  elif command -v dnf >/dev/null 2>&1; then PM=dnf
  elif command -v yum >/dev/null 2>&1; then PM=yum
  elif command -v zypper >/dev/null 2>&1; then PM=zypper
  elif command -v apk >/dev/null 2>&1; then PM=apk
  else PM=""
  fi
}

install_packages() {
  case "$PM" in
    apt) DEBIAN_FRONTEND=noninteractive apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "$@" ;;
    dnf) dnf install -y -q "$@" ;;
    yum) yum install -y -q "$@" ;;
    zypper) zypper --non-interactive --quiet install --no-recommends "$@" ;;
    apk) apk add --no-cache -q "$@" ;;
    *) return 1 ;;
  esac
}

# Dependencias mínimas: curl y certificados de CA. En macOS vienen de serie.
ensure_dependencies() {
  [ "$OS_ID" = linux ] || return 0
  local missing=""
  command -v curl >/dev/null 2>&1 || missing="curl"
  if ! { [ -s /etc/ssl/certs/ca-certificates.crt ] || [ -s /etc/pki/tls/certs/ca-bundle.crt ] || [ -s /etc/ssl/ca-bundle.pem ] || [ -s /etc/ssl/cert.pem ]; }; then
    missing="$missing ca-certificates"
  fi
  if [ -n "$missing" ]; then
    detect_pm
    [ -n "$PM" ] || die "Faltan dependencias ($missing) y no se encontró ningún gestor de paquetes (apt, dnf, yum, zypper o apk). Instálalas a mano y repite."
    say "Instalando dependencias:${missing} (con $PM)"
    # shellcheck disable=SC2086
    install_packages $missing || die "No se pudieron instalar las dependencias ($missing)."
  fi
  command -v curl >/dev/null 2>&1 || die "curl no está disponible tras instalar las dependencias."
  if ! command -v sha256sum >/dev/null 2>&1 && ! command -v shasum >/dev/null 2>&1 && ! command -v openssl >/dev/null 2>&1; then
    die "No se encontró sha256sum, shasum ni openssl para verificar el agente."
  fi
}

SERVICE_MANAGER=""
detect_service_manager() {
  if [ "${VEXGUARD_NO_SERVICE:-}" = 1 ]; then SERVICE_MANAGER=none
  elif [ "$OS_ID" = macos ]; then SERVICE_MANAGER=launchd
  elif [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then SERVICE_MANAGER=systemd
  elif command -v rc-update >/dev/null 2>&1 && [ -d /etc/init.d ]; then SERVICE_MANAGER=openrc
  else die "No se encontró systemd (ni OpenRC en Alpine). Instala el servicio a mano o usa VEXGUARD_NO_SERVICE=1 y lanza '${INSTALL_DIR}/vexguard-agent run' tú mismo."
  fi
}

# ───────────────────────── Descarga y verificación ─────────────────────────

# Modo local (instalador de doble clic de macOS): VEXGUARD_LOCAL_AGENT=<binario> y VEXGUARD_LOCAL_SUMS=<SHA256SUMS incluido> saltan la
# descarga, pero el binario SE VERIFICA igualmente contra esa lista (por el nombre del fichero: «.../vexguard-agent-macos-arm64»).
use_local_agent() {
  local src="$VEXGUARD_LOCAL_AGENT" sums="${VEXGUARD_LOCAL_SUMS:-}" name expected actual
  [ -f "$src" ] || die "No existe el agente incluido: $src"
  [ -n "$sums" ] && [ -f "$sums" ] || die "Falta la lista de huellas incluida (VEXGUARD_LOCAL_SUMS): no se instala nada sin verificar."
  name=$(basename "$src")
  expected=$(awk -v n="$name" '{f=$2; sub(/^\*/,"",f); k=split(f,a,"/"); if (a[k]==n) {print $1; exit}}' "$sums" | tr 'A-F' 'a-f')
  [ -n "$expected" ] || die "La lista de huellas incluida no trae ${name}. Abortando: no se instala nada sin verificar."
  actual=$(sha256_file "$src" | tr 'A-F' 'a-f')
  [ "$expected" = "$actual" ] || die "El SHA-256 del agente incluido no coincide (esperado ${expected}, calculado ${actual}): el instalador está dañado o manipulado. No se instala nada."
  say "SHA-256 del agente incluido verificado: ${actual}"
  AGENT_KIND=binary
  AGENT_FILE="$name"
  cp "$src" "$WORKDIR/$AGENT_FILE"
  chmod 0755 "$WORKDIR/$AGENT_FILE"
  AGENT_VERSION=$("$WORKDIR/$AGENT_FILE" version 2>/dev/null | head -n 1 || true)
  AGENT_VERSION=${AGENT_VERSION:-0.0.0}
}

download_agent() {
  if [ -n "${VEXGUARD_LOCAL_AGENT:-}" ]; then use_local_agent; return 0; fi
  local bin_file="vexguard-agent-${OS_ID}-${ARCH}"
  local sh_file="vexguard-agent-${OS_ID}-${ARCH}.sh"
  local sums_url="${VEXGUARD_BASE_URL}/downloads/agent/SHA256SUMS"
  vcurl -fsSL --retry 3 --retry-delay 2 --connect-timeout 15 --max-time 60 -o "$WORKDIR/SHA256SUMS" "$sums_url" \
    || die "No se pudo descargar la lista de huellas desde $sums_url"

  # Si la plataforma ya publica el binario real del agente (Go, estático), se instala ese. Si no, el marcador en sh
  # (el instalador sigue valiendo con los dos: así no hay que cambiarlo cuando llegue el binario).
  AGENT_KIND=marker
  AGENT_FILE="$sh_file"
  if awk -v f="${OS_ID}/${bin_file}" '$2 == f || $2 == "*" f {found = 1} END {exit !found}' "$WORKDIR/SHA256SUMS"; then
    AGENT_KIND=binary
    AGENT_FILE="$bin_file"
  fi
  local url="${VEXGUARD_BASE_URL}/downloads/agent/${OS_ID}/${AGENT_FILE}"
  say "Descargando el agente (${OS_ID}/${ARCH}, $([ "$AGENT_KIND" = binary ] && echo 'binario' || echo 'marcador'))"
  vcurl -fsSL --retry 3 --retry-delay 2 --connect-timeout 15 --max-time 300 -o "$WORKDIR/$AGENT_FILE" "$url" \
    || die "No se pudo descargar el agente desde $url"

  local expected actual
  expected=$(awk -v f="${OS_ID}/${AGENT_FILE}" '$2 == f || $2 == "*" f {print $1; exit}' "$WORKDIR/SHA256SUMS" | tr 'A-F' 'a-f')
  [ -n "$expected" ] || die "SHA256SUMS no incluye ${OS_ID}/${AGENT_FILE}. Abortando: no se instala nada sin verificar."
  actual=$(sha256_file "$WORKDIR/$AGENT_FILE" | tr 'A-F' 'a-f')
  if [ "$expected" != "$actual" ]; then
    die "El SHA-256 del agente no coincide (esperado ${expected}, calculado ${actual}). Descarga corrupta o manipulada: no se instala nada."
  fi
  say "SHA-256 verificado: ${actual}"
  if [ "$AGENT_KIND" = binary ]; then
    # El binario ya está verificado: se le pregunta su versión (no necesita red ni privilegios).
    chmod 0755 "$WORKDIR/$AGENT_FILE"
    AGENT_VERSION=$("$WORKDIR/$AGENT_FILE" version 2>/dev/null | head -n 1 || true)
  else
    AGENT_VERSION=$(sed -n 's/^AGENT_VERSION="\(.*\)"$/\1/p' "$WORKDIR/$AGENT_FILE" | head -n 1)
  fi
  AGENT_VERSION=${AGENT_VERSION:-0.0.0}
}

# ───────────────────────── Activación ─────────────────────────

explain_error() {
  case "$1" in
    invalid_license) echo "La clave de licencia no es válida o no existe. Revísala en la consola (Licencias) y vuelve a intentarlo." ;;
    license_revoked) echo "Esta licencia ha sido revocada. Habla con el administrador de tu cuenta." ;;
    subscription_inactive) echo "La suscripción de esta licencia no está activa (cancelada, caducada o con un pago pendiente). Regulariza el pago y repite la instalación." ;;
    class_mismatch) echo "La licencia no es del tipo de este equipo (equipo de usuario o servidor). Usa una licencia del tipo correcto o fuerza el tipo con VEXGUARD_DEVICE_CLASS=workstation|server." ;;
    no_seats_available) echo "Sin seats disponibles: todos los equipos de la licencia están ocupados. Libera uno desde la consola o amplía el plan." ;;
    device_already_licensed) echo "Este equipo ya ocupa un seat de otra licencia. Libéralo allí antes de activarlo con esta clave." ;;
    reactivation_limit) echo "Este equipo se ha reactivado demasiadas veces con esta licencia. Inténtalo más tarde o contacta con soporte." ;;
    rate_limited) echo "Demasiados intentos seguidos. Espera unos minutos y vuelve a probar." ;;
    invalid_fingerprint) echo "No se pudo calcular una huella válida de este equipo (faltan machine-id o MAC). Contacta con soporte indicando tu sistema operativo." ;;
    validation_error) echo "El servidor ha rechazado los datos de este equipo." ;;
    *) echo "El servidor ha respondido con un error inesperado." ;;
  esac
}

activate_device() {
  local fp host osv body_file code payload ecode emsg
  fp=$(compute_fingerprint) || exit 1
  [ -n "$fp" ] || die "No se pudo calcular la huella del equipo."
  host=$(json_esc "$(hostname 2>/dev/null || echo equipo)")
  [ -n "$host" ] || host="equipo"
  osv=$(json_esc "$(detect_os_version)")
  DEVICE_CLASS=$(detect_device_class)
  body_file="$WORKDIR/activate.json"

  say "Activando el equipo (${DEVICE_CLASS}) con tu licencia"
  # El cuerpo viaja por la entrada estándar: la clave no aparece ni en `ps` ni en ningún fichero.
  payload=$(printf '{"license_key":"%s","fingerprint":"%s","os":"%s","arch":"%s","hostname":"%s","os_version":"%s","device_class":"%s","agent_version":"%s"}' \
    "$LICENSE_KEY" "$fp" "$OS_ID" "$ARCH" "$host" "$osv" "$DEVICE_CLASS" "$AGENT_VERSION")
  code=$(printf '%s' "$payload" | vcurl -sS --max-time 30 -o "$body_file" -w '%{http_code}' -X POST \
    -H 'Content-Type: application/json' -H 'Accept: application/json' --data-binary @- \
    "${VEXGUARD_BASE_URL}/api/agent/activate") || code=000
  payload=""

  if [ "$code" = 000 ]; then
    die "No se pudo contactar con ${VEXGUARD_BASE_URL}. Comprueba la conexión a Internet y el cortafuegos."
  fi
  if [ "$code" != 200 ]; then
    ecode=$(json_str code < "$body_file" 2>/dev/null || true)
    emsg=$(json_str message < "$body_file" 2>/dev/null || true)
    printf 'Error: la activación ha fallado (HTTP %s%s).\n' "$code" "${ecode:+, $ecode}" >&2
    printf '       %s\n' "$(explain_error "$ecode")" >&2
    if [ -n "$emsg" ]; then printf '       Detalle del servidor: %s\n' "$emsg" >&2; fi
    exit 1
  fi

  DEVICE_TOKEN=$(json_str device_token < "$body_file")
  case "$DEVICE_TOKEN" in ''|*[!A-Za-z0-9._-]*) die "El servidor no devolvió un token de dispositivo válido." ;; esac
  SEATS_USED=$(json_num seats_used < "$body_file" || true)
  SEATS_TOTAL=$(json_num seats_total < "$body_file" || true)
  # La clave ya no hace falta: se borra de la memoria del script.
  LICENSE_KEY=""
  EMBEDDED_KEY=""
}

# ───────────────────────── Instalación ─────────────────────────

install_files() {
  umask 022
  mkdir -p "$INSTALL_DIR"
  install -m 0755 "$WORKDIR/$AGENT_FILE" "$INSTALL_DIR/vexguard-agent"
  mkdir -p "$CONF_DIR"
  chmod 700 "$CONF_DIR"
  umask 077
  printf "VEXGUARD_API_URL='%s'\n" "$VEXGUARD_BASE_URL" > "$CONF_DIR/agent.conf"
  # Solo el device_token (nunca la clave de licencia), con modo 600.
  printf '%s' "$DEVICE_TOKEN" > "$CONF_DIR/device_token.tmp"
  chmod 600 "$CONF_DIR/device_token.tmp"
  mv "$CONF_DIR/device_token.tmp" "$CONF_DIR/device_token"
  DEVICE_TOKEN=""
  if [ "$OS_ID" = macos ] && [ "${AGENT_KIND:-marker}" = binary ]; then
    # El agente real de macOS guarda su estado en <conf>/state y su registro en /Library/Logs/VexGuard (los crea él; aquí solo el registro de launchd).
    mkdir -p "$MAC_LOG_DIR"
    chmod 755 "$MAC_LOG_DIR"
    # Un binario bajado con curl no lleva la marca de cuarentena de Gatekeeper; por si acaso se quita (sin error si no la tiene).
    xattr -d com.apple.quarantine "$INSTALL_DIR/vexguard-agent" >/dev/null 2>&1 || true
    # Actualización limpia: el marcador de versiones anteriores vivía en /opt/vexguard y /etc/vexguard (su token ya no vale: se acaba de activar de nuevo).
    if [ -z "${VEXGUARD_INSTALL_DIR:-}" ] && [ -z "${VEXGUARD_CONF_DIR:-}" ] && [ -f /opt/vexguard/vexguard-agent ]; then
      rm -rf /opt/vexguard /etc/vexguard
    fi
  fi
}


# ───────────────────────── ClamAV opcional (Linux) ─────────────────────────
# VEXGUARD_INSTALL_CLAMAV=1 instala ClamAV (proceso APARTE: clamd + freshclam, con su actualización diaria de firmas) y activa la capa de
# ClamAV del agente: millones de firmas de malware conocido que se SUMAN a las capas propias. Cuesta memoria (clamd carga las firmas:
# ~1-1,5 GB de RAM) y la primera descarga de firmas tarda unos minutos; el agente usa ClamAV en cuanto responde. Solo Linux; el resto de
# sistemas no lo necesita para el motor propio.
install_clamav() {
  [ "${VEXGUARD_INSTALL_CLAMAV:-}" = 1 ] || return 0
  [ "$OS_ID" = linux ] && [ "${AGENT_KIND:-marker}" = binary ] || { warn "ClamAV opcional: solo se instala en Linux con el agente real."; return 0; }
  detect_pm
  local pkgs svc_d svc_f sock
  case "$PM" in
    apt) pkgs="clamav clamav-daemon"; svc_d=clamav-daemon; svc_f=clamav-freshclam; sock=/run/clamav/clamd.ctl ;;
    dnf|yum) pkgs="clamav clamd clamav-update"; svc_d="clamd@scan"; svc_f=clamav-freshclam; sock=/run/clamd.scan/clamd.sock ;;
    zypper) pkgs="clamav"; svc_d=clamd; svc_f=freshclam; sock=/run/clamav/clamd.ctl ;;
    apk) pkgs="clamav clamav-daemon freshclam"; svc_d=clamd; svc_f=freshclam; sock=/run/clamav/clamd.sock ;;
    *) warn "ClamAV opcional: no hay gestor de paquetes conocido; instálalo a mano y añade VEXGUARD_CLAMAV=1 a ${CONF_DIR}/agent.conf."; return 0 ;;
  esac
  say "Instalando ClamAV (capa opcional de firmas) con $PM"
  # shellcheck disable=SC2086
  if ! install_packages $pkgs; then warn "No se pudo instalar ClamAV: el agente sigue con su motor propio."; return 0; fi
  if [ "$PM" = dnf ] || [ "$PM" = yum ]; then
    # En RHEL/Fedora clamd@scan necesita su configuración de ejemplo activada.
    [ -f /etc/clamd.d/scan.conf ] && sed -i 's/^Example/#Example/; s/^#LocalSocket /LocalSocket /' /etc/clamd.d/scan.conf 2>/dev/null || true
  fi
  if [ "$SERVICE_MANAGER" = systemd ]; then
    systemctl enable --now "$svc_f" >/dev/null 2>&1 || warn "No se pudo arrancar $svc_f (actualización de firmas de ClamAV)."
    systemctl enable --now "$svc_d" >/dev/null 2>&1 || warn "ClamAV aún no arranca (normal hasta que se descarguen las firmas): se reintentará solo."
  fi
  # La capa de ClamAV del agente: solo se usa si clamd responde; mientras tanto, motor propio.
  printf "VEXGUARD_CLAMAV=1\nVEXGUARD_CLAMD_SOCKET='%s'\n" "$sock" >> "$CONF_DIR/agent.conf"
  say "ClamAV instalado: las firmas se descargan en segundo plano (unos minutos). Comprueba: ${INSTALL_DIR}/vexguard-agent status"
}

install_service_systemd() {
  if [ "${AGENT_KIND:-marker}" = binary ]; then
    # Agente real: ve /tmp y /home de verdad (nada de PrivateTmp/ProtectHome), edita cron y unidades al limpiar, mantiene su estado
    # en /var/lib/vexguard y su registro en /var/log/vexguard. Se ejecuta como root: el tiempo real (fanotify) lo exige.
    cat > "/etc/systemd/system/${UNIT_NAME}" <<EOF_UNIT_BIN
[Unit]
Description=VexGuard Agent
Documentation=${VEXGUARD_BASE_URL}
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=${INSTALL_DIR}/vexguard-agent run
Environment=VEXGUARD_CONF_DIR=${CONF_DIR}
Restart=on-failure
RestartSec=15
RestartPreventExitStatus=2 3
LimitNOFILE=65536
OOMScoreAdjust=-500
Nice=5

[Install]
WantedBy=multi-user.target
EOF_UNIT_BIN
    systemctl daemon-reload
    systemctl enable "$UNIT_NAME" >/dev/null 2>&1
    systemctl restart "$UNIT_NAME"
    return 0
  fi
  cat > "/etc/systemd/system/${UNIT_NAME}" <<EOF_UNIT
[Unit]
Description=VexGuard Agent (marcador)
Documentation=${VEXGUARD_BASE_URL}
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=${INSTALL_DIR}/vexguard-agent run
Environment=VEXGUARD_CONF_DIR=${CONF_DIR}
Restart=on-failure
RestartSec=15
RestartPreventExitStatus=2 3
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=full
ReadWritePaths=${CONF_DIR}

[Install]
WantedBy=multi-user.target
EOF_UNIT
  systemctl daemon-reload
  systemctl enable "$UNIT_NAME" >/dev/null 2>&1
  systemctl restart "$UNIT_NAME"
}

install_service_openrc() {
  cat > "/etc/init.d/${SERVICE_NAME}" <<EOF_RC
#!/sbin/openrc-run
description="VexGuard Agent (marcador)"
command="${INSTALL_DIR}/vexguard-agent"
command_args="run"
command_background=true
pidfile="/run/${SERVICE_NAME}.pid"
output_log="/var/log/${SERVICE_NAME}.log"
error_log="/var/log/${SERVICE_NAME}.log"
export VEXGUARD_CONF_DIR="${CONF_DIR}"
export VEXGUARD_EXIT_ZERO=1
depend() { need net; }
EOF_RC
  chmod 0755 "/etc/init.d/${SERVICE_NAME}"
  rc-update add "$SERVICE_NAME" default >/dev/null 2>&1 || true
  rc-service "$SERVICE_NAME" restart
}

install_service_launchd() {
  local plist="${LAUNCHD_DIR}/${LAUNCHD_LABEL}.plist"
  # Un marcador de versiones anteriores usaba otra etiqueta: se descarga para que no corran dos agentes.
  launchctl bootout "system/${LAUNCHD_LABEL_OLD}" >/dev/null 2>&1 || true
  rm -f "${LAUNCHD_DIR}/${LAUNCHD_LABEL_OLD}.plist"
  if [ "${AGENT_KIND:-marker}" = binary ]; then
    # Agente real: LaunchDaemon como root. VEXGUARD_EXIT_ZERO=1 hace que las salidas 2 (licencia revocada) y 3 (reactivar) acaben con
    # código 0: launchd no tiene el RestartPreventExitStatus de systemd y, con KeepAlive/SuccessfulExit=false, reiniciaría sin parar.
    cat > "$plist" <<EOF_PLIST_BIN
<?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>${LAUNCHD_LABEL}</string>
  <key>ProgramArguments</key><array><string>${INSTALL_DIR}/vexguard-agent</string><string>run</string></array>
  <key>EnvironmentVariables</key><dict>
    <key>VEXGUARD_CONF_DIR</key><string>${CONF_DIR}</string>
    <key>VEXGUARD_EXIT_ZERO</key><string>1</string>
  </dict>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
  <key>ThrottleInterval</key><integer>15</integer>
  <key>ProcessType</key><string>Background</string>
  <key>StandardOutPath</key><string>${MAC_LOG_DIR}/launchd.log</string>
  <key>StandardErrorPath</key><string>${MAC_LOG_DIR}/launchd.log</string>
</dict>
</plist>
EOF_PLIST_BIN
    chown root:wheel "$plist" 2>/dev/null || true
    chmod 0644 "$plist"
    launchctl bootout "system/${LAUNCHD_LABEL}" >/dev/null 2>&1 || true
    launchctl bootstrap system "$plist" || warn "launchctl bootstrap ha fallado: revisa ${MAC_LOG_DIR}/launchd.log o reinicia el equipo."
    launchctl kickstart -k "system/${LAUNCHD_LABEL}" >/dev/null 2>&1 || true
    return 0
  fi
  cat > "$plist" <<EOF_PLIST
<?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>${LAUNCHD_LABEL}</string>
  <key>ProgramArguments</key><array><string>${INSTALL_DIR}/vexguard-agent</string><string>run</string></array>
  <key>EnvironmentVariables</key><dict>
    <key>VEXGUARD_CONF_DIR</key><string>${CONF_DIR}</string>
    <key>VEXGUARD_EXIT_ZERO</key><string>1</string>
  </dict>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
  <key>StandardOutPath</key><string>/var/log/vexguard-agent.log</string>
  <key>StandardErrorPath</key><string>/var/log/vexguard-agent.log</string>
</dict>
</plist>
EOF_PLIST
  chmod 0644 "$plist"
  launchctl bootout "system/${LAUNCHD_LABEL}" >/dev/null 2>&1 || true
  launchctl bootstrap system "$plist"
  launchctl kickstart -k "system/${LAUNCHD_LABEL}" >/dev/null 2>&1 || true
}

install_service() {
  case "$SERVICE_MANAGER" in
    systemd) say "Instalando el servicio systemd (${UNIT_NAME})"; install_service_systemd ;;
    openrc) say "Instalando el servicio OpenRC (${SERVICE_NAME})"; install_service_openrc ;;
    launchd) say "Instalando el servicio launchd (${LAUNCHD_LABEL})"; install_service_launchd ;;
    none) warn "Servicio no instalado (VEXGUARD_NO_SERVICE=1). Lanza: VEXGUARD_CONF_DIR=${CONF_DIR} ${INSTALL_DIR}/vexguard-agent run" ;;
  esac
}

uninstall() {
  require_root
  apply_platform_defaults
  OS_ID=linux; [ "$OS_NAME" = Darwin ] && OS_ID=macos
  for p in "$INSTALL_DIR" "$CONF_DIR"; do safe_path "$p" || die "Ruta no válida: $p"; done
  say "Desinstalando el agente de VexGuard"
  if [ "${VEXGUARD_NO_SERVICE:-}" = 1 ]; then
    : # instalado sin servicio: no hay nada que desmontar
  elif [ "$OS_ID" = macos ]; then
    launchctl bootout "system/${LAUNCHD_LABEL}" >/dev/null 2>&1 || true
    launchctl bootout "system/${LAUNCHD_LABEL_OLD}" >/dev/null 2>&1 || true
    rm -f "${LAUNCHD_DIR}/${LAUNCHD_LABEL}.plist" "${LAUNCHD_DIR}/${LAUNCHD_LABEL_OLD}.plist"
  elif [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then
    systemctl disable --now "$UNIT_NAME" >/dev/null 2>&1 || true
    rm -f "/etc/systemd/system/${UNIT_NAME}"
    systemctl daemon-reload >/dev/null 2>&1 || true
  elif command -v rc-service >/dev/null 2>&1; then
    rc-service "$SERVICE_NAME" stop >/dev/null 2>&1 || true
    rc-update del "$SERVICE_NAME" default >/dev/null 2>&1 || true
    rm -f "/etc/init.d/${SERVICE_NAME}"
  fi
  if [ "$OS_ID" = macos ] && [ -d "$CONF_DIR/state" ]; then
    # En macOS el estado (cuarentena cifrada, clave, historial) cuelga de la carpeta de configuración: se conserva; se quita solo lo de la activación.
    rm -rf "$INSTALL_DIR"
    rm -f "$CONF_DIR/agent.conf" "$CONF_DIR/device_token"
    say "Se ha conservado el estado local en ${CONF_DIR}/state y los registros en /Library/Logs/VexGuard (bórralos a mano si no los quieres)."
  else
    rm -rf "$INSTALL_DIR" "$CONF_DIR"
  fi
  say "Agente desinstalado. El seat sigue ocupado hasta que lo liberes en la consola (Licencias → Equipos)."
}

# ───────────────────────── Programa principal ─────────────────────────

main() {
  case "${1:-}" in
    -h|--help|--ayuda) usage; return 0 ;;
    --uninstall|--desinstalar) uninstall; return 0 ;;
    "") ;;
    *) usage; die "Opción desconocida: $1" ;;
  esac

  apply_platform_defaults
  for p in "$INSTALL_DIR" "$CONF_DIR"; do safe_path "$p" || die "Ruta no válida: $p (solo letras, números, espacios y / . _ -)"; done
  require_root
  check_license_key
  detect_platform
  detect_service_manager
  ensure_dependencies

  WORKDIR=$(mktemp -d "${TMPDIR:-/tmp}/vexguard-install.XXXXXX")
  download_agent
  activate_device
  install_files
  install_clamav
  install_service

  if [ "${AGENT_KIND:-marker}" = binary ]; then
    say "Listo. Equipo activado${SEATS_TOTAL:+ (seats usados: ${SEATS_USED:-?} de ${SEATS_TOTAL})}. Agente VexGuard ${AGENT_VERSION} instalado en ${INSTALL_DIR}."
    say "Examinar ahora: ${INSTALL_DIR}/vexguard-agent scan --mode quick   ·   estado: ${INSTALL_DIR}/vexguard-agent status"
    if [ "$OS_ID" = macos ]; then
      warn "macOS: el agente NO está firmado ni notarizado por Apple. Para que pueda examinar ~/Library, Documentos, Escritorio y Descargas concédele «Acceso total al disco»: Ajustes del Sistema › Privacidad y seguridad › Acceso total al disco › + › ${INSTALL_DIR}/vexguard-agent. En macOS solo detecta tras escribir (no bloquea la ejecución); el estado lo detalla: ${INSTALL_DIR}/vexguard-agent status"
    fi
  else
    say "Listo. Equipo activado${SEATS_TOTAL:+ (seats usados: ${SEATS_USED:-?} de ${SEATS_TOTAL})}. Agente de marcador ${AGENT_VERSION} instalado en ${INSTALL_DIR}."
    say "Recuerda: esta versión es un marcador; todavía no protege el equipo."
  fi
}

# VEXGUARD_SOURCE_ONLY=1 permite cargar el script para probar sus funciones sin ejecutarlo.
if [ "${VEXGUARD_SOURCE_ONLY:-}" != 1 ]; then
  main "$@"
fi
