/**
 * typedoc-declarations.ts
 *
 * Semantic declaration renderer for candidate-facing typesReference docs.
 * Turns a typedoc JSON string into a `.d.ts`-with-JSDoc markdown view,
 * relevance-scoped to per-task symbol hints with one-level closure expansion.
 *
 * Per D0069 binding sub-decisions:
 *   1. Semantic representation: kind label + full signature + JSDoc summary + example
 *   2. Relevance scoping: hinted exports rendered fully; one-level type-ref closure;
 *      name-only tail index for everything not rendered
 *   3. Byte budget retained at DEFAULT_MAX_BYTES (30,000) — density beats budget
 *   6. No-hints fallback: declaration-order rendering until budget reached
 *   7. Unparseable attachments: return null so caller keeps raw inline path
 *
 * Budget degeneracy (sub-minimal budgets): when the full name-list tail line
 * itself exceeds the budget, the tail degrades to an explicit count-only line
 * (`*N more exports (signatures not expanded)*` — no names, never a partial
 * list, never a mid-name slice). If even that doesn't fit, markdown is the
 * blocks that fit (in practice empty) and the returned renderedCount /
 * indexedCount carry the information. The cap always wins; omissions are
 * always explicit via the count.
 *
 * Module independence per D0069 §4: does NOT modify symbol-index.ts logic.
 * Uses its own richer TypedocNode interface to capture the fields needed for
 * the full signature printer (types, parameters, signatures, typeParameters,
 * children, extendedTypes).
 */
/**
 * Returns null when `body` is not parseable typedoc JSON (caller falls back
 * to raw inline). Returns a result object otherwise — even if the budget
 * only fits a tail index.
 */
export function renderTypedocDeclarations(body, options) {
    let parsed;
    try {
        parsed = JSON.parse(body);
    }
    catch {
        return null;
    }
    // Defense-in-depth: degenerate external input (e.g. pathological nesting
    // that survives JSON.parse) must degrade to the D0069 §7 raw-inline
    // fallback, never crash the fetch-docs step. The depth caps below make
    // rendering total in practice; this catch maps ANY residual internal
    // failure onto the same documented fallback (caller keeps today's path).
    try {
        if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
            return null;
        const root = parsed;
        // Descend into the exports namespace when present (typedoc 2.x layout).
        const exportsNamespace = (root.children ?? []).find((c) => c?.kind === TYPEDOC_KIND.namespace);
        const declarations = exportsNamespace?.children ?? root.children ?? [];
        // Guard: non-typedoc JSON must have a non-empty array of named nodes.
        if (!Array.isArray(declarations) || declarations.length === 0)
            return null;
        const validDecls = declarations.filter((d) => d && typeof d === "object" && typeof d.name === "string");
        if (validDecls.length === 0)
            return null;
        return assembleOutput(validDecls, options);
    }
    catch {
        return null;
    }
}
/**
 * Extract identifier-shaped, symbol-looking tokens from free text (a task's
 * prompt body or title) for use as relevance hints (D0069 sub-decision 2,
 * source (b)).
 *
 * A token contributes only if it is a lexical JS identifier AND looks like a
 * code symbol rather than a prose word — camelCase, an underscore/digit/`$`,
 * or two-or-more leading capitals. This mirrors the `looksLikeSymbolIdentifier`
 * heuristic the grader's symbol index applies to headings; it is duplicated
 * (not imported) here to keep the grader module untouched per D0069 §4. Bare
 * common nouns ("Documents", "Reference") are rejected so they don't pull
 * unrelated exports into the candidate's relevance set.
 *
 * Returns deduped tokens in first-seen order (deterministic).
 */
