/**
 * symbol-index — Programmatic extractor that produces a flat list of
 * identifiers the canonical reference legitimizes, each with a one-line
 * provenance snippet.
 *
 * Used by the grader-context pathway (W0196 / W0197) so the LLM judge sees
 * a compact, deterministic recognition reference instead of the full
 * narrative doc — addresses the DOC-2117 prior-collision failure mode
 * where a grader claimed `useEditDocument` did not exist.
 *
 * Two upstream sources, both fully programmatic (no LLM in the extractor):
 *
 *   - `extractSymbolIndex(blocks)` — walks Sanity Portable Text content
 *     (article docs).
 *   - `extractSymbolsFromTypedoc(json, packageName)` — parses typedoc
 *     JSON (typesReference docs).
 *
 * `mergeSymbolIndexes(indexes)` combines indexes from multiple references
 * into a single deduped index for a task.
 *
 * Source precedence (higher wins on dedup):
 *   1. type-def      — typedoc declarations: literal authoritative type
 *                      surface, no editorial layer between extracted
 *                      symbol and the package's actual exports.
 *   2. heading       — Section headings (`block` style h1..h4), including
 *                      inline `code` marks within heading spans.
 *   3. inline-code   — Inline `code` marks within non-heading `block` spans.
 *   4. code-block    — Identifiers from `import` statements in `codeBlock`
 *                      bodies. Body identifiers (`const foo = ...`) are
 *                      intentionally not extracted — those are usage demos.
 */
const HEADING_STYLES = new Set(["h1", "h2", "h3", "h4"]);
// JS/TS keywords + truly common literals that are never a useful "symbol"
// to legitimize. Kept deliberately tight — anything that *could* be a
// library export passes through.
const JS_KEYWORDS = new Set([
    "abstract",
    "any",
    "as",
    "async",
    "await",
    "boolean",
    "break",
    "case",
    "catch",
    "class",
    "const",
    "constructor",
    "continue",
    "debugger",
    "declare",
    "default",
    "delete",
    "do",
    "else",
    "enum",
    "export",
    "extends",
    "false",
    "finally",
    "for",
    "from",
    "function",
    "get",
    "if",
    "implements",
    "import",
    "in",
    "instanceof",
    "interface",
    "is",
    "keyof",
    "let",
    "module",
    "namespace",
    "never",
    "new",
    "null",
    "number",
    "of",
    "package",
    "private",
    "protected",
    "public",
    "readonly",
    "require",
    "return",
    "set",
    "static",
    "string",
    "super",
    "switch",
    "symbol",
    "this",
    "throw",
    "true",
    "try",
    "type",
    "typeof",
    "undefined",
    "unknown",
    "var",
    "void",
    "while",
    "with",
    "yield",
]);
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
export function extractSymbolIndex(blocks) {
    if (!Array.isArray(blocks)) {
        return { symbols: [] };
    }
    const headingHits = new Map();
    const inlineHits = new Map();
    const codeBlockHits = new Map();
    for (const raw of blocks) {
        if (!raw || typeof raw !== "object")
            continue;
        if (raw._type === "block") {
            collectFromBlock(raw, headingHits, inlineHits);
        }
        else if (raw._type === "codeBlock") {
            collectFromCodeBlock(raw, codeBlockHits);
        }
    }
    return mergeAndSort({
        typeDefHits: new Map(),
        headingHits,
        inlineHits,
        codeBlockHits,
    });
}
/**
 * Extract a symbol index from a typedoc JSON document (schema 2.x — the
 * shape produced by `typedoc --json`). Each top-level export becomes a
 * `type-def` provenance entry with the symbol name, declaration kind
 * (function / interface / type / etc.), and the JSDoc summary as snippet.
 *
 * Type-def is the highest-precedence source: typedoc declarations are
 * literal authoritative type surface, no editorial layer between them
 * and the package's actual exports. If a symbol also appears in narrative
 * docs (heading, inline code, code-block import) the type-def entry wins
 * on dedup.
 *
 * `body` is the raw JSON string fetched from the typesReference's
 * attachment URL. Returns an empty index for any unparseable input — this
 * is best-effort recognition material; callers fall back to full-doc
 * injection when extraction yields nothing.
 */
