/**
 * document-renderers.ts
 *
 * Renderer registry that turns a Sanity document into both:
 *
 *   - Markdown content for inclusion in a literacy task's grader/candidate
 *     context (existing surface, used by the doc fetcher).
 *   - A symbol-reference index for the W0197 grader-context pathway —
 *     a flat list of identifiers the doc legitimizes, with provenance.
 *     The grader prefers this over the rendered markdown when available
 *     (smaller, deterministic, harder for the grader's prior to override).
 *
 * Both surfaces dispatch through the same registry. Articles,
 * typesReference, and openApiReference docs have hand-written renderers;
 * everything else falls through to the default walker. This keeps "what to
 * do with a document of type X" a single decision point regardless of
 * whether the doc was looked up by slug, path, perspective, or id.
 *
 * Two tiers of fidelity (rendered output):
 *
 *   1. Registered renderers (high fidelity) — `article`, `typesReference`,
 *      `openApiReference`. Hand-written for the document shapes we care
 *      about most.
 *   2. Default renderer (best effort) — walks the doc, flattens portable-text
 *      fields, surfaces top-level scalars, follows references one level deep,
 *      skips framework-internal fields. Lets pinning a `marketingPage`,
 *      `glossaryEntry`, etc. work without AILF code changes.
 *
 * Adding a new high-fidelity renderer: implement a `DocumentRenderer`
 * (both `render` and `extractSymbols`) and register it in
 * `BUILT_IN_RENDERERS` keyed by `_type`.
 */
import { CANONICAL_DOC_TYPES, CANONICAL_DOC_TYPES_GROQ, isCanonicalDocType, } from "../_vendor/ailf-shared/index.js";
import { listOperationRefs, renderOpenApiSpec } from "./openapi-spec.js";
import { toMarkdown } from "./portable-text.js";
import { extractSymbolIndex, extractSymbolsFromTypedoc, mergeSymbolIndexes, } from "./symbol-index.js";
import { renderTypedocDeclarations } from "./typedoc-declarations.js";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const DEFAULT_MAX_BYTES = 30_000;
const SKIP_FIELDS = new Set([
    "_createdAt",
    "_id",
    "_rev",
    "_system",
    "_type",
    "_updatedAt",
    "_key",
]);
function isPortableTextArray(value) {
    return (Array.isArray(value) &&
        value.length > 0 &&
        typeof value[0] === "object" &&
        value[0] !== null &&
        "_type" in value[0]);
}
function truncate(content, max) {
    if (content.length <= max)
        return content;
    return (content.slice(0, max) +
        `\n\n*[content truncated — ${content.length - max} more bytes omitted]*`);
}
/**
 * Stable display slug for a document. Articles use `slug.current`; other
 * types fall back to `<_type>:<_id>`. Exported so the doc fetcher can label a
 * deferred-render doc (e.g. a relevance-scoped `typesReference`) without first
 * rendering it.
 */
export function slugForDoc(doc) {
    const slugField = doc.slug;
    if (typeof slugField === "object" &&
        slugField !== null &&
        "current" in slugField &&
        typeof slugField.current === "string") {
        return slugField.current;
    }
    if (typeof slugField === "string")
        return slugField;
    return `${doc._type}:${doc._id}`;
}
function renderArticle(doc) {
    const title = doc.title ?? "(untitled)";
    const description = doc.description;
    const section = doc.section;
    const content = doc.content;
    const sectionLabel = section?.title ? `Section: ${section.title}\n` : "";
    const desc = description ? `${description}\n\n` : "";
    const markdown = toMarkdown(content ?? []);
    return {
        content: `## ${title}\n\n${sectionLabel}${desc}${markdown}`,
        fidelity: "high",
        slug: slugForDoc(doc),
    };
}
// The article id-ref projection body. Mirrors ARTICLE_PROJECTION's flatten +
// card-deref shape (queries.ts), minus the `_id/_type/_rev` the id-ref query
// projects in its shared header.
const ARTICLE_PROJECTION_BODY = `{
      title,
      description,
      "slug": slug.current,
      "section": primarySection->{ "slug": slug.current, title },
      "content": content[] {
        _type != "docsCardCollection" => { ... },
        _type == "docsCardCollection" => {
          ...,
          cards[] {
            ...,
            "resolvedTitle": reference->title,
            "resolvedSlug": reference->slug.current
          }
        }
      }
    }`;