export function extractRelevanceIdentifiers(text) {
    const tokens = text.match(/[A-Za-z_$][A-Za-z0-9_$]*/g);
    if (!tokens)
        return [];
    const seen = new Set();
    const out = [];
    for (const token of tokens) {
        if (seen.has(token))
            continue;
        if (!/[a-z][A-Z]|[_$0-9]|^[A-Z]{2,}/.test(token))
            continue;
        seen.add(token);
        out.push(token);
    }
    return out;
}
// ---------------------------------------------------------------------------
// Kind constants and labels
// ---------------------------------------------------------------------------
const TYPEDOC_KIND = {
    namespace: 2,
    enum: 8,
    variable: 32,
    function: 64,
    class: 128,
    interface: 256,
    property: 1024,
    method: 2048,
    callSignature: 4096,
    typeAlias: 2_097_152,
    typeAliasAlt: 4_194_304, // present in modern typedoc output
};
const KIND_LABEL = {
    [TYPEDOC_KIND.enum]: "enum",
    [TYPEDOC_KIND.variable]: "const",
    [TYPEDOC_KIND.function]: "function",
    [TYPEDOC_KIND.class]: "class",
    [TYPEDOC_KIND.interface]: "interface",
    [TYPEDOC_KIND.typeAlias]: "type",
    [TYPEDOC_KIND.typeAliasAlt]: "type",
};
// ---------------------------------------------------------------------------
// Type → string printer
// ---------------------------------------------------------------------------
/**
 * Recursion cap for type expressions. V8's JSON.parse accepts arbitrarily
 * deep nesting, so a degenerate/malicious typedoc attachment survives the
 * parse guard — without a cap the recursive printer would RangeError around
 * ~5,000 levels. Past the cap we print `...` (printer) or stop collecting
 * (closure walk), keeping both functions total per the "unknown kinds never
 * throw" design intent. 64 is far beyond any legitimate type in the corpus.
 */