export function extractSymbolsFromTypedoc(body, packageName) {
    let parsed;
    try {
        parsed = JSON.parse(body);
    }
    catch {
        return { symbols: [] };
    }
    if (!parsed || typeof parsed !== "object")
        return { symbols: [] };
    const root = parsed;
    const exportsNamespace = (root.children ?? []).find((c) => c?.kind === TYPEDOC_KIND.namespace);
    // Top-level exports are typedoc's "exports" namespace child when present;
    // otherwise the root itself carries the children.
    const declarations = exportsNamespace?.children ?? root.children ?? [];
    const typeDefHits = new Map();
    for (const node of declarations) {
        if (!node || typeof node.name !== "string")
            continue;
        const declarationKind = TYPEDOC_KIND_TO_LABEL[node.kind ?? -1];
        if (!declarationKind)
            continue;
        const summary = typedocNodeSummary(node);
        const snippet = summary
            ? clipSnippet(summary)
            : `${declarationKind} ${node.name}${packageName ? ` (from ${packageName})` : ""}`;
        addIfFirst(typeDefHits, node.name, {
            kind: "type-def",
            snippet,
            declarationKind,
            ...(packageName ? { package: packageName } : {}),
        });
    }
    return mergeAndSort({
        typeDefHits,
        headingHits: new Map(),
        inlineHits: new Map(),
        codeBlockHits: new Map(),
    });
}
function mergeAndSort(buckets) {
    // Lower-precedence first; later writes overwrite earlier ones, so the
    // highest-precedence kind wins on dedup.
    const merged = new Map();
    for (const [k, v] of buckets.codeBlockHits)
        merged.set(k, v);
    for (const [k, v] of buckets.inlineHits)
        merged.set(k, v);
    for (const [k, v] of buckets.headingHits)
        merged.set(k, v);
    for (const [k, v] of buckets.typeDefHits)
        merged.set(k, v);
    const symbols = [...merged.entries()]
        .map(([symbol, provenance]) => ({ symbol, provenance }))
        .sort(compareEntries);
    return { symbols };
}
/**
 * Compute per-tier counts for a SymbolIndex. Used by the fetcher to
 * populate the per-task manifest entry's `tierBreakdown` field.
 */
export function symbolIndexTierBreakdown(index) {
    const out = { typeDef: 0, heading: 0, inlineCode: 0, codeBlock: 0 };
    for (const entry of index.symbols) {
        if (entry.provenance.kind === "type-def")
            out.typeDef += 1;
        else if (entry.provenance.kind === "heading")
            out.heading += 1;
        else if (entry.provenance.kind === "inline-code")
            out.inlineCode += 1;
        else
            out.codeBlock += 1;
    }
    return out;
}
/**
 * Combine multiple `SymbolIndex` instances (typically from different
 * canonical references for the same task) into a single deduped index
 * preserving precedence.
 */
export function mergeSymbolIndexes(indexes) {
    const buckets = {
        typeDefHits: new Map(),
        headingHits: new Map(),
        inlineHits: new Map(),
        codeBlockHits: new Map(),
    };
    for (const index of indexes) {
        for (const entry of index.symbols) {
            const bucket = bucketForKind(entry.provenance.kind, buckets);
            addIfFirst(bucket, entry.symbol, entry.provenance);
        }
    }
    return mergeAndSort(buckets);
}
function bucketForKind(kind, buckets) {
    if (kind === "type-def")
        return buckets.typeDefHits;
    if (kind === "heading")
        return buckets.headingHits;
    if (kind === "inline-code")
        return buckets.inlineHits;
    return buckets.codeBlockHits;
}
const TYPEDOC_KIND = {
    namespace: 2,
    enum: 8,
    variable: 32,
    function: 64,
    class: 128,
    interface: 256,
    type: 2_097_152,
};
const TYPEDOC_KIND_TO_LABEL = {
    [TYPEDOC_KIND.enum]: "enum",
    [TYPEDOC_KIND.variable]: "variable",
    [TYPEDOC_KIND.function]: "function",
    [TYPEDOC_KIND.class]: "class",
    [TYPEDOC_KIND.interface]: "interface",
    [TYPEDOC_KIND.type]: "type",
    [TYPEDOC_KIND.namespace]: "namespace",
};
function typedocNodeSummary(node) {
    // Direct comment on the declaration, then first signature comment for
    // function declarations (overload 0 is the canonical one).
    const direct = renderTypedocSummary(node.comment?.summary);
    if (direct)
        return direct;
    const sig = node.signatures?.[0];
    return renderTypedocSummary(sig?.comment?.summary);
}
function renderTypedocSummary(parts) {
    if (!Array.isArray(parts) || parts.length === 0)
        return null;
    const text = parts.map((p) => p.text ?? "").join("");
    const firstLine = text.split("\n")[0].trim();
    return firstLine.length > 0 ? firstLine : null;
}
/**
 * Render a SymbolIndex as a compact markdown reference suitable for
 * injection into a grader's `rubricPrompt` as ground-truth recognition
 * material. Layout intentionally puts headings first so the most
 * authoritative symbols anchor the top of the list.
 */
