/**
 * @sanity/ailf-core — Pipeline Request schema
 *
 * The universal pipeline invocation contract. A single Zod schema shared
 * between the API gateway (POST /v1/pipeline), the CLI (--config flag),
 * and GitHub Actions workflows (repository_dispatch payload).
 *
 * This eliminates the ~100-line bash flag-building in the workflow YAML
 * by flowing the entire payload through the system untouched. Each layer
 * validates it against this schema but doesn't restructure it.
 *
 * @see docs/design-docs/api-service-gateway.md — Section 7.2
 * @see packages/eval/src/pipeline/map-request-to-config.ts — maps to ResolvedConfig
 */
import { z } from "zod";
import { LITERACY_VARIANTS, RAW_EVAL_MODES, RUN_CLASSIFICATIONS, RUN_EXECUTOR_SURFACES, } from "../../ailf-shared/index.js";
// ---------------------------------------------------------------------------
// Debug options — boolean shorthand or structured object
// ---------------------------------------------------------------------------
const DebugOptionsSchema = z.object({
    enabled: z.boolean().optional(),
    firstN: z.number().int().positive().optional(),
    pattern: z.string().optional(),
    sample: z.number().int().positive().optional(),
});
// ---------------------------------------------------------------------------
// Callback delivery — for API-triggered evaluations
// ---------------------------------------------------------------------------
const CallbackSchema = z.object({
    headers: z.record(z.string(), z.string()).optional(),
    url: z.string().url(),
});
// ---------------------------------------------------------------------------
// Caller Git context — for cross-repo evaluations
// ---------------------------------------------------------------------------
/**
 * Git metadata from the *calling* repository.
 *
 * When a cross-repo evaluation is triggered (via repository_dispatch or
 * the API), the GitHub Actions env vars (GITHUB_REPOSITORY, GITHUB_SHA,
 * etc.) reflect the *AILF core repo* where the workflow executes — not
 * the repo that requested the evaluation. This field carries the caller's
 * actual git context so provenance correctly attributes the evaluation.
 */
const CallerGitSchema = z.object({
    branch: z.string().optional(),
    prNumber: z.number().int().positive().optional(),
    repo: z.string(),
    sha: z.string().optional(),
});
// ---------------------------------------------------------------------------
// Caller envelope (D0037) — for --remote evaluations from external repos
// ---------------------------------------------------------------------------
/**
 * Caller-provided D0037 provenance envelope.
 *
 * When the CLI submits a PipelineRequest via `--remote`, the pipeline
 * runs server-side (Cloud Run / dispatched GH Actions) where the
 * caller's local env vars don't exist. Carrying the envelope on the
 * request lets the caller's `AILF_CLASSIFICATION` / `AILF_OWNER_TEAM`
 * / explicit CLI flags survive the API boundary so provenance
 * attributes to the caller, not the server runtime.
 *
 * Only caller-identity fields cross the wire. Server-environment
 * facts (`executor.email`, `tool.ailfVersion`, `tool.nodeVersion`,
 * `host.*`) stay server-inferred on the receiving side.
 *
 * @see docs/decisions/D0037-run-classification-and-ownership-taxonomy.md
 */
const CallerOwnerSchema = z.object({
    team: z.string().min(1),
    individual: z.string().optional(),
});
/**
 * Executor payload — user variant only. System executors (github-actions,
 * cloud-run) are always server-detected and must not be spoofable by a
 * caller, so the wire format excludes them.
 *
 * `.strict()` makes unknown keys fail the parse rather than be silently
 * stripped. Critical for PII: a caller that accidentally sends `email`
 * gets an immediate 400 instead of a silent drop that looks like success.
 * Server-inferred fields (`email`, `tool`, `host`) are NOT accepted here.
 */
const CallerExecutorSchema = z
    .object({
    type: z.literal("user"),
    surface: z.enum(RUN_EXECUTOR_SURFACES),
    name: z.string().optional(),
    githubActor: z.string().optional(),
})
    .strict();
// ---------------------------------------------------------------------------
// Generalized doc reference — polymorphic resolution by key presence
// ---------------------------------------------------------------------------
/**
 * `IdDocRef` is listed FIRST so an entry carrying both `id` and
 * `path`/`slug` matches it (preserving id/slug/path) instead of being
 * narrowed to `PathDocRef`/`SlugDocRef`. The `_id` is authoritative;
 * slug/path are informational only.
 */