const MAX_TYPE_DEPTH = 64;
function typeToString(t, depth = 0) {
    if (depth > MAX_TYPE_DEPTH)
        return "...";
    if (!t || typeof t !== "object")
        return "unknown";
    switch (t.type) {
        case "intrinsic":
            return t.name ?? "unknown";
        case "literal":
            return typeof t.value === "string"
                ? JSON.stringify(t.value)
                : String(t.value);
        case "reference": {
            const args = t.typeArguments && t.typeArguments.length > 0
                ? `<${t.typeArguments.map((a) => typeToString(a, depth + 1)).join(", ")}>`
                : "";
            return `${t.name ?? "unknown"}${args}`;
        }
        case "array":
            return `${typeToString(t.elementType, depth + 1)}[]`;
        case "union":
            return (t.types ?? []).map((x) => typeToString(x, depth + 1)).join(" | ");
        case "intersection":
            return (t.types ?? []).map((x) => typeToString(x, depth + 1)).join(" & ");
        case "tuple":
            return `[${(t.elements ?? []).map((x) => typeToString(x, depth + 1)).join(", ")}]`;
        case "indexedAccess":
            return `${typeToString(t.objectType, depth + 1)}[${typeToString(t.indexType, depth + 1)}]`;
        case "typeOperator":
            return `${t.operator} ${typeToString(t.target, depth + 1)}`;
        case "query":
            return `typeof ${typeToString(t.queryType, depth + 1)}`;
        case "predicate":
            return `${t.name} is ${typeToString(t.targetType, depth + 1)}`;
        case "conditional":
            return `${typeToString(t.checkType, depth + 1)} extends ${typeToString(t.extendsType, depth + 1)} ? ${typeToString(t.trueType, depth + 1)} : ${typeToString(t.falseType, depth + 1)}`;
        case "templateLiteral":
            return "string"; // collapse template-literal types for readability
        case "reflection": {
            const d = t.declaration;
            if (!d)
                return "object";
            if (d.signatures && d.signatures.length > 0) {
                const s = d.signatures[0];
                return `(${renderParams(s.parameters, depth + 1)}) => ${typeToString(s.type, depth + 1)}`;
            }
            if (d.children && d.children.length > 0) {
                const members = d.children
                    .map((c) => `${c.name}${c.flags?.isOptional ? "?" : ""}: ${memberType(c, depth + 1)}`)
                    .join("; ");
                return `{ ${members} }`;
            }
            return "object";
        }
        default:
            return t.name ?? "unknown";
    }
}
function memberType(node, depth = 0) {
    if (depth > MAX_TYPE_DEPTH)
        return "...";
    if (node.type)
        return typeToString(node.type, depth);
    if (node.signatures && node.signatures.length > 0) {
        const s = node.signatures[0];
        return `(${renderParams(s.parameters, depth)}) => ${typeToString(s.type, depth)}`;
    }
    return "unknown";
}
function renderTypeParams(typeParameters) {
    if (!typeParameters || typeParameters.length === 0)
        return "";
    const parts = typeParameters.map((tp) => {
        let s = tp.name ?? "";
        if (tp.type)
            s += ` extends ${typeToString(tp.type)}`;
        if (tp.default)
            s += ` = ${typeToString(tp.default)}`;
        return s;
    });
    return `<${parts.join(", ")}>`;
}
function renderParams(parameters, depth = 0) {
    if (!parameters || parameters.length === 0)
        return "";
    return parameters
        .map((p) => {
        const rest = p.flags?.isRest ? "..." : "";
        const opt = p.flags?.isOptional ? "?" : "";
        return `${rest}${p.name ?? ""}${opt}: ${typeToString(p.type, depth)}`;
    })
        .join(", ");
}
// ---------------------------------------------------------------------------
// JSDoc helpers
// ---------------------------------------------------------------------------
function summaryText(comment) {
    if (!comment?.summary)
        return "";
    return comment.summary
        .map((s) => s.text ?? "")
        .join("")
        .trim();
}
function firstParagraph(text) {
    return text.split("\n\n")[0].replace(/\s+/g, " ").trim();
}
function exampleBlocks(comment) {
    if (!comment?.blockTags)
        return [];
    return comment.blockTags
        .filter((b) => b.tag === "@example")
        .map((b) => (b.content ?? [])
        .map((c) => c.text ?? "")
        .join("")
        .trim())
        .filter((ex) => ex.length > 0);
}
// ---------------------------------------------------------------------------
// Declaration signature renderer
// ---------------------------------------------------------------------------
/**
 * Render up to 3 signatures of a function/method, appending a
 * `*(+N more overloads)*` line when there are more than 3.
 */
function renderFunctionSignatures(node) {
    const tp = renderTypeParams(node.typeParameters);
    const sigs = node.signatures ?? [];
    const toRender = sigs.slice(0, 3);
    const extra = sigs.length - toRender.length;
    const sigLines = [];
    for (const sig of toRender) {
        const sigTp = renderTypeParams(sig.typeParameters) || tp;
        sigLines.push(`function ${node.name}${sigTp}(${renderParams(sig.parameters)}): ${typeToString(sig.type)}`);
    }
    if (extra > 0) {
        sigLines.push(`*(+${extra} more overloads)*`);
    }
    return sigLines.join("\n");
}
function renderDeclarationSignature(node) {
    const tp = renderTypeParams(node.typeParameters);
    const kind = node.kind ?? -1;
    if (kind === TYPEDOC_KIND.function || kind === TYPEDOC_KIND.method) {
        return renderFunctionSignatures(node);
    }
    if (kind === TYPEDOC_KIND.interface) {
        const ext = node.extendedTypes && node.extendedTypes.length > 0
            ? ` extends ${node.extendedTypes.map(typeToString).join(", ")}`
            : "";
        const members = (node.children ?? []).map((c) => {
            const opt = c.flags?.isOptional ? "?" : "";
            return `  ${c.name}${opt}: ${memberType(c)}`;
        });
        return `interface ${node.name}${tp}${ext} {\n${members.join("\n")}\n}`;
    }
    if (kind === TYPEDOC_KIND.typeAlias || kind === TYPEDOC_KIND.typeAliasAlt) {
        return `type ${node.name}${tp} = ${typeToString(node.type)}`;
    }
    if (kind === TYPEDOC_KIND.variable) {
        return `const ${node.name}: ${typeToString(node.type)}`;
    }
    // Unknown kinds never throw — fall back to generic export form (per D0069 §3)
    return `${KIND_LABEL[kind] ?? "export"} ${node.name}`;
}
// ---------------------------------------------------------------------------
// Candidate block renderer
// ---------------------------------------------------------------------------
/**
 * Render a single declaration node as a candidate-facing markdown block:
 * ### heading, JSDoc summary, fenced ts signature, at most one @example block.
 */
