// Micara bridge — auto-update TRANSPARENT (t_2e77f88d)
// 1. GET /api/bridge/latest (serveur, endpoint PUBLIC — pas besoin du login)
// 2. comparaison semver (x.y.z) avec la version locale
// 3. si plus récent → télécharge le zip (streamé, avec progression) dans un
//    dossier CACHÉ (userData/updates — jamais Téléchargements : le client ne
//    doit rien voir, zéro action client).
// 4. installation semi-auto (app non signée, en attendant la licence Apple
//    99 €/an) : l'app propose « Mettre à jour maintenant » → elle se quitte,
//    un script bash DÉTACHÉ dézippe le zip, remplace le .app en place
//    (rm l'ancien + mv le nouveau), retire la quarantaine (xattr -cr,
//    jamais demandé au client), puis relance l'app (open --args
//    --micara-updated → la nouvelle instance notifie « mis à jour »).
// VOIE PRINCIPALE (app signée, futur) : electron-updater (Squirrel.Mac).
// NON activé : exige la signature Developer ID (licence 99 €/an) — sans
// elle autoUpdater.quitAndInstall() échoue. Le jour de la signature :
// ajouter electron-updater en dépendance + brancher autoUpdater ici avec
// la même api.latest()/url — le reste du flux (timer 1 h, notifications,
// état updateInfo) est déjà en place dans main.js.
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
const { once } = require('events');

function parseVersion(v) {
  const parts = String(v || '')
    .trim().replace(/^v/i, '').split('.')
    .map((n) => parseInt(n, 10) || 0);
  return [parts[0] || 0, parts[1] || 0, parts[2] || 0];
}

function isNewerVersion(current, latest) {
  const a = parseVersion(current);
  const b = parseVersion(latest);
  for (let i = 0; i < 3; i++) {
    if (b[i] !== a[i]) return b[i] > a[i];
  }
  return false;
}

// Télécharge url → dest en STREAMANT sur le disque (jamais tout en RAM :
// le zip fait ~120 Mo) avec progression. onProgress({bytes, total, percent}).
async function downloadTo(url, dest, { fetchImpl = globalThis.fetch, onProgress } = {}) {
  const res = await fetchImpl(url);
  if (!res.ok) throw new Error(`téléchargement échoué (HTTP ${res.status})`);
  if (!res.body) {
    // pas de body streamable (fetch de test) → tampon complet
    const buf = Buffer.from(await res.arrayBuffer());
    fs.mkdirSync(path.dirname(dest), { recursive: true });
    fs.writeFileSync(dest, buf);
    onProgress && onProgress({ bytes: buf.length, total: buf.length, percent: 100 });
    return dest;
  }
  const total = Number(res.headers.get('content-length')) || 0;
  const reader = res.body.getReader();
  fs.mkdirSync(path.dirname(dest), { recursive: true });
  const ws = fs.createWriteStream(dest);
  let bytes = 0;
  try {
    for (;;) {
      const { done, value } = await reader.read();
      if (done) break;
      if (!ws.write(Buffer.from(value))) await once(ws, 'drain');
      bytes += value.length;
      onProgress && onProgress({
        bytes,
        total,
        percent: total ? Math.min(100, Math.round((bytes / total) * 100)) : null,
      });
    }
    await new Promise((resolve, reject) => ws.end((e) => (e ? reject(e) : resolve())));
  } catch (e) {
    ws.destroy();
    throw e;
  }
  return dest;
}

// Retourne :
//   { update:false, latest }                       → à jour
//   { update:true, latest, download:dest }         → zip téléchargé (ou déjà
//                                                    présent : timer 1 h)
//   { update:true, latest, download:null, error }  → version plus récente mais
//                                                    téléchargement impossible
// onProgress reçoit { latest, bytes, total, percent } pendant le téléchargement.
async function checkForUpdate({ api, currentVersion, downloadDir, fetchImpl, onProgress }) {
  let latest;
  try {
    latest = await api.latest();
  } catch (e) {
    return { update: false, latest: null, error: `version indisponible (${e.message})` };
  }
  if (!isNewerVersion(currentVersion, latest.version)) return { update: false, latest: latest.version };
  if (!latest.url) return { update: true, latest: latest.version, download: null, error: 'URL de téléchargement manquante' };
  const name = latest.url.split('/').pop() || `micara-bridge-${latest.version}-mac.zip`;
  const dest = path.join(downloadDir, name);
  // déjà téléchargé (check périodique suivant) → pas de re-téléchargement
  if (fs.existsSync(dest)) {
    try {
      if (fs.statSync(dest).size > 0) return { update: true, latest: latest.version, download: dest };
    } catch { /* stat illisible → re-télécharge */ }
  }
  try {
    await downloadTo(latest.url, dest, {
      fetchImpl,
      onProgress: (p) => onProgress && onProgress({ latest: latest.version, ...p }),
    });
    return { update: true, latest: latest.version, download: dest };
  } catch (e) {
    return { update: true, latest: latest.version, download: null, error: e.message };
  }
}

