/**
 * CanvasDocFetcher — DocFetcher adapter for Sanity Canvas.
 *
 * Ref resolution: tagged `canvas-doc` refs resolve by `documentId`, and —
 * a deliberate, documented divergence from the Drive adapter — untagged
 * id-refs (`{id: "…"}`) are accepted with `id` read as the canvas
 * document's `_id`. Canvas documents ARE Content Lake documents
 * (`_type: sanity.canvas.document`) whose `_id` is exactly what the ref
 * carries, and untagged id-refs are the only ref form the repo task-file
 * schema accepts today (adapter ref shapes are tracked under W0218 /
 * W0220) — rejecting them would leave repo-authored tasks with no way to
 * name a canvas doc. Slug / path / perspective refs have no Canvas meaning
 * and throw `RefKindMismatchError`.
 *
 * Ref-less tasks get the whole source corpus (`source.documentIds`) as
 * context — the Canvas analogue of Drive's whole-folder mode.
 *
 * Failure policy (Drive precedent): auth failures (401/403) abort the run
 * naming the environment, API host, and token env var — nothing else will
 * succeed; other per-document failures accumulate as fetch failures and
 * the run continues; all-documents-failed aborts. Whole-source failures
 * are recorded once (attributed to the first ref-less task that hit them);
 * explicit-ref failures stay per task so grading can tell "the model
 * ignored its docs" from "this task's docs were unreadable".
 *
 * The output-side plumbing (artifact writing, zero-symbol manifest,
 * override narrowing) intentionally parallels the Drive fetcher rather
 * than extracting shared modules — this PR stays additive to the canvas
 * directory; a cross-adapter extraction is offered as a follow-up.
 */
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { RefKindMismatchError, } from "../../../_vendor/ailf-core/index.js";
import { escapeNunjucks } from "../escape-nunjucks.js";
import { DEFAULT_CANVAS_TOKEN_ENV_VAR } from "./auth.js";
import { CanvasRequestError, } from "./canvas-client.js";
import { canvasApiBase, canvasViewUrl } from "./hosts.js";
import { assertCanvasId } from "./ids.js";
export class CanvasDocFetcher {
    source;
    client;
    documents = new Map();
    rootDir;
    constructor(source, client, options = {}) {
        this.source = source;
        this.client = client;
        this.rootDir = options.rootDir ?? null;
    }
    async fetch(tasks, source) {
        const effectiveSource = this.resolveSource(source);
        const viewUrl = (documentId) => canvasViewUrl(effectiveSource, documentId);
        const contexts = [];
        const symbolIndexes = [];
        const fetchFailures = [];
        const fetchedById = new Map();
        let wholeSourceFailuresRecorded = false;
        for (const task of tasks) {
            const refs = task.context?.docs ?? [];
            const wholeSource = refs.length === 0;
            const ids = wholeSource
                ? effectiveSource.documentIds
                : refs.map((ref) => refDocumentId(ref, task.id));
            const docs = [];
            let failed = false;
            for (const id of ids) {
                try {
                    const doc = await this.loadDocument(id);
                    fetchedById.set(id, doc);
                    docs.push(doc);
                }
                catch (err) {
                    failed = true;
                    const failure = toFailure(viewUrl(id), err);
                    if (failure.status === 401 || failure.status === 403) {
                        throw new Error(`Canvas CRUD auth failed (${failure.status}) against the ` +
                            `${effectiveSource.environment} Canvas API ` +
                            `(${new URL(canvasApiBase(effectiveSource)).host}) fetching ${viewUrl(id)}: ` +
                            `${failure.error} — the token in ${effectiveSource.tokenEnvVar ?? DEFAULT_CANVAS_TOKEN_ENV_VAR} ` +
                            `must be a ${effectiveSource.environment} token with read access to the Canvas resource`, { cause: err });
                    }
                    if (!wholeSource || !wholeSourceFailuresRecorded) {
                        fetchFailures.push({ ...failure, taskId: task.id });
                    }
                }
            }
            if (wholeSource && failed) {
                wholeSourceFailuresRecorded = true;
            }
            contexts.push(buildContext(task.id, docs));
            symbolIndexes.push(zeroSymbolIndex(task.id));
        }
        const manifest = [...fetchedById.values()].map((doc) => toManifestEntry(effectiveSource, doc));
        if (fetchFailures.length > 0 && manifest.length === 0) {
            throw new Error(`Canvas adapter: all ${fetchFailures.length} fetch(es) failed; first error: ${fetchFailures[0].error}`);
        }
        if (this.rootDir) {
            await this.writeArtifacts(this.rootDir, contexts, symbolIndexes);
        }
        const metadata = {
            ...(manifest.length > 0 ? { manifest } : {}),
            ...(symbolIndexes.length > 0 ? { symbolIndexes } : {}),
            ...(fetchFailures.length > 0 ? { fetchFailures } : {}),
        };
        return Object.keys(metadata).length > 0
            ? { contexts, metadata }
            : { contexts };
    }
    /**
     * Narrow a per-call source override, mirroring the Drive adapter: the
     * pipeline's fetch-docs step passes the kind-erased `ResolvedSourceConfig`
     * on every call, so an untagged override falls back to the constructor
     * source; a tagged foreign source signals a caller bypassing the registry
     * and is rejected loudly. Beyond the Drive semantics, canvas overrides
     * that change transport coordinates are also rejected: the HTTP client is
     * bound to the constructor source's host + token at preset construction,
     * so such an override would silently keep fetching through the old
     * transport while error messages name the new one. Corpus-only overrides
     * (documentIds) are honored.
     */
    resolveSource(source) {
        if (!source)
            return this.source;
        const kind = source.kind;
        if (kind === undefined)
            return this.source;
        if (kind !== "sanity-canvas") {
            throw new RefKindMismatchError(`CanvasDocFetcher.fetch() received a non-Canvas source override (kind=${String(kind)}); pass a CanvasSource or omit the override`);
        }
        const override = source;
        if (override.environment !== this.source.environment ||
            override.resourceId !== this.source.resourceId ||
            override.organizationId !== this.source.organizationId) {
            throw new RefKindMismatchError("CanvasDocFetcher received a canvas source override with different " +
                "transport coordinates (environment/resourceId/organizationId); " +
                "the HTTP client binds these at preset construction — construct a " +
                "new fetcher instead");
        }
        return override;
    }
    /** Fetch each document once per run, shared across tasks referencing it. */
    loadDocument(documentId) {
        let pending = this.documents.get(documentId);
        if (!pending) {
            pending = this.client.getDocument(documentId);
            this.documents.set(documentId, pending);
        }
        return pending;
    }
    /**
     * Emit the per-task canonical context files and the symbol-index
     * manifest under `<rootDir>/contexts/`. `document-manifest.json` and
     * `fetch-failures.json` are written by `fetch-docs-step.ts` from the
     * returned `FetchMetadata` (same path as the Drive/Sanity adapters).
     * `escapeNunjucks` runs once at the write seam so the `DocContext`
     * returned from `fetch()` stays clean markdown.
     */
    async writeArtifacts(rootDir, contexts, symbolIndexes) {
        const canonicalDir = join(rootDir, "contexts", "canonical");
        const symbolsDir = join(rootDir, "contexts", "canonical-symbols");
        await mkdir(canonicalDir, { recursive: true });
        await mkdir(symbolsDir, { recursive: true });
        await Promise.all(contexts.map((ctx) => writeFile(join(canonicalDir, `${ctx.taskId}.md`), escapeNunjucks(ctx.content), "utf-8")));
        // W0197 observable-fallback manifest: one entry per task with
        // symbolCount: 0 — "extraction ran, produced no symbols".
        const sorted = [...symbolIndexes].sort((a, b) => a.taskId.localeCompare(b.taskId));
        await writeFile(join(symbolsDir, "manifest.json"), JSON.stringify({ entries: sorted }, null, 2), "utf-8");
    }
}
/**
 * Resolve a ref to a canvas document id. Accepts tagged `canvas-doc` refs
 * and untagged id-refs (see the class doc for why the latter diverges from
 * Drive's untagged-reject rule); everything else throws. Ref values arrive
 * from YAML task files that don't yet Zod-parse adapter ref shapes
 * (W0218 / W0220), so shape and charset are verified here — the runtime
 * trust boundary.
 */
