/**
 * remote-pipeline.ts — Remote execution flow for `ailf run --remote`.
 *
 * Reads local `.ailf/tasks/` YAML, validates locally via Zod schemas,
 * submits to the AILF API, polls for completion, and writes the same
 * output artifacts as local mode (score-summary.json, report.md,
 * job-metadata.json).
 *
 * This module is the CLI-side counterpart of the API's POST /v1/pipeline
 * endpoint. The CLI and API are published from the same monorepo, so the
 * request/response shapes are always in sync.
 *
 * @see packages/eval/src/adapters/api-client/ — HTTP client + request builder
 * @see docs/design-docs/cli-as-api-client.md — design doc
 */
import { ZodError } from "zod";
import { ApiClient, buildRemoteRequest, createProgressDisplay, formatJobError, NoRunnableTasksError, resolveTasksDir, } from "../adapters/api-client/index.js";
import { writeRemoteResults } from "./remote-results.js";
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Run the evaluation pipeline in remote mode.
 *
 * 1. Validate we have an API key
 * 2. Find and validate local tasks (fail-fast with Zod errors)
 * 3. Build the PipelineRequest payload
 * 4. Submit to the AILF API
 * 5. Poll for completion with progress display
 * 6. Write output artifacts (score-summary.json, report.md, job-metadata.json)
 */
export async function runRemotePipeline(opts, rootDir) {
    await dispatchRemote(toConfigSlice(opts), {
        apiKey: opts.apiKey,
        apiUrl: opts.apiUrl,
        rootDir,
        repoTasksPath: opts.repoTasksPath,
        outputDir: opts.outputDir,
        outputPath: opts.outputPath,
    });
}
/**
 * Run the pipeline in remote mode from a file-resolved `ResolvedConfig`
 * (`ailf run --config <file|json> --remote`, D0090).
 *
 * The document scope (the widened `changedDocs` union and `injectContextDocs`)
 * comes straight off the resolved config — it cannot survive the flag path's
 * slug-only `changedDocsOption`. The caller envelope, auth, and the
 * CLI-flag-only `--sanity-document` / `--sanity-perspective` overrides come
 * from `opts` (resolved from flags/env), since an `EvalConfig` cannot express
 * them. Both entry points share `dispatchRemote`, so a committed config
 * produces the same remote run a local run would.
 */
export async function runRemotePipelineFromConfig(config, opts, rootDir) {
    await dispatchRemote(configToRemoteSlice(config, opts), {
        apiKey: opts.apiKey,
        // The file config's apiUrl is file-authoritative for a `--config` run
        // (it already folded in `execution.apiUrl` or the default).
        apiUrl: config.apiUrl,
        rootDir,
        repoTasksPath: config.repoTasksPath,
        outputDir: config.outputDir,
        outputPath: config.outputPath,
    });
}
/**
 * Shared remote dispatch tail: validate the key, load + validate local tasks,
 * submit, poll, and write artifacts. Both `runRemotePipeline` (flag path) and
 * `runRemotePipelineFromConfig` (`--config` path) feed it a `RemoteConfigSlice`.
 */
async function dispatchRemote(configSlice, params) {
    // 1. Validate API key
    if (!params.apiKey) {
        console.error("❌ AILF_API_KEY is required for remote evaluation.");
        console.error("");
        console.error("   Set it in your environment:");
        console.error("   export AILF_API_KEY=ailf_live_sk_...");
        process.exit(2);
    }
    const client = new ApiClient({
        apiKey: params.apiKey,
        baseUrl: params.apiUrl,
    });
    // 2. Resolve the task source.
    // A content-lake source doesn't need a local `.ailf/tasks/` —
    // the server reads `ailf.task` documents.
    const contentLake = configSlice.taskSource?.type === "content-lake";
    const tasksDir = contentLake
        ? undefined
        : resolveTasksDir(params.rootDir, params.repoTasksPath);
    // 3. Build request from local state (validates locally first)
    let request;
    let taskCount;
    try {
        const result = await buildRemoteRequest({
            tasksDir,
            config: configSlice,
        });
        request = result.request;
        taskCount = result.taskCount;
    }
    catch (err) {
        if (err instanceof ZodError) {
            console.error("❌ Task validation failed:\n");
            for (const issue of err.issues) {
                console.error(`   ${issue.path.join(".")}: ${issue.message}`);
            }
            console.error("");
            console.error("💡 Fix the issues above in your .ailf/tasks/ YAML files.");
            process.exit(2);
        }
        if (err instanceof NoRunnableTasksError) {
            console.error(`❌ ${err.message}`);
            process.exit(2);
        }
        throw err;
    }
    if (contentLake) {
        console.log("📦 Task source: Content Lake (tasks resolved server-side)");
    }
    else {
        console.log(`📦 Found ${taskCount} task(s) in ${tasksDir}`);
    }
    // 4. Submit to API
    const submission = await client.submitPipeline(request);
    console.log(`🚀 Submitted job: ${submission.jobId}`);
    if (submission.estimatedDurationMs) {
        const mins = Math.ceil(submission.estimatedDurationMs / 60_000);
        console.log(`⏱️  Estimated duration: ~${mins} minute(s)`);
    }
    // 5. Poll for completion with progress display
    const job = await client.waitForCompletion(submission.jobId, {
        onProgress: createProgressDisplay(),
    });
    // 6. Handle result
    if (job.status !== "completed") {
        console.error("");
        console.error(formatJobError(job));
        process.exit(1);
    }
    // 7. Fetch and write output artifacts. A `completed` job that carries
    //    `job.error` is a degraded completion (DOC-2121 RC-3): a configured
    //    optional step failed end-to-end. Artifacts still write so the caller
    //    keeps useful local state, but the CLI exits non-zero so external
    //    `--remote` consumers don't mistake the placeholder for success.
    const outcome = await writeRemoteResults(client, job, {
        outputDir: params.outputDir,
        outputPath: params.outputPath,
        apiUrl: params.apiUrl,
    });
    if (outcome.degraded) {
        process.exit(1);
    }
}
/**
 * Map a file-resolved `ResolvedConfig` to the wire `RemoteConfigSlice` (D0090).
 *
 * Run selectors, source overrides, and the robust document scope come from the
 * file config (the `--config` file is authoritative for *what runs*). The
 * envelope/auth and CLI-flag-only overrides come from `opts`. `injectContextDocs`
 * already has any `--inject-doc` flag refs merged in by the `--config` handler,
 * so it is taken from the config verbatim (no double-count).
 */
