import { moduleForRelativePath } from './query.js';
import { rankNodesCached } from './rank.js';
import { compareCodeUnits } from '../utils.js';
/**
 * Order-independent seam key: stable while both directories exist. Directory
 * renames change the key by design — consumers treat an unresolvable key as
 * drift, never as clean.
 */
export function seamKeyFor(moduleA, moduleB) {
    return `seam:${[moduleA, moduleB].sort().join('::')}`;
}
/**
 * Enumerate seams — cross-module relationships derived at query time from
 * `imports`/`calls` edges between directory-modules (grouped via
 * `moduleForRelativePath`, shared with `analyzeModuleCoupling`).
 *
 * Pure with respect to git: when the caller supplies a `GitCochangeSummary`
 * (from `getGitCochange`), its file pairs are aggregated to module pairs
 * (count summed, lift max, latest `lastCochangedAt`) and attached to the
 * matching seams. Ranking: lift desc, then summed `rankNodes` file scores of
 * both modules desc, then `seamKey` asc for determinism. `minLift`/`limit`
 * apply after scoring, so `rank` reflects the full enumeration.
 */
export function listCodeSeams(index, options = {}) {
    const relativePathByPath = new Map();
    for (const file of index.files) {
        relativePathByPath.set(file.path, file.relativePath);
    }
    const accumulators = new Map();
    for (const edge of index.edges) {
        if (edge.type !== 'imports' && edge.type !== 'calls')
            continue;
        if (edge.type === 'imports' && edge.importKind === 'external')
            continue;
        const fromRelative = relativePathByPath.get(edge.fromPath);
        const toRelative = edge.toPath ? relativePathByPath.get(edge.toPath) : undefined;
        if (fromRelative == null || toRelative == null)
            continue;
        const fromModule = moduleForRelativePath(fromRelative);
        const toModule = moduleForRelativePath(toRelative);
        if (fromModule === toModule)
            continue;
        const seamKey = seamKeyFor(fromModule, toModule);
        let seam = accumulators.get(seamKey);
        if (!seam) {
            const [moduleA, moduleB] = [fromModule, toModule].sort();
            seam = { moduleA, moduleB, importEdgeCount: 0, callEdgeCount: 0 };
            accumulators.set(seamKey, seam);
        }
        if (edge.type === 'imports')
            seam.importEdgeCount++;
        else
            seam.callEdgeCount++;
    }
    if (options.cochange) {
        for (const [seamKey, cochange] of aggregateCochangeToModules(options.cochange.pairs)) {
            const seam = accumulators.get(seamKey);
            if (seam)
                seam.cochange = cochange;
        }
    }
    const moduleScores = collectModuleScores(index);
    const scored = [...accumulators.entries()].map(([seamKey, seam]) => ({
        seamKey,
        seam,
        lift: seam.cochange?.lift ?? 0,
        score: (moduleScores.get(seam.moduleA) ?? 0) + (moduleScores.get(seam.moduleB) ?? 0),
    }));
    scored.sort((a, b) => b.lift - a.lift || b.score - a.score || compareCodeUnits(a.seamKey, b.seamKey));
    let seams = scored.map((entry, position) => ({
        seamKey: entry.seamKey,
        moduleA: entry.seam.moduleA,
        moduleB: entry.seam.moduleB,
        importEdgeCount: entry.seam.importEdgeCount,
        callEdgeCount: entry.seam.callEdgeCount,
        cochange: entry.seam.cochange,
        rank: position + 1,
    }));
    if (options.minLift != null) {
        const minLift = options.minLift;
        seams = seams.filter(seam => (seam.cochange?.lift ?? 0) >= minLift);
    }
    if (options.limit != null) {
        seams = seams.slice(0, options.limit);
    }
    return seams;
}
/**
 * Re-derive the current seams and answer per key: `resolved` (with current
 * seam data) or `missing` when either module no longer exists or no
 * cross-module edge remains between them. `minLift`/`limit` are ignored —
 * resolution answers existence, not prominence.
 */
export function resolveCodeSeams(index, seamKeys, options = {}) {
    const seams = listCodeSeams(index, { cochange: options.cochange });
    const byKey = new Map(seams.map(seam => [seam.seamKey, seam]));
    return seamKeys.map(seamKey => {
        const seam = byKey.get(seamKey);
        return seam ? { seamKey, status: 'resolved', seam } : { seamKey, status: 'missing' };
    });
}
/**
 * Rebase co-change pairs from git-toplevel-relative paths onto an index root
 * below the toplevel. `getGitCochange` reports paths relative to the
 * repository toplevel, while seam modules derive from index `relativePath`
 * values — without rebasing, an index rooted in a subdirectory would join
 * zero pairs. `indexRootPrefix` is the index root's toplevel-relative path
 * with forward slashes (empty when the index root is the toplevel, which
 * returns the summary unchanged). Pairs outside the index subtree are
 * dropped — they cannot match any module.
 */
export function rebaseCochangeToIndexRoot(cochange, indexRootPrefix) {
    if (!indexRootPrefix)
        return cochange;
    const prefix = indexRootPrefix.endsWith('/') ? indexRootPrefix : `${indexRootPrefix}/`;
    const pairs = [];
    for (const pair of cochange.pairs) {
        if (!pair.fileA.startsWith(prefix) || !pair.fileB.startsWith(prefix))
            continue;
        pairs.push({ ...pair, fileA: pair.fileA.slice(prefix.length), fileB: pair.fileB.slice(prefix.length) });
    }
    return { ...cochange, pairs };
}
function aggregateCochangeToModules(pairs) {
    const byPair = new Map();
    for (const pair of pairs) {
        // Co-change paths are index-root-relative (the `relativePath` convention);
        // callers rebase toplevel-relative git paths via rebaseCochangeToIndexRoot.
        const moduleA = moduleForRelativePath(pair.fileA);
        const moduleB = moduleForRelativePath(pair.fileB);
        if (moduleA === moduleB)
            continue;
        const seamKey = seamKeyFor(moduleA, moduleB);
        const existing = byPair.get(seamKey);
        if (!existing) {
            byPair.set(seamKey, { count: pair.count, lift: pair.lift, lastCochangedAt: pair.lastCochangedAt });
            continue;
        }
        existing.count += pair.count;
        existing.lift = Math.max(existing.lift, pair.lift);
        if (pair.lastCochangedAt > existing.lastCochangedAt) {
            existing.lastCochangedAt = pair.lastCochangedAt;
        }
    }
    return byPair;
}
function collectModuleScores(index) {
    const moduleScores = new Map();
    for (const file of rankNodesCached(index).fileScores) {
        const moduleName = moduleForRelativePath(file.relativePath);
        moduleScores.set(moduleName, (moduleScores.get(moduleName) ?? 0) + file.score);
    }
    return moduleScores;
}
//# sourceMappingURL=seams.js.map