#!/usr/bin/env bash
#
# Installe (ou met à jour) l'agent Stream Control sur une VM Linux.
#
#   curl -fsSL https://git.jeanbonapp.com/api/packages/jeanbon/generic/stream-control-agent/latest/install-agent.sh \
#     | sudo bash -s -- \
#         --registry https://git.jeanbonapp.com \
#         --server ws://<IP_DU_CONTROLE>:8080/ws/agent \
#         --token <JETON_ENROLEMENT>
#
# Réexécuter le script met à jour le binaire sans toucher à l'identité d'un
# agent déjà enrôlé.

set -euo pipefail

REGISTRY=""
OWNER="jeanbon"
VERSION="latest"
PACKAGE_NAME="stream-control-agent"
PACKAGE_TOKEN=""

# Vide = option non fournie. Les valeurs par défaut ne s'appliquent qu'à une
# première installation ; sur une mise à jour, seules les options réellement
# passées écrasent la configuration existante.
SERVER_URL=""
TOKEN=""
AGENT_NAME=""
OBS_HOST=""
OBS_PORT=""
OBS_PASSWORD=""
OBS_PASSWORD_SET=false   # un mot de passe vide est une valeur légitime
RESET_IDENTITY=false

RUN_USER=""
INSTALL_DIR="/opt/stream-control-agent"
SERVICE="stream-control-agent"

die() { echo "✗ $*" >&2; exit 1; }
info() { echo "· $*"; }

usage() {
  cat <<'EOF'
Installe (ou met à jour) l'agent Stream Control sur une VM Linux.

  curl -fsSL https://git.jeanbonapp.com/api/packages/jeanbon/generic/stream-control-agent/latest/install-agent.sh \
    | sudo bash -s -- \
        --registry https://git.jeanbonapp.com \
        --server ws://<IP_DU_CONTROLE>:8080/ws/agent \
        --token <JETON_ENROLEMENT>

Réexécuter le script met à jour le binaire et applique les options fournies.
L'identité de l'agent (jeton permanent, agentId) est préservée : sans cela il
réapparaîtrait en double dans le dashboard.

Options :
  --registry URL        Base de l'instance Gitea (obligatoire)
  --server URL          WebSocket du plan de contrôle (obligatoire à la 1re install)
  --token JETON         Jeton d'enrôlement (obligatoire à la 1re install)
  --name NOM            Nom affiché dans le dashboard (défaut : hostname)
  --user UTILISATEUR    Compte exécutant l'agent (défaut : l'utilisateur sudo)
  --obs-host HOTE       Défaut 127.0.0.1
  --obs-port PORT       Défaut 4455
  --obs-password MDP    Mot de passe obs-websocket
  --reset-identity      Réenrôler avec le --token fourni (efface l'identité
                        existante — crée un nouvel agent dans le dashboard)
  --version VERSION     Version du paquet à installer (défaut : latest)
  --owner PROPRIETAIRE  Propriétaire du paquet Gitea (défaut : jeanbon)
  --package-token JETON Jeton de lecture si le paquet est privé
  --help
EOF
  exit 0
}

while [ $# -gt 0 ]; do
  case "$1" in
    --registry)       REGISTRY="${2:?}"; shift 2 ;;
    --server)         SERVER_URL="${2:?}"; shift 2 ;;
    --token)          TOKEN="${2:?}"; shift 2 ;;
    --name)           AGENT_NAME="${2:?}"; shift 2 ;;
    --user)           RUN_USER="${2:?}"; shift 2 ;;
    --obs-host)       OBS_HOST="${2:?}"; shift 2 ;;
    --obs-port)       OBS_PORT="${2:?}"; shift 2 ;;
    --obs-password)   OBS_PASSWORD="${2-}"; OBS_PASSWORD_SET=true; shift 2 ;;
    --reset-identity) RESET_IDENTITY=true; shift ;;
    --version)        VERSION="${2:?}"; shift 2 ;;
    --owner)          OWNER="${2:?}"; shift 2 ;;
    --package-token)  PACKAGE_TOKEN="${2:?}"; shift 2 ;;
    --help|-h)        usage ;;
    *) die "Option inconnue : $1  (--help pour l'aide)" ;;
  esac
done

[ "$(id -u)" -eq 0 ] || die "À exécuter en root (sudo)."
[ -n "$REGISTRY" ] || die "--registry est obligatoire."
REGISTRY="${REGISTRY%/}"

# L'agent doit tourner sous le compte qui ouvre la session graphique d'OBS :
# c'est ce qui lui donne accès à l'affichage pour le rappel plein écran.
if [ -z "$RUN_USER" ]; then
  RUN_USER="${SUDO_USER:-root}"
fi
id "$RUN_USER" >/dev/null 2>&1 || die "L'utilisateur « $RUN_USER » n'existe pas."

