core_backup_manifest.js

'use strict';

/**
 * @fileoverview Session manifest creation and persistence.
 * @module backup/manifest
 */

const fs = require('../fs-adapter');
const path = require('path');
const { atomicWrite, atomicWriteSync } = require('./file-ops');
const { MANIFEST_FILE, getSessionDir } = require('./constants');

const MANIFEST_VERSION = '1.0';

/**
 * Creates new session manifest
 * @param {string} sessionId
 * @param {string} command
 * @param {string} cwd
 * @returns {Object}
 */
function createManifest(sessionId, command, cwd) {
  return {
    version: MANIFEST_VERSION,
    id: sessionId,
    timestamp: new Date().toISOString(),
    command,
    cwd,
    status: 'pending',
    files: [],
    reportFile: null,
    stats: { filesModified: 0, filesBackedUp: 0 },
    error: null,
  };
}

/**
 * @param {Object} manifest
 * @param {{original: string, backup: string, size: number}} fileInfo
 */
function addFileToManifest(manifest, fileInfo) {
  manifest.files.push({
    original: fileInfo.original,
    backup: fileInfo.backup,
    size: fileInfo.size,
    backedUpAt: new Date().toISOString(),
  });
  manifest.stats.filesBackedUp = manifest.files.length;
}

/**
 * @param {Object} manifest
 * @param {Object} ctx - Context with status, stats, error properties
 */
function updateManifestStatus(manifest, ctx) {
  const { status, stats = null, error = null } = ctx;
  manifest.status = status;
  manifest.updatedAt = new Date().toISOString();
  if (stats) {
    manifest.stats = { ...manifest.stats, ...stats };
  }
  if (error) {
    manifest.error = {
      message: error.message,
      stack: error.stack,
      occurredAt: new Date().toISOString(),
    };
  }
}

function getManifestPath(cwd, sessionId) {
  return path.join(getSessionDir(cwd, sessionId), MANIFEST_FILE);
}

/**
 * @param {string} cwd
 * @param {string} sessionId
 * @param {Object} manifest
 * @returns {Promise<void>}
 */
async function writeManifest(cwd, sessionId, manifest) {
  const manifestPath = getManifestPath(cwd, sessionId);
  const content = JSON.stringify(manifest, null, 2);
  await atomicWrite(manifestPath, content);
}

/**
 * @param {string} cwd
 * @param {string} sessionId
 * @param {Object} manifest
 */
function writeManifestSync(cwd, sessionId, manifest) {
  const manifestPath = getManifestPath(cwd, sessionId);
  const content = JSON.stringify(manifest, null, 2);
  atomicWriteSync(manifestPath, content);
}

/**
 * @param {string} cwd
 * @param {string} sessionId
 * @returns {Promise<Object|null>}
 */
async function readManifest(cwd, sessionId) {
  const manifestPath = getManifestPath(cwd, sessionId);
  try {
    const content = await fs.readFile(manifestPath, 'utf-8');
    return JSON.parse(content);
  } catch {
    return null;
  }
}

/**
 * @param {string} cwd
 * @param {string} sessionId
 * @returns {Object|null}
 */
function readManifestSync(cwd, sessionId) {
  const manifestPath = getManifestPath(cwd, sessionId);
  try {
    const content = fs.readFileSync(manifestPath, 'utf-8');
    return JSON.parse(content);
  } catch {
    return null;
  }
}

function hasRequiredField(manifest, field) {
  return Boolean(manifest[field]);
}

/**
 * @param {Object} manifest
 * @returns {Object} Result with valid and error properties
 */
function validateManifest(manifest) {
  if (!manifest) {
    return { valid: false, error: 'Manifest is null' };
  }
  const requiredFields = [
    { field: 'version', error: 'Missing version' },
    { field: 'id', error: 'Missing session id' },
    { field: 'timestamp', error: 'Missing timestamp' },
  ];
  for (const { field, error } of requiredFields) {
    if (!hasRequiredField(manifest, field)) {
      return { valid: false, error };
    }
  }
  if (!Array.isArray(manifest.files)) {
    return { valid: false, error: 'Invalid files array' };
  }
  return { valid: true };
}

function formatDate(timestamp) {
  return timestamp ? new Date(timestamp).toLocaleString() : 'unknown';
}

/**
 * @param {Object} manifest
 * @returns {Object}
 */
function formatManifestForDisplay(manifest) {
  return {
    id: manifest.id,
    date: formatDate(manifest.timestamp),
    status: manifest.status || 'unknown',
    fileCount: manifest.files?.length || 0,
    command: manifest.command || 'unknown',
    hasReport: Boolean(manifest.reportFile),
  };
}

module.exports = {
  createManifest,
  addFileToManifest,
  updateManifestStatus,
  writeManifest,
  writeManifestSync,
  readManifest,
  readManifestSync,
  validateManifest,
  formatManifestForDisplay,
};