// atomic-writer.ts — Atomic JSON writes with backup rotation (SPEC-094 AC-1)
import { rename, copyFile, unlink, readFile, writeFile, mkdir } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { dirname } from 'node:path';
const DEFAULT_BACKUP_COUNT = 3;
/**
 * Write JSON data atomically with backup rotation.
 *
 * 1. Rotate existing backups (if target exists).
 * 2. Write to a `.tmp` sibling file.
 * 3. Optionally verify the `.tmp` contents parse correctly.
 * 4. Atomically rename `.tmp` to the target path.
 *
 * On failure during steps 2-4, the `.tmp` file is cleaned up silently.
 */
export async function writeJsonSafe(filePath, data, opts) {
    const backupCount = opts?.backupCount ?? DEFAULT_BACKUP_COUNT;
    const verify = opts?.verify ?? true;
    const tmpPath = `${filePath}.tmp`;
    // Ensure parent directory exists
    await mkdir(dirname(filePath), { recursive: true });
    // Rotate backups if target already exists
    if (existsSync(filePath)) {
        await rotateBackups(filePath, backupCount);
        await copyFile(filePath, `${filePath}.bak.1`);
    }
    // Serialize
    const json = JSON.stringify(data, null, 2);
    try {
        // Write to temp file
        await writeFile(tmpPath, json, 'utf-8');
        // Verify round-trip if requested
        if (verify) {
            const readBack = await readFile(tmpPath, 'utf-8');
            JSON.parse(readBack);
        }
        // Atomic rename
        await rename(tmpPath, filePath);
    }
    catch (err) {
        // Cleanup temp file silently
        try {
            await unlink(tmpPath);
        }
        catch {
            // Ignore cleanup errors
        }
        throw err;
    }
}
/**
 * Attempt to recover a file from its most recent backup.
 *
 * Checks `.bak.1`, `.bak.2`, `.bak.3` in order and copies the first
 * one found back to the target path.
 *
 * @returns `true` if recovery succeeded, `false` if no backup exists.
 */
export async function recoverFromBackup(filePath, backupCount = DEFAULT_BACKUP_COUNT) {
    for (let i = 1; i <= backupCount; i++) {
        const bakPath = `${filePath}.bak.${i}`;
        if (existsSync(bakPath)) {
            await copyFile(bakPath, filePath);
            return true;
        }
    }
    return false;
}
/**
 * Rotate backup files: `.bak.{count-1}` -> `.bak.{count}`, etc.
 *
 * The oldest backup (`.bak.{count}`) is discarded if it exists.
 */
async function rotateBackups(filePath, count) {
    // Delete the oldest if it exists
    const oldestPath = `${filePath}.bak.${count}`;
    if (existsSync(oldestPath)) {
        await unlink(oldestPath);
    }
    // Shift each backup up by one
    for (let i = count - 1; i >= 1; i--) {
        const src = `${filePath}.bak.${i}`;
        const dst = `${filePath}.bak.${i + 1}`;
        if (existsSync(src)) {
            await rename(src, dst);
        }
    }
}
//# sourceMappingURL=atomic-writer.js.map