const articleRenderer = {
    projection: ARTICLE_PROJECTION_BODY,
    render: renderArticle,
    extractSymbols(doc) {
        const content = doc.content;
        if (!Array.isArray(content))
            return { symbols: [] };
        return extractSymbolIndex(content);
    },
};
async function renderTypesReference(doc, ctx) {
    const title = doc.title ?? "(untitled)";
    const slug = slugForDoc(doc);
    const library = doc.library;
    const latestVersion = doc.latestVersion;
    const asset = latestVersion?.attachment?.asset;
    const max = ctx.maxBytes ?? DEFAULT_MAX_BYTES;
    const lines = [];
    lines.push(`## ${title}`, "");
    lines.push(`Slug: \`${slug}\``);
    if (library?.npmName)
        lines.push(`Package: \`${library.npmName}\``);
    if (latestVersion?.semver)
        lines.push(`Version: \`${latestVersion.semver}\``);
    if (latestVersion?.date)
        lines.push(`Released: ${latestVersion.date}`);
    lines.push("");
    if (!asset?.url) {
        lines.push("*[no attached type definitions found — resolver could not locate `latestVersion.attachment.asset.url`]*");
        return { content: lines.join("\n"), fidelity: "high", slug };
    }
    if (!ctx.fetchUrl) {
        lines.push(`*[type definitions live at ${asset.url} — fetcher not configured to retrieve attachments]*`);
        return { content: lines.join("\n"), fidelity: "high", slug };
    }
    const body = await ctx.fetchUrl(asset.url);
    if (body === null) {
        lines.push(`*[failed to fetch type definitions from ${asset.url}]*`);
        return { content: lines.join("\n"), fidelity: "high", slug };
    }
    const filename = asset.originalFilename ?? "types.json";
    // Semantic path (D0069): attempt to parse and render as declaration blocks.
    // Byte budget for the declarations section: subtract the header lines already
    // accumulated and a small fixed slack (~100 bytes) for the section heading.
    const headerBytes = lines.join("\n").length;
    const bodyBudget = Math.max(0, max - headerBytes - 100);
    const semantic = renderTypedocDeclarations(body, {
        maxBytes: bodyBudget,
        relevanceSymbols: ctx.relevanceSymbols,
    });
    if (semantic !== null) {
        // Semantic path: section heading + declaration blocks (no outer code fence).
        lines.push(`### API reference (${filename})`, "");
        lines.push(semantic.markdown);
    }
    else {
        // Raw fallback (D0069 sub-decision 7): unparseable typedoc attachment
        // keeps today's inline + truncation-notice behavior exactly.
        const lang = filename.endsWith(".json") ? "json" : "";
        lines.push(`### Type definitions (${filename})`, "");
        lines.push("```" + lang);
        // Reserve ~200 bytes for the fence + trailing notice
        lines.push(truncate(body, Math.max(0, max - 200)));
        lines.push("```");
    }
    return { content: lines.join("\n"), fidelity: "high", slug };
}
async function extractTypesReferenceSymbols(doc, ctx) {
    const library = doc.library;
    const latestVersion = doc.latestVersion;
    const asset = latestVersion?.attachment?.asset;
    // Re-fetches the same URL the renderer would; bounded to ≤ a handful of
    // typesReference docs per eval run, so the duplicate I/O is acceptable.
    // Fold into a single registry method later if profiling shows it bites.
    if (!asset?.url || !ctx.fetchUrl)
        return { symbols: [] };
    const body = await ctx.fetchUrl(asset.url);
    if (body === null)
        return { symbols: [] };
    return extractSymbolsFromTypedoc(body, library?.npmName);
}
// The typesReference id-ref projection body: derefs `library` and
// `latestVersion`, including the resolved file-asset URL so the renderer can
// fetch typedoc JSON without a follow-up round-trip. `slug` is a plain string
// on this type (not `slug.current`).
const TYPES_REFERENCE_PROJECTION_BODY = `{
      title,
      autoPublish,
      "slug": slug,
      "library": library->{ _id, _type, title, npmName },
      "latestVersion": latestVersion->{
        _id,
        _type,
        semver,
        date,
        "attachment": {
          "asset": attachment.asset->{
            _id,
            _type,
            url,
            originalFilename,
            mimeType,
            size,
            extension
          }
        }
      }
    }`;
