#!/bin/bash
# Installeur du nœud APIA pour Mac (bêta).
#   curl -fsSL https://apianyone.com/downloads/install-mac.sh | bash
#
# Ce que fait le script :
#   1. vérifie macOS et la présence de Node.js (>= 20.9) ; sinon installe Node.js 22 (Homebrew s'il est là,
#      sinon le paquet officiel nodejs.org via sudo installer) ;
#   2. installe le nœud dans ~/Library/Application Support/APIA (archive ${base}/downloads/apia-node-mac.tar.gz,
#      puis npm install --omit=dev) ;
#   3. crée ~/APIA/recu et ~/APIA/envoyer ;
#   4. vous connecte : ouvre ${base}/auth/app/google?platform=mac, vous vous connectez avec Google, la page
#      affiche un code XXXX-XXXX (10 min) que vous collez ici ; le script l'échange contre un jeton, déclare ce
#      Mac comme appareil et écrit la configuration ;
#   5. installe et charge le LaunchAgent be.themaul.apia.node (démarre à l'ouverture de session, relancé s'il tombe).
# Relancer le script met à jour le nœud sans casser la connexion existante (idempotent).
#
# Variables d'environnement (facultatives) :
#   APIA_BASE=https://...      serveur APIA (défaut : https://apianyone.com)
#   APIA_CODE=XXXX-XXXX        code de connexion, pour ne pas le saisir au clavier
#   APIA_RELOGIN=1             force une nouvelle connexion même si la configuration actuelle est valide
#   APIA_NODE_INSTALL=pkg|brew méthode d'installation de Node.js si absent (défaut : brew s'il est présent, sinon pkg)
#   APIA_NODE_BIN=/chemin/node binaire Node.js à utiliser
#   APIA_NODE_TGZ=/chemin.tgz  archive locale du nœud au lieu du téléchargement (tests)
#   APIA_FAKE_OS=darwin        considère le système comme macOS (tests sous Linux : sans launchd, le nœud est
#                              lancé en arrière-plan avec nohup, pid dans node.pid)
# Compatible bash 3.2 (celui de macOS).

set -euo pipefail

LABEL="be.themaul.apia.node"
BASE="${APIA_BASE:-https://apianyone.com}"
BASE="${BASE%/}"
SUPPORT="$HOME/Library/Application Support/APIA"
LOGS="$SUPPORT/logs"
CFG="$SUPPORT/apia-node.json"
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
APIA_DIR="$HOME/APIA"
NODE_BIN=""
NPM_BIN=""
HTTP_CODE="000"
APP_TOKEN=""; NODE_TOKEN=""; NODE_SECRET=""; DEVICE_ID=""; DEVICE_NAME=""; ACCOUNT=""
MYUID="$(id -u)"
REAL_MAC=0
TMP=""

# ---------- affichage ----------
say()  { printf '\033[1m▸ %s\033[0m\n' "$*"; }
ok()   { printf '  ✓ %s\n' "$*"; }
warn() { printf '  ! %s\n' "$*" >&2; }
die()  { printf '\n\033[31mErreur : %s\033[0m\n' "$*" >&2; exit 1; }

cleanup() { [ -n "$TMP" ] && rm -rf "$TMP"; }

# ---------- détection ----------
check_os() {
  local os
  os="$(uname -s)"
  [ "$os" = "Darwin" ] && REAL_MAC=1
  if [ -n "${APIA_FAKE_OS:-}" ]; then os="$APIA_FAKE_OS"; fi
  case "$os" in
    Darwin|darwin) ;;
    *) die "Ce script est prévu pour macOS (système détecté : $os). Pour Windows : ${BASE}/downloads/" ;;
  esac
  if [ "$MYUID" = "0" ] && [ -z "${APIA_FAKE_OS:-}" ]; then
    die "Ne lancez pas ce script avec sudo : le nœud s'installe dans votre session utilisateur."
  fi
}

has_launchd() { [ "$REAL_MAC" = 1 ] && command -v launchctl >/dev/null 2>&1; }

