"use strict";
// backend/src/backup/engine.ts
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.computeHash = computeHash;
exports.shouldExclude = shouldExclude;
exports.scanDirectory = scanDirectory;
exports.runBackup = runBackup;
exports.runBackupAll = runBackupAll;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const minimatch_1 = require("minimatch");
const uuid_1 = require("uuid");
const config_1 = require("./config");
const providers_1 = require("./providers");
const config_2 = require("../config");
function computeHash(filePath) {
    return new Promise((resolve, reject) => {
        const hash = crypto.createHash('sha256');
        const stream = fs.createReadStream(filePath);
        stream.on('data', (chunk) => hash.update(chunk));
        stream.on('end', () => resolve(`sha256:${hash.digest('hex')}`));
        stream.on('error', reject);
    });
}
function shouldExclude(relativePath, patterns) {
    const segments = relativePath.split('/');
    for (const pattern of patterns) {
        // Match full relative path
        if ((0, minimatch_1.minimatch)(relativePath, pattern, { dot: true, matchBase: true })) {
            return true;
        }
        // Match each path segment individually
        for (const segment of segments) {
            if ((0, minimatch_1.minimatch)(segment, pattern, { dot: true, matchBase: true })) {
                return true;
            }
        }
    }
    return false;
}
const MAX_SCAN_DEPTH = 50;
function scanDirectory(dirPath, excludePatterns, basePath, depth = 0) {
    const result = new Map();
    if (depth > MAX_SCAN_DEPTH)
        return result; // prevent symlink cycles
    const base = basePath ?? dirPath;
    let entries;
    try {
        entries = fs.readdirSync(dirPath, { withFileTypes: true });
    }
    catch {
        return result;
    }
    for (const entry of entries) {
        const fullPath = path.join(dirPath, entry.name);
        const relativePath = path.relative(base, fullPath).replace(/\\/g, '/');
        if (shouldExclude(relativePath, excludePatterns)) {
            continue;
        }
        if (entry.isDirectory()) {
            const subResult = scanDirectory(fullPath, excludePatterns, base, depth + 1);
            for (const [relPath, info] of subResult) {
                result.set(relPath, info);
            }
        }
        else if (entry.isFile()) {
            try {
                const stat = fs.statSync(fullPath);
                result.set(relativePath, {
                    mtime: stat.mtimeMs,
                    size: stat.size,
                });
            }
            catch {
                // Skip unreadable files
            }
        }
    }
    return result;
}
async function runBackup(projectId, onProgress) {
    const config = (0, config_1.getBackupConfig)();
    const authorizedProviders = config.providers.filter((p) => p.tokens);
    if (authorizedProviders.length === 0) {
        return [];
    }
    const projects = (0, config_2.getProjects)();
    const project = projects.find((p) => p.id === projectId && !p.archived);
    if (!project) {
        throw new Error(`Project ${projectId} not found or archived`);
    }
    if (!fs.existsSync(project.folderPath)) {
        throw new Error(`Project folder does not exist: ${project.folderPath}`);
    }
    const excludePatterns = config.excludePatterns ?? [];
    // Step 1: Scan local files
    const scanned = scanDirectory(project.folderPath, excludePatterns);
    // Step 2: Load previous backup state
    const prevState = (0, config_1.getBackupState)(project.folderPath);
    // Step 3: Determine which files need uploading (changed or new)
    const newFiles = {};
    const toUpload = []; // relative paths
    for (const [relPath, { mtime, size }] of scanned) {
        const prev = prevState.files[relPath];
        if (prev && prev.mtime === mtime && prev.size === size) {
            // Unchanged: keep previous snapshot
            newFiles[relPath] = prev;
        }
        else {
            // Changed or new: compute hash
            const fullPath = path.join(project.folderPath, relPath);
            let hash;
            try {
                hash = await computeHash(fullPath);
            }
            catch {
                // Unreadable file, skip
                continue;
            }
            if (prev && prev.hash === hash) {
                // Content unchanged, just update mtime/size
                newFiles[relPath] = { mtime, size, hash };
            }
            else {
                // Content changed: queue for upload
                newFiles[relPath] = { mtime, size, hash };
                toUpload.push(relPath);
            }
        }
    }
    // Step 4: Detect deletions (files in prev state no longer present locally)
    const toDelete = [];
    for (const relPath of Object.keys(prevState.files)) {
        if (!scanned.has(relPath)) {
            toDelete.push(relPath);
        }
    }
    const totalFiles = scanned.size;
    const startTime = new Date().toISOString();
    const results = [];
    // Step 5: Run backup for each provider in parallel
    const providerResults = await Promise.all(authorizedProviders.map(async (providerConfig) => {
        const provider = (0, providers_1.createProvider)(providerConfig);
        let filesUploaded = 0;
        let filesDeleted = 0;
        let entryStatus = 'success';
        let errorMsg;
        const reportProgress = (status, uploaded = filesUploaded, deleted = filesDeleted) => {
            onProgress?.({
                projectId,
                projectName: project.name,
                providerId: providerConfig.id,
                providerLabel: providerConfig.label,
                status,
                filesUploaded: uploaded,
                filesDeleted: deleted,
                filesTotal: totalFiles,
            });
        };
        try {
            const tokensBefore = JSON.stringify(providerConfig.tokens);
            await provider.ensureAuth();
            // Persist refreshed tokens to disk if they changed
            // Re-read config to avoid overwriting other providers' token changes
            if (JSON.stringify(providerConfig.tokens) !== tokensBefore) {
                const freshConfig = (0, config_1.getBackupConfig)();
                const freshIdx = freshConfig.providers.findIndex((p) => p.id === providerConfig.id);
                if (freshIdx >= 0) {
                    freshConfig.providers[freshIdx] = { ...freshConfig.providers[freshIdx], tokens: providerConfig.tokens };
                    (0, config_1.saveBackupConfig)(freshConfig);
                }
            }
            // Create base remote directory
            const baseRemotePath = `CCWeb/${project.name}`;
            await provider.mkdir(baseRemotePath);
            // Upload changed files
            reportProgress('uploading');
            for (const relPath of toUpload) {
                const localPath = path.join(project.folderPath, relPath);
                const remotePath = `${baseRemotePath}/${relPath}`;
                // Ensure parent directory exists
                const remoteDir = path.dirname(remotePath).replace(/\\/g, '/');
                if (remoteDir !== baseRemotePath) {
                    await provider.mkdir(remoteDir);
                }
                try {
                    await provider.uploadFile(localPath, remotePath);
                    filesUploaded++;
                    reportProgress('uploading');
                }
                catch (err) {
                    // Continue on individual file error
                    entryStatus = 'partial';
                }
            }
            // Delete orphaned remote files
            reportProgress('deleting', filesUploaded, filesDeleted);
            for (const relPath of toDelete) {
                const remotePath = `${baseRemotePath}/${relPath}`;
                try {
                    await provider.deleteFile(remotePath);
                    filesDeleted++;
                    reportProgress('deleting', filesUploaded, filesDeleted);
                }
                catch {
                    // Ignore delete errors (file may already be gone)
                }
            }
            reportProgress('done', filesUploaded, filesDeleted);
        }
        catch (err) {
            entryStatus = 'failed';
            errorMsg = err?.message ?? String(err);
            reportProgress('error', filesUploaded, filesDeleted);
        }
        const endTime = new Date().toISOString();
        const entry = {
            id: (0, uuid_1.v4)(),
            projectId,
            projectName: project.name,
            providerId: providerConfig.id,
            providerType: providerConfig.type,
            providerLabel: providerConfig.label,
            startTime,
            endTime,
            status: entryStatus,
            filesUploaded,
            filesDeleted,
            filesTotal: totalFiles,
            error: errorMsg,
        };
        return { entry, success: entryStatus !== 'failed' };
    }));
    // Step 6: If any provider succeeded, save new backup state
    const anySuccess = providerResults.some((r) => r.success);
    if (anySuccess) {
        const newState = {
            lastBackupTime: new Date().toISOString(),
            files: newFiles,
        };
        (0, config_1.saveBackupState)(project.folderPath, newState);
    }
    // Step 7: Save history entries and collect results
    for (const { entry } of providerResults) {
        (0, config_1.addBackupHistory)(entry);
        results.push(entry);
    }
    return results;
}
async function runBackupAll(onProgress) {
    const projects = (0, config_2.getProjects)().filter((p) => !p.archived);
    const allResults = [];
    for (const project of projects) {
        try {
            const entries = await runBackup(project.id, onProgress);
            allResults.push(...entries);
        }
        catch {
            // Continue with other projects
        }
    }
    return allResults;
}
//# sourceMappingURL=engine.js.map