/**
 * PromptfooCompiler — compiles a TaskGraph into Promptfoo YAML configuration.
 *
 * The compiler is the core of the new architecture. It takes a validated
 * TaskGraph and produces a Promptfoo config that can be executed via
 * `promptfoo eval`.
 *
 * Compilation pipeline:
 *   TaskGraph → resolve fixtures → resolve variables → map assertions
 *             → assemble prompts → assemble providers → emit YAML
 *
 * @see docs/archive/exec-plans/architecture-overhaul/phase-2-config-compiler.md
 */
import { mapAssertions } from "./assertion-mapper.js";
import { resolveTaskFixtures } from "./fixture-resolver.js";
import { LiteracyVariant } from "../normalize-mode.js";
import { resolveVariables } from "./variable-resolver.js";
import { MOCK_PROVIDER_ABSPATH } from "../../promptfoo-providers/mock-path.js";
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Compile a TaskGraph into a Promptfoo configuration.
 *
 * Traverses the graph in topological order, resolves fixtures and
 * variables for each node, maps assertions, and assembles the final
 * Promptfoo config.
 */
export function compileToPromptfoo(graph, options) {
    const warnings = [];
    const tests = [];
    // Sort nodes by priority (topological order)
    const sortedNodes = [...graph.nodes.values()].sort((a, b) => a.priority - b.priority);
    // Compile each node into test cases
    for (const node of sortedNodes) {
        const compiled = compileNode(node, graph, options, warnings);
        tests.push(...compiled);
    }
    // Build providers list from model registry
    const providers = buildProviders(options.models, options.mode);
    // Prompt resolution: handler-owned → explicit override → built-in defaults
    const prompts = resolvePrompts(options);
    const config = {
        description: `AILF evaluation — ${options.mode} mode (${tests.length} test cases)`,
        prompts,
        providers,
        tests,
        ...(options.outputPath ? { outputPath: options.outputPath } : {}),
        ...(options.graderProvider
            ? {
                defaultTest: {
                    options: {
                        provider: options.graderProvider,
                    },
                },
            }
            : {}),
    };
    return {
        config,
        taskCount: sortedNodes.length,
        testCaseCount: tests.length,
        warnings,
    };
}
// ---------------------------------------------------------------------------
// Node compilation
// ---------------------------------------------------------------------------
function compileNode(node, graph, options, warnings) {
    // Resolve fixtures using a minimal GeneralizedTaskDefinition stub.
    // The fixture resolver needs task.id and context.docs — we use the
    // node's mode (propagated from the original task definition) to
    // construct the correct variant stub.
    const nodeMode = node.mode ?? options.mode ?? "literacy";
    const fixtureResult = resolveTaskFixtures({
        mode: nodeMode,
        id: node.taskId,
        title: node.taskId,
        prompt: { text: node.resolvedPrompt },
    }, node.resolvedVariables, { rootDir: options.rootDir });
    warnings.push(...fixtureResult.warnings);
    // Resolve dynamic variables
    const varResult = resolveVariables(fixtureResult.updatedVars);
    warnings.push(...varResult.warnings);
    // Map assertions from the node's metadata
    // For now, nodes carry assertion data in resolvedVariables.values.__assertions
    // (set by the TaskGraphBuilder when it has generalized task data)
    const rawAssertions = varResult.envelope.values.__assertions ?? [];
    const { mapped: assertions, warnings: assertionWarnings } = mapAssertions(rawAssertions, { mode: options.mode, graderProvider: options.graderProvider });
    warnings.push(...assertionWarnings);
    // Build test case vars (exclude internal __ prefixed keys)
    const vars = {};
    for (const [key, value] of Object.entries(varResult.envelope.values)) {
        if (!key.startsWith("__")) {
            vars[key] = value;
        }
    }
    // Create the gold test case
    const goldTest = {
        description: node.taskId,
        vars,
        ...(assertions.length > 0 ? { assert: assertions } : {}),
    };
    const tests = [goldTest];
    // For literacy/baseline mode, also create a baseline variant (no docs).
    // Route on the node's mode (from the task definition) rather than
    // the global options.mode, so heterogeneous graphs compile correctly.
    if (nodeMode === LiteracyVariant.STANDARD || nodeMode === "literacy") {
        const baselineVars = { ...vars, docs: "" };
        const baselineTest = {
            description: `${node.taskId} [baseline]`,
            vars: baselineVars,
            prompts: ["without-docs"],
            ...(assertions.length > 0 ? { assert: assertions } : {}),
        };
        tests.push(baselineTest);
    }
    return tests;
}
// ---------------------------------------------------------------------------
// Provider assembly
// ---------------------------------------------------------------------------
/**
 * Build the Promptfoo providers list from the model registry.
 */
