/**
 * webhook/eval-request-handler.ts
 *
 * Server-side handler for `ailf.evalRequest` documents from the Sanity
 * Content Lake. This is the counterpart to the Studio's "Request Evaluation"
 * and "Run Task Eval" actions — when a content editor creates an eval
 * request document via the Studio UI, a Sanity webhook fires and calls
 * this handler.
 *
 * Designed to run in any HTTP environment: Cloudflare Workers, Vercel
 * functions, Express, Hono, etc.
 *
 * The eval-request document carries a canonical `PipelineRequest` JSON
 * blob in its `pipelineRequest` field (see W0239). The handler parses it
 * via `PipelineRequestSchema` from `@sanity/ailf-core` and forwards it
 * to the dispatcher as-is. Scoping (release-scoped via `perspective`,
 * task-scoped via `tasks`) is asserted on the parsed `PipelineRequest`
 * — at least one must be present.
 *
 * Flow:
 * 1. Receive eval request payload (from Sanity webhook projection)
 * 2. Validate envelope: must be `ailf.evalRequest` type, `pending` status,
 *    `pipelineRequest` present
 * 3. Parse + Zod-validate `pipelineRequest` against `PipelineRequestSchema`
 * 4. Assert scoping: parsed request must have `perspective` or `tasks`
 * 5. Dispatch evaluation to GitHub Actions via `repository_dispatch`
 *    with `external-eval` event type — the parsed `PipelineRequest`
 *    rides as `client_payload.request` unchanged
 * 6. On success: PATCH the eval request document → `status: "dispatched"`
 * 7. On failure: PATCH the eval request document → `status: "failed"` + error
 * 8. Return a structured result
 *
 * ## Sanity Manage Webhook Configuration
 *
 * The Sanity webhook projection MUST include all fields consumed by
 * EvalRequestPayload. The recommended projection is `{...}` (spread),
 * which passes the entire document and avoids silently dropping fields
 * when the schema evolves.
 *
 * Recommended projection:
 * ```
 * {...}
 * ```
 *
 * @see packages/api/src/routes/webhooks.ts — API gateway webhook handler
 * @see packages/studio/src/actions/RunEvaluationAction.tsx — release eval
 * @see packages/studio/src/actions/RunTaskEvaluationAction.tsx — task eval
 * @see .github/workflows/external-eval.yml — receiving workflow
 * @see docs/design-docs/report-store/visibility-workflows.md
 */
import { createClient } from "@sanity/client";
import { isPipelineRequestScoped, PipelineRequestSchema, } from "../_vendor/ailf-core/index.js";
import { AILF_FRAMEWORK_REPO } from "../_vendor/ailf-shared/index.js";
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const DEFAULT_REPO = AILF_FRAMEWORK_REPO;
const GITHUB_API = "https://api.github.com";
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Handle an incoming `ailf.evalRequest` document from a Sanity webhook.
 *
 * Validates the payload, dispatches a GitHub Actions evaluation, and
 * updates the eval request document status in the Content Lake.
 *
 * ```ts
 * // In a Cloudflare Worker or Vercel function:
 * export default async function handler(req: Request) {
 *   const payload = await req.json()
 *   const result = await handleEvalRequest(payload, {
 *     githubToken: env.GITHUB_TOKEN,
 *     sanityToken: env.SANITY_API_TOKEN,
 *   })
 *   return Response.json(result, { status: result.ok ? 200 : 400 })
 * }
 * ```
 */
