/**
 * Literacy task compilation — core compilation logic.
 *
 * Produces the same structure as the legacy expand-tasks.ts path:
 * - Gold entry with with-docs prompt and canonical doc context
 * - Baseline entry with without-docs prompt and empty docs
 * - Rubric assertions with structured dimension metadata
 */
import { existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
import { LiteracyVariant, } from "../../../normalize-mode.js";
import { buildBaselineAssertions, resolveAssertions } from "./assertions.js";
import { LITERACY_PROMPT_TEMPLATES } from "./prompts.js";
import { validateLiteracyTask } from "./validation.js";
/**
 * Variable keys reserved by the AILF compilers. Authoring these via
 * `prompt.vars` is rejected by `PromptVars` at compile time and by
 * `TaskPromptSchema` at parse time; this constant exists to defend
 * the literacy compiler at runtime against legacy-shape `*.task.ts`
 * files that bypass both gates.
 */
const RESERVED_PROMPT_VAR_KEYS = [
    "task",
    "docs",
    "__featureArea",
];
/**
 * Compile a literacy task into Promptfoo configuration.
 */
export function compileLiteracyTask(task, options) {
    const warnings = [];
    const evalMode = options?.evalMode ?? LiteracyVariant.STANDARD;
    // Validation
    for (const err of validateLiteracyTask(task)) {
        warnings.push(`Literacy task "${task.id}": ${err.field} — ${err.message}`);
    }
    const providers = buildProviders(options);
    const prompts = buildPrompts(evalMode);
    const tests = buildTestCases(task, evalMode, options, warnings);
    return { providers, tests, prompts, warnings };
}
// ---------------------------------------------------------------------------
// Provider assembly
// ---------------------------------------------------------------------------
function buildProviders(options) {
    if (options?.models && options.models.length > 0) {
        return options.models.map((m) => ({
            id: m.id,
            label: m.label,
            config: m.config,
        }));
    }
    return [];
}
// ---------------------------------------------------------------------------
// Prompt assembly
// ---------------------------------------------------------------------------
function templateToPromptfoo(pt) {
    return { id: pt.id, label: pt.label, raw: pt.template };
}
function buildPrompts(evalMode) {
    if (evalMode === "agentic") {
        return [templateToPromptfoo(LITERACY_PROMPT_TEMPLATES["agentic"])];
    }
    return [
        templateToPromptfoo(LITERACY_PROMPT_TEMPLATES["with-docs"]),
        templateToPromptfoo(LITERACY_PROMPT_TEMPLATES["without-docs"]),
    ];
}
// ---------------------------------------------------------------------------
// Test case assembly
// ---------------------------------------------------------------------------
function buildTestCases(task, evalMode, options, warnings) {
    const tests = [];
    // W0193: type-erased read of prompt.vars so we can defensively detect
    // reserved keys on legacy-shape `*.task.ts` files (the type narrow makes
    // `task.prompt.vars.task` `never`, but TS task files bypass both the
    // type and the parse-time schema). YAML/inline-task paths have already
    // been migrated by `migratePromptShape` upstream.
    const rawVars = (task.prompt?.vars ?? {});
    const legacyTaskBody = typeof rawVars.task === "string" ? rawVars.task : undefined;
    const promptText = task.prompt?.text ?? legacyTaskBody ?? task.prompt?.template ?? "";
    const contextDocs = task.context?.docs ?? [];
    const taskArea = task.area ?? "";
    const taskTitle = task.title;
    // Strip reserved keys from the vars spread so they cannot override the
    // canonical assignments below. `safePromptVars` carries only freeform
    // template extras.
    const safePromptVars = {};
    const presentReserved = [];
    for (const [key, value] of Object.entries(rawVars)) {
        if (RESERVED_PROMPT_VAR_KEYS.includes(key)) {
            presentReserved.push(key);
            continue;
        }
        safePromptVars[key] = value;
    }
    // Single deduplicated deprecation warning per task — even when several
    // reserved keys are present.
    if (presentReserved.length > 0) {
        warnings.push(`Literacy task "${task.id}": deprecated prompt.vars keys ` +
            `(${presentReserved.join(", ")}) — use prompt.text for the prompt ` +
            `body and context.docs for documentation references. The compiler ` +
            `migrated them in-memory, but the task source should be updated.`);
    }
    // Counting declared refs alone misses a whole-folder source (Google Drive),
    // which produces a canonical context for tasks that declare no per-task refs.
    // The added check is purely additive — a task that declares refs is decided
    // exactly as before — and keeps the injection decision source-agnostic.
    const hasDocs = contextDocs.length > 0 || canonicalDocExists(task.id, options?.rootDir);
    const docsVar = hasDocs ? `file://contexts/canonical/${task.id}.md` : "";
    // When `graderContext` is "with-docs" and the task has canonical docs
    // declared, read the resolved doc file so we can inject it into the
    // assertion rubricPrompt as authoritative ground truth. The file is
    // produced by `FetchDocsStep` (which runs before `GenerateConfigsStep`),
    // so it should exist on disk by the time we're compiling.
    const canonicalReference = options?.graderContext === "with-docs" && hasDocs
        ? readGraderReference(task.id, options?.rootDir, warnings)
        : undefined;
    const assertions = resolveAssertions(task, options, warnings, canonicalReference);
    // Gold entry — canonical docs injected. Spread freeform extras first so
    // canonical keys (task / docs / __featureArea) cannot be overridden.
    const goldVars = {
        ...safePromptVars,
        task: promptText,
        docs: docsVar,
        __featureArea: taskArea,
    };
    tests.push({
        description: `${taskTitle} (gold)`,
        vars: goldVars,
        ...(evalMode === LiteracyVariant.STANDARD
            ? { prompts: ["with-docs"] }
            : {}),
        ...(assertions.length > 0 ? { assert: assertions } : {}),
    });
    // Baseline entry — no docs (floor measurement)
    if (evalMode !== "agentic") {
        const baselineEnabled = task.baseline?.enabled !== false;
        if (baselineEnabled) {
            const baselineAssertions = buildBaselineAssertions(assertions, task.baseline?.rubric, options?.rubricConfig);
            tests.push({
                description: `${taskTitle} (baseline)`,
                vars: {
                    ...safePromptVars,
                    task: promptText,
                    docs: "",
                    __featureArea: taskArea,
                },
                prompts: ["without-docs"],
                ...(baselineAssertions.length > 0
                    ? { assert: baselineAssertions }
                    : {}),
            });
        }
    }
    return tests;
}
// ---------------------------------------------------------------------------
// Canonical doc resolution
// ---------------------------------------------------------------------------
/**
 * Read the grader-context reference for a task, preferring the W0197
 * symbol-reference index when one was emitted by the doc fetcher and
 * falling back to the full canonical doc otherwise.
 *
 * Resolution is driven by the per-task manifest at
 * `<rootDir>/contexts/canonical-symbols/manifest.json` (W0197). The
 * manifest distinguishes three states:
 *   1. Manifest missing → fetcher hadn't run with W0197. Silent fallback
 *      to the full canonical doc; this is the backwards-compat path.
 *   2. Manifest present, entry has `symbolCount > 0` → use the per-task
 *      symbol-index file as the grader reference (W0197 path).
 *   3. Manifest present, entry has `symbolCount === 0` → extraction ran
 *      and produced nothing. Observable fallback (warning surfaced)
 *      to the full canonical doc.
 *
 * Returns undefined and pushes a warning when the canonical doc itself
 * is missing or unreadable, when rootDir is unset, or when a resolved
 * path escapes rootDir (path-traversal guard mirroring
 * `fixture-resolver.ts:resolveFileRef`).
 */
function readGraderReference(taskId, rootDir, warnings) {
    if (!rootDir) {
        warnings.push(`graderContext "with-docs" requires rootDir — canonical reference for "${taskId}" not injected`);
        return undefined;
    }
    const manifestEntry = readSymbolIndexManifestEntry(taskId, rootDir, warnings);
    if (manifestEntry !== "missing-manifest") {
        if (manifestEntry === "path-traversal")
            return undefined;
        if (manifestEntry === null) {
            // Manifest exists but task isn't listed — shouldn't happen for tasks
            // the fetcher processed. Treat as silent fallback (matches "missing
            // manifest" semantics for safety) but log a warning so an unexpected
            // mismatch is observable.
            warnings.push(`graderContext: task "${taskId}" not present in symbol-index manifest — falling back to full canonical doc injection`);
        }
        else if (manifestEntry.symbolCount === 0) {
            warnings.push(`graderContext: symbol index empty for "${taskId}" — falling back to full canonical doc injection`);
        }
        else {
            // Symbol-index path: read the per-task .md file.
            const symbolsRelative = manifestEntry.path;
            const symbolsAbsolute = resolveWithinRoot(rootDir, symbolsRelative);
            if (symbolsAbsolute === null) {
                warnings.push(`graderContext: path traversal blocked for task "${taskId}" — canonical reference not injected`);
                return undefined;
            }
            if (existsSync(symbolsAbsolute)) {
                const content = readFileSafe(symbolsAbsolute, symbolsRelative, warnings, taskId);
                if (content && content.trim().length > 0)
                    return content;
            }
            // Manifest claimed symbols but the file is missing/unreadable —
            // surface as a warning and continue to full-doc fallback.
            warnings.push(`graderContext: symbol index file at ${symbolsRelative} is missing or empty despite manifest entry — falling back to full canonical doc for "${taskId}"`);
        }
    }
    // Fall back to the full canonical doc (W0196 path).
    const canonicalRelative = `contexts/canonical/${taskId}.md`;
    const canonicalAbsolute = resolveWithinRoot(rootDir, canonicalRelative);
    if (canonicalAbsolute === null) {
        warnings.push(`graderContext: path traversal blocked for task "${taskId}" — canonical reference not injected`);
        return undefined;
    }
    if (!existsSync(canonicalAbsolute)) {
        warnings.push(`graderContext "with-docs": canonical doc not found at ${canonicalRelative} — run fetch-docs first; reference not injected for "${taskId}"`);
        return undefined;
    }
    return (readFileSafe(canonicalAbsolute, canonicalRelative, warnings, taskId) ??
        undefined);
}
/**
 * Whether FetchDocsStep produced a canonical context for this task. Lets a
 * source-level fetcher (e.g. Google Drive, which supplies the whole folder for
 * tasks that declare no per-task refs) drive the docs-injection decision —
 * without `context.docs` declared. Reuses the path-traversal guard.
 */
function canonicalDocExists(taskId, rootDir) {
    if (!rootDir) {
        return false;
    }
    const abs = resolveWithinRoot(rootDir, `contexts/canonical/${taskId}.md`);
    return abs !== null && existsSync(abs);
}
function readSymbolIndexManifestEntry(taskId, rootDir, warnings) {
    const manifestRelative = "contexts/canonical-symbols/manifest.json";
    const manifestAbsolute = resolveWithinRoot(rootDir, manifestRelative);
    if (manifestAbsolute === null)
        return "path-traversal";
    if (!existsSync(manifestAbsolute))
        return "missing-manifest";
    const body = readFileSafe(manifestAbsolute, manifestRelative, warnings, taskId);
    if (body === null)
        return "missing-manifest";
    let parsed;
    try {
        parsed = JSON.parse(body);
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        warnings.push(`graderContext: symbol-index manifest at ${manifestRelative} is unparseable (${msg}) — falling back to full canonical doc for "${taskId}"`);
        return "missing-manifest";
    }
    if (!parsed || typeof parsed !== "object")
        return null;
    const entries = parsed.entries;
    if (!Array.isArray(entries))
        return null;
    for (const e of entries) {
        if (!e || typeof e !== "object")
            continue;
        const entry = e;
        if (typeof entry.taskId === "string" &&
            typeof entry.path === "string" &&
            typeof entry.symbolCount === "number" &&
            entry.taskId === taskId) {
            return {
                taskId: entry.taskId,
                path: entry.path,
                symbolCount: entry.symbolCount,
            };
        }
    }
    return null;
}
function resolveWithinRoot(rootDir, relativePath) {
    const absolutePath = resolve(rootDir, relativePath);
    const normalizedBase = resolve(rootDir) + "/";
    if (!absolutePath.startsWith(normalizedBase) &&
        absolutePath !== resolve(rootDir)) {
        return null;
    }
    return absolutePath;
}
function readFileSafe(absolutePath, relativePath, warnings, taskId) {
    try {
        return readFileSync(absolutePath, "utf-8");
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        warnings.push(`graderContext: failed to read ${relativePath}: ${msg} — reference not injected for "${taskId}"`);
        return null;
    }
}