function renderForCandidate(node) {
    const label = KIND_LABEL[node.kind ?? -1] ?? "export";
    // For functions, fall back to first signature's comment when node has none.
    const comment = node.comment ?? node.signatures?.[0]?.comment;
    const summary = firstParagraph(summaryText(comment));
    const lines = [];
    lines.push(`### \`${node.name}\` — ${label}`);
    if (summary)
        lines.push("", summary);
    lines.push("", "```ts", renderDeclarationSignature(node), "```");
    const examples = exampleBlocks(comment);
    if (examples.length > 0) {
        // Examples in this corpus already contain their own fences — emit verbatim.
        lines.push("", examples[0]);
    }
    return lines.join("\n");
}
// ---------------------------------------------------------------------------
// Closure expansion
// ---------------------------------------------------------------------------
/**
 * Collect reference-type names from a signature's parameters and return type.
 */
function refsFromSignature(sig) {
    const names = [];
    for (const p of sig.parameters ?? []) {
        collectRefNames(p.type, names);
    }
    collectRefNames(sig.type, names);
    return names;
}
/**
 * Collect `reference` type names within a single type expression. Recursive
 * through typeArguments, union/intersection members, and array element types,
 * bounded by MAX_TYPE_DEPTH against degenerate nesting. Note the ONE-LEVEL
 * property of closure expansion (D0069 §2) lives at the declaration axis —
 * a hinted declaration pulls in the declarations it references, with no
 * transitive pass — not within type expressions.
 */
function collectRefNames(t, out, depth = 0) {
    if (depth > MAX_TYPE_DEPTH)
        return;
    if (!t || typeof t !== "object")
        return;
    if (t.type === "reference" && typeof t.name === "string") {
        out.push(t.name);
    }
    if (t.typeArguments)
        for (const a of t.typeArguments)
            collectRefNames(a, out, depth + 1);
    if (t.types)
        for (const a of t.types)
            collectRefNames(a, out, depth + 1);
    if (t.elementType)
        collectRefNames(t.elementType, out, depth + 1);
}
/**
 * Collect reference-type names from a declaration node's own type and children
 * (for type aliases and interfaces).
 */