// Chemin du bundle .app depuis process.execPath (macOS packagé) :
//   …/Micara Bridge.app/Contents/MacOS/Micara Bridge → …/Micara Bridge.app
// (3 niveaux : MacOS/ est le répertoire de l'exécutable, Contents/ celui du bundle)
function appBundlePath(execPath) {
  return path.dirname(path.dirname(path.dirname(execPath)));
}

// Script d'installation DÉTACHÉ : lancé par l'app juste avant app.quit().
// Remplace le manuel « zip → Applications » (t_2e77f88d). Sûreté :
// l'ancien .app n'est supprimé qu'UNE FOIS le nouveau en place (mv sous nom
// temporaire → rm ancien → renommage final). Échec → l'ancienne app reste et
// le zip est conservé (réinstallation manuelle possible). Le disque reste
// propre : zip supprimé après succès.
// Args : $1 = zip téléchargé, $2 = bundle .app actuel.
// Le `open --args --micara-updated` fait afficher à la nouvelle instance la
// notification « Micara a été mis à jour » (voir main.js).
const INSTALL_SCRIPT = `#!/bin/bash
# Micara — installation silencieuse d'une mise à jour (t_2e77f88d)
# Lancé DÉTACHÉ par l'app juste avant qu'elle se quitte. Zéro action client :
# dézippe, remplace le .app en place, relance. Gatekeeper géré ici
# (xattr -cr) — jamais demandé au client.
set -u
ZIP="$1"
OLD_APP="$2"
log() { echo "[micara-update] $(date '+%F %T') $*"; }
log "début — zip=$ZIP app=$OLD_APP"
sleep 2   # laisse l'ancienne instance finir de se quitter (app.quit)
TMP="$(mktemp -d "\${TMPDIR:-/tmp}/micara-update.XXXXXX")" || { log "mktemp échoué"; exit 10; }
if ! unzip -q -o "$ZIP" -d "$TMP"; then
  log "unzip échoué — zip conservé ($ZIP)"
  rm -rf "$TMP"; exit 11
fi
NEW_APP="$(find "$TMP" -maxdepth 1 -type d -name '*.app' | head -n 1)"
if [ -z "$NEW_APP" ]; then
  log "aucun .app dans le zip"
  rm -rf "$TMP"; exit 12
fi
# Gatekeeper : retire la quarantaine éventuelle du .app neuf. Sans effet sur
# la signature ad-hoc (elle vit dans le binaire, pas dans les xattr).
if command -v xattr >/dev/null 2>&1; then
  xattr -cr "$NEW_APP" 2>/dev/null || log "xattr -cr ignoré"
fi
NEW_NAME="$(basename "$OLD_APP")"
APP_DIR="$(dirname "$OLD_APP")"
rm -rf "$APP_DIR/.\${NEW_NAME}.new"   # reliquat éventuel d'un essai précédent
if ! mv "$NEW_APP" "$APP_DIR/.\${NEW_NAME}.new"; then
  log "mv échoué (permissions sur $APP_DIR ?)"
  rm -rf "$TMP"; exit 13
fi
rm -rf "$OLD_APP" || log "rm ancien échoué (ignoré)"
if ! mv "$APP_DIR/.\${NEW_NAME}.new" "$APP_DIR/$NEW_NAME"; then
  log "renommage final échoué — .\${NEW_NAME}.new conservé dans $APP_DIR"
  rm -rf "$TMP"; exit 14
fi
rm -rf "$TMP"
rm -f "$ZIP"   # 120 Mo récupérés — le disque reste propre
log "installé — relance de $APP_DIR/$NEW_NAME"
if command -v open >/dev/null 2>&1; then
  open "$APP_DIR/$NEW_NAME" --args --micara-updated || log "open échoué — l'app redémarrera au prochain login"
fi
exit 0
`;

// Écrit le script d'installation et le lance DÉTACHÉ (survit à app.quit()).
function launchInstaller({ zipPath, appPath }) {
  try {
    const dir = path.dirname(zipPath);
    const scriptPath = path.join(dir, 'install-update.sh');
    fs.writeFileSync(scriptPath, INSTALL_SCRIPT, { mode: 0o755 });
    const child = spawn('/bin/bash', [scriptPath, zipPath, appPath], {
      detached: true,
      stdio: 'ignore',
    });
    child.unref();
    return { ok: true, script: scriptPath, pid: child.pid };
  } catch (e) {
    return { ok: false, error: e.message };
  }
}

module.exports = {
  parseVersion,
  isNewerVersion,
  downloadTo,
  checkForUpdate,
  appBundlePath,
  launchInstaller,
  INSTALL_SCRIPT,
};