const GeneralizedDocRefSchema = z.union([
    z.object({
        id: z.string().min(1),
        slug: z.string().optional(),
        path: z.string().optional(),
        reason: z.string().optional(),
    }),
    z.object({ path: z.string().min(1), reason: z.string().optional() }),
    z.object({ slug: z.string().min(1), reason: z.string().optional() }),
    z.object({ perspective: z.string().min(1), reason: z.string().optional() }),
]);
// ---------------------------------------------------------------------------
// Pipeline Request — the universal invocation contract
// ---------------------------------------------------------------------------
export const PipelineRequestSchema = z.object({
    allowedOrigins: z.array(z.string()).optional(),
    backend: z.enum(["github-actions", "cloud-run"]).optional(),
    areas: z.array(z.string()).optional(),
    callerGit: CallerGitSchema.optional(),
    callback: CallbackSchema.optional(),
    changedDocs: z.array(z.string()).optional(),
    compare: z.boolean().optional(),
    compareBaseline: z.string().optional(),
    compareBaselineReportId: z.string().min(1).optional(),
    compareThreshold: z.number().min(0).optional(),
    concurrency: z.number().int().positive().optional(),
    dataset: z.string().optional(),
    debug: z.union([z.boolean(), DebugOptionsSchema]).optional(),
    gapAnalysis: z.boolean().optional(),
    /**
     * Grader-context policy (W0196 / DOC-2117). When `"with-docs"`, the
     * canonical reference is injected into the LLM grader's `rubricPrompt`
     * as authoritative ground truth. When omitted or `"rubric-only"`, the
     * grader sees only the rubric template (legacy behavior).
     */
    graderContext: z.enum(["rubric-only", "with-docs"]).optional(),
    graderReplications: z.number().int().positive().optional(),
    borderlineReplications: z.number().int().positive().optional(),
    headers: z.record(z.string(), z.string()).optional(),
    injectContextDocs: z.array(GeneralizedDocRefSchema).optional(),
    inlineTasks: z.array(z.record(z.string(), z.unknown())).optional(),
    jobId: z.string().optional(),
    /**
     * Evaluation mode — accepts both canonical and legacy names.
     * Legacy names must pass through normalizeMode() before entering typed pipeline code.
     */
    mode: z.enum(RAW_EVAL_MODES).optional(),
    /**
     * Filter the evaluation cohort to a subset of the configured model IDs
     * (W0281). Unknown IDs are dropped at the runner with a structured
     * warning + job-error patch.
     */
    models: z.array(z.string().min(1)).optional(),
    noAutoScope: z.boolean().optional(),
    noCache: z.boolean().optional(),
    noRemoteCache: z.boolean().optional(),
    perspective: z.string().optional(),
    projectId: z.string().optional(),
    publish: z.boolean().optional(),
    publishTag: z.string().optional(),
    /**
     * Opt-in registration of the submitted task suite into the Content Lake
     * (external-task mirroring). Omitted/false = no registration (back-compat
     * for every existing caller). Behavior and the trusted-team gate live in
     * the mirror step and the gateway pre-flight; this field is the opt-in.
     */
    registerTasks: z.boolean().optional(),
    searchMode: z.enum(["off", "open", "origin-only"]).optional(),
    source: z.string().optional(),
    sourceReportId: z.string().optional(),
    studioOrigin: z.string().url().optional(),
    taskMode: z.enum(["content-lake", "inline"]).optional(),
    /**
     * Task-source configuration (W0077 Phase 6h). Mirrors
     * `EvalConfigSchema.taskSource` so consumers can express task-source
     * intent through the universal request payload instead of CLI flags.
     *
     * - `type`           — `content-lake` (default) or `repo`
     * - `repoTasksPath`  — path interpreted relative to `rootDir` when
     *                      mapped through `mapRequestToConfig`
     *
     * When both `taskSource.type` and the legacy `taskMode` are present,
     * `taskSource.type` wins.
     */
    taskSource: z
        .object({
        type: z.enum(["content-lake", "repo"]).optional(),
        repoTasksPath: z.string().min(1).optional(),
    })
        .optional(),
    tasks: z.array(z.string()).optional(),
    urls: z.array(z.string().url()).optional(),
    /**
     * Literacy variant — only meaningful when mode is "literacy".
     *
     * When provided with a canonical mode (`mode: "literacy"`), this field
     * specifies the variant directly. When mode is a legacy alias (e.g.,
     * `mode: "baseline"`), the variant is derived from the mode name and
     * this field is ignored.
     *
     * Prefer explicit `mode: "literacy", variant: "baseline"` over the
     * legacy `mode: "baseline"` form.
     */
    variant: z.enum(LITERACY_VARIANTS).optional(),
    /** External preset file paths or npm package names to load */
    presets: z.array(z.string()).optional(),
    // -------------------------------------------------------------------------
    // D0037 caller envelope — classification, attribution, and intent
    // -------------------------------------------------------------------------
    /**
     * How this run should be treated for reporting and trend tracking.
     * Orthogonal to `trigger.type` (captured server-side). When omitted,
     * the server defaults to `"adhoc"`.
     */
    classification: z.enum(RUN_CLASSIFICATIONS).optional(),
    /** Team and (optionally) individual this run is attributable to. */
    owner: CallerOwnerSchema.optional(),
    /** Caller executor identity (user variant only). */
    executor: CallerExecutorSchema.optional(),
    /** Human-authored "why I ran this". */
    purpose: z.string().optional(),
    /** Free-form searchable tags (release IDs, regression hunts, experiments). */
    labels: z.array(z.string().min(1)).optional(),
});
