import { createHash } from 'node:crypto';
import path from 'node:path';
import { z } from 'zod';
import { compareCodeUnits, normalizeRelativePath } from '../utils.js';
import { buildCodeModuleClusterAssignment } from './clusters.js';
import { CodeNodeTypeSchema, CodeSourceSpanV1Schema, } from './types.js';
export const CODE_FLOW_TRACES_VERSION = 1;
export const CODE_FLOW_DEFAULT_LIMIT = 50;
export const CODE_FLOW_HARD_LIMIT = 500;
export const CODE_FLOW_DEFAULT_MAX_CANDIDATES = 100;
export const CODE_FLOW_HARD_MAX_CANDIDATES = 5_000;
export const CODE_FLOW_DEFAULT_MAX_DEPTH = 10;
export const CODE_FLOW_HARD_MAX_DEPTH = 50;
export const CODE_FLOW_DEFAULT_MAX_BRANCHING = 5;
export const CODE_FLOW_HARD_MAX_BRANCHING = 100;
export const CODE_FLOW_DEFAULT_MAX_STEPS = 500;
export const CODE_FLOW_HARD_MAX_STEPS = 10_000;
export const CodeFlowTraceOptionsSchema = z.object({
    limit: z.number().int().positive().max(CODE_FLOW_HARD_LIMIT).optional(),
    maxCandidates: z.number().int().positive().max(CODE_FLOW_HARD_MAX_CANDIDATES).optional(),
    maxDepth: z.number().int().positive().max(CODE_FLOW_HARD_MAX_DEPTH).optional(),
    maxBranching: z.number().int().positive().max(CODE_FLOW_HARD_MAX_BRANCHING).optional(),
    maxSteps: z.number().int().min(2).max(CODE_FLOW_HARD_MAX_STEPS).optional(),
});
export const CodeFlowStepSchema = z.object({
    nodeId: z.string(),
    name: z.string(),
    kind: CodeNodeTypeSchema,
    relativePath: z.string(),
    span: CodeSourceSpanV1Schema,
    depth: z.number().int().nonnegative(),
    clusterId: z.string().optional(),
});
export const CodeFlowEntryPointSchema = CodeFlowStepSchema.omit({ depth: true, clusterId: true }).extend({
    score: z.number().positive(),
    clusterId: z.string().optional(),
});
export const CodeFlowTraceSchema = z.object({
    id: z.string(),
    label: z.string(),
    entryPoint: CodeFlowEntryPointSchema,
    steps: z.array(CodeFlowStepSchema).min(2),
    crossCluster: z.boolean(),
    clusterIds: z.array(z.string()),
    truncated: z.boolean(),
    omitted: z.object({
        branchingTargets: z.number().int().nonnegative(),
        depthFrontierTargets: z.number().int().nonnegative(),
        stepLimitTargets: z.number().int().nonnegative(),
    }),
    cycleEdges: z.number().int().nonnegative(),
    revisitedEdges: z.number().int().nonnegative(),
});
const CallCoverageCountsSchema = z.object({
    full: z.number().int().nonnegative(),
    partial: z.number().int().nonnegative(),
    unavailable: z.number().int().nonnegative(),
});
export const CodeFlowTraceResultSchema = z.object({
    status: z.literal('resolved'),
    version: z.literal(CODE_FLOW_TRACES_VERSION),
    traces: z.array(CodeFlowTraceSchema),
    total: z.number().int().nonnegative(),
    omitted: z.number().int().nonnegative(),
    truncated: z.boolean(),
    honesty: z.object({
        freshness: z.literal('index-snapshot'),
        stalenessReason: z.string(),
        coverage: z.object({
            status: z.enum(['full', 'partial']),
            indexedFiles: z.number().int().nonnegative(),
            parserCallCapability: CallCoverageCountsSchema,
            callEdges: z.object({
                resolved: z.number().int().nonnegative(),
                ambiguous: z.number().int().nonnegative(),
                unresolved: z.number().int().nonnegative(),
            }),
            dynamicDispatchRefusals: z.number().int().nonnegative(),
            resolverJoinMisses: z.number().int().nonnegative(),
            unreachableResolvedEndpoints: z.number().int().nonnegative(),
            skippedFiles: z.number().int().nonnegative(),
            budgetDroppedFiles: z.number().int().nonnegative(),
        }),
        omissions: z.object({
            candidates: z.object({
                total: z.number().int().nonnegative(),
                evaluated: z.number().int().nonnegative(),
                omitted: z.number().int().nonnegative(),
            }),
            results: z.object({
                total: z.number().int().nonnegative(),
                returned: z.number().int().nonnegative(),
                omitted: z.number().int().nonnegative(),
                deduplicated: z.number().int().nonnegative(),
                belowMinimumSteps: z.number().int().nonnegative(),
            }),
            frontier: z.object({
                branchingTargets: z.number().int().nonnegative(),
                depthFrontierTargets: z.number().int().nonnegative(),
                stepLimitTargets: z.number().int().nonnegative(),
            }),
        }),
        bounds: z.object({
            maxTraces: z.number().int().positive(),
            maxCandidates: z.number().int().positive(),
            maxDepth: z.number().int().positive(),
            maxBranching: z.number().int().positive(),
            maxSteps: z.number().int().min(2),
        }),
    }),
});
function lengthFramedFlowId(nodeIds) {
    const hash = createHash('sha256');
    hash.update('occ.flow-traces.v1\0', 'utf8');
    for (const nodeId of nodeIds) {
        const bytes = Buffer.from(nodeId, 'utf8');
        hash.update(`${bytes.byteLength}:`, 'ascii');
        hash.update(bytes);
    }
    return `flow1:${hash.digest('hex')}`;
}
function nodeRelativePath(node) {
    return normalizeRelativePath(node.relativePath ?? node.path);
}
function nodeSpan(node) {
    if (node.span)
        return node.span;
    const startLine = Math.max(1, node.line ?? 1);
    return {
        version: 1,
        offsetEncoding: 'utf8-bytes',
        endExclusive: true,
        startLine,
        endLine: Math.max(startLine, node.endLine ?? startLine),
        exact: false,
    };
}
function compareNodes(left, right) {
    const leftSpan = nodeSpan(left);
    const rightSpan = nodeSpan(right);
    return (compareCodeUnits(nodeRelativePath(left), nodeRelativePath(right)) ||
        leftSpan.startLine - rightSpan.startLine ||
        (leftSpan.startColumn ?? 0) - (rightSpan.startColumn ?? 0) ||
        compareCodeUnits(left.id, right.id));
}
function isFrameworkEntryPath(relativePath) {
    const normalized = `/${normalizeRelativePath(relativePath)}`;
    if (/\/(?:pages|app|controllers|routes)\//u.test(normalized))
        return true;
    const basename = path.posix.basename(normalized).replace(/\.[^./]+$/u, '');
    if (/^[A-Z][A-Za-z\d]*$/u.test(basename))
        return true;
    return new Set(['app', 'cli', 'handler', 'index', 'main', 'server']).has(basename.toLowerCase());
}
function rankCandidates(index, assignment, nodeById) {
    const importedFiles = new Set(assignment.resolvedLocalImports.map(edge => edge.to));
    const byNodeId = new Map();
    for (const file of index.files) {
        const relativePath = normalizeRelativePath(file.relativePath);
        for (const symbol of file.symbols) {
            if (symbol.exported !== true || (symbol.type !== 'function' && symbol.type !== 'class') || !symbol.nodeId)
                continue;
            const node = nodeById.get(symbol.nodeId);
            if (!node)
                continue;
            let score = 1;
            if (!importedFiles.has(relativePath))
                score *= 2;
            if (isFrameworkEntryPath(relativePath))
                score *= 1.5;
            score *= 1.2;
            byNodeId.set(node.id, { node, symbol, file, relativePath, score: Number(score.toFixed(6)) });
        }
    }
    return [...byNodeId.values()].sort((left, right) => right.score - left.score || compareNodes(left.node, right.node));
}
function buildCallAdjacency(index, nodeById) {
    const targetIds = new Map();
    let unreachableResolvedEndpoints = 0;
    const orderedEdges = [...index.edges].sort((left, right) => compareCodeUnits(left.id, right.id));
    for (const edge of orderedEdges) {
        if (edge.type !== 'calls' || edge.status !== 'resolved' || edge.to === undefined)
            continue;
        const source = nodeById.get(edge.from);
        const target = nodeById.get(edge.to);
        if (!source || !target) {
            unreachableResolvedEndpoints++;
            continue;
        }
        const targets = targetIds.get(edge.from) ?? new Set();
        targets.add(target.id);
        targetIds.set(edge.from, targets);
    }
    const adjacency = new Map();
    for (const [source, ids] of targetIds) {
        adjacency.set(source, [...ids]
            .map(id => nodeById.get(id))
            .filter((node) => node !== undefined)
            .sort(compareNodes));
    }
    return { adjacency, unreachableResolvedEndpoints };
}
function toStep(node, depth, assignment) {
    const relativePath = nodeRelativePath(node);
    const clusterId = assignment.clusterIdByPath.get(relativePath);
    return {
        nodeId: node.id,
        name: node.name,
        kind: node.type,
        relativePath,
        span: nodeSpan(node),
        depth,
        ...(clusterId === undefined ? {} : { clusterId }),
    };
}
function traceLabel(steps, parents) {
    // Arrows assert proved caller→callee links, so the label follows the BFS
    // parent chain instead of joining adjacent (possibly sibling) steps.
    const first = steps[0];
    if (!first)
        return '';
    const chain = [first];
    while (chain.length < 3) {
        const tail = chain[chain.length - 1];
        const next = steps.find(step => parents.get(step.nodeId) === tail?.nodeId);
        if (!next)
            break;
        chain.push(next);
    }
    const shown = chain.map(step => step.name);
    return `${shown.join(' → ')}${steps.length > shown.length ? ' → …' : ''}`;
}
function traceCandidate(candidate, adjacency, assignment, options) {
    const steps = [toStep(candidate.node, 0, assignment)];
    const pending = [{ nodeId: candidate.node.id, depth: 0 }];
    const seen = new Set([candidate.node.id]);
    const parents = new Map();
    let branchingTargets = 0;
    let depthFrontierTargets = 0;
    let stepLimitTargets = 0;
    let cycleEdges = 0;
    let revisitedEdges = 0;
    let stoppedForSteps = false;
    // A re-encountered target is a cycle only when it sits on the current
    // node's BFS ancestor path; a shared callee (diamond) is a revisit.
    const countReencounter = (targetId, fromId) => {
        let cursor = fromId;
        while (cursor !== undefined) {
            if (cursor === targetId) {
                cycleEdges++;
                return;
            }
            cursor = parents.get(cursor);
        }
        revisitedEdges++;
    };
    while (pending.length > 0 && !stoppedForSteps) {
        const current = pending.shift();
        if (!current)
            break;
        const targets = adjacency.get(current.nodeId) ?? [];
        if (current.depth >= options.maxDepth) {
            for (const target of targets) {
                if (seen.has(target.id))
                    countReencounter(target.id, current.nodeId);
                else
                    depthFrontierTargets++;
            }
            continue;
        }
        const unseen = [];
        for (const target of targets) {
            if (seen.has(target.id))
                countReencounter(target.id, current.nodeId);
            else
                unseen.push(target);
        }
        const selected = unseen.slice(0, options.maxBranching);
        branchingTargets += Math.max(0, unseen.length - selected.length);
        for (let index = 0; index < selected.length; index++) {
            const target = selected[index];
            if (!target)
                continue;
            if (steps.length >= options.maxSteps) {
                stepLimitTargets += selected.length - index;
                const pendingIds = new Set(pending.map(item => item.nodeId));
                for (const frontier of pending) {
                    for (const next of adjacency.get(frontier.nodeId) ?? []) {
                        if (!seen.has(next.id) && !pendingIds.has(next.id))
                            stepLimitTargets++;
                    }
                }
                stoppedForSteps = true;
                break;
            }
            seen.add(target.id);
            parents.set(target.id, current.nodeId);
            const depth = current.depth + 1;
            steps.push(toStep(target, depth, assignment));
            pending.push({ nodeId: target.id, depth });
        }
    }
    if (steps.length < 2)
        return undefined;
    const clusterIds = [...new Set(steps.flatMap(step => (step.clusterId ? [step.clusterId] : [])))].sort(compareCodeUnits);
    const omitted = { branchingTargets, depthFrontierTargets, stepLimitTargets };
    const entryClusterId = assignment.clusterIdByPath.get(candidate.relativePath);
    return {
        id: lengthFramedFlowId(steps.map(step => step.nodeId)),
        label: traceLabel(steps, parents),
        entryPoint: {
            nodeId: candidate.node.id,
            name: candidate.node.name,
            kind: candidate.node.type,
            relativePath: candidate.relativePath,
            span: nodeSpan(candidate.node),
            score: candidate.score,
            ...(entryClusterId === undefined ? {} : { clusterId: entryClusterId }),
        },
        steps,
        crossCluster: clusterIds.length > 1,
        clusterIds,
        truncated: branchingTargets + depthFrontierTargets + stepLimitTargets > 0,
        omitted,
        cycleEdges,
        revisitedEdges,
    };
}
function callCapabilityCounts(files) {
    const counts = { full: 0, partial: 0, unavailable: 0 };
    for (const file of files)
        counts[file.provenance?.capabilities.calls ?? 'unavailable']++;
    return counts;
}
function resolverJoinMisses(files) {
    let misses = 0;
    for (const file of files) {
        for (const diagnostic of file.provenance?.resolver.diagnostics ?? []) {
            if (diagnostic.reason === 'resolver-join-miss')
                misses += diagnostic.counts?.misses ?? 1;
        }
    }
    return misses;
}
/** Trace bounded deterministic call flows from ranked exported checkout entry points. */
export function traceCodeFlows(index, rawOptions = {}) {
    const parsed = CodeFlowTraceOptionsSchema.parse(rawOptions);
    const options = {
        limit: parsed.limit ?? CODE_FLOW_DEFAULT_LIMIT,
        maxCandidates: parsed.maxCandidates ?? CODE_FLOW_DEFAULT_MAX_CANDIDATES,
        maxDepth: parsed.maxDepth ?? CODE_FLOW_DEFAULT_MAX_DEPTH,
        maxBranching: parsed.maxBranching ?? CODE_FLOW_DEFAULT_MAX_BRANCHING,
        maxSteps: parsed.maxSteps ?? CODE_FLOW_DEFAULT_MAX_STEPS,
    };
    const assignment = buildCodeModuleClusterAssignment(index);
    const nodeById = new Map(index.nodes.map(node => [node.id, node]));
    const allCandidates = rankCandidates(index, assignment, nodeById);
    const candidates = allCandidates.slice(0, options.maxCandidates);
    const callGraph = buildCallAdjacency(index, nodeById);
    const unique = new Map();
    let deduplicated = 0;
    let belowMinimumSteps = 0;
    for (const candidate of candidates) {
        const trace = traceCandidate(candidate, callGraph.adjacency, assignment, options);
        if (!trace) {
            belowMinimumSteps++;
            continue;
        }
        const chainKey = JSON.stringify(trace.steps.map(step => step.nodeId));
        if (unique.has(chainKey))
            deduplicated++;
        else
            unique.set(chainKey, trace);
    }
    const allTraces = [...unique.values()];
    const traces = allTraces.slice(0, options.limit);
    const omitted = Math.max(0, allTraces.length - traces.length);
    const frontier = traces.reduce((totals, trace) => ({
        branchingTargets: totals.branchingTargets + trace.omitted.branchingTargets,
        depthFrontierTargets: totals.depthFrontierTargets + trace.omitted.depthFrontierTargets,
        stepLimitTargets: totals.stepLimitTargets + trace.omitted.stepLimitTargets,
    }), { branchingTargets: 0, depthFrontierTargets: 0, stepLimitTargets: 0 });
    const callEdges = { resolved: 0, ambiguous: 0, unresolved: 0 };
    for (const edge of index.edges) {
        if (edge.type !== 'calls')
            continue;
        callEdges[edge.status]++;
    }
    const parserCallCapability = callCapabilityCounts(index.files);
    const dynamicDispatchRefusals = index.parserRefusals?.dynamicDispatch ?? 0;
    const joinMisses = resolverJoinMisses(index.files);
    const budgetDroppedFiles = index.truncated?.droppedFiles ?? 0;
    const coveragePartial = parserCallCapability.partial > 0 ||
        parserCallCapability.unavailable > 0 ||
        callEdges.ambiguous > 0 ||
        callEdges.unresolved > 0 ||
        dynamicDispatchRefusals > 0 ||
        joinMisses > 0 ||
        callGraph.unreachableResolvedEndpoints > 0 ||
        index.skippedFiles.length > 0 ||
        budgetDroppedFiles > 0;
    const truncated = omitted > 0 || allCandidates.length > candidates.length || traces.some(trace => trace.truncated);
    return CodeFlowTraceResultSchema.parse({
        status: 'resolved',
        version: CODE_FLOW_TRACES_VERSION,
        traces,
        total: allTraces.length,
        omitted,
        truncated,
        honesty: {
            freshness: 'index-snapshot',
            stalenessReason: 'working tree was not rechecked after index construction',
            coverage: {
                status: coveragePartial ? 'partial' : 'full',
                indexedFiles: index.files.length,
                parserCallCapability,
                callEdges,
                dynamicDispatchRefusals,
                resolverJoinMisses: joinMisses,
                unreachableResolvedEndpoints: callGraph.unreachableResolvedEndpoints,
                skippedFiles: index.skippedFiles.length,
                budgetDroppedFiles,
            },
            omissions: {
                candidates: {
                    total: allCandidates.length,
                    evaluated: candidates.length,
                    omitted: allCandidates.length - candidates.length,
                },
                results: {
                    total: allTraces.length,
                    returned: traces.length,
                    omitted,
                    deduplicated,
                    belowMinimumSteps,
                },
                frontier,
            },
            bounds: {
                maxTraces: options.limit,
                maxCandidates: options.maxCandidates,
                maxDepth: options.maxDepth,
                maxBranching: options.maxBranching,
                maxSteps: options.maxSteps,
            },
        },
    });
}
//# sourceMappingURL=flows.js.map