# Lit une réponse au clavier même quand le script arrive par un tube (curl | bash) : on passe par /dev/tty.
ask() { # ask "question" -> REPLY (vide si impossible)
  REPLY=""
  if { : </dev/tty; } 2>/dev/null; then
    printf '%s' "$1" >/dev/tty
    IFS= read -r REPLY </dev/tty || true
  elif [ ! -t 0 ]; then
    IFS= read -r REPLY || true
  fi
}

# ---------- HTTP (curl) ----------
# api METHODE CHEMIN [fichier-json] [jeton] : corps dans $TMP/resp, code HTTP dans HTTP_CODE
api() {
  local m="$1" p="$2" bodyf="${3:-}" tok="${4:-}"
  set -- -sS --max-time 60 -X "$m" "$BASE$p" -H 'accept: application/json' -o "$TMP/resp" -w '%{http_code}'
  if [ -n "$bodyf" ]; then set -- "$@" -H 'content-type: application/json' --data-binary "@$bodyf"; fi
  if [ -n "$tok" ]; then
    printf 'Authorization: Bearer %s\n' "$tok" >"$TMP/hdr"   # jamais le jeton sur la ligne de commande
    set -- "$@" -H "@$TMP/hdr"
  fi
  : >"$TMP/resp"
  HTTP_CODE="$(curl "$@" 2>"$TMP/curlerr")" || HTTP_CODE="000"
}
curl_err() { if [ -s "$TMP/curlerr" ]; then cat "$TMP/curlerr"; else cat "$TMP/resp"; fi; }