const typesReferenceRenderer = {
    projection: TYPES_REFERENCE_PROJECTION_BODY,
    render: renderTypesReference,
    extractSymbols: extractTypesReferenceSymbols,
};
// ---------------------------------------------------------------------------
// renderOpenApiReference — high-fidelity renderer for editorial
// `openApiReference` docs (the docs site's HTTP API references).
//
// These store their entire spec as a raw OpenAPI 3.x YAML string inside a
// single typed field (`specification = { _type: "code", code: "openapi: …" }`)
// plus a short Portable Text `content` intro. The default walker drops the
// spec — a `{_type:"code"}` object has no `_id`, so `renderField` returns
// null — so a pinned doc would render to title + intro only, with the entire
// API surface (paths, operations, params, responses) invisible to the grader.
//
// This renderer prefixes the intro, then parses the spec and emits operations
// as Markdown via `renderOpenApiSpec`. When the spec is unparseable it falls
// back to a fenced raw-spec block honoring `ctx.maxBytes`, mirroring the
// D0069 raw-fallback path used by `renderTypesReference`.
// ---------------------------------------------------------------------------
function renderOpenApiReference(doc, ctx) {
    const title = doc.title ?? "(untitled)";
    const slug = slugForDoc(doc);
    const description = doc.description;
    const specification = doc.specification;
    const content = doc.content;
    const max = ctx.maxBytes ?? DEFAULT_MAX_BYTES;
    const lines = [];
    lines.push(`## ${title}`, "");
    lines.push(`Slug: \`${slug}\``, "");
    if (description)
        lines.push(description, "");
    // Portable Text intro (the `content` field), when present.
    if (isPortableTextArray(content)) {
        const intro = toMarkdown(content);
        if (intro.trim())
            lines.push(intro, "");
    }
    const specCode = typeof specification?.code === "string" ? specification.code : "";
    if (!specCode) {
        lines.push("*[no OpenAPI specification found on this document]*");
        return { content: lines.join("\n").trimEnd(), fidelity: "high", slug };
    }
    // Semantic path: parse the YAML and render operations. Budget for the body
    // is the cap minus the header already accumulated and a small slack for the
    // section heading.
    const headerBytes = lines.join("\n").length;
    const bodyBudget = Math.max(0, max - headerBytes - 100);
    const semantic = renderOpenApiSpec(specCode, { maxBytes: bodyBudget });
    if (semantic !== null) {
        lines.push(`### API reference (${semantic.operationCount} operation${semantic.operationCount === 1 ? "" : "s"})`, "");
        lines.push(semantic.markdown);
    }
    else {
        // Raw fallback (mirrors typesReference D0069 §7): inline the raw spec in a
        // fenced block, head-truncated to the remaining budget.
        lines.push("### OpenAPI specification", "");
        lines.push("```yaml");
        lines.push(truncate(specCode, Math.max(0, max - headerBytes - 200)));
        lines.push("```");
    }
    return { content: lines.join("\n").trimEnd(), fidelity: "high", slug };
}
function extractOpenApiReferenceSymbols(doc) {
    // Surface the spec's operationIds as recognition anchors for the W0197
    // grader-context pathway (AC#3). They are tagged `heading` — the closest
    // existing provenance tier for "authoritative names from the documented
    // surface" (each operation is rendered as a heading), which avoids rippling
    // a new kind through the symbol-index merge / tier-breakdown / render path.
    // The snippet locates each id by method + path (+ summary). When the spec is
    // unparseable or carries no operationIds the index is empty and the consumer
    // falls back to the full rendered Markdown — also sanctioned by the registry
    // contract.
    const specification = doc.specification;
    const code = typeof specification?.code === "string" ? specification.code : "";
    if (!code)
        return { symbols: [] };
    const symbols = listOperationRefs(code).map((ref) => ({
        symbol: ref.operationId,
        provenance: {
            kind: "heading",
            snippet: `${ref.method.toUpperCase()} ${ref.path}${ref.summary ? ` — ${ref.summary}` : ""}`,
        },
    }));
    return { symbols };
}
// The openApiReference id-ref projection body: the Portable Text `content`
// intro plus the `specification` object holding the raw OpenAPI YAML in
// `specification.code`. The default `{...}` walker would preserve
// `specification` but drop its render (a typed `{_type:"code"}` object with no
// `_id` renders to null), so a dedicated branch + renderer is required.
const OPENAPI_REFERENCE_PROJECTION_BODY = `{
      title,
      description,
      "slug": slug.current,
      "content": content[] { ... },
      specification
    }`;
