/**
 * Incremental Certification Caching
 *
 * Tracks file hashes to enable incremental certification. Instead of re-auditing
 * the entire codebase, only changed files are analyzed in subsequent runs.
 *
 * Cache structure:
 * ```
 * .vaspera/certifications/{cert-id}/cache.json
 * ```
 *
 * @module certification/cache
 */
import { createHash } from "crypto";
import { readFile, writeFile, readdir, stat } from "fs/promises";
import { join, relative } from "path";
import { logger } from "../logger.js";
/**
 * Default file patterns to include in hashing
 */
const DEFAULT_INCLUDE_PATTERNS = [
    /\.(ts|tsx|js|jsx|mjs|cjs)$/,
    /\.(json)$/,
    /\.(sql)$/,
    /\.(env\.example)$/,
];
/**
 * Patterns to exclude from hashing
 */
const EXCLUDE_PATTERNS = [
    /node_modules/,
    /\.git/,
    /dist/,
    /build/,
    /coverage/,
    /\.vaspera/,
    /\.next/,
    /\.turbo/,
    /\.cache/,
];
/**
 * Compute SHA-256 hash of a file
 *
 * @param filePath - Absolute path to the file
 * @returns Hex-encoded SHA-256 hash
 */
export async function hashFile(filePath) {
    const content = await readFile(filePath);
    return createHash("sha256").update(content).digest("hex");
}
/**
 * Compute a combined hash of multiple file hashes
 *
 * @param hashes - Array of file hashes to combine
 * @returns Hex-encoded SHA-256 hash of all files
 */
export function computeProjectHash(hashes) {
    const combined = hashes
        .slice()
        .sort((a, b) => a.path.localeCompare(b.path))
        .map((f) => `${f.path}:${f.hash}`)
        .join("\n");
    return createHash("sha256").update(combined).digest("hex");
}
/**
 * Check if a file path should be included in hashing
 */
function shouldIncludeFile(filePath) {
    // Check exclusions first
    for (const pattern of EXCLUDE_PATTERNS) {
        if (pattern.test(filePath)) {
            return false;
        }
    }
    // Check inclusions
    for (const pattern of DEFAULT_INCLUDE_PATTERNS) {
        if (pattern.test(filePath)) {
            return true;
        }
    }
    return false;
}
/**
 * Recursively scan a directory for hashable files
 *
 * @param dirPath - Directory to scan
 * @param projectRoot - Project root for relative paths
 * @returns Array of absolute file paths
 */
async function scanDirectory(dirPath, projectRoot) {
    const files = [];
    try {
        const entries = await readdir(dirPath, { withFileTypes: true });
        for (const entry of entries) {
            const fullPath = join(dirPath, entry.name);
            const relativePath = relative(projectRoot, fullPath);
            // Skip excluded directories
            let skip = false;
            for (const pattern of EXCLUDE_PATTERNS) {
                if (pattern.test(relativePath)) {
                    skip = true;
                    break;
                }
            }
            if (skip)
                continue;
            if (entry.isDirectory()) {
                const subFiles = await scanDirectory(fullPath, projectRoot);
                files.push(...subFiles);
            }
            else if (entry.isFile() && shouldIncludeFile(entry.name)) {
                files.push(fullPath);
            }
        }
    }
    catch (error) {
        logger.warn("cache.scan_error", { dir: dirPath, error: String(error) });
    }
    return files;
}
/**
 * Generate file hashes for all relevant files in a project
 *
 * @param projectPath - Absolute path to project root
 * @returns Array of file hashes
 */
export async function generateFileHashes(projectPath) {
    const filePaths = await scanDirectory(projectPath, projectPath);
    const hashes = [];
    const now = new Date().toISOString();
    for (const filePath of filePaths) {
        try {
            const [hash, stats] = await Promise.all([hashFile(filePath), stat(filePath)]);
            hashes.push({
                path: relative(projectPath, filePath),
                hash,
                lastChecked: now,
                size: stats.size,
            });
        }
        catch (error) {
            logger.warn("cache.hash_error", { file: filePath, error: String(error) });
        }
    }
    logger.debug("cache.hashes_generated", {
        project: projectPath,
        fileCount: hashes.length,
    });
    return hashes;
}
/**
 * Load certification cache from disk
 *
 * @param certDir - Certification directory path
 * @returns Cache object or null if not found/invalid
 */
export async function loadCache(certDir) {
    try {
        const cachePath = join(certDir, "cache.json");
        const content = await readFile(cachePath, "utf-8");
        const cache = JSON.parse(content);
        // Validate cache version
        if (cache.version !== 1) {
            logger.warn("cache.version_mismatch", { expected: 1, got: cache.version });
            return null;
        }
        return cache;
    }
    catch {
        return null;
    }
}
/**
 * Save certification cache to disk
 *
 * @param certDir - Certification directory path
 * @param cache - Cache object to save
 */
export async function saveCache(certDir, cache) {
    const cachePath = join(certDir, "cache.json");
    await writeFile(cachePath, JSON.stringify(cache, null, 2), "utf-8");
    logger.debug("cache.saved", {
        certDir,
        fileCount: cache.files.length,
        projectHash: cache.projectHash.slice(0, 12),
    });
}
/**
 * Create a new cache from current project state
 *
 * @param projectPath - Absolute path to project root
 * @returns New certification cache
 */
export async function createCache(projectPath) {
    const files = await generateFileHashes(projectPath);
    const projectHash = computeProjectHash(files);
    const now = new Date().toISOString();
    return {
        version: 1,
        projectHash,
        createdAt: now,
        updatedAt: now,
        files,
    };
}
/**
 * Compare current project state against a cached state
 *
 * @param projectPath - Absolute path to project root
 * @param cache - Previous certification cache
 * @returns Comparison result showing what changed
 */
export async function compareWithCache(projectPath, cache) {
    const currentHashes = await generateFileHashes(projectPath);
    const oldHashMap = new Map(cache.files.map((f) => [f.path, f.hash]));
    const newHashMap = new Map(currentHashes.map((f) => [f.path, f.hash]));
    const changed = [];
    const added = [];
    const removed = [];
    const unchanged = [];
    // Check current files against cache
    for (const file of currentHashes) {
        const oldHash = oldHashMap.get(file.path);
        if (oldHash === undefined) {
            added.push(file.path);
        }
        else if (oldHash !== file.hash) {
            changed.push(file.path);
        }
        else {
            unchanged.push(file.path);
        }
    }
    // Check for removed files
    for (const oldFile of cache.files) {
        if (!newHashMap.has(oldFile.path)) {
            removed.push(oldFile.path);
        }
    }
    const cacheValid = changed.length === 0 && added.length === 0 && removed.length === 0;
    logger.info("cache.compared", {
        changed: changed.length,
        added: added.length,
        removed: removed.length,
        unchanged: unchanged.length,
        cacheValid,
    });
    return {
        changed,
        added,
        removed,
        unchanged,
        cacheValid,
    };
}
/**
 * Get list of files that need re-auditing based on cache comparison
 *
 * @param comparison - Result of cache comparison
 * @returns Array of file paths that need to be re-audited
 */
export function getFilesToAudit(comparison) {
    return [...comparison.changed, ...comparison.added];
}
/**
 * Check if incremental audit is possible (cache exists and is valid structure)
 *
 * @param certDir - Certification directory path
 * @returns Whether incremental audit can be performed
 */
export async function canUseIncrementalAudit(certDir) {
    const cache = await loadCache(certDir);
    return cache !== null && cache.version === 1 && cache.files.length > 0;
}
//# sourceMappingURL=cache.js.map