export async function handleEvalRequest(payload, config) {
    const requestId = payload._id ?? "unknown";
    // -------------------------------------------------------------------------
    // 1. Create Sanity client early so validation failures can mark the
    //    document as "failed" instead of leaving it stuck at "pending".
    // -------------------------------------------------------------------------
    const client = payload.projectId && payload.dataset
        ? createClient({
            apiVersion: "2026-03-11",
            dataset: payload.dataset,
            projectId: payload.projectId,
            token: config.sanityToken,
            useCdn: false,
        })
        : null;
    // Helper: mark the eval request as failed in the Content Lake so the
    // Studio UI can show the error instead of polling forever.
    async function markFailed(errorMessage) {
        if (client && payload._id) {
            try {
                await client
                    .patch(payload._id)
                    .set({ error: errorMessage, status: "failed" })
                    .commit();
            }
            catch (err) {
                console.warn(`  ⚠️  Failed to update document with error status: ${err instanceof Error ? err.message : String(err)}`);
            }
        }
        return { error: errorMessage, ok: false, requestId };
    }
    // -------------------------------------------------------------------------
    // 2. Validate payload
    // -------------------------------------------------------------------------
    if (payload._type !== "ailf.evalRequest") {
        return markFailed(`Unexpected document type: "${payload._type}" (expected "ailf.evalRequest")`);
    }
    if (payload.status !== "pending") {
        // Don't mark as failed — it's already in a non-pending state
        return {
            error: `Eval request is not pending (status: "${payload.status}")`,
            ok: false,
            requestId,
        };
    }
    if (!payload.pipelineRequest) {
        return markFailed("Missing required field: pipelineRequest. The eval-request document " +
            "must carry a canonical PipelineRequest JSON serialization.");
    }
    let parsedRequest;
    try {
        parsedRequest = JSON.parse(payload.pipelineRequest);
    }
    catch (err) {
        return markFailed(`pipelineRequest is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
    }
    const parseResult = PipelineRequestSchema.safeParse(parsedRequest);
    if (!parseResult.success) {
        return markFailed(`pipelineRequest failed PipelineRequestSchema validation: ${parseResult.error.message}`);
    }
    const request = reconcileCallerIdentity(parseResult.data, payload.requestedBy);
    if (!isPipelineRequestScoped(request)) {
        return markFailed("pipelineRequest must scope the evaluation: provide a " +
            "`perspective` (release-scoped), `tasks` (task-scoped), or " +
            "`areas` (area-scoped).");
    }
    // -------------------------------------------------------------------------
    // 3. Dispatch evaluation via GitHub Actions
    // -------------------------------------------------------------------------
    const repo = config.githubRepo ?? DEFAULT_REPO;
    const dispatchResult = await dispatchGitHubEval(repo, request, config);
    // -------------------------------------------------------------------------
    // 4. Update eval request document status
    // -------------------------------------------------------------------------
    if (dispatchResult.ok) {
        if (client) {
            try {
                await client
                    .patch(payload._id)
                    .set({
                    dispatchedAt: new Date().toISOString(),
                    status: "dispatched",
                })
                    .commit();
            }
            catch (err) {
                // Dispatch succeeded but status update failed — log and still return ok
                console.warn(`  ⚠️  Dispatch succeeded but failed to update document status: ${err instanceof Error ? err.message : String(err)}`);
            }
        }
        return { ok: true, requestId };
    }
    // Dispatch failed — mark the document as failed
    return markFailed(dispatchResult.error ?? "Unknown dispatch error");
}
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
/**
 * Reconcile caller-claimed identity against the trustworthy Sanity write
 * context.
 *
 * The `pipelineRequest` blob is authored by whoever wrote the Sanity
 * document — a browser writer (App SDK dashboard) can set
 * `executor.name` / `owner.individual` to any string, including
 * someone else's. The webhook's only trustworthy identity signal is
 * `payload.requestedBy` (the Sanity-session-authenticated writer).
 *
 * Per D0037, `owner.team` is caller-supplied (the caller knows their
 * team); `executor.surface` / `executor.type` are caller-supplied.
 * Identity fields (`executor.name`, `executor.githubActor`,
 * `owner.individual`) are overwritten or stripped server-side here so
 * downstream provenance reflects who actually wrote the document, not
 * what they claimed.
 *
 * When `requestedBy` is missing (legacy documents), the executor/owner
 * identity fields are stripped — the pipeline's server-side detection
 * fills them as best it can.
 */
function reconcileCallerIdentity(request, requestedBy) {
    const out = { ...request };
    if (request.executor) {
        out.executor = {
            ...request.executor,
            ...(requestedBy ? { name: requestedBy } : { name: undefined }),
            githubActor: undefined,
        };
    }
    if (request.owner) {
        out.owner = {
            ...request.owner,
            ...(requestedBy
                ? { individual: requestedBy }
                : { individual: undefined }),
        };
    }
    return out;
}
/**
 * Dispatch an evaluation via GitHub Actions repository_dispatch.
 *
 * Forwards the already-validated `PipelineRequest` as-is under
 * `client_payload.request` — no field translation, no hardcoded
 * overrides. The workflow passes the request to the CLI via `--config`.
 *
 * Workflow-level metadata (`caller_repo`) stays at the top level of
 * `client_payload` for the workflow to read, separate from the
 * pipeline-invocation contract.
 */
async function dispatchGitHubEval(repo, request, config) {
    const url = `${GITHUB_API}/repos/${repo}/dispatches`;
    const body = {
        client_payload: {
            caller_repo: "sanity-io/www-sanity-io",
            request,
        },
        event_type: "external-eval",
    };
    try {
        const response = await fetch(url, {
            body: JSON.stringify(body),
            headers: {
                Accept: "application/vnd.github+json",
                Authorization: `Bearer ${config.githubToken}`,
                "X-GitHub-Api-Version": "2022-11-28",
            },
            method: "POST",
        });
        if (response.status === 204) {
            return { ok: true };
        }
        // Try to extract error message from GitHub response
        let error;
        try {
            const responseBody = (await response.json());
            error = responseBody.message ?? `HTTP ${response.status}`;
        }
        catch {
            error = `HTTP ${response.status}: ${response.statusText}`;
        }
        return { error, ok: false };
    }
    catch (err) {
        return {
            error: err instanceof Error ? err.message : String(err),
            ok: false,
        };
    }
}