export function renderSymbolIndex(index, title) {
    if (index.symbols.length === 0)
        return "";
    const lines = [];
    if (title)
        lines.push(`# Symbols referenced in ${title}`, "");
    else
        lines.push("# Canonical symbols", "");
    lines.push("The following identifiers are referenced in the canonical documentation", "for this task. Each entry is tagged with its source (type-def, heading,", "inline, code) — type-def and heading entries are authoritative names", "from the documented surface. Inline and code entries may include", "field names or local variable names from example usage; use the", "tag to weight your confidence.", "");
    for (const entry of index.symbols) {
        lines.push(formatEntry(entry));
    }
    return lines.join("\n");
}
// ---------------------------------------------------------------------------
// Heading + inline collection (block children)
// ---------------------------------------------------------------------------
function collectFromBlock(block, headingHits, inlineHits) {
    const children = block.children ?? [];
    const text = blockPlainText(children);
    const trimmed = text.trim();
    if (!trimmed)
        return;
    const isHeading = block.style ? HEADING_STYLES.has(block.style) : false;
    // Pull every span carrying a `code` decorator mark.
    const inlineCodeSymbols = collectInlineCodeSymbols(children);
    if (isHeading) {
        // Headings legitimize whatever inline code they contain *and* the
        // heading itself names a symbol when the heading text is itself an
        // identifier-shaped token (e.g. "useEditDocument" as an h3 in
        // `sdk-react-hooks`).
        const headingProvenance = {
            kind: "heading",
            snippet: trimmed,
            style: block.style,
        };
        if (looksLikeSymbolIdentifier(trimmed)) {
            addIfFirst(headingHits, trimmed, headingProvenance);
        }
        for (const symbol of inlineCodeSymbols) {
            addIfFirst(headingHits, symbol, headingProvenance);
        }
    }
    else {
        const inlineProvenance = {
            kind: "inline-code",
            snippet: clipSnippet(trimmed),
        };
        for (const symbol of inlineCodeSymbols) {
            addIfFirst(inlineHits, symbol, inlineProvenance);
        }
    }
}
function blockPlainText(children) {
    return children
        .filter((c) => c && c._type === "span")
        .map((c) => c.text ?? "")
        .join("");
}
function collectInlineCodeSymbols(children) {
    const out = [];
    for (const span of children) {
        if (!span || span._type !== "span")
            continue;
        const marks = span.marks ?? [];
        if (!marks.includes("code"))
            continue;
        const text = (span.text ?? "").trim();
        if (!text)
            continue;
        // Inline code spans are usually a single identifier or a small
        // expression like `client.create()` or `useFoo().bar`. We emit only
        // the *rightmost* identifier — that's the operation/method/field
        // being demonstrated, the part the doc is actually legitimizing.
        // The leftmost identifier in a member expression is typically a
        // local variable name from example code (`client`, `result`, `editor`),
        // not a documented export, so it's noise at the grader's recognition
        // surface.
        const rightmost = rightmostIdentifier(text);
        if (rightmost)
            out.push(rightmost);
    }
    return out;
}
/**
 * Pull the rightmost identifier-token from a small inline-code span.
 *
 * `client.create()` → `create`
 * `useFoo().bar`    → `bar`
 * `useEditDocument` → `useEditDocument`
 * `Array<string>`   → `string`  (acceptable: the doc is referencing it
 *                                  even if as a type parameter)
 */
