/**
 * compiler-to-yaml.ts — Serialize compiled Promptfoo config to YAML files.
 *
 * This is the bridge between the compiler pipeline (in-memory
 * CompiledPromptfooConfig) and the RunEvalStep which reads
 * YAML config files from disk.
 *
 * @see packages/eval/src/orchestration/steps/run-eval-step.ts — consumer
 */
import { existsSync, mkdirSync, writeFileSync } from "fs";
import { tmpdir } from "os";
import { resolve } from "path";
import { dump } from "js-yaml";
import { simpleHash } from "./hash.js";
/**
 * URL extraction assertion — diagnostic-only, excluded from scoring.
 * Extracts all URLs from the output for telemetry. The `metadata.scoring:
 * false` marker keeps it out of `assertion-pass-rate` (D0083; before that
 * this was a `weight: 0` sentinel).
 */
const URL_EXTRACTION_ASSERT = {
    type: "javascript",
    value: `const urlPattern = /https?:\\/\\/[^\\s\\)\\"\\'\\\`>]+/g;
const urls = [...new Set((output.match(urlPattern) || []))];
const sanityUrls = urls.filter(u => u.includes('sanity.io'));
return {
  pass: true,
  score: 1,
  reason: JSON.stringify({
    sanityUrls,
    otherUrls: urls.filter(u => !u.includes('sanity.io')),
    totalUrlCount: urls.length,
    sanityUrlCount: sanityUrls.length
  })
};`,
    metadata: { scoring: false },
};
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Write compiled literacy results to the same YAML files the legacy
 * path produces, so RunEvalStep can consume them unchanged.
 *
 * Writes:
 *   - tasks/.expanded.yaml (gold + baseline test entries for baseline mode)
 *   - tasks/.expanded.agentic.yaml (gold-only entries for agentic mode)
 * Write a single Promptfoo config YAML file for any non-literacy mode.
 *
 * This is the generic writer that works with any ModeCompileResult.
 * Literacy uses its own multi-file writer below.
 */
export function writeCompiledModeConfig(result, mode, options) {
    const filename = `promptfooconfig.${mode}.yaml`;
    const outputPath = `results/latest/eval-results-${mode}.json`;
    // Ensure results directory exists
    const resultsDir = resolve(options.rootDir, "results/latest");
    if (!existsSync(resultsDir)) {
        mkdirSync(resultsDir, { recursive: true });
    }
    // Write expanded test entries to a JSON file
    const testsFilename = `results/latest/${mode}-tests.json`;
    const testsPath = resolve(options.rootDir, testsFilename);
    const expandedTests = testsToExpandedEntries(result.tests);
    writeFileSync(testsPath, JSON.stringify(expandedTests, null, 2), "utf-8");
    // Build grader options
    const graderOpts = {};
    if (options.graderProvider) {
        graderOpts.provider = options.graderProvider;
    }
    // For agent-harness mode, create sandbox directories and inject working_dir
    // into provider configs. The sandbox must exist before the provider initializes
    // (the Claude Agent SDK reads working_dir at construction time).
    // Both working_dir and __workingDir use absolute paths to avoid ambiguity.
    //
    // The sandbox lives under os.tmpdir() (not in the repo) so the afterEach
    // teardown guard — which only deletes dirs under os.tmpdir() — actually runs.
    // The rootDir hash keeps the path stable per checkout and avoids collisions
    // across concurrent workspaces. The dir is shared across the mode's tasks;
    // per-task isolation comes from the beforeEach wipe-and-reprovision.
    // @see https://www.promptfoo.dev/docs/providers/claude-agent-sdk/
    const sandboxAbsPath = result.extras?.sandboxConfig
        ? resolve(tmpdir(), `ailf-sandbox-${mode}-${simpleHash(options.rootDir)}`)
        : undefined;
    if (sandboxAbsPath) {
        mkdirSync(sandboxAbsPath, { recursive: true });
    }
    // Agent-harness tasks share one sandbox working dir; the beforeEach wipe gives
    // per-task isolation only when rows run sequentially. Force serial execution so
    // concurrent rows can't wipe each other's in-flight sandbox.
    const effectiveMaxConcurrency = mode === "agent-harness" ? 1 : options.maxConcurrency;
    // Build provider entries, injecting working_dir for agent-harness providers
    const providerEntries = result.providers.map((p) => {
        if (!p.config)
            return p.label ? { id: p.id, label: p.label } : p.id;
        const config = { ...p.config };
        if (sandboxAbsPath && p.id === "anthropic:claude-agent-sdk") {
            config.working_dir = sandboxAbsPath;
        }
        return { id: p.id, label: p.label, config };
    });
    // Inject __workingDir into test vars so assertions can find the sandbox
    if (sandboxAbsPath) {
        for (const test of expandedTests) {
            if (test.vars) {
                ;
                test.vars.__workingDir = sandboxAbsPath;
            }
        }
        // Re-write the tests file with the injected paths
        writeFileSync(testsPath, JSON.stringify(expandedTests, null, 2), "utf-8");
    }
    // Build prompt entries
    const prompts = result.prompts.map((p) => ({
        id: p.id,
        label: p.label,
        raw: p.raw,
    }));
    // Assemble and write
    const config = assembleConfig({
        description: `AILF ${mode} evaluation`,
        graderOpts,
        maxConcurrency: effectiveMaxConcurrency,
        outputPath,
        prompts,
        providers: providerEntries,
        tests: [testsFilename],
    });
    // Include extensions if present (agent-harness mode)
    // Promptfoo expects extensions as string[] (file paths to JS modules),
    // so we materialize the { type, code } objects as a .cjs file on disk.
    if (result.extras?.extensions) {
        const extPaths = writeExtensionFile(options.rootDir, mode, result.extras.extensions);
        config.extensions = extPaths;
    }
    writeConfig(options.rootDir, filename, config, options.logger);
}
/**
 * Write three literacy-specific Promptfoo config YAML files.
 *
 *   - promptfooconfig.yaml (baseline config)
 *   - promptfooconfig.agentic.yaml (agentic config)
 *   - promptfooconfig.observed.yaml (observed config — same structure as baseline)
 */
