import { hashProjectPath } from '../../storage/base-store.js';
import { buildProjectKnowledgeGraph } from './builder.js';
import { getProjectGraphFreshness, loadProjectGraphPolicy, readProjectGraph } from './cache.js';
function escapeInert(text) {
    return text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
export function redactGraphText(text, policy, maxChars = policy.redaction.maxSnippetChars) {
    const truncated = text.slice(0, maxChars);
    return policy.redaction.redactPatterns.reduce((current, pattern) => {
        const re = new RegExp(pattern, 'gi');
        return current.replace(re, '[redacted]');
    }, truncated);
}
function sanitizeLabel(label, policy) {
    return escapeInert(redactGraphText(label, policy));
}
function relatedNodeIds(nodes, edges, input) {
    const seeds = new Set();
    for (const node of nodes) {
        const specMatch = input.specId !== undefined &&
            (node.id === `spec:${input.specId}` || node.metadata?.specId === input.specId);
        const fileMatch = input.filePath !== undefined &&
            (node.id === `file:${input.filePath}` || node.label === input.filePath);
        const nodeMatch = input.nodeId !== undefined && node.id === input.nodeId;
        if (specMatch || fileMatch || nodeMatch) {
            seeds.add(node.id);
        }
    }
    if (seeds.size === 0 && input.specId === undefined && input.filePath === undefined) {
        for (const node of nodes.slice(0, 20)) {
            seeds.add(node.id);
        }
    }
    const related = new Set(seeds);
    for (const edge of edges) {
        if (seeds.has(edge.from)) {
            related.add(edge.to);
        }
        if (seeds.has(edge.to)) {
            related.add(edge.from);
        }
    }
    return related;
}
function summarize(nodes) {
    const count = (type) => nodes.filter((node) => node.type === type).length;
    return {
        specs: count('spec'),
        criteria: count('criterion'),
        files: count('file'),
        tests: count('test'),
        decisions: count('decision'),
        risks: count('risk'),
        tools: count('tool'),
        releases: count('release'),
    };
}
export async function queryProjectGraphSlice(input) {
    const projectId = input.projectId ?? hashProjectPath(input.projectPath);
    const policy = await loadProjectGraphPolicy();
    if (!policy.enabled) {
        return null;
    }
    let freshness = await getProjectGraphFreshness({
        projectId,
        projectPath: input.projectPath,
        policy,
    });
    if (!freshness.exists ||
        freshness.reason === 'source_changed' ||
        freshness.reason === 'corrupt') {
        await buildProjectKnowledgeGraph({ projectId, projectPath: input.projectPath, policy });
        freshness = await getProjectGraphFreshness({
            projectId,
            projectPath: input.projectPath,
            policy,
        });
    }
    const graph = await readProjectGraph(projectId, policy);
    if (graph === null) {
        return null;
    }
    const maxNodes = input.maxNodes ?? policy.query.maxNodes;
    const maxEdges = input.maxEdges ?? policy.query.maxEdges;
    const ids = relatedNodeIds(graph.nodes, graph.edges, input);
    const nodes = graph.nodes.filter((node) => ids.has(node.id)).slice(0, maxNodes);
    const nodeIds = new Set(nodes.map((node) => node.id));
    const edges = graph.edges
        .filter((edge) => nodeIds.has(edge.from) && nodeIds.has(edge.to))
        .slice(0, maxEdges);
    return {
        graphVersion: graph.graphVersion,
        generatedAt: graph.generatedAt,
        freshness,
        nodes,
        edges,
        summary: summarize(nodes),
        tokenSavings: {
            compactNodes: nodes.length,
            compactEdges: edges.length,
            estimatedRawContextAvoided: Math.max(0, graph.nodes.length + graph.edges.length - nodes.length - edges.length),
        },
    };
}
function listByType(slice, type, policy) {
    return slice.nodes
        .filter((node) => node.type === type)
        .map((node) => sanitizeLabel(node.label, policy))
        .slice(0, policy.query.maxListItems);
}
export async function formatProjectGraphContext(slice) {
    const policy = await loadProjectGraphPolicy();
    const files = listByType(slice, 'file', policy);
    const tests = listByType(slice, 'test', policy);
    const decisions = listByType(slice, 'decision', policy);
    const risks = listByType(slice, 'risk', policy);
    const specs = listByType(slice, 'spec', policy);
    const tools = listByType(slice, 'tool', policy);
    const lines = ['## Graph Context', ''];
    lines.push(`Freshness: ${slice.freshness.stale ? `stale (${slice.freshness.reason})` : 'fresh'}; compact slice: ${String(slice.nodes.length)} nodes / ${String(slice.edges.length)} edges.`);
    if (files.length > 0) {
        lines.push(`Related files: ${files.join(', ')}`);
    }
    if (tests.length > 0) {
        lines.push(`Related tests: ${tests.join(', ')}`);
    }
    if (decisions.length > 0) {
        lines.push(`Decisions: ${decisions.join('; ')}`);
    }
    if (risks.length > 0) {
        lines.push(`Risks: ${risks.join('; ')}`);
    }
    if (specs.length > 1) {
        lines.push(`Neighboring specs: ${specs.join(', ')}`);
    }
    if (tools.length > 0) {
        lines.push(`Tools: ${tools.join(', ')}`);
    }
    lines.push('');
    return lines.join('\n');
}
export async function getProjectGraphFreshnessHint(args) {
    const projectId = args.projectId ?? hashProjectPath(args.projectPath);
    const policy = await loadProjectGraphPolicy();
    if (!policy.enabled) {
        return undefined;
    }
    const freshness = await getProjectGraphFreshness({
        projectId,
        projectPath: args.projectPath,
        policy,
    });
    if (!freshness.stale) {
        return undefined;
    }
    const suffix = freshness.changedSources.length > 0
        ? `; ${String(freshness.changedSources.length)} changed source(s)`
        : '';
    return `project graph ${freshness.reason}${suffix}`;
}
//# sourceMappingURL=query.js.map