/**
 * Workspace-wide asset reference scan (assets-referenced-not-owned).
 *
 * Two features need the same walk: "which assets does this entry SOLELY
 * reference" (P2 delete prompt) and "how many entries reference each asset"
 * (P3 loose/unreferenced filter — the inverted index). This is the one
 * implementation of that walk; both are thin consumers.
 *
 * The walk is:
 *   - LIVE-RECONCILED (eng review D1): for an entry with an open editor doc the
 *     live Y.Doc body REPLACES the disk body (a reference just pasted blocks an
 *     offer / inflates a count; one just removed does the opposite). Disk
 *     FRONTMATTER is always scanned (it isn't editable live). Live-only docs
 *     (a fresh create still being typed, no disk file yet) are swept directly.
 *   - BUDGETED (fail-safe): a soft wall-clock cap. On overrun the walk stops and
 *     `partial` is true — the user-initiated, low-frequency scan must never
 *     stall the UI on a huge workspace, and callers must NEVER treat an
 *     unscanned asset as unreferenced (P2 offers nothing; P3 marks nothing loose).
 *   - ALL repos, even ones the caller can't read: a reference is a reference
 *     wherever it lives. Consumers reveal only an aggregate (a boolean / a
 *     count), never the referencing entry or its folder.
 */
import { readFileSync } from 'node:fs';
import { setImmediate } from 'node:timers';
import { extractImmutableAssetRefs, stripFrontmatter } from '../core/embeds.js';
/** Soft wall-clock budget for the workspace reference scan. User-initiated and
 *  low-frequency, so a full walk is acceptable — but it must never stall the UI
 *  indefinitely on a huge workspace. Shared by every consumer so the fail-safe
 *  boundary is identical. */
export const DEFAULT_SCAN_BUDGET_MS = 4000;
/**
 * Walk every entry in every repo (live-reconciled, budgeted) and hand the
 * `visit` callback the set of immutable assetIds each entry references. The
 * callback owns what to do with them (intersect a target set, accumulate a
 * count, …). `visit` is never called for the `skipKey` entry.
 */
export async function scanWorkspaceAssetReferences(wm, visit, opts = {}) {
    const budgetMs = opts.budgetMs ?? DEFAULT_SCAN_BUDGET_MS;
    const deadline = Date.now() + budgetMs;
    const { skipKey, stopWhen } = opts;
    let partial = false;
    // Cheap substring pre-check before the regex — the common entry embeds no
    // asset, so most bodies skip extraction entirely.
    const extractInto = (text, into) => {
        if (!text || text.indexOf('/api/assets/') === -1)
            return;
        for (const ref of extractImmutableAssetRefs(text))
            into.add(ref.assetId);
    };
    const visited = (() => {
        let n = 0;
        return () => ++n;
    })();
    scan: for (const cfg of wm.getAllRepoConfigs()) {
        // Per-repo deadline check too: listEntityIds builds the repo's file index on
        // first touch, which itself can be slow on a huge cold repo.
        if (Date.now() > deadline) {
            partial = true;
            break;
        }
        let g;
        try {
            g = wm.getGraph(cfg.name);
        }
        catch {
            continue;
        }
        for (const { entityType: eType, id: eId } of g.listEntityIds()) {
            if (Date.now() > deadline) {
                partial = true;
                break scan;
            }
            // Yield the event loop periodically — the walk is sync I/O and must not
            // starve other requests for the full budget on a big workspace.
            if (visited() % 200 === 0)
                await new Promise(r => setImmediate(r));
            const key = `${cfg.name}/${eType}/${eId}`;
            if (key === skipKey)
                continue;
            const path = g.getEntityFilePath(eType, eId);
            if (!path)
                continue;
            let raw;
            try {
                raw = readFileSync(path, 'utf-8');
            }
            catch {
                continue;
            }
            const ids = new Set();
            // Live reconcile (D1): an open editor's Y.Doc body REPLACES the disk body
            // — additions inflate, removals deflate. Disk FRONTMATTER is always
            // scanned (not editable in the live doc).
            const live = g.getLiveBody(eType, eId);
            if (live === null) {
                extractInto(raw, ids); // body + frontmatter in one pass
            }
            else {
                // BOM-strip before the frontmatter split: an externally-edited file with
                // a leading BOM would otherwise defeat stripFrontmatter and the
                // frontmatter reference would never be scanned.
                const norm = raw.replace(/^\uFEFF/, '');
                const diskBody = stripFrontmatter(norm);
                const frontmatterRaw = norm.slice(0, norm.length - diskBody.length);
                extractInto(live, ids);
                extractInto(frontmatterRaw, ids);
            }
            visit(ids, { repo: cfg.name, entityType: eType, entityId: eId, key });
            if (stopWhen?.())
                break scan;
        }
    }
    // Live-only docs: an open editor for an entity that has no disk file yet (a
    // fresh create mid-typing) is in no file index — peek those directly. Skipped
    // when the scan went partial (we can't claim completeness) or already stopped.
    if (!partial && !stopWhen?.()) {
        for (const docName of wm.getBodyWriteCoordinator()?.activeDocNames() ?? []) {
            if (Date.now() > deadline) {
                partial = true;
                break;
            }
            if (docName === skipKey)
                continue;
            const parts = docName.split('/');
            if (parts.length !== 3)
                continue;
            const [dRepo, dType, dId] = parts;
            let dGraph;
            try {
                dGraph = wm.getGraph(dRepo);
            }
            catch {
                continue;
            }
            // Disk-backed docs were already live-reconciled in the walk above.
            if (dGraph.getEntityFilePath(dType, dId))
                continue;
            const live = dGraph.getLiveBody(dType, dId);
            if (live === null)
                continue;
            const ids = new Set();
            extractInto(live, ids);
            visit(ids, { repo: dRepo, entityType: dType, entityId: dId, key: docName });
            if (stopWhen?.())
                break;
        }
    }
    return { partial };
}
//# sourceMappingURL=asset-reference-scan.js.map