export function writeCompiledLiteracyConfigs(baselineResult, agenticResult, providers, options) {
    const { rootDir, graderProvider, logger: log } = options;
    // Ensure tasks/ directory exists
    const tasksDir = resolve(rootDir, "tasks");
    if (!existsSync(tasksDir)) {
        mkdirSync(tasksDir, { recursive: true });
    }
    // Write expanded test entries (same format as expand-tasks.ts output)
    const baselineEntries = testsToExpandedEntries(baselineResult.tests);
    const agenticEntries = testsToExpandedEntries(agenticResult.tests);
    writeYaml(resolve(rootDir, "tasks", ".expanded.yaml"), baselineEntries, "# .expanded.yaml\n#\n# AUTO-GENERATED by compiler pipeline — do not edit directly.\n# Run: npx @sanity/ailf generate-configs\n");
    log?.info(`  ✓ tasks/.expanded.yaml (${baselineEntries.length} entries)`);
    writeYaml(resolve(rootDir, "tasks", ".expanded.agentic.yaml"), agenticEntries, "# .expanded.agentic.yaml\n#\n# AUTO-GENERATED by compiler pipeline — do not edit directly.\n# Gold entries only (no baseline) for agentic evaluation mode.\n# Run: npx @sanity/ailf generate-configs\n");
    log?.info(`  ✓ tasks/.expanded.agentic.yaml (${agenticEntries.length} entries)`);
    const taskFiles = ["file://tasks/.expanded.yaml"];
    const agenticTaskFiles = ["file://tasks/.expanded.agentic.yaml"];
    // Assemble and write promptfoo config files
    const graderOpts = {
        provider: graderProvider,
        rubricProvider: graderProvider,
    };
    const maxConc = options.maxConcurrency;
    // Baseline config
    const baselineConfig = assembleConfig({
        description: "Sanity AI Literacy Evaluation — Baseline",
        prompts: baselineResult.prompts,
        providers: providers.baseline,
        tests: taskFiles,
        outputPath: "results/latest/eval-results.json",
        graderOpts,
        maxConcurrency: maxConc,
    });
    writeConfig(rootDir, "promptfooconfig.yaml", baselineConfig, log);
    // Observed config (same structure as baseline, different providers)
    const observedConfig = assembleConfig({
        description: "Sanity AI Literacy Evaluation — Observed",
        prompts: baselineResult.prompts,
        providers: providers.observed,
        tests: taskFiles,
        outputPath: "results/latest/eval-results-observed.json",
        graderOpts,
        maxConcurrency: maxConc,
    });
    writeConfig(rootDir, "promptfooconfig.observed.yaml", observedConfig, log);
    // Agentic config
    const agenticConfig = assembleConfig({
        description: "Sanity AI Literacy Evaluation — Agentic (naive vs optimized)",
        prompts: agenticResult.prompts,
        providers: providers.agentic,
        tests: agenticTaskFiles,
        outputPath: "results/latest/eval-results-agentic.json",
        graderOpts,
        maxConcurrency: maxConc,
    });
    writeConfig(rootDir, "promptfooconfig.agentic.yaml", agenticConfig, log);
    log?.info("\nDone! Configs are ready (via compiler pipeline).");
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Convert compiler test cases to expanded entry format (matching legacy YAML) */
function testsToExpandedEntries(tests) {
    return tests.map((test) => {
        const entry = {};
        if (test.description)
            entry.description = test.description;
        if (test.vars)
            entry.vars = test.vars;
        if (test.assert && test.assert.length > 0)
            entry.assert = test.assert;
        if (test.prompts)
            entry.prompts = test.prompts;
        return entry;
    });
}
function assembleConfig(opts) {
    return {
        commandLineOptions: { table: false },
        defaultTest: {
            assert: [URL_EXTRACTION_ASSERT],
            options: opts.graderOpts,
        },
        description: opts.description,
        ...(opts.maxConcurrency
            ? { evaluateOptions: { maxConcurrency: opts.maxConcurrency } }
            : {}),
        outputPath: opts.outputPath,
        prompts: opts.prompts,
        providers: opts.providers,
        tests: opts.tests,
    };
}
function writeConfig(rootDir, filename, config, log) {
    const yamlStr = dump(config, {
        forceQuotes: false,
        lineWidth: 120,
        noRefs: true,
        quotingType: "'",
    });
    const header = `# ${filename}\n#\n# AUTO-GENERATED by compiler pipeline — do not edit directly.\n# Run: npx @sanity/ailf generate-configs\n`;
    const outPath = resolve(rootDir, filename);
    writeFileSync(outPath, `${header}\n${yamlStr}`, "utf-8");
    log?.info(`  ✓ ${filename}`);
}
function writeYaml(path, data, header) {
    const yamlStr = dump(data, {
        forceQuotes: false,
        lineWidth: 120,
        noRefs: true,
        quotingType: "'",
    });
    writeFileSync(path, `${header}\n${yamlStr}`, "utf-8");
}
/**
 * Materialize Promptfoo lifecycle extensions as a .cjs file on disk.
 *
 * Promptfoo extensions use a single-function dispatch pattern:
 *   module.exports = async function(hookName, context) { ... }
 *
 * Each extension entry in the YAML references:
 *   file://path/to/file.cjs:exportedFunctionName
 *
 * @see https://www.promptfoo.dev/docs/configuration/reference/ — extensions
 */
function writeExtensionFile(rootDir, mode, extensions) {
    // Build a dispatch map: hookName → handler code
    const hookMap = {};
    for (const ext of extensions) {
        hookMap[ext.type] = ext.code;
    }
    // Generate the single dispatch function that promptfoo expects
    const hookCases = Object.entries(hookMap)
        .map(([hookName, code]) => `  if (hookName === '${hookName}') {\n` +
        `    const handler = ${code};\n` +
        `    return handler(context);\n` +
        `  }`)
        .join("\n");
    const fileContent = [
        "// AUTO-GENERATED by compiler pipeline — do not edit directly.",
        "// Run: npx @sanity/ailf generate-configs",
        "//",
        "// Promptfoo extension dispatch function.",
        `// @see https://www.promptfoo.dev/docs/configuration/reference/`,
        "",
        "async function extensionHook(hookName, context) {",
        hookCases,
        "}",
        "",
        "module.exports = extensionHook;",
        "",
    ].join("\n");
    const filename = `results/latest/${mode}-extensions.cjs`;
    const outPath = resolve(rootDir, filename);
    writeFileSync(outPath, fileContent, "utf-8");
    // Single entry pointing to the dispatch function
    return [`file://${filename}:extensionHook`];
}
