/**
 * FileConfigAdapter — resolves pipeline config from a local config file.
 *
 * Enables `ailf run --config <path>` to load all pipeline options
 * from a file instead of CLI flags. Supports multiple formats in
 * priority order:
 *
 * 1. TypeScript (.ts) — loaded via jiti, validated through Zod
 * 2. JavaScript (.js) — loaded via jiti, validated through Zod
 * 3. YAML (.yaml/.yml) — parsed via js-yaml, validated through Zod
 * 4. JSON (.json) — parsed via JSON.parse, tries PipelineRequestSchema
 *    first (API contract), then EvalConfigSchema (developer config)
 * 5. Inline JSON — a `--config` value whose first non-whitespace character
 *    is `{` or `[` is parsed in place (no file read) and validated through
 *    the same dual JSON path (D0090). Such a value was never a valid path,
 *    so every existing `--config <path>` invocation is unaffected.
 *
 * Schemas:
 * - PipelineRequestSchema (JSON only) — the universal API contract.
 *   Used by: API gateway payloads, GitHub Actions dispatch.
 * - EvalConfigSchema (all formats) — the developer-facing config.
 *   Used by: local config files, manual --config usage.
 *
 * @see packages/core/src/schemas/pipeline-request.ts — PipelineRequestSchema
 * @see packages/core/src/schemas/eval-config.ts — EvalConfigSchema
 * @see docs/design-docs/architecture-overhaul/typescript-configuration.md
 */
import { readFileSync } from "fs";
import { extname, resolve } from "path";
import { EvalConfigSchema, PipelineRequestSchema, } from "../../_vendor/ailf-core/index.js";
import { mapRequestToConfig } from "../../pipeline/map-request-to-config.js";
import { normalizeMode } from "../../pipeline/normalize-mode.js";
import { loadTsConfig } from "./ts-config-loader.js";
/**
 * A `--config` value is inline JSON when its first non-whitespace character
 * is `{` or `[`. A path never starts with either, so this heuristic never
 * misclassifies an existing `--config <path>` invocation (D0090).
 */
