/**
 * Bridge from legacy ResolvedOptions to the new AppContext.
 *
 * This mapping function exists during the Phase 3→7 migration. It converts
 * the old ResolvedOptions shape to ResolvedConfig, then delegates to
 * createAppContext() (the composition root).
 *
 * Once all commands construct ResolvedConfig directly (or use --config),
 * this bridge can be deleted.
 */
import { isLiteracyVariant, isRunClassification, } from "../_vendor/ailf-shared/index.js";
import { createAppContext } from "../composition-root.js";
import { tryLoadConfigFile } from "../pipeline/compiler/config-loader.js";
/**
 * Map legacy ResolvedOptions to ResolvedConfig.
 *
 * This is a mechanical mapping — every field in ResolvedConfig has a
 * corresponding field in ResolvedOptions. Fields that don't map directly
 * are derived (e.g., areas from areaOption).
 */
export function mapToResolvedConfig(opts, rootDir) {
    // `opts.variant` is a free-form string from CLI / config flags; narrow it
    // to the closed `LiteracyVariant` set so downstream consumers (the report
    // provenance derivation, in particular) never see a bogus string.
    // Unknown values silently drop to undefined — the legacy behavior — but a
    // narrowing surface is in place for the day we want to error here.
    const variant = isLiteracyVariant(opts.variant) ? opts.variant : undefined;
    return {
        rootDir,
        mode: opts.mode,
        variant,
        noAutoScope: opts.noAutoScope ?? false,
        debug: opts.debug,
        areas: opts.areaOption
            ?.split(",")
            .map((s) => s.trim())
            .filter(Boolean),
        tasks: opts.taskOption
            ?.split(",")
            .map((s) => s.trim())
            .filter(Boolean),
        tags: opts.tagOption,
        changedDocs: opts.changedDocsOption
            ?.split(",")
            .map((s) => s.trim())
            .filter(Boolean),
        source: opts.source,
        skipFetch: opts.skipFetch,
        skipEval: opts.skipEval,
        compareEnabled: opts.compareEnabled,
        compareThreshold: opts.compareThreshold,
        compareBaseline: opts.compareBaseline,
        gapAnalysisEnabled: opts.gapAnalysisEnabled,
        publishEnabled: opts.publishEnabled,
        publishTag: opts.publishTag,
        noCache: opts.noCache,
        noRemoteCache: opts.noRemoteCache,
        graderReplications: opts.graderReplications,
        borderlineReplications: opts.borderlineReplications,
        graderContext: opts.graderContext,
        outputDir: opts.outputDir,
        outputPath: opts.outputPath,
        urls: opts.urlArgs.length > 0 ? opts.urlArgs : undefined,
        headers: opts.headerArgs.length > 0
            ? Object.fromEntries(opts.headerArgs.map((h) => {
                const [key, ...rest] = h.split(":");
                return [key.trim(), rest.join(":").trim()];
            }))
            : undefined,
        allowedOrigins: opts.allowedOriginArgs.length > 0 ? opts.allowedOriginArgs : undefined,
        searchMode: opts.searchMode ?? "open",
        concurrency: opts.concurrency,
        promptfooUrl: opts.promptfooUrl,
        baseUrlOverride: opts.baseUrlOverride,
        datasetOverride: opts.datasetOverride,
        projectIdOverride: opts.projectIdOverride,
        perspectiveOverride: opts.perspectiveOverride,
        studioOriginOverride: opts.studioOriginOverride,
        sanityDocumentArgs: opts.sanityDocumentArgs.length > 0 ? opts.sanityDocumentArgs : undefined,
        // `--inject-doc` flag refs (D0090). The non-config local flow has no
        // config-file `injectContextDocs` source (the auto-loaded RepoConfig does
        // not carry the field), so the flag refs are the whole list. The
        // flag-plus-config MERGE happens in the `--config` branch of
        // `executePipeline`, where both a config-file list and flag refs coexist.
        injectContextDocs: opts.injectContextDocsOption,
        beforeOption: opts.beforeOption,
        taskSourceType: opts.taskSourceType,
        repoTasksPath: opts.repoTasksPath,
        reportStoreProjectId: opts.reportProjectId,
        reportStoreDataset: opts.reportDataset,
        remote: opts.remote ?? false,
        apiUrl: opts.apiUrl ?? "https://ailf-api.sanity.build",
        apiKey: opts.apiKey,
        artifactsDisabled: opts.artifactsDisabled,
        artifactsDir: opts.artifactsDir,
        artifactsDryRun: opts.artifactsDryRun,
        artifactsExclude: opts.artifactsExclude,
        artifactGcsBucket: process.env.AILF_GCS_ARTIFACT_BUCKET,
        artifactUpload: parseArtifactUploadEnv(process.env.AILF_ARTIFACT_UPLOAD),
        // D0037 caller envelope for LOCAL runs. The `--remote` path builds this
        // on the wire (`api-client/build-request.ts` → `mapRequestToConfig`); the
        // local path historically left it undefined, so `--classification`,
        // `--purpose`, and `--label` were resolved into ResolvedOptions but never
        // reached provenance — every local published report defaulted to
        // classification "adhoc" with null purpose/labels.
        callerEnvelope: buildLocalCallerEnvelope(opts),
        // Project-level provenance policy (strengthen-only). The T2 pre-publish
        // gate merges this onto SERVER_FLOOR; undefined = floor only.
        provenance: opts.provenanceOption,
    };
}
/**
 * Build the D0037 caller envelope for a LOCAL `ailf run` from resolved CLI
 * options. The `--remote` twin is `buildCallerEnvelope` in
 * `adapters/api-client/build-request.ts`; this mirrors its precedence rules
 * with two deliberate differences for the local path:
 *
 * - **No `executor`.** On a local run `buildRunContext`'s `detectExecutor()`
 *   captures the real CLI user (git name + email + surface). Forcing an
 *   envelope executor here would suppress that and drop the email. The remote
 *   twin sets `executor` precisely because the server-side detection would
 *   otherwise attribute the run to the CI runner, not the caller.
 * - **Per-field, not whole-envelope, precedence.** `buildRunContext` reads
 *   each field as `envelope?.field ?? detect…()`, so any field omitted here
 *   still falls back to the `AILF_*` env var. We therefore only populate a
 *   field when its CLI flag (or, for owner, the already-folded env/config
 *   value) is present — env-only runs are unaffected.
 *
 * Returns `undefined` when nothing is set so the fully env-driven detection
 * path in `buildRunContext` runs unchanged.
 */
