/**
 * job-store.ts
 *
 * Job tracking store backed by the Sanity Content Lake.
 * Provides CRUD operations for `ailf.job` documents that track
 * API-triggered pipeline evaluations.
 *
 * Job documents are created by the API gateway and updated by the
 * CLI as the pipeline progresses through steps.
 *
 * @see docs/design-docs/api-service-gateway.md
 * @see packages/studio/src/schema/job.ts — Sanity document schema
 */
import { JOB_EXECUTION_DEADLINE_MS } from "./_vendor/ailf-core/index.js";
import { getAilfSanityClient } from "./sanity/client.js";
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const JOB_TYPE = "ailf.job";
/** Default job deadline, single-sourced with the gateway + CLI poll loop. */
const DEFAULT_DEADLINE_MS = JOB_EXECUTION_DEADLINE_MS;
// ---------------------------------------------------------------------------
// Job Store
// ---------------------------------------------------------------------------
export class JobStore {
    client;
    constructor(options = {}) {
        if (options.client) {
            this.client = options.client;
        }
        else {
            this.client = getAilfSanityClient({
                ...(options.dataset ? { dataset: options.dataset } : {}),
                ...(options.projectId ? { projectId: options.projectId } : {}),
                ...(options.token ? { token: options.token } : {}),
            });
        }
    }
    /**
     * Create a new job document in the Content Lake.
     *
     * Generates a job ID (e.g. "job_a1b2c3d4e5f6") and writes the
     * initial document with status "received". The job ID is used
     * directly as the Sanity document `_id`.
     *
     * @returns The generated job ID, or null on failure
     */
    async createJob(options) {
        const jobId = generateJobId();
        const now = new Date().toISOString();
        const deadlineMs = options.deadlineMs ?? DEFAULT_DEADLINE_MS;
        const deadlineAt = new Date(Date.now() + deadlineMs).toISOString();
        try {
            await this.client.create({
                _id: jobId,
                _type: JOB_TYPE,
                apiKeyPrefix: options.apiKeyPrefix,
                callback: options.callback ?? null,
                completedAt: null,
                config: options.config,
                createdAt: now,
                deadlineAt,
                error: null,
                execution: null,
                jobId,
                progress: null,
                reportId: null,
                startedAt: null,
                status: "received",
            });
            return jobId;
        }
        catch (error) {
            console.warn(`  ⚠️  Failed to create job: ${error instanceof Error ? error.message : String(error)}`);
            return null;
        }
    }
    /**
     * Count jobs created today for a given API key prefix.
     * Used for daily eval budget enforcement.
     */
    async countTodayJobs(apiKeyPrefix) {
        try {
            const todayStart = new Date();
            todayStart.setUTCHours(0, 0, 0, 0);
            const count = await this.client.fetch(`count(*[_type == $type && apiKeyPrefix == $prefix && createdAt >= $start])`, {
                prefix: apiKeyPrefix,
                start: todayStart.toISOString(),
                type: JOB_TYPE,
            });
            return count;
        }
        catch {
            return 0;
        }
    }
    /**
     * Count active (non-terminal) jobs for a given API key prefix.
     * Used for concurrent job limit enforcement.
     */
    async countActiveJobs(apiKeyPrefix) {
        try {
            const count = await this.client.fetch(`count(*[_type == $type && apiKeyPrefix == $prefix && status in ["received", "queued", "running"]])`, {
                prefix: apiKeyPrefix,
                type: JOB_TYPE,
            });
            return count;
        }
        catch {
            return 0;
        }
    }
    /**
     * Find stale jobs that have exceeded their deadline.
     * Used by the timeout cron job.
     */
    async findStaleJobs() {
        try {
            const now = new Date().toISOString();
            const docs = await this.client.fetch(`*[_type == $type && status in ["received", "queued", "running"] && deadlineAt < $now]`, { now, type: JOB_TYPE });
            return docs.map(toJobDocument);
        }
        catch {
            return [];
        }
    }
    /**
     * Read a job by its job ID.
     */
    async readJob(jobId) {
        try {
            const doc = await this.client.fetch(`*[_type == $type && jobId == $id][0]`, { id: jobId, type: JOB_TYPE });
            return doc ? toJobDocument(doc) : null;
        }
        catch (error) {
            console.warn(`  ⚠️  Failed to read job: ${error instanceof Error ? error.message : String(error)}`);
            return null;
        }
    }
    /**
     * Update a job's status and optional associated data.
     */
    async updateJob(jobId, update) {
        try {
            await this.client.patch(jobId).set(update).commit();
            return true;
        }
        catch (error) {
            console.warn(`  ⚠️  Failed to update job ${jobId}: ${error instanceof Error ? error.message : String(error)}`);
            return false;
        }
    }
    /**
     * Patch the parent ailf.evalRequest doc when this job reaches a terminal
     * state. The webhook handler writes `jobId` onto the evalRequest at
     * dispatch time, so we look the parent up by that field.
     *
     * Best-effort: returns `false` on lookup miss or write failure, never
     * throws. Closes the S1-B gap from the 2026-05-24 new-eval audit — until
     * this runs, evalRequest docs stay `status: "dispatched"` indefinitely
     * and the dashboard can't surface completion or errors to users.
     *
     * @returns true on successful patch, false on lookup miss or write error
     */
    async patchEvalRequestForJob(jobId, patch) {
        try {
            const evalRequest = await this.client.fetch(`*[_type == "ailf.evalRequest" && jobId == $jobId][0]{_id}`, { jobId });
            if (!evalRequest?._id) {
                return false;
            }
            await this.client
                .patch(evalRequest._id)
                .set({
                status: patch.status,
                completedAt: patch.completedAt,
                ...(patch.reportId ? { reportId: patch.reportId } : {}),
                ...(patch.error ? { error: patch.error } : {}),
            })
                .commit();
            return true;
        }
        catch (error) {
            console.warn(`  ⚠️  Failed to patch ailf.evalRequest for jobId ${jobId}: ${error instanceof Error ? error.message : String(error)}`);
            return false;
        }
    }
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
 * Generate a job ID with "job_" prefix and random suffix.
 *
 * Uses crypto.randomUUID() for uniqueness, shortened for readability.
 * The returned value is used directly as the Sanity document `_id`
 * (no additional prefix wrapping).
 *
 * Example: "job_a1b2c3d4e5f6"
 */
function generateJobId() {
    const uuid = crypto.randomUUID().replace(/-/g, "");
    return `job_${uuid.slice(0, 12)}`;
}
/**
 * Convert a raw Sanity document to a typed JobDocument.
 */
function toJobDocument(doc) {
    return {
        apiKeyPrefix: doc.apiKeyPrefix,
        callback: doc.callback,
        completedAt: doc.completedAt,
        config: doc.config,
        createdAt: doc.createdAt,
        deadlineAt: doc.deadlineAt,
        error: doc.error,
        execution: doc.execution,
        jobId: doc.jobId,
        progress: doc.progress,
        reportId: doc.reportId,
        startedAt: doc.startedAt,
        status: doc.status,
    };
}