function refDocumentId(ref, taskId) {
    const kind = "kind" in ref && typeof ref.kind === "string" ? ref.kind : undefined;
    if (kind !== undefined && kind !== "canvas-doc") {
        throw new RefKindMismatchError(`CanvasDocFetcher accepts canvas-doc or untagged id refs; got ${kind} on task ${taskId}`);
    }
    const candidate = ref;
    const documentId = kind === "canvas-doc" ? candidate.documentId : candidate.id;
    if (typeof documentId !== "string") {
        throw new RefKindMismatchError(`CanvasDocFetcher resolves refs by canvas document id ({kind: "canvas-doc", documentId} or {id}); task ${taskId} declares ${JSON.stringify(ref)}`);
    }
    assertCanvasId("documentId", documentId);
    return documentId;
}
function buildContext(taskId, docs) {
    // Canvas markdown carries its own leading heading, so documents
    // concatenate without synthesized titles.
    const content = docs.map((doc) => doc.content.trim()).join("\n\n---\n\n");
    return {
        taskId,
        content,
        slugs: docs.map((doc) => slugify(doc.title ?? doc._id)),
        tokenCount: Math.ceil(content.length / 4),
    };
}
function toFailure(resource, err) {
    const message = err instanceof Error ? err.message : String(err);
    const status = err instanceof CanvasRequestError ? err.status : undefined;
    return {
        resource,
        error: message,
        ...(status !== undefined ? { status } : {}),
    };
}
/**
 * Stamp a `sanity-canvas` locator so the entry is self-describing (D0062):
 * without it `resolveLocator` synthesizes a `sanity-content-lake` default at
 * `3do82whm`/`next` with this `_id`, and the dashboard's Apply/Export
 * affordances mis-target that nonexistent Sanity doc instead of disabling.
 * `revision` is omitted on the `"unknown"` sentinel (mirroring Drive); `_rev`
 * keeps it as the required staleness handle.
 */
function toManifestEntry(source, doc) {
    return {
        locator: {
            kind: "sanity-canvas",
            environment: source.environment,
            organizationId: source.organizationId,
            resourceId: source.resourceId,
            documentId: doc._id,
            ...(doc.revision && doc.revision !== "unknown"
                ? { revision: doc.revision }
                : {}),
        },
        _id: doc._id,
        _rev: doc.revision,
        slug: slugify(doc.title ?? doc._id),
        title: doc.title ?? doc._id,
    };
}
function zeroSymbolIndex(taskId) {
    return {
        taskId,
        path: `contexts/canonical-symbols/${taskId}.json`,
        symbolCount: 0,
        tierBreakdown: { typeDef: 0, heading: 0, inlineCode: 0, codeBlock: 0 },
    };
}
function slugify(name) {
    return name
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, "-")
        .replace(/^-+|-+$/g, "");
}