CONFIG_FILE="$INSTALL_DIR/agent.config.json"
IS_UPGRADE=false
[ -f "$CONFIG_FILE" ] && IS_UPGRADE=true

if [ "$IS_UPGRADE" = false ]; then
  [ -n "$SERVER_URL" ] || die "--server est obligatoire pour une première installation."
  [ -n "$TOKEN" ] || die "--token est obligatoire pour une première installation."
  # Les défauts ne s'appliquent qu'ici : sur une mise à jour, une option absente
  # doit laisser la valeur existante intacte, pas la remplacer par un défaut.
  [ -n "$AGENT_NAME" ] || AGENT_NAME="$(hostname)"
  [ -n "$OBS_HOST" ] || OBS_HOST="127.0.0.1"
  [ -n "$OBS_PORT" ] || OBS_PORT="4455"
fi

echo
echo "Installation de l'agent Stream Control"
echo "  utilisateur : $RUN_USER"
echo "  destination : $INSTALL_DIR"
echo "  version     : $VERSION"
echo

# --- Node.js ----------------------------------------------------------------
NODE_MAJOR=0
if command -v node >/dev/null 2>&1; then
  NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)"
fi

if [ "$NODE_MAJOR" -lt 22 ]; then
  info "Node.js 22+ requis (trouvé : ${NODE_MAJOR:-aucun}) — installation depuis NodeSource"
  command -v apt-get >/dev/null 2>&1 \
    || die "Distribution non gérée automatiquement : installe Node.js 22+ puis relance."
  curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
  apt-get install -y nodejs
else
  info "Node.js $(node -v) présent"
fi

# --- Prérequis du rappel plein écran ----------------------------------------
# Firefox est le prérequis du mode de pilotage « WebDriver BiDi », le seul qui
# fonctionne en session Wayland. xdotool ne sert plus qu'au mode historique.
if ! command -v firefox >/dev/null 2>&1; then
  echo "! Firefox absent : le pilotage « WebDriver BiDi » sera indisponible."
  echo "  Installe-le sur cette VM (snap install firefox, ou apt install firefox)."
fi

if ! command -v xdotool >/dev/null 2>&1; then
  if command -v apt-get >/dev/null 2>&1; then
    info "Installation de xdotool (mode de pilotage historique)"
    apt-get install -y xdotool >/dev/null
  else
    echo "! xdotool absent : seul le pilotage « WebDriver BiDi » sera disponible."
  fi
fi

# --- Téléchargement du bundle -----------------------------------------------
URL="$REGISTRY/api/packages/$OWNER/generic/$PACKAGE_NAME/$VERSION/agent.cjs"
info "Téléchargement depuis $URL"

mkdir -p "$INSTALL_DIR"
CURL_AUTH=()
[ -n "$PACKAGE_TOKEN" ] && CURL_AUTH=(--header "Authorization: token $PACKAGE_TOKEN")

# Le fichier temporaire doit garder l'extension .cjs : `node --check` déduit le
# format du module de l'extension et rejette tout ce qu'il ne reconnaît pas
# (« Unknown file extension .tmp »), quel que soit le contenu.
TMP_FILE="$INSTALL_DIR/agent-new.cjs"

curl -fsSL "${CURL_AUTH[@]}" -o "$TMP_FILE" "$URL" \
  || die "Téléchargement impossible. Vérifie --registry, --version, et --package-token si le paquet est privé."

if ! CHECK_ERR="$(node --check "$TMP_FILE" 2>&1)"; then
  echo "$CHECK_ERR" >&2
  echo "  (fichier conservé pour inspection : $TMP_FILE)" >&2
  die "Le fichier téléchargé n'est pas un script Node valide — voir l'erreur ci-dessus."
fi

mv "$TMP_FILE" "$INSTALL_DIR/agent.cjs"

# --- Configuration ----------------------------------------------------------
if [ "$IS_UPGRADE" = true ]; then
  # Fusion, et non conservation en bloc : l'identité (jeton, agentId) est
  # préservée — l'écraser ferait réapparaître l'agent en double dans le
  # dashboard — mais toute option explicitement passée doit s'appliquer.
  RESET_FLAG=0
  [ "$RESET_IDENTITY" = true ] && RESET_FLAG=1
  PW_FLAG=0
  [ "$OBS_PASSWORD_SET" = true ] && PW_FLAG=1

  node -e '
const fs = require("node:fs");
const a = process.argv.slice(1);
const [file, serverUrl, name, obsHost, obsPort, pwFlag, obsPassword, resetFlag, token, packageUrl] = a;

const current = JSON.parse(fs.readFileSync(file, "utf8").replace(/^\uFEFF/, ""));
const next = { ...current, obs: { ...(current.obs || {}) } };
const applied = [];

