// Import-graph adapter (Epic #177, Issue #180). Pure-JS regex extractor: scans discovered
// files for ESM imports/re-exports and CJS requires, emits an EvidenceAtom for each file
// whose specifier matches the query text. Fixed, anchored, non-nested-quantifier regexes
// keep the scan ReDoS-safe; the binary probe and read cap come from the shared workspace
// primitives. Stays within ADR-0019 rule 3b: imports only @oscharko-dev/keiko-contracts,
// sibling workspace modules, and Node stdlib (node:crypto).
import { createHash } from "node:crypto";
import { looksBinary } from "./binaryDetect.js";
import { readWorkspaceFile } from "./discovery.js";
import { RepoSearchInvalidQueryError } from "./errors.js";
import { resolveWithinWorkspace } from "./paths.js";
import { assertContainedRealPath } from "./realpath.js";
import { buildAtom, gatherCandidates } from "./repoSearchScan.js";
function queryFingerprint(query) {
    const canonical = JSON.stringify({ kind: query.kind, text: query.text });
    return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
}
// ESM static imports: import X from "y"; import * as X from "y"; import "y"; import { X } from "y".
// [ \t]+\S+ tokens in the import clause avoid ReDoS: [ \t] and \S are complementary so the engine
// never tries alternative splits. [ \t] (not \s) prevents the clause from crossing newlines.
const ESM_IMPORT = /^\s*import(?:[ \t]+\S+(?:[ \t]+\S+)*[ \t]+from)?\s+["']([^"'\n]+)["']/gm;
// ESM re-exports: export * from "y"; export { X } from "y".
const ESM_REEXPORT = /^\s*export\s+(?:\*|\{[^}]*\})\s+from\s+["']([^"'\n]+)["']/gm;
// CommonJS: require("y").
const CJS_REQUIRE = /\brequire\s*\(\s*["']([^"'\n]+)["']\s*\)/g;
const BINARY_PROBE_BYTES = 512;
async function probeBinary(fs, abs, size) {
    const cap = Math.min(BINARY_PROBE_BYTES, size);
    if (cap === 0) {
        return false;
    }
    if (fs.readFileBytes !== undefined) {
        return looksBinary(await fs.readFileBytes(abs, cap));
    }
    const text = fs.readFileUtf8(abs);
    return looksBinary(new TextEncoder().encode(text.slice(0, cap)));
}
function specifierMatches(specifier, query) {
    if (query.kind === "exact-symbol") {
        return specifier === query.text;
    }
    return specifier.toLowerCase().includes(query.text.toLowerCase());
}
function scoreFor(specifier, query) {
    return specifier === query.text ? 1.0 : 0.7;
}
function lineNumberOf(text, charIndex) {
    let line = 1;
    for (let i = 0; i < charIndex && i < text.length; i += 1) {
        if (text.charCodeAt(i) === 10) {
            line += 1;
        }
    }
    return line;
}
function collectHits(text, query) {
    const hits = [];
    for (const regex of [ESM_IMPORT, ESM_REEXPORT, CJS_REQUIRE]) {
        regex.lastIndex = 0;
        let m = regex.exec(text);
        while (m !== null) {
            const specifier = m[1] ?? "";
            if (specifierMatches(specifier, query)) {
                hits.push({ specifier, line: lineNumberOf(text, m.index) });
            }
            m = regex.exec(text);
        }
    }
    return hits;
}
function emitHitAtom(ctx, relativePath, hit) {
    return buildAtom({
        scopeId: ctx.scope.scopeId,
        scopePath: relativePath,
        lineRange: { startLine: hit.line, endLine: hit.line },
        provenanceKind: "structural",
        tool: "import-graph",
        queryFingerprint: ctx.fingerprint,
        score: scoreFor(hit.specifier, ctx.query),
        emittedAtMs: ctx.nowMs(),
    });
}
function elapsedOver(ctx) {
    return ctx.nowMs() - ctx.startMs > ctx.limits.elapsedMsMax;
}
async function scanFileForImports(ctx, relativePath, atoms) {
    const abs = resolveWithinWorkspace(ctx.scope.workspace.root, relativePath);
    assertContainedRealPath(ctx.fs, ctx.scope.workspace.root, abs, "scope");
    const stat = ctx.fs.stat(abs);
    if (await probeBinary(ctx.fs, abs, stat.size)) {
        return;
    }
    const content = readWorkspaceFile(ctx.scope.workspace, relativePath, { maxBytes: ctx.limits.maxBytesPerFileScanned }, ctx.fs);
    const hits = collectHits(content.text, ctx.query);
    for (const hit of hits) {
        if (atoms.length >= ctx.limits.maxMatchesReturned) {
            return;
        }
        atoms.push(emitHitAtom(ctx, relativePath, hit));
    }
}
export const importGraphAdapter = {
    name: "import-graph",
    isAvailable: () => Promise.resolve(true),
    lookup: async (scope, query, limits, fs, deps) => {
        if (query.kind !== "natural-language" && query.kind !== "exact-symbol") {
            throw new RepoSearchInvalidQueryError(`import-graph adapter does not accept query kind: ${query.kind}`);
        }
        const ctx = {
            scope,
            fs,
            limits,
            query,
            fingerprint: queryFingerprint(query),
            startMs: (deps?.nowMs ?? Date.now)(),
            nowMs: deps?.nowMs ?? Date.now,
        };
        const candidateSet = gatherCandidates(scope, limits, fs);
        const atoms = [];
        for (const file of candidateSet.files) {
            if (atoms.length >= limits.maxMatchesReturned || elapsedOver(ctx)) {
                break;
            }
            await scanFileForImports(ctx, file.relativePath, atoms);
        }
        return atoms;
    },
};