const openApiReferenceRenderer = {
    projection: OPENAPI_REFERENCE_PROJECTION_BODY,
    render: renderOpenApiReference,
    extractSymbols: extractOpenApiReferenceSymbols,
};
// ---------------------------------------------------------------------------
// formatDefault — generic walker for any unknown `_type`.
//
// Walks top-level fields, renders portable-text arrays as Markdown, surfaces
// scalars as labeled key/value lines, and follows resolved references one
// level deep. The output is "best effort" — not as tight as a hand-written
// renderer, but feeds the LLM grader real content for any pinned doc type.
// ---------------------------------------------------------------------------
function renderScalar(value) {
    if (typeof value === "string")
        return value;
    if (typeof value === "number" || typeof value === "boolean") {
        return String(value);
    }
    return null;
}
function renderField(key, value, depth = 0) {
    if (value === null || value === undefined)
        return null;
    if (SKIP_FIELDS.has(key))
        return null;
    const indent = "  ".repeat(depth);
    if (isPortableTextArray(value)) {
        const md = toMarkdown(value);
        if (!md.trim())
            return null;
        return `${indent}**${key}:**\n\n${md}`;
    }
    // Sanity slug field: { _type: "slug", current: "…" }
    if (typeof value === "object" &&
        value !== null &&
        !Array.isArray(value) &&
        value._type === "slug") {
        const current = value.current;
        if (typeof current === "string")
            return `${indent}**${key}:** \`${current}\``;
        return null;
    }
    const scalar = renderScalar(value);
    if (scalar !== null)
        return `${indent}**${key}:** ${scalar}`;
    // Single-level deref: a previously-resolved reference shows up as an
    // object with its own `_id`/`_type` (the projection joined it in). Walk
    // its fields one level deep.
    if (typeof value === "object" &&
        !Array.isArray(value) &&
        depth === 0 &&
        value._id !== undefined) {
        const inner = Object.entries(value)
            .map(([k, v]) => renderField(k, v, depth + 1))
            .filter((line) => line !== null);
        if (inner.length === 0)
            return null;
        return `${indent}**${key}:**\n${inner.join("\n")}`;
    }
    if (Array.isArray(value)) {
        const items = value
            .map((item, i) => renderField(String(i), item, depth + 1))
            .filter((line) => line !== null);
        if (items.length === 0)
            return null;
        return `${indent}**${key}:**\n${items.join("\n")}`;
    }
    return null;
}
function renderDefault(doc) {
    const title = doc.title ?? `(${doc._type})`;
    const slug = slugForDoc(doc);
    const lines = [`## ${title}`, "", `Type: \`${doc._type}\``];
    if (slug !== `${doc._type}:${doc._id}`)
        lines.push(`Slug: \`${slug}\``);
    lines.push("");
    const fieldLines = [];
    for (const [key, value] of Object.entries(doc)) {
        if (key === "title")
            continue;
        if (key === "slug" && slug !== `${doc._type}:${doc._id}`)
            continue;
        const rendered = renderField(key, value);
        if (rendered !== null)
            fieldLines.push(rendered);
    }
    if (fieldLines.length > 0) {
        lines.push(...fieldLines);
    }
    else {
        lines.push("*[no renderable content — all fields were null, framework metadata, or unrecognized shapes]*");
    }
    return { content: lines.join("\n"), fidelity: "default", slug };
}
function extractDefaultSymbols(doc) {
    // For unknown types we don't know the doc's intent — but if it has any
    // Portable Text fields, those probably contain prose-with-inline-code
    // that names symbols. Walk top-level fields, run the PT extractor on
    // each PT array, and tag each extracted entry with the originating
    // field name so reviewers can trace which field a symbol came from.
    // Returns empty for shapes with no PT content (purely scalar docs like
    // `marketingPage`); caller falls back to the rendered markdown.
    const indexes = [];
    for (const [key, value] of Object.entries(doc)) {
        if (SKIP_FIELDS.has(key))
            continue;
        if (isPortableTextArray(value)) {
            indexes.push(tagWithFieldName(extractSymbolIndex(value), key));
        }
    }
    return mergeSymbolIndexes(indexes);
}
function tagWithFieldName(index, fieldName) {
    return {
        symbols: index.symbols.map((entry) => ({
            symbol: entry.symbol,
            provenance: {
                ...entry.provenance,
                snippet: `[${fieldName}] ${entry.provenance.snippet}`,
            },
        })),
    };
}
// The catch-all projection: the raw document fields (`{ ... }`), which the
// default walker flattens to Markdown. Used as the `!(_type in [...])` branch
// of the derived id-ref query so any unregistered type still resolves with
// best-effort content (W0195).
const DEFAULT_PROJECTION_BODY = `{ ... }`;
const defaultRenderer = {
    projection: DEFAULT_PROJECTION_BODY,
    render: renderDefault,
    extractSymbols: extractDefaultSymbols,
};
// ---------------------------------------------------------------------------
// Registry
// ---------------------------------------------------------------------------
// The high-fidelity renderer registry. Typed as `Record<CanonicalDocType, …>`
// so the key set is locked to the shared `CANONICAL_DOC_TYPES` list at compile
// time: a registered type missing from the shared list is an excess-property
// error, and a canonical type with no renderer is a missing-property error.
// That `shared ⇄ registry` agreement, plus the registry-derived
// `DOCS_BY_IDS_QUERY` below, plus Studio re-exporting the same shared list, is
// the doctype renderer contract (D0085) — no surface can silently drift.
const BUILT_IN_RENDERERS = {
    article: articleRenderer,
    openApiReference: openApiReferenceRenderer,
    typesReference: typesReferenceRenderer,
};
function rendererFor(type) {
    return isCanonicalDocType(type) ? BUILT_IN_RENDERERS[type] : defaultRenderer;
}
/**
 * Assemble `DOCS_BY_IDS_QUERY` from the renderer registry: a shared header of
 * system fields, one `_type == "<type>" => <projection>` branch per registered
 * renderer (in {@link CANONICAL_DOC_TYPES} order), and the default renderer's
 * projection as the `!(_type in [...])` catch-all. The exclusion list is the
 * same shared `CANONICAL_DOC_TYPES_GROQ`, so it can never fall out of sync with
 * the branch set — a type added to the registry (and the shared list) gains its
 * branch and leaves the catch-all in one place. `queries.ts` re-exports the
 * result as `DOCS_BY_IDS_QUERY`.
 *
 * @see docs/decisions/D0085-doctype-renderer-contract.md
 */