# ---------- JSON (via node, toujours présent à ce stade) ----------
jget() { # jget FICHIER CLE [CLE2…] : première valeur trouvée, vide sinon
  "$NODE_BIN" -e '
    const fs = require("fs"); let d;
    try { d = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); } catch (e) { process.exit(0); }
    for (const k of process.argv.slice(2)) { const v = d && d[k]; if (v != null) { process.stdout.write(typeof v === "object" ? JSON.stringify(v) : String(v)); break; } }
  ' -- "$@"
}
jbody() { # jbody FICHIER clé=valeur… : écrit un objet JSON (valeurs chaînes)
  local out="$1"; shift
  "$NODE_BIN" -e '
    const o = {}; for (const a of process.argv.slice(2)) { const i = a.indexOf("="); o[a.slice(0, i)] = a.slice(i + 1); }
    require("fs").writeFileSync(process.argv[1], JSON.stringify(o));
  ' -- "$out" "$@"
}
device_alive() { # device_alive FICHIER ID : 0 si l'appareil est listé et non révoqué
  "$NODE_BIN" -e '
    let d = []; try { d = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")); } catch (e) {}
    process.exit(Array.isArray(d) && d.some(x => x.id === process.argv[2] && !x.revoked_at) ? 0 : 1);
  ' -- "$1" "$2"
}

# ---------- Node.js ----------
node_ok() { # node_ok BINAIRE : version >= 20.9
  [ -n "$1" ] && [ -x "$1" ] || return 1
  local v
  v="$("$1" -p 'const [a,b]=process.versions.node.split(".").map(Number); (a>20||(a===20&&b>=9))?"ok":"old"' 2>/dev/null)" || return 1
  [ "$v" = "ok" ]
}

ensure_node() {
  export PATH="$PATH:/usr/local/bin:/opt/homebrew/bin"
  local cand
  for cand in "${APIA_NODE_BIN:-}" "$(command -v node 2>/dev/null || true)" /opt/homebrew/opt/node@22/bin/node /usr/local/opt/node@22/bin/node /usr/local/bin/node /opt/homebrew/bin/node; do
    if node_ok "$cand"; then NODE_BIN="$cand"; break; fi
  done
  if [ -z "$NODE_BIN" ]; then
    if command -v node >/dev/null 2>&1; then
      warn "Node.js $(node -v 2>/dev/null) est trop ancien (il faut 20.9 ou plus) : installation de Node.js 22."
    else
      say "Node.js n'est pas installé : installation de Node.js 22."
    fi
    install_node
  fi
  local node_dir
  node_dir="$(dirname "$NODE_BIN")"
  export PATH="$node_dir:$PATH"
  NPM_BIN="$node_dir/npm"
  [ -x "$NPM_BIN" ] || NPM_BIN="$(command -v npm 2>/dev/null || true)"
  [ -n "$NPM_BIN" ] || die "npm introuvable à côté de $NODE_BIN"
  ok "Node.js $("$NODE_BIN" -v) ($NODE_BIN)"
}

install_node() {
  [ "$REAL_MAC" = 1 ] || die "Node.js >= 20.9 est requis (hors macOS, installez-le avant de lancer le script)."
  local mode="${APIA_NODE_INSTALL:-}"
  if [ -z "$mode" ]; then
    if command -v brew >/dev/null 2>&1; then mode="brew"; else mode="pkg"; fi
  fi
  if [ "$mode" = "brew" ]; then
    say "Installation avec Homebrew : brew install node@22 (une à deux minutes)"
    if HOMEBREW_NO_AUTO_UPDATE=1 brew install node@22 </dev/null; then
      local p
      p="$(brew --prefix node@22 2>/dev/null || true)"
      if node_ok "$p/bin/node"; then NODE_BIN="$p/bin/node"; return 0; fi
    fi
    warn "Homebrew n'a pas abouti, passage au paquet officiel nodejs.org."
  fi
  install_node_pkg
}

sha256_file() { # empreinte hexadécimale d'un fichier (shasum, sinon openssl, sinon sha256sum)
  if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d' ' -f1
  elif command -v openssl >/dev/null 2>&1; then openssl dgst -sha256 "$1" | sed 's/^.*= *//'
  else sha256sum "$1" | cut -d' ' -f1; fi
}

install_node_pkg() {
  local pkg
  say "Téléchargement de Node.js 22 (nodejs.org, paquet officiel universel)"
  curl -fsSL --max-time 60 "https://nodejs.org/dist/latest-v22.x/SHASUMS256.txt" -o "$TMP/sums" || die "Impossible de joindre nodejs.org : $(curl_err)"
  pkg="$(grep -o 'node-v22\.[0-9]*\.[0-9]*\.pkg' "$TMP/sums" | head -1)"
  [ -n "$pkg" ] || die "Paquet Node.js 22 introuvable dans la liste de nodejs.org"
  curl -fL --progress-bar --max-time 900 "https://nodejs.org/dist/latest-v22.x/$pkg" -o "$TMP/$pkg" || die "Téléchargement de $pkg interrompu"
  local want got
  want="$({ grep -F -- "  $pkg" "$TMP/sums" || true; } | head -1 | cut -d' ' -f1)"
  [ -n "$want" ] || die "Empreinte de $pkg absente"
  got="$(sha256_file "$TMP/$pkg")"
  [ "$got" = "$want" ] || die "Empreinte SHA-256 de $pkg incorrecte : téléchargement corrompu"
  ok "$pkg téléchargé et vérifié (SHA-256)"
  say "Installation du paquet (macOS demande votre mot de passe administrateur)"
  sudo installer -pkg "$TMP/$pkg" -target / || die "L'installation de Node.js a échoué. Installez-le depuis https://nodejs.org puis relancez ce script."
  hash -r 2>/dev/null || true
  NODE_BIN="/usr/local/bin/node"
  node_ok "$NODE_BIN" || die "Node.js installé mais introuvable dans /usr/local/bin ; ouvrez un nouveau Terminal et relancez."
}

# ---------- nœud ----------
fetch_bundle() {
  if [ -n "${APIA_NODE_TGZ:-}" ]; then
    cp "$APIA_NODE_TGZ" "$TMP/node.tgz"
    ok "Archive locale : $APIA_NODE_TGZ"
  else
    say "Téléchargement du nœud : $BASE/downloads/apia-node-mac.tar.gz"
    curl -fsSL --max-time 300 "$BASE/downloads/apia-node-mac.tar.gz" -o "$TMP/node.tgz" || die "Téléchargement impossible : $(curl_err)"
  fi
  tar -tzf "$TMP/node.tgz" >/dev/null 2>&1 || die "Archive du nœud invalide"
}

# Attend (5 s max) que launchd ait bien retiré le service, sinon « bootstrap » peut répondre « already in progress »
launchd_wait_gone() { local i=0; while launchctl print "gui/$MYUID/$LABEL" >/dev/null 2>&1 && [ "$i" -lt 10 ]; do sleep 0.5; i=$((i + 1)); done; }

stop_agent() {
  if has_launchd; then
    if [ -f "$PLIST" ]; then launchctl bootout "gui/$MYUID" "$PLIST" >/dev/null 2>&1 || true; fi
    launchctl bootout "gui/$MYUID/$LABEL" >/dev/null 2>&1 || true
    launchd_wait_gone
  else
    if [ -f "$SUPPORT/node.pid" ]; then kill "$(cat "$SUPPORT/node.pid")" >/dev/null 2>&1 || true; rm -f "$SUPPORT/node.pid"; fi
    pkill -f "$SUPPORT/apia-node.js" >/dev/null 2>&1 || true
  fi
}

install_bundle() {
  mkdir -p "$SUPPORT" "$LOGS"
  chmod 700 "$SUPPORT"
  tar -xzf "$TMP/node.tgz" -C "$SUPPORT"
  say "Installation des dépendances du nœud (npm, une minute la première fois)"
  (cd "$SUPPORT" && "$NPM_BIN" install --omit=dev --no-fund --no-audit --loglevel=error </dev/null) || die "npm install a échoué dans $SUPPORT"
  "$NODE_BIN" --check "$SUPPORT/apia-node.js" || die "apia-node.js n'est pas valide pour $("$NODE_BIN" -v)"
  ok "Nœud installé dans $SUPPORT (version $(cat "$SUPPORT/VERSION" 2>/dev/null || echo '?'))"
}

make_folders() {
  mkdir -p "$APIA_DIR/recu" "$APIA_DIR/envoyer"
  if [ ! -f "$APIA_DIR/LISEZMOI.txt" ]; then
    cat >"$APIA_DIR/LISEZMOI.txt" <<EOF
Dossier APIA

recu/      : ce que vos contacts vous envoient ou vous accordent arrive ici (un sous-dossier par livraison).
envoyer/   : déposez un fichier dans envoyer/@pseudo/ (le pseudo APIA d'un contact) : il part tout seul,
             puis est rangé dans envoyer/@pseudo/envoyés/.

Le nœud tourne en arrière-plan (LaunchAgent $LABEL).
Journal : ~/Library/Application Support/APIA/logs/node.log
Désinstaller : bash ~/Library/Application\ Support/APIA/uninstall.sh (ce dossier est conservé).
EOF
  fi
  ok "Dossier $APIA_DIR (recu, envoyer)"
}

# ---------- connexion ----------
device_name() {
  local n=""
  if command -v scutil >/dev/null 2>&1; then n="$(scutil --get ComputerName 2>/dev/null || true)"; fi
  [ -n "$n" ] || n="$(hostname -s 2>/dev/null || hostname 2>/dev/null || echo Mac)"
  printf '%s' "$n"   # le serveur limite lui-même à 60 caractères (cut -c couperait un accent en deux)
}
short_host() { hostname -s 2>/dev/null || hostname 2>/dev/null || echo mac; }

login() {
  local url="$BASE/auth/app/google?platform=mac" code tries=0
  say "Connexion à APIA avec votre compte Google"
  printf "\n  Ouvrez cette page (elle s'ouvre normalement toute seule), connectez-vous, puis recopiez le code affiché :\n\n    %s\n\n" "$url"
  if [ "$REAL_MAC" = 1 ] && command -v open >/dev/null 2>&1; then open "$url" >/dev/null 2>&1 || true; fi
  while :; do
    tries=$((tries + 1))
    if [ -n "${APIA_CODE:-}" ]; then
      code="$APIA_CODE"; APIA_CODE=""
    else
      ask "  Code affiché (XXXX-XXXX) : "
      code="$REPLY"
    fi
    code="$(printf '%s' "$code" | tr -d '[:space:]')"
    [ -n "$code" ] || die "Aucun code saisi. Relancez le script dans le Terminal, ou avec APIA_CODE=XXXX-XXXX."
    jbody "$TMP/body" "code=$code" "device=mac-$(short_host)"
    api POST /v1/app/auth/exchange "$TMP/body"
    case "$HTTP_CODE" in
      200) APP_TOKEN="$(jget "$TMP/resp" token)"; [ -n "$APP_TOKEN" ] && break; die "Réponse inattendue du serveur : $(cat "$TMP/resp")" ;;
      400) if [ "$tries" -lt 3 ]; then warn "Code invalide ou expiré (il vaut 10 minutes). Réessayez."; continue; fi
           die "Code refusé trois fois. Rouvrez $url pour obtenir un nouveau code." ;;
      000) die "Serveur injoignable ($BASE) : $(curl_err)" ;;
      *)   die "Connexion refusée (HTTP $HTTP_CODE) : $(cat "$TMP/resp")" ;;
    esac
  done
  api GET /v1/app/me "" "$APP_TOKEN"
  ACCOUNT="$(jget "$TMP/resp" email)"
  ok "Connecté : ${ACCOUNT:-compte APIA}"
}