function refsFromNode(node) {
    const names = [];
    collectRefNames(node.type, names);
    for (const c of node.children ?? []) {
        collectRefNames(c.type, names);
        for (const s of c.signatures ?? []) {
            names.push(...refsFromSignature(s));
        }
    }
    return names;
}
// ---------------------------------------------------------------------------
// Main assembly
// ---------------------------------------------------------------------------
function assembleOutput(decls, options) {
    const { maxBytes, relevanceSymbols } = options;
    const hintSet = relevanceSymbols && relevanceSymbols.length > 0
        ? new Set(relevanceSymbols)
        : null;
    let renderList;
    let tailNames;
    if (hintSet !== null) {
        // --- Relevance-scoped path ---
        // Hinted declarations first (declaration order).
        const hinted = decls.filter((d) => typeof d.name === "string" && hintSet.has(d.name));
        // One-level closure: collect all reference-type names from hinted declarations,
        // then add matching declarations (deduped, in declaration order, after hinted).
        const closureNames = new Set();
        for (const h of hinted) {
            for (const s of h.signatures ?? []) {
                for (const n of refsFromSignature(s))
                    closureNames.add(n);
            }
            for (const n of refsFromNode(h))
                closureNames.add(n);
        }
        // Remove names already in hintSet so we don't double-add
        for (const n of hintSet)
            closureNames.delete(n);
        const closure = decls.filter((d) => typeof d.name === "string" &&
            closureNames.has(d.name) &&
            !hintSet.has(d.name));
        // Everything else is tail
        const fullSet = new Set([
            ...hinted.map((d) => d.name),
            ...closure.map((d) => d.name),
        ]);
        const tail = decls.filter((d) => typeof d.name === "string" && !fullSet.has(d.name));
        renderList = [...hinted, ...closure];
        tailNames = tail.map((d) => d.name);
    }
    else {
        // --- No-hints fallback: declaration order ---
        renderList = [...decls];
        tailNames = [];
    }
    // Tail line variants. The named form lists every omitted export; the
    // count-only form is the explicit degenerate fallback when the name list
    // itself cannot fit the budget — the count is what makes the omission
    // non-silent. Never emit a partial name list; never slice mid-name.
    const namedTail = (names) => {
        const listed = names.map((n) => `\`${n}\``).join(", ");
        return `*${names.length} more exports (signatures not expanded): ${listed}*`;
    };
    const countOnlyTail = (count) => `*${count} more exports (signatures not expanded)*`;
    // Render each candidate block and accumulate within budget, reserving room
    // for the full named tail line of whatever would be omitted at that point.
    // A block is accepted only if all omitted names can still be listed — so
    // whenever at least one block renders, the named tail is guaranteed to fit.
    const renderedNames = [];
    const sections = [];
    let usedBytes = 0;
    let breakIdx = renderList.length; // renderList.length = rendered all
    for (let i = 0; i < renderList.length; i++) {
        const node = renderList[i];
        const block = renderForCandidate(node);
        const blockStr = sections.length === 0 ? block : "\n\n" + block;
        // Tail for the scenario where everything after this block is omitted:
        // (renderList items from i+1 on) + tailNames. "\n" separator included.
        const projectedOmitted = [
            ...renderList.slice(i + 1).map((d) => d.name),
            ...tailNames,
        ];
        const tailReserve = projectedOmitted.length === 0 ? 0 : namedTail(projectedOmitted).length + 1;
        if (usedBytes + blockStr.length + tailReserve > maxBytes) {
            breakIdx = i;
            break;
        }
        sections.push(blockStr);
        usedBytes += blockStr.length;
        renderedNames.push(node.name);
    }
    // Everything from breakIdx onward was not rendered.
    const unrenderedNames = [];
    for (let i = breakIdx; i < renderList.length; i++) {
        const name = renderList[i].name;
        if (typeof name === "string")
            unrenderedNames.push(name);
    }
    const omittedNames = [...unrenderedNames, ...tailNames];
    // Assemble: blocks, then the tail line. Cap always wins; drops are explicit:
    //   1. full name list fits        → list ALL omitted names
    //   2. name list doesn't fit      → count-only line (no partial lists)
    //   3. even count-only doesn't fit → markdown is just the blocks that fit
    //      (in practice "" — block acceptance reserved the named tail — and the
    //      returned counts carry the information)
    const body = sections.join("");
    let markdown = body;
    if (omittedNames.length > 0) {
        const sep = body.length > 0 ? "\n" : "";
        const named = sep + namedTail(omittedNames);
        const countOnly = sep + countOnlyTail(omittedNames.length);
        if (body.length + named.length <= maxBytes) {
            markdown = body + named;
        }
        else if (body.length + countOnly.length <= maxBytes) {
            markdown = body + countOnly;
        }
    }
    return {
        markdown,
        renderedCount: renderedNames.length,
        indexedCount: omittedNames.length,
    };
}