export function buildDocsByIdsQuery() {
    const branches = CANONICAL_DOC_TYPES.map((type) => `    _type == "${type}" => ${BUILT_IN_RENDERERS[type].projection},`).join("\n");
    return `
  *[_id in $ids] {
    _id,
    _type,
    _system,
    _createdAt,
    _updatedAt,
    _rev,
${branches}
    !(_type in ${CANONICAL_DOC_TYPES_GROQ}) => ${defaultRenderer.projection}
  }
`;
}
/**
 * Render a document using the registered renderer for its `_type`, falling
 * back to the default walker. The returned `fidelity` flag tells callers
 * whether to emit the "info: dedicated renderer would help" log.
 */
export async function renderDocument(doc, ctx = {}) {
    return rendererFor(doc._type).render(doc, ctx);
}
/**
 * Extract a symbol-reference index for a document using its registered
 * renderer (or the default walker for unknown types). Used by the
 * grader-context pathway (W0197) to feed the LLM judge a compact
 * deterministic recognition reference instead of the full rendered doc.
 *
 * Returns an empty `SymbolIndex` (`{ symbols: [] }`) when extraction
 * yields nothing — callers interpret this as the signal to fall back to
 * the rendered markdown.
 */
export async function extractSymbolsForDoc(doc, ctx = {}) {
    return rendererFor(doc._type).extractSymbols(doc, ctx);
}
/** Exported for tests and consumers that want the registered set. */
export const REGISTERED_RENDERER_TYPES = Object.keys(BUILT_IN_RENDERERS).sort();