create_device() {
  DEVICE_NAME="$(device_name)"
  jbody "$TMP/body" "name=$DEVICE_NAME" "kind=pc"
  api POST /v1/app/devices "$TMP/body" "$APP_TOKEN"
  [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ] || die "Déclaration de l'appareil refusée (HTTP $HTTP_CODE) : $(cat "$TMP/resp")"
  DEVICE_ID="$(jget "$TMP/resp" id)"; NODE_TOKEN="$(jget "$TMP/resp" token)"; NODE_SECRET="$(jget "$TMP/resp" node_secret)"
  [ -n "$DEVICE_ID" ] && [ -n "$NODE_TOKEN" ] && [ -n "$NODE_SECRET" ] || die "Réponse incomplète du serveur : $(cat "$TMP/resp")"
  ok "Appareil déclaré : $DEVICE_NAME ($DEVICE_ID)"
}

# Réutilise ce qui est encore valide dans la configuration existante : la session (sinon reconnexion), puis l'appareil
# (sinon nouvelle déclaration). APIA_RELOGIN=1 force la reconnexion ; l'appareil est repris s'il appartient au compte.
reuse_or_login() {
  local old_base="" old_token="" old_node="" old_secret="" old_id="" old_name=""
  if [ -f "$CFG" ]; then
    old_base="$(jget "$CFG" base)"; old_token="$(jget "$CFG" token)"; old_node="$(jget "$CFG" node_token)"
    old_secret="$(jget "$CFG" node_secret)"; old_id="$(jget "$CFG" device_id)"; old_name="$(jget "$CFG" device_name)"
    if [ "${old_base%/}" != "$BASE" ]; then old_token=""; old_node=""; fi
  fi
  if [ -z "${APIA_RELOGIN:-}" ] && [ -n "$old_token" ]; then
    api GET /v1/app/me "" "$old_token"
    case "$HTTP_CODE" in
      200) APP_TOKEN="$old_token"; ACCOUNT="$(jget "$TMP/resp" email)"; ok "Connexion existante conservée : ${ACCOUNT:-compte APIA}" ;;
      401) warn "La connexion enregistrée n'est plus valide : nouvelle connexion." ;;
      *)   die "Serveur injoignable ($BASE, HTTP $HTTP_CODE) : $(curl_err)" ;;
    esac
  fi
  [ -n "$APP_TOKEN" ] || login
  if [ -n "$old_node" ] && [ -n "$old_secret" ] && [ -n "$old_id" ]; then
    api GET /v1/app/devices "" "$APP_TOKEN"
    if [ "$HTTP_CODE" = "200" ] && device_alive "$TMP/resp" "$old_id"; then
      NODE_TOKEN="$old_node"; NODE_SECRET="$old_secret"; DEVICE_ID="$old_id"; DEVICE_NAME="${old_name:-$(device_name)}"
      ok "Appareil déjà déclaré : $DEVICE_NAME ($DEVICE_ID)"
    else
      warn "L'appareil $old_id n'est plus valable (révoqué, ou autre compte) : nouvelle déclaration."
    fi
  fi
  [ -n "$NODE_TOKEN" ] || create_device
}

