#!/usr/bin/env node
/**
 * docker-backup.js — Nox 🌑
 * Backup automatique des volumes Docker critiques via l'API Portainer.
 * Exécute un cp dans chaque conteneur cible et notifie via Telegram.
 *
 * Usage :
 *   node docker-backup.js              → backup de tous les services configurés
 *   node docker-backup.js vaultwarden  → backup d'un seul service
 *   node docker-backup.js --list       → liste les services configurés
 *
 * Cron OpenClaw : chaque dimanche à 3h00
 */

process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

const PORTAINER_URL = 'https://192.168.1.150:9443';
const PORTAINER_TOKEN = process.env.PORTAINER_API_KEY;
const ENDPOINT_ID = 2;

// ─── Configuration des services à sauvegarder ────────────────────────────────
// Pour chaque service :
//   container  : nom exact du conteneur Docker
//   dataPath   : chemin du volume DANS le conteneur
//   backupDir  : sous-dossier de backup (dans dataPath)
//   keepLast   : nombre de backups à conserver (les plus anciens sont supprimés)
//   files      : (optionnel) liste de fichiers spécifiques à copier (sinon tout dataPath)
// ─────────────────────────────────────────────────────────────────────────────
const SERVICES = [
  {
    container: 'vaultwarden',
    dataPath: '/data',
    backupDir: '/data/backups',
    keepLast: 4,
    files: ['db.sqlite3', 'db.sqlite3-shm', 'db.sqlite3-wal', 'config.json', 'rsa_key.pem', 'rsa_key.pub.pem'],
    description: 'Gestionnaire de mots de passe (Bitwarden)',
  },
  {
    container: 'vikunja-vikunja-1',
    dataPath: '/app/vikunja/files',
    backupDir: '/app/vikunja/files/backups',
    keepLast: 3,
    files: null, // tout le dossier
    description: 'Vikunja — gestionnaire de tâches',
  },
  {
    container: 'nocodb',
    dataPath: '/usr/app/data',
    backupDir: '/usr/app/data/backups',
    keepLast: 3,
    files: null,
    description: 'NocoDB — base de données no-code',
  },
  {
    container: 'freshrss',
    dataPath: '/var/www/FreshRSS/data',
    backupDir: '/var/www/FreshRSS/data/backups',
    keepLast: 3,
    files: null,
    description: 'FreshRSS — agrégateur RSS',
  },
];
// ─────────────────────────────────────────────────────────────────────────────

const args = process.argv.slice(2);
const TARGET = args.find(a => !a.startsWith('--'));
const LIST_ONLY = args.includes('--list');

// ─── Helpers ─────────────────────────────────────────────────────────────────

async function portainerFetch(path, opts = {}) {
  const res = await fetch(PORTAINER_URL + path, {
    ...opts,
    headers: { 'X-API-Key': PORTAINER_TOKEN, 'Content-Type': 'application/json', ...(opts.headers || {}) },
  });
  return res;
}

async function getContainerId(name) {
  const r = await portainerFetch(`/api/endpoints/${ENDPOINT_ID}/docker/containers/json?all=true`);
  const containers = await r.json();
  const c = containers.find(c => c.Names.some(n => n === '/' + name || n === name));
  return c?.Id;
}

async function execInContainer(containerId, cmd) {
  const ec = await portainerFetch(`/api/endpoints/${ENDPOINT_ID}/docker/containers/${containerId}/exec`, {
    method: 'POST',
    body: JSON.stringify({ AttachStdout: true, AttachStderr: true, Cmd: cmd }),
  });
  const { Id } = await ec.json();
  const es = await portainerFetch(`/api/endpoints/${ENDPOINT_ID}/docker/exec/${Id}/start`, {
    method: 'POST',
    body: JSON.stringify({ Detach: false, Tty: false }),
  });
  const buf = await es.arrayBuffer();
  const bytes = new Uint8Array(buf);
  let result = '';
  let i = 0;
  while (i < bytes.length) {
    if (i + 8 > bytes.length) break;
    const sz = (bytes[i+4]<<24)|(bytes[i+5]<<16)|(bytes[i+6]<<8)|bytes[i+7];
    result += new TextDecoder().decode(bytes.slice(i+8, i+8+sz));
    i += 8 + sz;
  }
  return result.trim();
}

function getTimestamp() {
  const now = new Date();
  return now.toISOString().replace(/T/, '_').replace(/:/g, '-').split('.')[0];
}

function getDateStr() {
  return new Date().toISOString().split('T')[0];
}

// ─── Backup d'un service ──────────────────────────────────────────────────────

