core_backup_restore.js

'use strict';

/**
 * @fileoverview Session restoration and rollback operations.
 * @module backup/restore
 */

const { Session } = require('./session');
const { restoreFileSync } = require('./file-ops');
const { SessionStatus, getSessionDir } = require('./constants');
const fs = require('../fs-adapter');

function logIfVerbose(verbose, log, message) {
  if (verbose) {
    log(message);
  }
}

function handleMissingBackup(file, ctx) {
  logIfVerbose(ctx.verbose, ctx.log, `  Skip (backup missing): ${file.original}`);
  return { restored: false, skipped: true };
}

function handleDryRun(file, ctx) {
  ctx.log(`  Would restore: ${file.original}`);
  return { restored: true, skipped: false };
}

function performRestore(file, ctx) {
  restoreFileSync(file.backup, file.original);
  logIfVerbose(ctx.verbose, ctx.log, `  Restored: ${file.original}`);
  return { restored: true, skipped: false };
}

function restoreFileWithLogging(file, ctx) {
  if (!fs.existsSync(file.backup)) {
    return handleMissingBackup(file, ctx);
  }
  return ctx.dryRun ? handleDryRun(file, ctx) : performRestore(file, ctx);
}

function processRestoreFile(file, ctx) {
  try {
    return restoreFileWithLogging(file, ctx);
  } catch (err) {
    logIfVerbose(ctx.verbose, ctx.log, `  Error restoring ${file.original}: ${err.message}`);
    return { restored: false, skipped: true };
  }
}

function loadSessionOrThrow(cwd, sessionId) {
  const session = Session.load(cwd, sessionId);
  if (!session) {
    throw new Error(`Session not found: ${sessionId}`);
  }
  return session;
}

function countResults(results) {
  return {
    restored: results.filter(r => r.restored).length,
    skipped: results.filter(r => r.skipped).length,
  };
}

function executeRestore(files, ctx) {
  const results = files.map(file => processRestoreFile(file, ctx));
  return countResults(results);
}

function loadFilesOrEmpty(cwd, sessionId, log) {
  const session = loadSessionOrThrow(cwd, sessionId);
  const files = session.getBackedUpFiles();
  if (files.length === 0) {
    log('No files to restore');
  }
  return files;
}

function buildRestoreContext(options) {
  const { dryRun = false, verbose = false, log = console.log } = options;
  return { dryRun, verbose, log };
}

/**
 * Restores files from a backup session
 * @param {string} cwd
 * @param {string} sessionId
 * @param {Object} [options]
 * @returns {{restored: number, skipped: number}}
 */
function restoreSession(cwd, sessionId, options = {}) {
  const ctx = buildRestoreContext(options);
  const files = loadFilesOrEmpty(cwd, sessionId, ctx.log);
  if (files.length === 0) {
    return { restored: 0, skipped: 0 };
  }
  return executeRestore(files, ctx);
}

/**
 * Restores files from the latest backup session
 * @param {string} cwd
 * @param {Object} [options]
 * @returns {{restored: number, skipped: number}}
 */
function restoreLatest(cwd, options = {}) {
  const latestId = Session.getLatestId(cwd);
  if (!latestId) {
    throw new Error('No backup sessions found');
  }
  return restoreSession(cwd, latestId, options);
}

/**
 * Auto-rollback on failure during in-progress session
 * @param {string} cwd
 * @param {Session} session
 * @param {Object} [options]
 * @returns {{restored: number, skipped: number}}
 */
function autoRollback(cwd, session, options = {}) {
  const { log = console.log } = options;
  log('Auto-rollback initiated...');
  const result = restoreSession(cwd, session.id, options);
  session.rollback();
  log(`Rolled back ${result.restored} files`);
  return result;
}

/**
 * @param {string} cwd
 * @returns {Object|null}
 */
function checkIncompleteSession(cwd) {
  const incomplete = Session.findIncomplete(cwd);
  return incomplete.length > 0 ? incomplete[0] : null;
}

/**
 * Returns preview of restore operation
 * @param {string} cwd
 * @param {string} sessionId
 * @returns {Object|null}
 */
function formatRestorePreview(cwd, sessionId) {
  const session = Session.load(cwd, sessionId);
  if (!session) {
    return null;
  }
  const files = session.getBackedUpFiles();
  const sessionDir = getSessionDir(cwd, sessionId);
  const { command, timestamp } = session.manifest;
  return {
    sessionId,
    command,
    timestamp,
    status: session.status,
    fileCount: files.length,
    files: files.map(f => ({ path: f.original, exists: fs.existsSync(f.backup) })),
    sessionDir,
  };
}

function buildRecoveryActions(id) {
  return [
    { command: `i18nkit --restore ${id}`, description: 'Rollback to pre-modification state' },
    { command: 'i18nkit --force', description: 'Continue without rollback (risky)' },
  ];
}

function getInProgressRecovery(id) {
  return {
    severity: 'warning',
    message: `Incomplete session found: ${id}`,
    suggestion: `Files may have been partially modified. Run 'i18nkit --restore ${id}' to rollback.`,
    actions: buildRecoveryActions(id),
  };
}

function getBackingUpRecovery() {
  return {
    severity: 'info',
    message: 'Interrupted backup session',
    suggestion: 'Backup was incomplete. No files were modified. Safe to continue.',
    actions: [{ command: 'i18nkit [command]', description: 'Continue normally' }],
  };
}

/**
 * Returns recovery instructions for incomplete session
 * @param {Object|null} incomplete
 * @returns {Object|null}
 */
function getRecoveryInstructions(incomplete) {
  if (!incomplete) {
    return null;
  }
  const { status, id } = incomplete;
  if (status === SessionStatus.IN_PROGRESS) {
    return getInProgressRecovery(id);
  }
  if (status === SessionStatus.BACKING_UP) {
    return getBackingUpRecovery();
  }
  return null;
}

module.exports = {
  restoreSession,
  restoreLatest,
  autoRollback,
  checkIncompleteSession,
  formatRestorePreview,
  getRecoveryInstructions,
};