function rightmostIdentifier(text) {
    const matches = tokenizeIdentifiers(text);
    if (matches.length === 0)
        return null;
    return matches[matches.length - 1];
}
// ---------------------------------------------------------------------------
// codeBlock body lexing
// ---------------------------------------------------------------------------
function collectFromCodeBlock(cb, hits) {
    // Anchor extraction on `import` statements only: those are the symbols
    // the doc authoritatively *legitimizes* (this is the surface the doc
    // claims you can reach for from the named package). Body identifiers
    // — `const foo = ...`, ad-hoc variable names, prop names — are demos,
    // not legitimization, so they are intentionally dropped here. If a
    // doc only legitimizes a symbol via prose-with-backticks, the inline
    // tier picks it up. If it only appears in non-import code, the doc is
    // not being explicit about it and we should not put words in the
    // doc's mouth.
    const inner = cb.blocks ?? [];
    for (const tab of inner) {
        const code = tab.code?.code ?? "";
        if (!code)
            continue;
        // Strip comments so commented-out imports don't legitimize symbols.
        // Strings are intentionally left intact: the import regex anchors on
        // the source-package quoted string (`from "..."` / `from '...'`) and
        // would not match if the quotes were already replaced.
        const decommented = stripComments(code);
        const filename = tab.filename ?? tab.code?.filename;
        const language = tab.code?.language;
        for (const importMatch of extractImportSymbols(decommented)) {
            const provenance = {
                kind: "code-block",
                snippet: clipSnippet(importMatch.snippet),
                ...(filename ? { filename } : {}),
                ...(language ? { language } : {}),
            };
            for (const symbol of importMatch.symbols) {
                if (JS_KEYWORDS.has(symbol))
                    continue;
                addIfFirst(hits, symbol, provenance);
            }
        }
    }
}
/**
 * Extract symbols from JS/TS `import` and re-`export` statements.
 *
 * Handles:
 *   import foo from "pkg"               → ["foo"]
 *   import * as foo from "pkg"          → ["foo"]
 *   import { a, b as c } from "pkg"     → ["a", "c"]
 *   import { type T, fn } from "pkg"    → ["T", "fn"]
 *   import foo, { a } from "pkg"        → ["foo", "a"]
 *   export { a, b as c } from "pkg"     → ["a", "c"]
 *   export * as ns from "pkg"           → ["ns"]
 *   export * from "pkg"                 → [] (no named bindings, skipped)
 *
 * Re-exports are common in barrel files and contribute as much
 * legitimization as imports — both name the same export surface.
 *
 * The snippet returned is the full single-line declaration so the grader
 * can see the source package alongside the symbol.
 */
function extractImportSymbols(code) {
    const matches = [];
    // Match both `import` and `export ... from`. `export` without `from` is
    // a local declaration (e.g. `export const x = ...`) — those don't
    // legitimize from a named source so we require the `from` clause.
    const stmtRe = /\b(import|export)\s+([\s\S]+?)\s+from\s+["']([^"']+)["']/g;
    let m;
    while ((m = stmtRe.exec(code)) !== null) {
        const keyword = m[1];
        const clause = m[2];
        const source = m[3];
        const symbols = parseImportClause(clause);
        if (symbols.length === 0)
            continue;
        matches.push({
            symbols,
            snippet: `${keyword} ${flattenWhitespace(clause)} from "${source}"`,
        });
    }
    return matches;
}
function parseImportClause(clause) {
    const out = [];
    // Strip braces for named-imports section, but remember its content.
    // Three permitted shapes after `import `:
    //   <default>
    //   * as <ns>
    //   { ... }
    //   <default>, { ... }
    //   <default>, * as <ns>
    const namedMatch = clause.match(/\{([^}]*)\}/);
    const beforeBraces = namedMatch ? clause.slice(0, namedMatch.index) : clause;
    for (const piece of beforeBraces.split(",")) {
        const trimmed = piece.trim();
        if (!trimmed)
            continue;
        const nsMatch = trimmed.match(/^\*\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)$/);
        if (nsMatch) {
            out.push(nsMatch[1]);
            continue;
        }
        if (looksLikeIdentifier(trimmed)) {
            out.push(trimmed);
        }
    }
    if (namedMatch) {
        for (const item of namedMatch[1].split(",")) {
            const piece = item.trim().replace(/^type\s+/, "");
            if (!piece)
                continue;
            // `a as b` → emit b (the local-binding name), since that's the name
            // a candidate would actually use.
            const aliasMatch = piece.match(/^([A-Za-z_$][A-Za-z0-9_$]*)\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)$/);
            if (aliasMatch) {
                out.push(aliasMatch[2]);
                continue;
            }
            if (looksLikeIdentifier(piece)) {
                out.push(piece);
            }
        }
    }
    return out;
}
function flattenWhitespace(s) {
    return s.replace(/\s+/g, " ").trim();
}
/**
 * Strip line + block comments. Strings are left intact because the
 * import regex anchors on the source-package quoted string and would
 * fail to match if the quotes were stripped.
 */