export function configToRemoteSlice(config, opts) {
    return {
        mode: config.mode,
        variant: config.variant,
        debug: config.debug,
        areas: config.areas ? [...config.areas] : undefined,
        tasks: config.tasks ? [...config.tasks] : undefined,
        changedDocs: config.changedDocs ? [...config.changedDocs] : undefined,
        injectContextDocs: config.injectContextDocs
            ? [...config.injectContextDocs]
            : undefined,
        sanityDocumentArgs: opts.sanityDocumentArgs.length > 0 ? opts.sanityDocumentArgs : undefined,
        source: config.source,
        // SDK-1815 — forward an explicit content-lake/repo task source so a
        // `--remote` run can read `ailf.task` documents server-side without a
        // local `.ailf/tasks/` directory.
        taskSource: config.taskSourceType
            ? { type: config.taskSourceType }
            : undefined,
        compareEnabled: config.compareEnabled,
        compareThreshold: config.compareThreshold,
        publishEnabled: config.publishEnabled,
        publishTag: config.publishTag,
        concurrency: config.concurrency,
        baseUrlOverride: config.baseUrlOverride,
        datasetOverride: config.datasetOverride,
        projectIdOverride: config.projectIdOverride,
        perspectiveOverride: opts.perspectiveOverride,
        studioOriginOverride: config.studioOriginOverride,
        graderContext: config.graderContext,
        graderReplications: config.graderReplications,
        borderlineReplications: config.borderlineReplications,
        gapAnalysisEnabled: config.gapAnalysisEnabled,
        noRemoteCache: config.noRemoteCache,
        classificationOption: opts.classificationOption,
        ownerTeamOption: opts.ownerTeamOption,
        ownerIndividualOption: opts.ownerIndividualOption,
        purposeOption: opts.purposeOption,
        labelOptions: opts.labelOptions,
    };
}
/**
 * Extract the subset of ResolvedOptions needed for building a remote request.
 * This mapping keeps remote-pipeline.ts decoupled from the full ResolvedConfig.
 */
function toConfigSlice(opts) {
    return {
        mode: opts.mode,
        variant: opts.variant,
        debug: opts.debug,
        areas: opts.areaOption
            ?.split(",")
            .map((s) => s.trim())
            .filter(Boolean),
        tasks: opts.taskOption
            ?.split(",")
            .map((s) => s.trim())
            .filter(Boolean),
        changedDocs: opts.changedDocsOption
            ?.split(",")
            .map((s) => s.trim())
            .filter(Boolean),
        // D0090 — forward the `--sanity-document` fetch override so a `--remote`
        // run applies the same replace/append overlay a local run does (it was
        // silently dropped before this milestone).
        sanityDocumentArgs: opts.sanityDocumentArgs.length > 0 ? opts.sanityDocumentArgs : undefined,
        // SDK-1815 — forward an explicit content-lake/repo task source so a
        // `--remote` run can read `ailf.task` documents server-side without a
        // local `.ailf/tasks/` directory.
        taskSource: opts.taskSourceType ? { type: opts.taskSourceType } : undefined,
        // D0090 — forward `--inject-doc` refs so a `--remote` run injects the same
        // per-task context a local run does. `buildRemoteRequest` emits these as
        // `injectContextDocs` on the PipelineRequest.
        injectContextDocs: opts.injectContextDocsOption,
        source: opts.source,
        compareEnabled: opts.compareEnabled,
        compareThreshold: opts.compareThreshold,
        // Only send publish preference when the user explicitly passed
        // --publish or --no-publish. When omitted, the API applies its own
        // default (publish: true for API-triggered jobs with a jobId).
        publishEnabled: opts.publishExplicit ? opts.publishEnabled : undefined,
        publishTag: opts.publishTag,
        concurrency: opts.concurrency,
        baseUrlOverride: opts.baseUrlOverride,
        datasetOverride: opts.datasetOverride,
        projectIdOverride: opts.projectIdOverride,
        perspectiveOverride: opts.perspectiveOverride,
        studioOriginOverride: opts.studioOriginOverride,
        graderContext: opts.graderContext,
        graderReplications: opts.graderReplications,
        borderlineReplications: opts.borderlineReplications,
        gapAnalysisEnabled: opts.gapAnalysisEnabled,
        noRemoteCache: opts.noRemoteCache,
        // D0037 / W0069 caller envelope overrides — flags override env vars
        // inside buildCallerEnvelope(), which also merges AILF_* defaults.
        classificationOption: opts.classificationOption,
        ownerTeamOption: opts.ownerTeamOption,
        ownerIndividualOption: opts.ownerIndividualOption,
        purposeOption: opts.purposeOption,
        labelOptions: opts.labelOptions,
    };
}
