import { walkFileHistory } from './git.js';
import { blobEntityKey, readBlobEntities } from './indexer.js';
import { classifyEntityChange, isUnchanged } from './lineage.js';
import { DEFAULT_ENTITY_BOUNDS, SEMANTIC_CHANGE_TYPES } from './types.js';
/**
 * Attribution for entities the indexed window never saw.
 *
 * `blame` and `history` can only speak from lineage events, and those exist only
 * inside the indexed commit window. An entity whose last real change predates
 * the window therefore reported "no history" — an answer that is honest about
 * OCC and wrong about the repository, because the history is right there in git.
 *
 * The fix is a bounded, per-query walk rather than a larger index. Two
 * properties make it affordable enough to run by default:
 *
 * - Every entity in a file shares one path history, so a whole file's blame
 *   costs **one** walk, not one per entity.
 * - `git log --raw` carries both blob ids per commit, so the walk is one git
 *   invocation plus one batched blob read for the entire window.
 *
 * It is deliberately not a fallback that widens the index: nothing is cached,
 * nothing is written, and the cost is paid by the query that asked.
 */
/** How many commits touching a path a single walk-back will examine. */
export const ENTITY_WALK_BACK_COMMITS = 40;
const NULL_OID = /^0+$/;
function usableOid(oid) {
    return typeof oid === 'string' && oid.length > 0 && !NULL_OID.test(oid);
}
/**
 * Identity across the walk.
 *
 * The full state key includes the path, which `--follow` is specifically there
 * to let change, so identity within a walk is the path-free remainder: an
 * entity is the same entity when its kind, qualified name, and ordinal match.
 * That is the `exact-identity` rung with the one component the walk is crossing
 * removed, not a looser rule invented here.
 */
function walkKey(record) {
    return `${record.kind}\u0000${record.qualifiedName}\u0000${record.ordinal}`;
}
/**
 * Every semantic change to every entity in a path, going back from a ref.
 *
 * Cosmetic edits and pure reorderings are recorded but not reported, for the
 * same reason blame skips them inside the window: "who last changed this" must
 * not answer with whoever most recently reformatted the file.
 */
export async function walkBackAttribution(options) {
    const bounds = options.bounds ?? DEFAULT_ENTITY_BOUNDS;
    const limit = options.limit ?? ENTITY_WALK_BACK_COMMITS;
    const byEntity = new Map();
    const window = await walkFileHistory({
        repoRoot: options.repoRoot,
        relativePath: options.relativePath,
        startRef: options.startRef,
        limit,
        signal: options.signal,
        counters: options.counters,
    });
    if (window.commits.length === 0)
        return { byEntity, commitsScanned: 0, exhausted: false };
    // One batched read for the whole walk. Doing it per commit would turn a
    // bounded query into `limit` round trips for the same bytes.
    const requests = [];
    for (const commit of window.commits) {
        for (const change of window.changesByCommit.get(commit.sha) ?? []) {
            if (usableOid(change.newBlobSha))
                requests.push({ blobSha: change.newBlobSha, relativePath: change.path });
            if (usableOid(change.oldBlobSha)) {
                requests.push({ blobSha: change.oldBlobSha, relativePath: change.oldPath ?? change.path });
            }
        }
    }
    const read = await readBlobEntities(requests, {
        repoRoot: options.repoRoot,
        bounds,
        guardrails: options.guardrails,
        cache: options.cache,
        signal: options.signal,
        counters: options.counters,
    });
    for (const commit of window.commits) {
        for (const change of window.changesByCommit.get(commit.sha) ?? []) {
            const previousPath = change.oldPath ?? change.path;
            const after = usableOid(change.newBlobSha)
                ? (read.entities.get(blobEntityKey(change.newBlobSha, change.path))?.entities ?? [])
                : [];
            const before = usableOid(change.oldBlobSha)
                ? (read.entities.get(blobEntityKey(change.oldBlobSha, previousPath))?.entities ?? [])
                : [];
            const beforeByKey = new Map(before.map(record => [walkKey(record), record]));
            for (const current of after) {
                const key = walkKey(current);
                const previous = beforeByKey.get(key);
                let changeType;
                let flags;
                if (!previous) {
                    changeType = 'added';
                    flags = {};
                }
                else {
                    // `classifyEntityChange` assumes its caller has already established
                    // that something changed — it labels an untouched pair `modified`
                    // rather than reporting nothing. Every commit touching the file would
                    // otherwise look like a change to every entity in it.
                    if (isUnchanged(previous, current))
                        continue;
                    const classified = classifyEntityChange({ previous, current });
                    changeType = classified.changeType;
                    flags = classified.flags;
                }
                if (!SEMANTIC_CHANGE_TYPES.has(changeType))
                    continue;
                const entry = {
                    commit: commit.sha,
                    authorName: commit.authorName,
                    authorEmail: commit.authorEmail,
                    authoredAt: commit.authoredAt,
                    subject: commit.subject,
                    changeType,
                    flags,
                    // The path is the one component the walk crosses, so a pairing that
                    // spans a rename is exactly what git's rename detection reported.
                    matchReason: previous && previous.relativePath !== current.relativePath ? 'git-rename-identity' : 'exact-identity',
                    currentPath: current.relativePath,
                    currentName: current.name,
                };
                if (previous) {
                    entry.previousPath = previous.relativePath;
                    entry.previousName = previous.name;
                }
                const bucket = byEntity.get(key);
                if (bucket)
                    bucket.push(entry);
                else
                    byEntity.set(key, [entry]);
            }
        }
    }
    return {
        byEntity,
        commitsScanned: window.commits.length,
        exhausted: window.commits.length >= limit,
    };
}
/** The walk-back key for a live entity, so callers need not know how it is built. */
export function walkBackKeyFor(record) {
    return walkKey(record);
}
//# sourceMappingURL=walkback.js.map