"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createBackup = createBackup;
exports.restoreBackup = restoreBackup;
exports.listBackups = listBackups;
exports.verifyBackup = verifyBackup;
exports.startAutoBackup = startAutoBackup;
exports.stopAutoBackup = stopAutoBackup;
const fs_1 = require("fs");
const promises_1 = require("fs/promises");
const path_1 = require("path");
const crypto_1 = require("crypto");
const archiver_1 = __importDefault(require("archiver"));
const adm_zip_1 = __importDefault(require("adm-zip"));
const node_cron_1 = __importDefault(require("node-cron"));
const config_js_1 = require("../../utils/config.js");
const logger_js_1 = require("../../utils/logger.js");
const file_ops_js_1 = require("./file-ops.js");
const log = (0, logger_js_1.createModuleLogger)('backup');
/** Max number of automatic backups to keep. Oldest are deleted. */
const MAX_AUTO_BACKUPS = 7;
/** Directories included in backup (everything under data/ except backups). */
const BACKUP_DIRS = ['config', 'characters', 'templates', 'queue', 'analytics', 'stars', 'translations', 'logs'];
/**
 * Creates a ZIP backup of the entire data directory (hot-backup safe).
 * Stores a .sha256 checksum file alongside the archive.
 */
async function createBackup(dataDir) {
    const dir = dataDir || config_js_1.config.dataDir;
    const backupDir = (0, path_1.resolve)(dir, 'backups');
    await (0, file_ops_js_1.ensureDir)(backupDir);
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    const filename = `backup-${timestamp}.zip`;
    const zipPath = (0, path_1.resolve)(backupDir, filename);
    return new Promise((resolvePromise, reject) => {
        const output = (0, fs_1.createWriteStream)(zipPath);
        const archive = (0, archiver_1.default)('zip', { zlib: { level: 9 } });
        output.on('close', async () => {
            try {
                const checksum = await calculateChecksum(zipPath);
                await (0, promises_1.writeFile)(`${zipPath}.sha256`, checksum, 'utf-8');
                const stats = (0, fs_1.statSync)(zipPath);
                const info = {
                    filename,
                    path: zipPath,
                    checksum,
                    createdAt: new Date().toISOString(),
                    sizeBytes: stats.size,
                };
                log.info({ filename, sizeBytes: stats.size }, 'Backup created');
                resolvePromise(info);
            }
            catch (err) {
                reject(err);
            }
        });
        archive.on('error', reject);
        archive.pipe(output);
        for (const d of BACKUP_DIRS) {
            const fullPath = (0, path_1.resolve)(dir, d);
            if ((0, fs_1.existsSync)(fullPath)) {
                archive.directory(fullPath, d);
            }
        }
        archive.finalize();
    });
}
/**
 * Restores data from a ZIP backup.
 * Verifies checksum when a .sha256 file exists alongside the archive.
 * Performs atomic restore: current data is moved to a temp location first,
 * and rolled back if extraction fails.
 */
async function restoreBackup(zipPath, dataDir) {
    const dir = dataDir || config_js_1.config.dataDir;
    // Verify checksum if .sha256 file exists
    const checksumPath = `${zipPath}.sha256`;
    if ((0, fs_1.existsSync)(checksumPath)) {
        const expected = (await (0, promises_1.readFile)(checksumPath, 'utf-8')).trim();
        const actual = await calculateChecksum(zipPath);
        if (expected !== actual) {
            throw new Error('Backup checksum verification failed');
        }
        log.info('Checksum verified');
    }
    // Temp directory for rollback safety
    const tempBackupDir = (0, path_1.resolve)(dir, '..', '.data-pre-restore');
    if ((0, fs_1.existsSync)(tempBackupDir))
        (0, fs_1.rmSync)(tempBackupDir, { recursive: true, force: true });
    // Move current dirs to temp
    (0, fs_1.mkdirSync)(tempBackupDir, { recursive: true });
    for (const d of BACKUP_DIRS) {
        const src = (0, path_1.resolve)(dir, d);
        if ((0, fs_1.existsSync)(src)) {
            (0, fs_1.renameSync)(src, (0, path_1.resolve)(tempBackupDir, d));
        }
    }
    try {
        const zip = new adm_zip_1.default(zipPath);
        zip.extractAllTo(dir, true);
        // Clean up temp backup on success
        (0, fs_1.rmSync)(tempBackupDir, { recursive: true, force: true });
        log.info({ zipPath }, 'Backup restored successfully');
    }
    catch (err) {
        // Rollback: restore original dirs from temp
        log.error({ err }, 'Restore failed, rolling back');
        for (const d of BACKUP_DIRS) {
            const tempSrc = (0, path_1.resolve)(tempBackupDir, d);
            const dest = (0, path_1.resolve)(dir, d);
            if ((0, fs_1.existsSync)(tempSrc)) {
                if ((0, fs_1.existsSync)(dest))
                    (0, fs_1.rmSync)(dest, { recursive: true, force: true });
                (0, fs_1.renameSync)(tempSrc, dest);
            }
        }
        (0, fs_1.rmSync)(tempBackupDir, { recursive: true, force: true });
        throw err;
    }
}
/**
 * Lists all available backups sorted by creation date (newest first).
 */