write_config() {
  # Les jetons passent par l'environnement, pas par les arguments (visibles de tous dans ps pendant l'exécution)
  APIA_CFG_OUT="$CFG" APIA_CFG_BASE="$BASE" APIA_CFG_TOKEN="$APP_TOKEN" APIA_CFG_NODE_TOKEN="$NODE_TOKEN" APIA_CFG_NODE_SECRET="$NODE_SECRET" \
  APIA_CFG_DEVICE_ID="$DEVICE_ID" APIA_CFG_DEVICE_NAME="$DEVICE_NAME" APIA_CFG_HOME="$HOME" "$NODE_BIN" -e '
    const fs = require("fs"), E = process.env;
    const [out, base, token, node_token, node_secret, device_id, device_name, home] =
      [E.APIA_CFG_OUT, E.APIA_CFG_BASE, E.APIA_CFG_TOKEN, E.APIA_CFG_NODE_TOKEN, E.APIA_CFG_NODE_SECRET, E.APIA_CFG_DEVICE_ID, E.APIA_CFG_DEVICE_NAME, E.APIA_CFG_HOME];
    let old = {}; try { old = JSON.parse(fs.readFileSync(out, "utf8")); } catch (e) {}
    const apia = home + "/APIA";
    const cfg = {
      base, token, node_token, node_secret, device_id, device_name,
      folder: apia + "/recu",
      send_folder: apia + "/envoyer",
      roots: Object.assign({ home, gallery: home + "/Pictures", drop: apia + "/recu" }, old.roots || {}),
      intervalSec: Number(old.intervalSec) || 60,
    };
    fs.writeFileSync(out + ".tmp", JSON.stringify(cfg, null, 2) + "\n", { mode: 0o600 });
    fs.renameSync(out + ".tmp", out);
  '
  chmod 600 "$CFG"
  ok "Configuration écrite : $CFG"
}

