/**
 * Pipeline step: Generate Promptfoo configuration files.
 *
 * ALL modes route through the Plugin Registry. The step looks up the mode
 * handler via ctx.registry.getMode() and delegates compilation to it.
 *
 * Literacy mode has a variant strategy: baseline/agentic/observed/full.
 * When the variant is "full", the handler is called twice (baseline + agentic)
 * and three YAML files are written. Other modes produce one YAML file.
 */
import { existsSync } from "node:fs";
import { resolve } from "node:path";
import { assoc, } from "../../_vendor/ailf-core/index.js";
import { emitFileContents } from "../../artifact-capture/emit-file.js";
import { LiteracyVariant } from "../../pipeline/normalize-mode.js";
import { modelMatchesLiteracyVariant } from "../../pipeline/compiler/mode-bases/literacy.js";
import { getStepInputPaths } from "../../pipeline/cache.js";
import { buildCacheContext } from "../cache-context.js";
import { checkGeneratedConfigsExist } from "../../pipeline/checks.js";
import { validateModelsYaml } from "../../pipeline/validate.js";
import { loadPreflightContext } from "../../pipeline/preflight/load-preflight-context.js";
import { loadSource } from "../../sources.js";
import { configToSourceOverrides } from "../config-to-source-overrides.js";
export class GenerateConfigsStep {
    name = "generate-configs";
    /** Task IDs from the last loadTasks call (pre-filter), for error messages. */
    lastLoadedTaskIds = [];
    check(ctx) {
        const issues = validateModelsYaml(ctx.config.rootDir);
        return issues.filter((i) => i.severity === "error");
    }
    async execute(ctx, state) {
        const start = Date.now();
        const mode = ctx.config.mode;
        // Look up mode handler in the registry
        const registration = ctx.registry.getMode(mode);
        if (!registration) {
            return {
                durationMs: Date.now() - start,
                error: `No handler registered for mode "${mode}". ` +
                    `Available modes: ${ctx.registry
                        .getModes()
                        .map((m) => m.id)
                        .join(", ")}`,
                status: "failed",
            };
        }
        try {
            // Dynamically import the handler module
            const handlerModule = await import(`../../pipeline/compiler/${registration.handlerModule}`);
            const handler = handlerModule.handler;
            if (!handler?.compileTask) {
                return {
                    durationMs: Date.now() - start,
                    error: `Handler module for "${mode}" does not export a valid ModeHandler`,
                    status: "failed",
                };
            }
            // Load tasks
            const tasks = await this.loadTasks(ctx, mode, state);
            if (tasks.length === 0) {
                const error = this.buildNoTasksError(ctx, mode);
                return {
                    durationMs: Date.now() - start,
                    error,
                    status: "failed",
                };
            }
            // Load models
            const { loadModelsAndProviders, loadModelsYaml } = await import("../../pipeline/compiler/provider-assembler.js");
            const overrides = configToSourceOverrides(ctx.config);
            const resolvedSource = ctx.config.source
                ? loadSource(ctx.config.source, overrides)
                : undefined;
            // W0281: when the caller pinned a subset of models via
            // `PipelineRequest.models`, filter the cohort BEFORE provider
            // assembly. Filtering only the returned `models` field would silently
            // defeat the filter — promptfoo decides which LLMs to call from the
            // providers array, which is assembled from the unfiltered set unless
            // we hand the assembler a pre-filtered ModelsConfig. Unknown IDs are
            // surfaced via a structured warning AND a failed step result (whose
            // message lands on `ailf.job.error`) so callers can detect typos
            // instead of silently running the full default cohort.
            const rawModels = loadModelsYaml(ctx.config.rootDir);
            const filtered = filterModelsByRequest(rawModels, ctx.config.models, ctx.logger);
            if (filtered.kind === "no-match") {
                return {
                    durationMs: Date.now() - start,
                    error: filtered.reason,
                    status: "failed",
                };
            }
            const loaded = loadModelsAndProviders(ctx.config.rootDir, resolvedSource, ctx.config.searchMode, ctx.config.allowedOrigins, filtered.models);
            const models = loaded.models;
            const providers = loaded.providers;
            // Literacy mode: variant expansion (baseline + agentic → 3 YAML files)
            if (mode === "literacy") {
                return this.compileLiteracyVariants(ctx, handler, tasks, models, providers, start);
            }
            // All other modes: single compilation → single YAML file
            return this.compileSingleMode(ctx, handler, tasks, mode, models, start);
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            return {
                durationMs: Date.now() - start,
                error: `${mode} compilation failed: ${msg}`,
                status: "failed",
            };
        }
    }
    // ---------------------------------------------------------------------------
    // Literacy variant compilation (baseline + agentic → 3 YAML files)
    // ---------------------------------------------------------------------------
    async compileLiteracyVariants(ctx, handler, tasks, models, providers, start) {
        ctx.logger.info(`Compiling ${tasks.length} literacy task(s) via registry handler...`);
        // Filter models per variant using shared literacy variant matcher
        const baselineModels = models.models
            .filter((m) => modelMatchesLiteracyVariant(m, "baseline"))
            .map((m) => ({ id: m.id, label: m.label }));
        const agenticModels = models.models
            .filter((m) => modelMatchesLiteracyVariant(m, "agentic-naive") ||
            modelMatchesLiteracyVariant(m, "agentic-optimized"))
            .map((m) => ({ id: m.id, label: m.label }));
        // Load rubric config for template resolution
        let rubricConfig;
        try {
            const { loadRubricTemplates } = await import("../../pipeline/rubric-loader.js");
            rubricConfig = loadRubricTemplates(ctx.config.rootDir);
        }
        catch {
            ctx.logger.warn("  ⚠ Could not load rubric config — templates will not resolve");
        }
        // Compile for each variant. `graderContext` defaults to "with-docs" here
        // (flipped in W0200 after PR #555 validation receipts) so handlers see a
        // definite value rather than implicit-undefined.
        const graderContext = ctx.config.graderContext ?? "with-docs";
        // W0198 Phase 6 — when the package-surface manifest is authored, pass
        // the in-scope package list down so the literacy mode handler can
        // prefix the `code-correctness` rubric with the deterministic-lane
        // system instruction. Silent fall-through when absent.
        const preflightContext = await loadPreflightContext(ctx.config.rootDir, ctx.logger);
        const baselineResults = this.compileAll(handler, tasks, {
            rootDir: ctx.config.rootDir,
            graderProvider: models.grader.id,
            models: baselineModels,
            rubricConfig,
            graderContext,
            preflightContext,
            evalMode: LiteracyVariant.STANDARD,
        });
        const agenticResults = this.compileAll(handler, tasks, {
            rootDir: ctx.config.rootDir,
            graderProvider: models.grader.id,
            models: agenticModels,
            rubricConfig,
            graderContext,
            preflightContext,
            evalMode: LiteracyVariant.AGENTIC,
        });
        // Log warnings
        for (const w of [...baselineResults.warnings, ...agenticResults.warnings]) {
            ctx.logger.warn(`  ⚠ ${w}`);
        }
        ctx.logger.info(`  Compiled ${tasks.length} task(s) → ${baselineResults.tests.length} baseline + ${agenticResults.tests.length} agentic entries`);
        // Write 3 YAML files via the literacy-specific writer
        const { writeCompiledLiteracyConfigs } = await import("../../pipeline/compiler/compiler-to-yaml.js");
        writeCompiledLiteracyConfigs(baselineResults, agenticResults, providers, {
            rootDir: ctx.config.rootDir,
            graderProvider: models.grader.id,
            maxConcurrency: models.maxConcurrency,
            logger: ctx.logger,
        });
        // W0050 — configSnapshot is per-entry keyed by mode. For literacy,
        // each variant produces a distinct config, so the variant name is the
        // mode-axis value here.
        const { configFileForMode } = await import("../../pipeline/eval-constants.js");
        for (const variant of ["baseline", "agentic", "observed"]) {
            const configPath = resolve(ctx.config.rootDir, configFileForMode(variant));
            if (existsSync(configPath)) {
                await emitFileContents(ctx.artifactWriter, "configSnapshot", assoc(ctx, { mode: `literacy-${variant}` }), configPath);
            }
        }
        return this.checkLiteracyPostconditions(ctx, start);
    }
    // ---------------------------------------------------------------------------
    // Single-mode compilation (all non-literacy modes)
    // ---------------------------------------------------------------------------
    async compileSingleMode(ctx, handler, tasks, mode, models, start) {
        ctx.logger.info(`Compiling ${tasks.length} ${mode} task(s) via registry handler...`);
        // Filter models to those that declare this mode in their modes array
        const modeModels = models.models
            .filter((m) => !m.modes || m.modes.includes(mode))
            .map((m) => ({
            id: m.id,
            label: m.label,
            config: m.config,
        }));
        // Load rubric config for template resolution (needed by modes that use
        // templated LLM-rubric assertions, e.g., agent-harness with agent-output
        // and agent-tool-usage templates)
        let rubricConfig;
        try {
            const { loadRubricTemplates } = await import("../../pipeline/rubric-loader.js");
            rubricConfig = loadRubricTemplates(ctx.config.rootDir);
        }
        catch {
            ctx.logger.warn("  ⚠ Could not load rubric config — templates will not resolve");
        }
        const preflightContext = await loadPreflightContext(ctx.config.rootDir, ctx.logger);
        const merged = this.compileAll(handler, tasks, {
            rootDir: ctx.config.rootDir,
            graderProvider: models.grader.id,
            models: modeModels,
            rubricConfig,
            graderContext: ctx.config.graderContext ?? "with-docs",
            preflightContext,
        });
        for (const w of merged.warnings) {
            ctx.logger.warn(`  ⚠ ${w}`);
        }
        ctx.logger.info(`  Compiled ${tasks.length} task(s) → ${merged.tests.length} test entries`);
        const { writeCompiledModeConfig } = await import("../../pipeline/compiler/compiler-to-yaml.js");
        writeCompiledModeConfig(merged, mode, {
            rootDir: ctx.config.rootDir,
            graderProvider: models.grader.id,
            maxConcurrency: models.maxConcurrency,
            logger: ctx.logger,
        });
        // W0050 — configSnapshot for a single-mode compile.
        const configPath = resolve(ctx.config.rootDir, `promptfooconfig.${mode}.yaml`);
        if (existsSync(configPath)) {
            await emitFileContents(ctx.artifactWriter, "configSnapshot", assoc(ctx, { mode }), configPath);
        }
        // W0050 — the mode-specific `${mode}-tests.json` file was an
        // extras-only capture with no registered descriptor. Dropped; the
        // same information lives in the configSnapshot + rawResults chain.
        return {
            durationMs: Date.now() - start,
            status: "success",
            summary: `Generated promptfooconfig.${mode}.yaml`,
        };
    }
    // ---------------------------------------------------------------------------
    // Task loading — unified for all modes
    // ---------------------------------------------------------------------------
    /**
     * Load tasks via ctx.taskSource — the single adapter wired by the
     * composition root for every taskSourceType (W0146). FetchDocsStep
     * and GenerateConfigsStep MUST go through the same adapter so configs
     * reference context files that were actually fetched.
     *
     * Filtering by area/task/tag is delegated to the adapter:
     * ContentLakeTaskSource pushes it into the GROQ query;
     * RepoTaskSource applies it in-memory.
     */
    async loadTasks(ctx, mode, state) {
        const filter = {
            ...(ctx.config.areas?.length ? { areas: ctx.config.areas } : {}),
            ...(ctx.config.tasks?.length ? { taskIds: ctx.config.tasks } : {}),
            ...(ctx.config.tags?.length ? { tags: ctx.config.tags } : {}),
            ...(ctx.config.changedDocs?.length
                ? { changedDocs: ctx.config.changedDocs }
                : {}),
        };
        const allTasks = await ctx.taskSource.loadTasks(Object.keys(filter).length > 0 ? filter : undefined);
        // Mode filter — the adapter may return a mixed-mode set (e.g. a user's
        // `--repo-tasks-path` containing tasks of multiple modes). Skip
        // non-matching modes with a warning so unintentional misclassification
        // is visible without breaking the run.
        const tasks = [];
        const skippedByMode = new Map();
        for (const task of allTasks) {
            if (!("mode" in task) || task.mode === mode) {
                tasks.push(task);
            }
            else {
                const taskMode = task.mode ?? "unknown";
                skippedByMode.set(taskMode, (skippedByMode.get(taskMode) ?? 0) + 1);
            }
        }
        if (skippedByMode.size > 0) {
            const total = [...skippedByMode.values()].reduce((a, b) => a + b, 0);
            const summary = [...skippedByMode.entries()]
                .map(([m, n]) => `${n} ${m}`)
                .join(", ");
            ctx.logger.warn(`  ⚠ Skipped ${total} task(s) with non-matching mode (${summary}). Current pipeline mode: ${mode}. Run with --mode <mode> to include them.`);
        }
        this.lastLoadedTaskIds = tasks
            .map((t) => t.id)
            .filter((id) => !!id);
        if (state.releaseAutoScope && !ctx.config.noAutoScope) {
            const scopedIds = new Set(state.releaseAutoScope.affectedTaskIds);
            const beforeCount = tasks.length;
            const scoped = tasks.filter((t) => "id" in t && scopedIds.has(t.id));
            ctx.logger.info(`  🎯 Auto-scoped to ${scoped.length} of ${beforeCount} task(s) affected by release`);
            return scoped;
        }
        return tasks;
    }
    /**
     * Build a descriptive error message when no tasks match the current filters.
     * Distinguishes between "no tasks exist" and "tasks exist but filters exclude them".
     */
    buildNoTasksError(ctx, mode) {
        const filters = [];
        if (ctx.config.tasks?.length) {
            filters.push(`--task ${ctx.config.tasks.join(", ")}`);
        }
        if (ctx.config.areas?.length) {
            filters.push(`--area ${ctx.config.areas.join(", ")}`);
        }
        if (ctx.config.tags?.length) {
            filters.push(`--tag ${ctx.config.tags.join(", ")}`);
        }
        if (filters.length > 0) {
            // Collect available task IDs for the hint
            const availableIds = this.lastLoadedTaskIds ?? [];
            const hint = availableIds.length > 0
                ? `\n  Available ${mode} task IDs: ${availableIds.join(", ")}`
                : "";
            return (`No ${mode} tasks match the current filters (${filters.join("; ")}).` +
                hint);
        }
        return (`No ${mode} tasks found. Create *.task.ts files in ` +
            `packages/eval/tasks/${mode}/`);
    }
    // ---------------------------------------------------------------------------
    // Compilation helpers
    // ---------------------------------------------------------------------------
    /**
     * Compile all tasks through a handler, merging results.
     * For literacy mode, ctx can carry evalMode as an extension.
     */
    compileAll(handler, tasks, ctx) {
        const results = [];
        const warnings = [];
        for (const task of tasks) {
            const result = handler.compileTask(task, ctx);
            results.push(result);
            warnings.push(...result.warnings);
        }
        return mergeCompileResults(results);
    }
    // ---------------------------------------------------------------------------
    // Postcondition checks
    // ---------------------------------------------------------------------------
    checkLiteracyPostconditions(ctx, start) {
        const configIssues = checkGeneratedConfigsExist(ctx.config.rootDir);
        const configErrors = configIssues.filter((i) => i.severity === "error");
        if (configErrors.length > 0) {
            return {
                durationMs: Date.now() - start,
                error: `Postcondition failed: ${configErrors.map((e) => e.message).join("; ")}`,
                status: "failed",
            };
        }
        return {
            durationMs: Date.now() - start,
            status: "success",
            summary: "Generated promptfoo config files",
        };
    }
    cacheInputs(ctx) {
        return getStepInputPaths(ctx.config.rootDir, "generate-configs");
    }
    cacheContext(ctx) {
        return buildCacheContext(ctx.config);
    }
}
export function filterModelsByRequest(loaded, requested, logger) {
    if (!requested || requested.length === 0) {
        return { kind: "unfiltered", models: loaded };
    }
    const availableIds = new Set(loaded.models.map((m) => m.id));
    const requestedSet = new Set(requested);
    const kept = loaded.models.filter((m) => requestedSet.has(m.id));
    const unknown = requested.filter((id) => !availableIds.has(id));
    if (kept.length === 0) {
        const reason = `[generate-configs] PipelineRequest.models rejected — none of ` +
            `[${requested.join(", ")}] match config/models.ts. ` +
            `Available IDs: ${[...availableIds].join(", ") || "(none configured)"}.`;
        logger.warn(reason);
        return { kind: "no-match", reason };
    }
    if (unknown.length > 0) {
        logger.warn(`[generate-configs] PipelineRequest.models partial match — ignoring ` +
            `unknown ID(s) [${unknown.join(", ")}]; ` +
            `running ${kept.length}/${requested.length} requested.`);
    }
    return { kind: "filtered", models: { ...loaded, models: kept } };
}
function mergeCompileResults(results) {
    const tests = results.flatMap((r) => r.tests);
    const warnings = results.flatMap((r) => r.warnings);
    const providers = results[0]?.providers ?? [];
    const prompts = results[0]?.prompts ?? [];
    const extras = {};
    for (const r of results) {
        if (r.extras)
            Object.assign(extras, r.extras);
    }
    return {
        providers,
        tests,
        prompts,
        warnings,
        ...(Object.keys(extras).length > 0 ? { extras } : {}),
    };
}