async function listBackups(dataDir) {
    const dir = dataDir || config_js_1.config.dataDir;
    const backupDir = (0, path_1.resolve)(dir, 'backups');
    if (!(0, fs_1.existsSync)(backupDir))
        return [];
    const files = (0, fs_1.readdirSync)(backupDir).filter(f => f.endsWith('.zip'));
    const backups = [];
    for (const file of files) {
        const filePath = (0, path_1.resolve)(backupDir, file);
        const stats = (0, fs_1.statSync)(filePath);
        const checksumPath = `${filePath}.sha256`;
        let checksum = '';
        if ((0, fs_1.existsSync)(checksumPath)) {
            checksum = (await (0, promises_1.readFile)(checksumPath, 'utf-8')).trim();
        }
        backups.push({
            filename: file,
            path: filePath,
            checksum,
            createdAt: stats.mtime.toISOString(),
            sizeBytes: stats.size,
        });
    }
    return backups.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}
/**
 * Verifies a backup's integrity by comparing its SHA-256 checksum.
 * Returns true if valid, false if checksum file is missing or mismatch.
 */
async function verifyBackup(zipPath) {
    const checksumPath = `${zipPath}.sha256`;
    if (!(0, fs_1.existsSync)(checksumPath))
        return false;
    const expected = (await (0, promises_1.readFile)(checksumPath, 'utf-8')).trim();
    const actual = await calculateChecksum(zipPath);
    return expected === actual;
}
/**
 * Computes SHA-256 checksum of a file via streaming.
 */
async function calculateChecksum(filePath) {
    return new Promise((resolve, reject) => {
        const hash = (0, crypto_1.createHash)('sha256');
        const stream = (0, fs_1.createReadStream)(filePath);
        stream.on('data', (data) => hash.update(data));
        stream.on('end', () => resolve(hash.digest('hex')));
        stream.on('error', reject);
    });
}
// ── Automatic Backup Scheduling ──
let scheduledTask = null;
/**
 * Prunes old automatic backups, keeping only the newest `MAX_AUTO_BACKUPS`.
 */
async function pruneOldBackups(dataDir) {
    const backups = await listBackups(dataDir);
    if (backups.length <= MAX_AUTO_BACKUPS)
        return;
    const toDelete = backups.slice(MAX_AUTO_BACKUPS);
    for (const b of toDelete) {
        try {
            if ((0, fs_1.existsSync)(b.path))
                (0, fs_1.unlinkSync)(b.path);
            const csPath = `${b.path}.sha256`;
            if ((0, fs_1.existsSync)(csPath))
                (0, fs_1.unlinkSync)(csPath);
            log.info({ filename: b.filename }, 'Pruned old backup');
        }
        catch (err) {
            log.warn({ filename: b.filename, err: err.message }, 'Failed to prune old backup');
        }
    }
}
/**
 * Starts an automatic daily backup cron job (runs at 03:00 every day).
 * Old backups beyond `MAX_AUTO_BACKUPS` are automatically pruned.
 */
function startAutoBackup(cronExpr = '0 3 * * *') {
    if (scheduledTask) {
        log.warn('Auto-backup already running, skipping duplicate start');
        return;
    }
    scheduledTask = node_cron_1.default.schedule(cronExpr, async () => {
        try {
            log.info('Running scheduled automatic backup...');
            const info = await createBackup();
            log.info({ filename: info.filename, sizeBytes: info.sizeBytes }, 'Automatic backup completed');
            await pruneOldBackups();
        }
        catch (err) {
            log.error({ err: err.message }, 'Automatic backup failed');
        }
    });
    log.info({ cronExpr }, 'Automatic backup scheduler started');
}
/**
 * Stops the automatic backup cron job.
 */
function stopAutoBackup() {
    if (scheduledTask) {
        scheduledTask.stop();
        scheduledTask = null;
        log.info('Automatic backup scheduler stopped');
    }
}
//# sourceMappingURL=backup.js.map