# ---------- LaunchAgent ----------
xml_esc() { local s="$1"; s="${s//&/&amp;}"; s="${s//</&lt;}"; s="${s//>/&gt;}"; s="${s//\"/&quot;}"; printf '%s' "$s"; }

write_plist() {
  mkdir -p "$HOME/Library/LaunchAgents"
  local node_dir
  node_dir="$(dirname "$NODE_BIN")"
  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</string>
	<key>ProgramArguments</key>
	<array>
		<string>$(xml_esc "$NODE_BIN")</string>
		<string>$(xml_esc "$SUPPORT/apia-node.js")</string>
	</array>
	<key>WorkingDirectory</key>
	<string>$(xml_esc "$SUPPORT")</string>
	<key>EnvironmentVariables</key>
	<dict>
		<key>PATH</key>
		<string>$(xml_esc "$node_dir"):/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
		<key>HOME</key>
		<string>$(xml_esc "$HOME")</string>
	</dict>
	<key>RunAtLoad</key>
	<true/>
	<key>KeepAlive</key>
	<true/>
	<key>ThrottleInterval</key>
	<integer>10</integer>
	<key>LimitLoadToSessionType</key>
	<string>Aqua</string>
	<key>StandardOutPath</key>
	<string>$(xml_esc "$LOGS/node.log")</string>
	<key>StandardErrorPath</key>
	<string>$(xml_esc "$LOGS/node.err.log")</string>
</dict>
</plist>
EOF
  mv -f "$PLIST.tmp" "$PLIST"
  chmod 644 "$PLIST"
  if has_launchd && command -v plutil >/dev/null 2>&1; then plutil -lint "$PLIST" >/dev/null || die "LaunchAgent invalide : $PLIST"; fi
  ok "LaunchAgent écrit : $PLIST"
}

