// @crash-shield-ignore-file — graph artifacts are Planu-owned JSON written by this subsystem.
import { createHash } from 'node:crypto';
import { readdir, readFile, stat } from 'node:fs/promises';
import { dirname, join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { projectDataDir, readJson, writeJson } from '../../storage/base-store.js';
const POLICY_PATH = join(dirname(fileURLToPath(import.meta.url)), '../../config/project-knowledge-graph.json');
export async function loadProjectGraphPolicy() {
    const raw = await readFile(POLICY_PATH, 'utf-8');
    return JSON.parse(raw);
}
export function hashText(text) {
    return createHash('sha256').update(text, 'utf8').digest('hex');
}
export function projectGraphDir(projectId, policy) {
    return join(projectDataDir(projectId), policy.artifactPaths.directory);
}
export function projectGraphPath(projectId, policy) {
    return join(projectGraphDir(projectId, policy), policy.artifactPaths.graph);
}
export function projectGraphCachePath(projectId, policy) {
    return join(projectGraphDir(projectId, policy), policy.artifactPaths.sourceHashes);
}
export async function readProjectGraph(projectId, policy) {
    return readJson(projectGraphPath(projectId, policy), null);
}
export async function writeProjectGraph(projectId, policy, graph) {
    await writeJson(projectGraphPath(projectId, policy), graph);
}
export async function readProjectGraphSourceHashes(projectId, policy) {
    return readJson(projectGraphCachePath(projectId, policy), {});
}
export async function writeProjectGraphSourceHashes(projectId, policy, hashes) {
    await writeJson(projectGraphCachePath(projectId, policy), hashes);
}
export function diffSourceHashes(sources, previous) {
    const next = Object.fromEntries(sources.map((source) => [source.id, source.hash]));
    const changed = sources
        .filter((source) => previous[source.id] !== source.hash)
        .map((source) => source.id);
    const skipped = sources
        .filter((source) => previous[source.id] === source.hash)
        .map((source) => source.id);
    return { changed, skipped, next };
}
async function fileExists(path) {
    try {
        const info = await stat(path);
        return info.isFile();
    }
    catch {
        return false;
    }
}
async function collectFiles(dir, predicate) {
    try {
        const entries = await readdir(dir, { withFileTypes: true });
        const nested = await Promise.all(entries.map(async (entry) => {
            const path = join(dir, entry.name);
            if (entry.isDirectory()) {
                return collectFiles(path, predicate);
            }
            return entry.isFile() && predicate(path) ? [path] : [];
        }));
        return nested.flat();
    }
    catch {
        return [];
    }
}
async function sourceFromFile(args) {
    try {
        const content = await readFile(args.path, 'utf-8');
        return { ...args, content, hash: hashText(content) };
    }
    catch {
        return null;
    }
}
export async function collectProjectGraphSources(args) {
    const projectRoot = resolve(args.projectPath);
    const dataRoot = projectDataDir(args.projectId);
    const specFiles = await collectFiles(join(projectRoot, 'planu', 'specs'), (path) => path.endsWith('/spec.md'));
    const handoffFiles = await collectFiles(join(dataRoot, 'handoffs'), (path) => /\.(json|md)$/.test(path));
    const validationFiles = handoffFiles.filter((path) => path.includes('validation'));
    const transitionLog = join(dataRoot, 'transition-log.jsonl');
    const releaseEvents = join(dataRoot, '..', '..', 'release-events.jsonl');
    const raw = [
        ...specFiles.map((path) => ({
            id: `spec:${relative(projectRoot, path)}`,
            kind: 'spec',
            path,
        })),
        ...handoffFiles.map((path) => ({
            id: `handoff:${relative(dataRoot, path)}`,
            kind: validationFiles.includes(path) ? 'validation' : 'handoff',
            path,
        })),
        ...((await fileExists(transitionLog))
            ? [{ id: 'transition-log', kind: 'transition-log', path: transitionLog }]
            : []),
        ...((await fileExists(releaseEvents))
            ? [{ id: 'release-events', kind: 'release-events', path: releaseEvents }]
            : []),
    ];
    const sources = await Promise.all(raw.map((source) => sourceFromFile(source)));
    return sources.filter((source) => source !== null);
}
export async function getProjectGraphFreshness(args) {
    const policy = args.policy ?? (await loadProjectGraphPolicy());
    const graphPath = projectGraphPath(args.projectId, policy);
    const graph = await readProjectGraph(args.projectId, policy);
    if (graph === null) {
        return { exists: false, stale: true, reason: 'missing', graphPath, changedSources: [] };
    }
    if (!Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) {
        return { exists: true, stale: true, reason: 'corrupt', graphPath, changedSources: [] };
    }
    const generatedAtMs = Date.parse(graph.generatedAt);
    if (Number.isFinite(generatedAtMs)) {
        const ageMinutes = (Date.now() - generatedAtMs) / 60_000;
        if (ageMinutes > policy.freshness.staleAfterMinutes) {
            return {
                exists: true,
                stale: true,
                reason: 'expired',
                graphPath,
                generatedAt: graph.generatedAt,
                changedSources: [],
            };
        }
    }
    const sources = await collectProjectGraphSources({
        projectId: args.projectId,
        projectPath: args.projectPath,
        policy,
    });
    const changedSources = sources
        .filter((source) => graph.sourceHashes[source.id] !== source.hash)
        .map((source) => source.id);
    if (changedSources.length > 0) {
        return {
            exists: true,
            stale: true,
            reason: 'source_changed',
            graphPath,
            generatedAt: graph.generatedAt,
            changedSources,
        };
    }
    return {
        exists: true,
        stale: false,
        reason: 'fresh',
        graphPath,
        generatedAt: graph.generatedAt,
        changedSources: [],
    };
}
//# sourceMappingURL=cache.js.map