function buildLocalCallerEnvelope(opts) {
    const envelope = {};
    // classification — `--classification` flag only; the AILF_CLASSIFICATION env
    // var is handled by `detectClassification` when this field is omitted. Local
    // runs have no Zod boundary (unlike --remote), so validate against the closed
    // enum here and warn-then-drop on a typo, matching `detectClassification`.
    const classification = opts.classificationOption;
    if (classification) {
        if (isRunClassification(classification)) {
            envelope.classification = classification;
        }
        else {
            console.warn(`  ⚠️  --classification "${classification}" is not a recognized value; ` +
                `ignoring (run will use AILF_CLASSIFICATION or default "adhoc"). ` +
                `Valid: official, adhoc, experimental, test, external.`);
        }
    }
    // owner — `ownerTeamOption` already folds AILF_OWNER_TEAM env > repo config in
    // computeResolvedOptions. The init-template placeholder is dropped downstream
    // by `buildRunContext` (sanitizedEnvelopeOwner), so pass the value through.
    if (opts.ownerTeamOption) {
        envelope.owner = opts.ownerIndividualOption
            ? { individual: opts.ownerIndividualOption, team: opts.ownerTeamOption }
            : { team: opts.ownerTeamOption };
    }
    // purpose — `--purpose` flag only; AILF_PURPOSE handled by the fallback.
    if (opts.purposeOption)
        envelope.purpose = opts.purposeOption;
    // labels — `--label` is documented as additive to AILF_LABELS, but
    // `buildRunContext` *replaces* (`envelope?.labels ?? detectLabels()`), so
    // merge flag + env here to preserve additivity. Only engage when the flag was
    // passed; an env-only run still flows through `detectLabels` unchanged.
    const flagLabels = opts.labelOptions ?? [];
    if (flagLabels.length > 0) {
        const envLabels = (process.env.AILF_LABELS ?? "")
            .split(",")
            .map((s) => s.trim())
            .filter(Boolean);
        envelope.labels = Array.from(new Set([...envLabels, ...flagLabels]));
    }
    return Object.keys(envelope).length > 0 ? envelope : undefined;
}
/**
 * Parse the AILF_ARTIFACT_UPLOAD env var into a tri-state.
 *
 * - "0" | "false" → false (force-disable)
 * - "1" | "true"  → true  (force-enable; still a no-op without credentials)
 * - unset | other → undefined (auto-detect from credentials)
 */
export function parseArtifactUploadEnv(value) {
    if (value === undefined)
        return undefined;
    const normalized = value.trim().toLowerCase();
    if (normalized === "0" || normalized === "false")
        return false;
    if (normalized === "1" || normalized === "true")
        return true;
    return undefined;
}
/**
 * Build an AppContext from legacy ResolvedOptions.
 *
 * Delegates to createAppContext() (composition root) after mapping
 * ResolvedOptions → ResolvedConfig.
 */
export function buildAppContext(opts, rootDir) {
    const config = mapToResolvedConfig(opts, rootDir);
    // Inject config-file-only values that don't come from CLI options.
    // evalBudgetMs lives on ModelsConfig, not CLI flags.
    const models = tryLoadConfigFile("models", rootDir);
    if (models?.data?.evalBudgetMs) {
        config.evalBudgetMs = models.data.evalBudgetMs;
    }
    return createAppContext(config);
}
