import assert from 'assert';
import backups from './backups.js';
import backupFormats from './backupformats.js';
import backupSites from './backupsites.js';
import BoxError from './boxerror.js';
import consumers from 'node:stream/consumers';
import crypto from 'node:crypto';
import logger from './logger.js';
import safe from 'safetydance';

const { log } = logger('backupintegrity');


async function downloadBackupInfo(backupSite, backup) {
    const stream = await backupSites.storageApi(backupSite).download(backupSite.config, `${backup.remotePath}.backupinfo`);
    const buffer = await consumers.buffer(stream);
    return buffer;
}

async function verify(backup, backupSite, progressCallback) {
    assert.strictEqual(typeof backup, 'object');
    assert.strictEqual(typeof backupSite, 'object');
    assert.strictEqual(typeof progressCallback, 'function');

    if (backup === null) return [`${backup.id} is missing in database`];

    const stats = { startTime: Date.now(), duration: null };

    const [downloadError, backupInfoBuffer] = await safe(downloadBackupInfo(backupSite, backup));
    if (downloadError) {
        const messages = [`Failed to download ${backup.remotePath}.backupinfo: ${downloadError.message}`];
        stats.duration = Date.now() - stats.startTime;
        return { stats, messages };
    }

    const validSignature = crypto.verify(null /* algo */, backupInfoBuffer, backupSite.integrityKeyPair.publicKey, Buffer.from(backup.integrity.signature, 'hex'));
    progressCallback({ message: `${backup.remotePath}.backupinfo has ${validSignature ? 'valid': 'invalid' } signature`});

    const backupInfo = JSON.parse(backupInfoBuffer.toString('utf8'));
    const integrityMap = new Map(Object.entries(backupInfo));

    const [verifyError, verifyMessages] = await safe(backupFormats.api(backupSite.format).verify(backupSite, backup.remotePath, integrityMap, progressCallback));
    progressCallback({ message: `Verification of ${backup.remotePath} done` });

    const messages = [];
    if (!validSignature) messages.push(`${backup.remotePath}.backupinfo has invalid signature`);
    if (verifyError) messages.push(`Failed to verify ${backup.remotePath}: ${verifyError.message}`);
    if (verifyMessages) messages.push(...verifyMessages);

    log(`verified: ${backup.remotePath} ${JSON.stringify(messages, null, 4)}`);

    stats.duration = Date.now() - stats.startTime;
    return { stats, messages: messages.slice(0, 50) }; // keep rsync fails to 50 to not overflow db
}

async function check(backupId, progressCallback) {
    const backup = await backups.get(backupId);
    if (!backup) throw new BoxError(BoxError.BAD_FIELD, 'Backup not found');

    const backupSite = await backupSites.get(backup.siteId);
    if (!backupSite) throw new BoxError(BoxError.BAD_FIELD, 'Backup site not found');

    const total = backup.dependsOn.length + 1;
    let completed = 0;

    const aggregatedStats = { startTime: Date.now(), duration: null };
    const aggregatedMessages = [];
    for (const depId of backup.dependsOn) {
        const depBackup = await backups.get(depId);
        progressCallback({ percent: Math.round(completed / total * 100), message: `Verifying ${depBackup.remotePath}` });

        if (!depBackup.integrity) {
            // old backups (from stopped apps) do not integrity information
            progressCallback({ message: `Skipping ${depBackup.remotePath} (no integrity data)` });
            await backups.setIntegrityResult(depBackup, 'skipped', { stats: null, messages: [ 'Old backups do not have integrity information' ] });
            completed++;
            continue;
        }

        const result = await verify(depBackup, backupSite, progressCallback); // { stats, messages }
        completed++;

        await backups.setIntegrityResult(depBackup, result.messages.length === 0 ? 'passed' : 'failed', result);
        if (result.messages.length) aggregatedMessages.push(`Integrity check of dependent backup ${depBackup.remotePath} failed`);
    }

    progressCallback({ percent: Math.round(completed / total * 100), message: `Verifying ${backup.remotePath}` });
    const result = await verify(backup, backupSite, progressCallback);
    aggregatedStats.duration = Date.now() - aggregatedStats.startTime;
    aggregatedMessages.push(...result.messages);
    const status = aggregatedMessages.length === 0 ? 'passed' : 'failed';
    await backups.setIntegrityResult(backup, status, { stats: aggregatedStats, messages: aggregatedMessages });
    return status;
}

export default {
    check
};