function buildProviders(models, mode) {
    const providers = [];
    for (const model of models.models) {
        // Check mode compatibility
        if (model.modes && model.modes.length > 0) {
            if (!modelMatchesMode(model, mode))
                continue;
        }
        // Check env gate
        if (model.env && !process.env[model.env]) {
            continue; // Skip models whose API key isn't set
        }
        providers.push({
            id: model.id,
            label: model.label,
            config: {
                ...models.defaults,
                ...model.config,
            },
        });
    }
    // Replay swap — when AILF_REPLAY_LLMS=1 is set, rewrite every provider's
    // `id` to the file-based AILF mock provider so the Promptfoo subprocess
    // never makes a live LLM call. We preserve `label` and stash the
    // original `id` in `config.originalId` so the mock provider can surface
    // model identity in its output and reports remain interpretable.
    // `originalId ?? p.id` guards against double-swap clobbering a real id
    // that's already been preserved on a prior pass.
    // See W0110 (M5.1) and docs/design-docs/testing-strategy.md.
    if (process.env.AILF_REPLAY_LLMS === "1") {
        return providers.map((p) => ({
            id: `file://${MOCK_PROVIDER_ABSPATH}`,
            label: p.label,
            config: {
                ...p.config,
                originalId: p.config?.originalId ?? p.id,
            },
        }));
    }
    return providers;
}
/**
 * Check if a model entry matches the current evaluation mode.
 *
 * Uses the typed EvalMode values on model.modes. Variant-specific
 * provider filtering is handled by the provider-assembler and
 * generate-configs-step, not here.
 */
function modelMatchesMode(model, mode) {
    if (!model.modes || model.modes.length === 0)
        return true;
    return model.modes.includes(mode);
}
// ---------------------------------------------------------------------------
// Prompt resolution
// ---------------------------------------------------------------------------
/**
 * Resolve prompts with a three-level fallback chain:
 *   1. handler.getPrompts() — mode-handler-owned templates
 *   2. options.prompts — explicit caller-provided templates
 *   3. buildDefaultPrompts() — built-in defaults per mode
 */
function resolvePrompts(options) {
    // 1. Check handler-owned prompts
    const handlerPrompts = options.handler?.getPrompts?.();
    if (handlerPrompts && Object.keys(handlerPrompts).length > 0) {
        return Object.values(handlerPrompts).map(promptTemplateToPromptfoo);
    }
    // 2. Check explicit override
    if (options.prompts)
        return options.prompts;
    // 3. Built-in defaults
    return buildDefaultPrompts(options.mode);
}
/**
 * Convert a PromptTemplate (core port type) to a PromptfooPrompt (compiler type).
 */
function promptTemplateToPromptfoo(pt) {
    return { id: pt.id, label: pt.label, raw: pt.template };
}
// ---------------------------------------------------------------------------
// Default prompts
// ---------------------------------------------------------------------------
/**
 * Build default prompt entries for a mode.
 *
 * Handler-owned prompts (via getPrompts()) take precedence over these
 * built-in defaults. This fallback exists for modes that haven't yet
 * migrated to handler-owned prompts.
 */
function buildDefaultPrompts(mode) {
    switch (mode) {
        case "literacy":
            return [
                {
                    id: "with-docs",
                    label: "With documentation context",
                    raw: "{{task}}\n\nDocumentation context:\n{{docs}}",
                },
                {
                    id: "without-docs",
                    label: "Without documentation context",
                    raw: "{{task}}",
                },
            ];
        default:
            return [
                {
                    id: "default",
                    label: "Default prompt",
                    raw: "{{task}}",
                },
            ];
    }
}
