/**
 * 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 and
 * typesReference 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`.
 *      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 { toMarkdown } from "./portable-text.js";
import { extractSymbolIndex, extractSymbolsFromTypedoc, mergeSymbolIndexes, } from "./symbol-index.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]*`);
}
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),
    };
}
const articleRenderer = {
    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";
    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);
}
const typesReferenceRenderer = {
    render: renderTypesReference,
    extractSymbols: extractTypesReferenceSymbols,
};
// ---------------------------------------------------------------------------
// 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}`,
            },
        })),
    };
}
const defaultRenderer = {
    render: renderDefault,
    extractSymbols: extractDefaultSymbols,
};
// ---------------------------------------------------------------------------
// Registry
// ---------------------------------------------------------------------------
const BUILT_IN_RENDERERS = {
    article: articleRenderer,
    typesReference: typesReferenceRenderer,
};
function rendererFor(type) {
    return BUILT_IN_RENDERERS[type] ?? defaultRenderer;
}
/**
 * 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();