export function isInlineJson(value) {
    const first = value.trimStart()[0];
    return first === "{" || first === "[";
}
export class FileConfigAdapter {
    filePath;
    rootDir;
    label;
    constructor(filePath, rootDir) {
        this.filePath = filePath;
        this.rootDir = rootDir;
        this.label = isInlineJson(filePath)
            ? "config: inline JSON"
            : `config file: ${filePath}`;
    }
    async resolve() {
        // Inline JSON (`--config '{...}'`) — parse in place and validate through
        // the same dual JSON path a `.json` file uses (D0090). No file read.
        if (isInlineJson(this.filePath)) {
            const raw = JSON.parse(this.filePath);
            return this.validateAndMap(raw, ".json");
        }
        const ext = extname(this.filePath).toLowerCase();
        // TypeScript / JavaScript files — load via jiti
        if (ext === ".ts" || ext === ".js") {
            const result = await loadTsConfig(this.filePath);
            if (!result.ok) {
                throw new Error(result.error);
            }
            return this.validateAndMap(result.value, ext);
        }
        // YAML / JSON files — load via fs
        const raw = await readConfigFile(this.filePath);
        return this.validateAndMap(raw, ext);
    }
    /**
     * Validate the raw config object through Zod and map to ResolvedConfig.
     */
    validateAndMap(raw, ext) {
        // For JSON files, try PipelineRequestSchema first (API contract)
        if (ext === ".json") {
            const pipelineResult = PipelineRequestSchema.safeParse(raw);
            if (pipelineResult.success) {
                // PipelineRequestSchema strips unknown keys, so a config that also
                // parses as a pipeline request would silently lose its
                // `consumerVerification` lane — warn instead of letting it vanish.
                if (typeof raw === "object" &&
                    raw !== null &&
                    "consumerVerification" in raw) {
                    console.warn("  ⚠️  config: `consumerVerification` is ignored for pipeline-request-shaped JSON configs — the key is only honored by developer configs (EvalConfigSchema); use a .yaml/.ts config or drop the pipeline-request-only fields");
                }
                return mapRequestToConfig(pipelineResult.data, this.rootDir);
            }
        }
        // Fall back to EvalConfigSchema (developer config) — works for all formats
        const validated = EvalConfigSchema.parse(raw);
        return mapEvalConfigToResolvedConfig(validated, this.rootDir);
    }
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
async function readConfigFile(filePath) {
    const content = readFileSync(filePath, "utf-8");
    const ext = extname(filePath).toLowerCase();
    if (ext === ".yaml" || ext === ".yml") {
        // Dynamic ESM import — only loaded when reading YAML configs.
        const { parse } = await import("yaml");
        return parse(content);
    }
    return JSON.parse(content);
}
/**
 * Map a validated `EvalConfig` to a `ResolvedConfig`.
 *
 * Exported so the `--config <file> --remote` dispatch path can reuse the
 * exact local resolution (D0090): the same `ResolvedConfig` that drives a
 * local run is mapped to the wire `RemoteConfigSlice` by `configToRemoteSlice`,
 * so a committed config produces an identical document scope locally and
 * remotely.
 */
export function mapEvalConfigToResolvedConfig(config, rootDir) {
    // Normalize debug option
    let debug;
    if (config.debug === true) {
        debug = { enabled: true };
    }
    else if (typeof config.debug === "object") {
        debug = { enabled: config.debug.enabled ?? true, ...config.debug };
    }
    // Normalize legacy mode names (e.g., "full" → literacy + variant)
    const normalized = normalizeMode(config.mode ?? "literacy");
    // Output directory (W0077 Phase 6c) — resolve `output.dir` relative to the
    // rootDir (the caller's workspace, set by the FileConfigAdapter caller).
    // When unset, fall back to <rootDir>/.ailf/results/latest/ to mirror the
    // CLI's default. This matches `resolveOutputDir` for the auto-load path.
    const outputDir = config.output?.dir
        ? resolve(rootDir, config.output.dir)
        : resolve(rootDir, ".ailf", "results", "latest");
    // Source union (D0060 mitigation #2) — a string is a named source resolved
    // against config/sources.ts; an object is the same inline-location shape
    // RepoConfig.source uses, routed into the existing *Override fields that
    // `configToSourceOverrides` already reads. Deliberate asymmetry with the
    // auto-loaded repo-config path: that path applies an env-wins cascade
    // (SANITY_PROJECT_ID / SANITY_DATASET / SANITY_STUDIO_ORIGIN /
    // DOC_BASE_URL win over `source.*`), while an explicit `--config` file is
    // file-authoritative — what the job description says is what runs.
    const inlineSource = typeof config.source === "object" ? config.source : undefined;
    return {
        rootDir,
        outputDir,
        mode: normalized.mode,
        variant: normalized.variant,
        noAutoScope: config.noAutoScope ?? false,
        debug,
        areas: config.areas,
        tasks: config.tasks,
        // Document-reference scoping (D0090). `changedDocs` is the widened
        // `(string | GeneralizedDocRef)[]` union; `injectContextDocs` is the
        // additive `GeneralizedDocRef[]` context-injection list. Both carry
        // through to `ResolvedConfig` unchanged, matching the types
        // `mapRequestToConfig` already threads from `PipelineRequest`. The
        // remote builder consumes these in the next milestone
        // (W-docref-config-inline-remote).
        changedDocs: config.changedDocs,
        injectContextDocs: config.injectContextDocs,
        source: typeof config.source === "string" ? config.source : undefined,
        projectIdOverride: inlineSource?.projectId,
        datasetOverride: inlineSource?.dataset,
        baseUrlOverride: inlineSource?.baseUrl,
        studioOriginOverride: inlineSource?.studioOrigin,
        skipFetch: config.skipFetch ?? false,
        skipEval: config.skipEval ?? false,
        compareEnabled: config.compare ?? false,
        compareThreshold: config.compareThreshold,
        compareBaseline: config.compareBaseline,
        gapAnalysisEnabled: config.execution?.gapAnalysis ?? true,
        // W0077 Phase 4 — `publish` is now a policy object. Map the auto value
        // to a boolean for the file-config path. Absence of publish.auto mirrors
        // the CLI's "full-runs" default (enable publish; composition root gates on
        // token availability). Only "never" explicitly disables auto-publish.
        publishEnabled: config.publish?.auto !== "never",
        publishTag: config.publish?.tag,
        noCache: config.noCache ?? false,
        noRemoteCache: config.noRemoteCache ?? false,
        graderReplications: config.execution?.graderReplications,
        borderlineReplications: config.execution?.borderlineReplications,
        graderContext: config.grader?.context,
        urls: config.urls,
        headers: config.agentic?.headers,
        allowedOrigins: config.agentic?.allowedOrigins,
        searchMode: config.searchMode ?? "open",
        concurrency: config.execution?.concurrency,
        remote: false,
        apiUrl: config.execution?.apiUrl ?? "https://ailf-api.sanity.build",
        // W0077 Phase 6g — artifact writer settings. `enabled: false` flips
        // `artifactsDisabled` so composition-root selects the NoOp writer.
        artifactsDisabled: config.artifacts?.enabled === false ? true : undefined,
        artifactsDir: config.artifacts?.dir
            ? resolve(rootDir, config.artifacts.dir)
            : undefined,
        artifactsExclude: config.artifacts?.exclude,
        // W0077 Phase 6h / SDK-1815 — preserve the full task-source type. An
        // explicit `content-lake` is honored on both `--remote` (forwarded to the
        // API) and local `--config` runs: the AILF executor evaluates dispatched
        // jobs through the local `--config` path, so it must read the Content Lake
        // rather than collapse to the bundled corpus
        // (W-honor-content-lake-tasksource-local-config). Unset stays undefined
        // (bundled corpus); when `repo`, the composition-root resolves
        // `repoTasksPath` (defaulting to `<rootDir>/.ailf/tasks/` when unset).
        taskSourceType: config.taskSource?.type,
        repoTasksPath: config.taskSource?.repoTasksPath
            ? resolve(rootDir, config.taskSource.repoTasksPath)
            : undefined,
        presets: config.presets,
        // Phase 6 / DIAG-06 — thread summary.onRun into ResolvedConfig so the
        // file-config exit branch in executePipeline can pass it to
        // runPostPipelineHooks.
        summaryOnRun: config.summary?.onRun,
        // Carried verbatim — the schema guarantees `module` is an absolute
        // path or file:// URL, so no rootDir resolution happens here.
        consumerVerification: config.consumerVerification,
    };
}