async function backupService(service) {
  const { container, dataPath, backupDir, keepLast, files, description } = service;
  const timestamp = getTimestamp();
  const backupPath = `${backupDir}/${timestamp}`;

  console.log(`\n📦 [${container}] ${description}`);
  console.log(`   → Backup vers ${backupPath}`);

  // Trouver le conteneur
  const containerId = await getContainerId(container);
  if (!containerId) {
    console.log(`   ⚠️  Conteneur "${container}" introuvable — ignoré`);
    return { service: container, status: 'skipped', reason: 'container not found' };
  }

  // Créer le dossier de backup
  await execInContainer(containerId, ['mkdir', '-p', backupPath]);

  // Copier les fichiers
  let copyResult;
  if (files && files.length > 0) {
    // Copier fichiers spécifiques — essayer sh puis ash
    for (const shell of ['sh', 'ash', 'bash']) {
      const cmd = [shell, '-c', `cp -p ${files.map(f => `${dataPath}/${f}`).join(' ')} ${backupPath}/ 2>&1`];
      copyResult = await execInContainer(containerId, cmd);
      if (!copyResult.includes('not found')) break;
    }
  } else {
    // Copier tout le dossier (sauf le dossier backups lui-même)
    // Essayer sh puis ash (Alpine/BusyBox)
    for (const shell of ['sh', 'ash', 'bash']) {
      const cmd = [shell, '-c', `find ${dataPath} -maxdepth 1 -not -name backups -not -path ${dataPath} | xargs -I{} cp -rp {} ${backupPath}/ 2>&1`];
      copyResult = await execInContainer(containerId, cmd);
      if (!copyResult.includes('not found')) break;
    }
  }

  if (copyResult && copyResult.includes('error')) {
    console.log(`   ❌ Erreur lors de la copie : ${copyResult}`);
    return { service: container, status: 'error', reason: copyResult };
  }

  // Vérifier le backup
  const checkResult = await execInContainer(containerId, ['ls', '-lh', backupPath]);
  const fileCount = checkResult.split('\n').filter(l => l.trim() && !l.startsWith('total')).length;
  const totalLine = checkResult.split('\n').find(l => l.startsWith('total')) || '';
  console.log(`   ✅ ${fileCount} fichier(s) copiés (${totalLine.replace('total ', '')})`);

  // Nettoyage : garder seulement les N derniers backups
  // Essayer sh, puis ash (Alpine/BusyBox), puis liste manuelle
  const shells = ['sh', 'ash', 'bash'];
  let listResult = '';
  for (const shell of shells) {
    const res = await execInContainer(containerId, [shell, '-c', `ls -1t ${backupDir} | tail -n +${keepLast + 1}`]);
    if (!res.includes('not found') && !res.includes('No such')) {
      listResult = res;
      break;
    }
  }
  const toDelete = listResult.split('\n').filter(Boolean);
  if (toDelete.length > 0) {
    for (const old of toDelete) {
      await execInContainer(containerId, ['rm', '-rf', `${backupDir}/${old}`]);
      console.log(`   🗑️  Ancien backup supprimé : ${old}`);
    }
  }

  return { service: container, status: 'ok', path: backupPath, files: fileCount };
}

// ─── Main ─────────────────────────────────────────────────────────────────────

async function main() {
  if (LIST_ONLY) {
    console.log('\n📋 Services configurés pour le backup :\n');
    SERVICES.forEach((s, i) => {
      console.log(`  ${i + 1}. ${s.container.padEnd(25)} ${s.description}`);
      console.log(`     Volume: ${s.dataPath}  |  Garde: ${s.keepLast} backups`);
      if (s.files) console.log(`     Fichiers: ${s.files.join(', ')}`);
    });
    return;
  }

  const targets = TARGET
    ? SERVICES.filter(s => s.container.toLowerCase().includes(TARGET.toLowerCase()))
    : SERVICES;

  if (targets.length === 0) {
    console.log(`❌ Aucun service trouvé pour "${TARGET}"`);
    process.exit(1);
  }

  console.log(`\n🌑 Nox — Docker Backup — ${getDateStr()}`);
  console.log(`   ${targets.length} service(s) à sauvegarder\n`);
  console.log('─'.repeat(60));

  const results = [];
  for (const service of targets) {
    try {
      const result = await backupService(service);
      results.push(result);
    } catch (err) {
      console.log(`   ❌ Exception : ${err.message}`);
      results.push({ service: service.container, status: 'error', reason: err.message });
    }
  }

  // Résumé
  console.log('\n' + '─'.repeat(60));
  console.log('📊 Résumé :');
  const ok = results.filter(r => r.status === 'ok').length;
  const errors = results.filter(r => r.status === 'error').length;
  const skipped = results.filter(r => r.status === 'skipped').length;
  console.log(`   ✅ ${ok} OK  |  ❌ ${errors} erreur(s)  |  ⏭️  ${skipped} ignoré(s)`);

  if (errors > 0) {
    console.log('\n⚠️  Erreurs :');
    results.filter(r => r.status === 'error').forEach(r => {
      console.log(`   - ${r.service}: ${r.reason}`);
    });
    process.exit(1);
  }
}

main().catch(err => {
  console.error('❌ Fatal:', err.message);
  process.exit(1);
});