// Toujours rafraichie : infrastructure et non identite. Sans elle, un agent
// ne saurait pas ou chercher sa propre mise a jour.
// (Pas d apostrophe dans ce bloc : il vit dans une chaine bash entre quotes.)
if (packageUrl && packageUrl !== current.packageUrl) {
  next.packageUrl = packageUrl;
  applied.push("packageUrl");
}
if (serverUrl) { next.serverUrl = serverUrl; applied.push("serverUrl"); }
if (name)      { next.name = name;           applied.push("name"); }
if (obsHost)   { next.obs.host = obsHost;    applied.push("obs.host"); }
if (obsPort)   { next.obs.port = Number(obsPort); applied.push("obs.port"); }
if (pwFlag === "1") { next.obs.password = obsPassword; applied.push("obs.password"); }

if (resetFlag === "1" && token) {
  next.token = token;
  delete next.agentId;
  applied.push("identité réinitialisée");
} else if (token && token !== current.token) {
  console.log("  (--token ignoré : identité conservée. Utilise --reset-identity pour forcer.)");
}

fs.writeFileSync(file, JSON.stringify(next, null, 2) + "\n");
console.log(applied.length
  ? "  champs mis à jour : " + applied.join(", ")
  : "  aucun changement demandé");
' "$CONFIG_FILE" "$SERVER_URL" "$AGENT_NAME" "$OBS_HOST" "$OBS_PORT" \
  "$PW_FLAG" "$OBS_PASSWORD" "$RESET_FLAG" "$TOKEN" "$URL" \
  || die "Fusion de la configuration existante impossible ($CONFIG_FILE)."

  info "Configuration mise à jour, identité de l'agent préservée"
else
  umask 077
  cat > "$CONFIG_FILE" <<EOF
{
  "serverUrl": "$SERVER_URL",
  "token": "$TOKEN",
  "name": "$AGENT_NAME",
  "packageUrl": "$URL",
  "obs": {
    "host": "$OBS_HOST",
    "port": $OBS_PORT,
    "password": "$OBS_PASSWORD"
  }
}
EOF
  info "Configuration écrite dans $CONFIG_FILE"
fi

chown -R "$RUN_USER" "$INSTALL_DIR"
chmod 600 "$CONFIG_FILE"

# --- Service systemd --------------------------------------------------------
USER_HOME="$(getent passwd "$RUN_USER" | cut -d: -f6)"

cat > "/etc/systemd/system/$SERVICE.service" <<EOF
[Unit]
Description=Stream Control — agent OBS
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=$RUN_USER
WorkingDirectory=$INSTALL_DIR
Environment=NODE_ENV=production
Environment=AGENT_CONFIG=$CONFIG_FILE
# Accès à la session graphique. Ni DISPLAY ni XAUTHORITY ne sont figés ici :
# leurs valeurs dépendent du gestionnaire de session (GDM place XAUTHORITY sous
# /run/user/<uid>, Wayland lui donne un suffixe aléatoire ; l'écran X n'est pas
# toujours :0) et changent d'une session à l'autre. L'agent les résout à
# l'exécution (packages/agent/src/x11.ts). En figer un ici serait pire que
# rien : une valeur périmée serait retenue de préférence à la bonne, et cette
# unité ne le saurait jamais — elle a déjà causé ce bug une fois avec
# XAUTHORITY, ne pas répéter l'erreur avec DISPLAY.
ExecStart=$(command -v node) $INSTALL_DIR/agent.cjs
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable "$SERVICE" >/dev/null 2>&1 || true
systemctl restart "$SERVICE"

# --- Vérification -----------------------------------------------------------
echo
# Mêmes variables que l'unité systemd, sinon le diagnostic signale un DISPLAY
# absent que le service, lui, aura bien. Le cookie X est résolu par l'agent.
CHECK_OK=true
sudo -u "$RUN_USER" \
  AGENT_CONFIG="$CONFIG_FILE" \
  DISPLAY=":0" \
  "$(command -v node)" "$INSTALL_DIR/agent.cjs" --check || CHECK_OK=false

sleep 2
if systemctl is-active --quiet "$SERVICE"; then
  if [ "$CHECK_OK" = true ]; then
    echo "✓ Agent installé et démarré."
  else
    echo "✓ Agent installé et démarré — mais le diagnostic ci-dessus signale un"
    echo "  problème bloquant. L'agent retentera en boucle jusqu'à sa résolution ;"
    echo "  il n'apparaîtra pas dans le dashboard d'ici là."
  fi
else
  echo "✗ Le service n'est pas actif. Journal :"
  journalctl -u "$SERVICE" -n 20 --no-pager || true
  exit 1
fi

cat <<EOF

  état   : systemctl status $SERVICE
  journal: journalctl -u $SERVICE -f
  test   : sudo -u $RUN_USER node $INSTALL_DIR/agent.cjs --check

EOF