function stripComments(code) {
    let out = code.replace(/\/\*[\s\S]*?\*\//g, " ");
    out = out.replace(/\/\/[^\n]*/g, " ");
    return out;
}
function tokenizeIdentifiers(text) {
    const matches = text.match(/[A-Za-z_$][A-Za-z0-9_$]*/g);
    if (!matches)
        return [];
    return matches;
}
function looksLikeIdentifier(text) {
    return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(text);
}
/**
 * Stricter than `looksLikeIdentifier`: the text must not just *be* an
 * identifier, it must be an identifier that could plausibly name a code
 * symbol. A bare title-cased word like "Summary" or "Prerequisites"
 * passes the lexical identifier test but is almost certainly a section
 * label, not a symbol — including it as a heading-tier "symbol" only
 * adds noise to the grader's recognition surface.
 *
 * Heuristic, accepts text that is a lexical identifier AND meets one of:
 *   - contains camelCase (`useEditDocument`, `S3Client`)
 *   - contains underscore, digit, or `$` (`URL_PATTERN`, `_ailfPrivate`)
 *   - starts with two consecutive uppercase letters — admits all-caps
 *     acronyms and acronym-prefixed PascalCase (`URL`, `API`, `JSON`,
 *     `RegExp`, `HTMLElement`)
 *
 * Rejects bare common-noun cases like `Summary`, `Prerequisites`,
 * `Actions`, `Array`, `Promise`, `Date` — those are almost always
 * section labels or built-in types the grader's prior already handles.
 */
function looksLikeSymbolIdentifier(text) {
    if (!looksLikeIdentifier(text))
        return false;
    return /[a-z][A-Z]|[_$0-9]|^[A-Z]{2,}/.test(text);
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const SNIPPET_MAX = 160;
function clipSnippet(text) {
    if (text.length <= SNIPPET_MAX)
        return text;
    return text.slice(0, SNIPPET_MAX - 1).trimEnd() + "…";
}
function addIfFirst(bucket, symbol, provenance) {
    if (JS_KEYWORDS.has(symbol))
        return;
    if (!bucket.has(symbol))
        bucket.set(symbol, provenance);
}
function precedenceRank(kind) {
    if (kind === "type-def")
        return 0;
    if (kind === "heading")
        return 1;
    if (kind === "inline-code")
        return 2;
    return 3;
}
function compareEntries(a, b) {
    const rankDiff = precedenceRank(a.provenance.kind) - precedenceRank(b.provenance.kind);
    if (rankDiff !== 0)
        return rankDiff;
    return a.symbol.localeCompare(b.symbol);
}
function formatEntry(entry) {
    const { symbol, provenance } = entry;
    const tag = provenanceTag(provenance);
    return `- \`${symbol}\` — ${tag}: ${provenance.snippet}`;
}
function provenanceTag(provenance) {
    if (provenance.kind === "type-def") {
        const declKind = provenance.declarationKind ?? "type";
        return provenance.package ? `${declKind} (${provenance.package})` : declKind;
    }
    if (provenance.kind === "heading") {
        return provenance.style ? provenance.style.toUpperCase() : "heading";
    }
    if (provenance.kind === "inline-code")
        return "inline";
    // code-block
    if (provenance.filename)
        return `code (${provenance.filename})`;
    if (provenance.language)
        return `code (${provenance.language})`;
    return "code";
}