start_agent() {
  mkdir -p "$LOGS"
  if [ -s "$LOGS/node.log" ]; then mv -f "$LOGS/node.log" "$LOGS/node.log.1"; fi
  if has_launchd; then
    launchctl bootout "gui/$MYUID" "$PLIST" >/dev/null 2>&1 || true
    launchd_wait_gone
    launchctl enable "gui/$MYUID/$LABEL" >/dev/null 2>&1 || true
    if ! launchctl bootstrap "gui/$MYUID" "$PLIST" 2>"$TMP/lerr"; then
      warn "launchctl bootstrap : $(cat "$TMP/lerr")"
      launchctl load -w "$PLIST" 2>"$TMP/lerr" || die "Impossible de charger le LaunchAgent : $(cat "$TMP/lerr")"
    fi
    ok "Agent chargé (gui/$MYUID/$LABEL)"
  else
    # Tests hors macOS : lancement détaché ; une commande simple en arrière-plan pour que $! soit bien le pid du nœud
    (
      cd "$SUPPORT" || exit 1
      nohup "$NODE_BIN" "$SUPPORT/apia-node.js" >>"$LOGS/node.log" 2>>"$LOGS/node.err.log" </dev/null &
      echo $! >"$SUPPORT/node.pid"
    )
    ok "Pas de launchd ici : nœud lancé en arrière-plan (pid $(cat "$SUPPORT/node.pid"))"
  fi
}

wait_connected() {
  local i=0
  while [ "$i" -lt 30 ]; do
    if [ -f "$LOGS/node.log" ] && grep -q "connecté au relais" "$LOGS/node.log" 2>/dev/null; then return 0; fi
    sleep 1; i=$((i + 1))
  done
  return 1
}

summary() {
  printf '\n\033[1mAPIA est installé sur ce Mac.\033[0m\n'
  printf '  Compte        : %s\n' "${ACCOUNT:-?}"
  printf '  Appareil      : %s (%s)\n' "$DEVICE_NAME" "$DEVICE_ID"
  printf '  Reçu dans     : %s/recu\n' "$APIA_DIR"
  printf '  Envoyer       : déposez un fichier dans %s/envoyer/@pseudo/\n' "$APIA_DIR"
  printf '  Journal       : %s/node.log\n' "$LOGS"
  printf '  Mettre à jour : relancez cette même commande\n'
  printf '  Désinstaller  : bash "%s/uninstall.sh"\n\n' "$SUPPORT"
  printf "Ouvrez maintenant l'application APIA : ce Mac apparaît dans vos appareils.\n"
}

main() {
  printf '\n\033[1mAPIA pour Mac (bêta)\033[0m  serveur : %s\n\n' "$BASE"
  check_os
  TMP="$(mktemp -d "${TMPDIR:-/tmp}/apia.XXXXXX")"
  trap cleanup EXIT
  ensure_node
  fetch_bundle
  install_bundle
  make_folders
  reuse_or_login
  # L'ancien nœud tourne jusqu'ici : si le téléchargement, npm ou la saisie du code échouent, il n'est pas arrêté pour rien
  stop_agent
  write_config
  write_plist
  start_agent
  if wait_connected; then
    ok "Nœud connecté au relais APIA"
  else
    warn "Le nœud ne s'est pas encore connecté (il réessaie tout seul). Dernières lignes du journal :"
    tail -n 5 "$LOGS/node.log" "$LOGS/node.err.log" 2>/dev/null | sed 's/^/    /' >&2 || true
  fi
  summary
}

main "$@"
