import { flattenHeaders, getContext, getTestId, isErrorResponse, isVideoResponse, resolveResponse, resolveStrictMode, serializeErrorResponse, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
import { matchFixtureDiagnostic } from "./router.js";
import { writeErrorResponse } from "./sse-writer.js";
import { applyChaos } from "./chaos.js";
import { resolveUpstreamUrl } from "./url.js";
import { buildFixtureMatch, buildForwardHeaders, persistFixture, sanitizeHeaderValue } from "./recorder.js";
import { handleVideoStatus } from "./video.js";
import { resolveProgression } from "./fal.js";
import { readEnvelopeText, testIdSuffix, upstreamTimeoutSignal } from "./video-proxy-shared.js";
import crypto from "node:crypto";

//#region src/grok-video.ts
const DEFAULT_GROK_VIDEO_MODEL = "grok-imagine-video";
const GROK_USD_TICKS_PER_USD = 1e10;
const GROK_DEFAULT_VIDEO_URL = "https://cdn.x.ai/aimock-placeholder-video.mp4";
const GROK_VIDEO_MAX_ENTRIES = 1e4;
const GROK_VIDEO_TTL_MS = 36e5;
/**
* Per-testId job state for the Grok video handler. Mirrors OpenRouterVideoJobMap
* (openrouter-video.ts): lazy TTL eviction on `get`, FIFO eviction of the
* oldest entries on `set` when over capacity, delete-before-set TTL refresh,
* monotonic world-generation counter for reset-mid-flight detection, no
* background sweep timer. Keys are `${testId}:${requestId}`.
*/
var GrokVideoJobMap = class {
	entries = /* @__PURE__ */ new Map();
	worldGeneration = 0;
	get generation() {
		return this.worldGeneration;
	}
	get(key) {
		const entry = this.entries.get(key);
		if (!entry) return void 0;
		if (Date.now() - entry.createdAt > GROK_VIDEO_TTL_MS) {
			this.entries.delete(key);
			return;
		}
		return entry.job;
	}
	set(key, job) {
		this.entries.delete(key);
		this.entries.set(key, {
			job,
			createdAt: Date.now()
		});
		if (this.entries.size > GROK_VIDEO_MAX_ENTRIES) {
			const excess = this.entries.size - GROK_VIDEO_MAX_ENTRIES;
			const iter = this.entries.keys();
			for (let i = 0; i < excess; i++) {
				const next = iter.next();
				if (!next.done) this.entries.delete(next.value);
			}
		}
	}
	delete(key) {
		return this.entries.delete(key);
	}
	clear() {
		this.entries.clear();
		this.worldGeneration++;
	}
	get size() {
		return this.entries.size;
	}
};
/**
* Maps the fixture's terminal video status onto the job lifecycle. Anything
* that is not "failed" — including a fixture authored as "processing" — is
* treated as completed, since this surface always drives jobs to a terminal
* state.
*/
function terminalStatus(job) {
	return job.video.status === "failed" ? "failed" : "completed";
}
/**
* Mutates a job in place to advance its state on a status poll.
* `pending → in_progress → completed | failed` based on poll-count thresholds.
* No-op once terminal. The in_progress threshold is checked first so a job
* whose thresholds are equal still spends one poll in in_progress instead of
* jumping straight to the terminal status (fal advanceJob semantics).
*/
function advanceJob(job) {
	if (job.status === "completed" || job.status === "failed") return;
	job.pollCount += 1;
	if (job.status === "pending" && job.pollCount >= job.pollsBeforeInProgress) job.status = "in_progress";
	else if (job.pollCount >= job.pollsBeforeCompleted) job.status = terminalStatus(job);
}
/**
* Synthesize the Grok wire `progress` (0..100) from poll-count progress toward
* the completed threshold. A terminal-completed job is always 100 (the only way
* to reach 100 — it is the wire signal for "done"). Every other state reports
* its progress-at-failure/at-poll, capped at 99. A seed-terminal job with a
* non-positive completion target has done zero polls toward completion, so it
* reports 0 (a failed seed job is 0%, never 100).
*/
function grokProgress(job) {
	if (job.status === "completed") return 100;
	const target = job.pollsBeforeCompleted;
	if (target <= 0) return 0;
	const ratio = Math.min(1, job.pollCount / target);
	return Math.min(99, Math.round(ratio * 100));
}
/** Map the internal job status onto the Grok wire `status` token. */
function grokWireStatus(status) {
	if (status === "completed") return "done";
	if (status === "failed") return "failed";
	return "pending";
}
/**
* Synthesizes a structurally valid journal body for field-validation 400s.
* Mirrors openrouter-video's validationJournalBody: `model` stays a string and
* `messages` is an empty array; underscore-prefixed keys are stripped so a
* request cannot spoof handler-set discriminators.
*/
function validationJournalBody(videoReq) {
	const rawModel = videoReq.model;
	const model = typeof rawModel === "string" ? rawModel : rawModel === void 0 ? "" : JSON.stringify(rawModel);
	return {
		...Object.fromEntries(Object.entries(videoReq).filter(([key]) => !key.startsWith("_"))),
		model,
		messages: []
	};
}
async function handleGrokVideoCreate(req, res, raw, fixtures, journal, defaults, setCorsHeaders, jobs) {
	setCorsHeaders(res);
	const path = req.url ?? "/v1/videos/generations";
	const method = req.method ?? "POST";
	if (((Array.isArray(req.headers["content-type"]) ? req.headers["content-type"][0] : req.headers["content-type"]) ?? "").toLowerCase().includes("multipart/form-data")) {
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: null,
			response: {
				status: 400,
				fixture: null
			}
		});
		writeErrorResponse(res, 400, JSON.stringify({
			code: "invalid_request",
			error: "multipart/form-data is not supported — the Grok video API is JSON-only"
		}));
		return;
	}
	let videoReq;
	try {
		videoReq = JSON.parse(raw);
	} catch (parseErr) {
		const detail = parseErr instanceof Error ? parseErr.message : "unknown";
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: null,
			response: {
				status: 400,
				fixture: null
			}
		});
		writeErrorResponse(res, 400, JSON.stringify({
			code: "invalid_json",
			error: `Malformed JSON: ${detail}`
		}));
		return;
	}
	if (videoReq === null || typeof videoReq !== "object" || Array.isArray(videoReq)) {
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: null,
			response: {
				status: 400,
				fixture: null
			}
		});
		writeErrorResponse(res, 400, JSON.stringify({
			code: "invalid_request",
			error: "Request body must be a JSON object"
		}));
		return;
	}
	const parsedBody = validationJournalBody(videoReq);
	if (typeof videoReq.prompt !== "string" || !videoReq.prompt) {
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: parsedBody,
			response: {
				status: 400,
				fixture: null
			}
		});
		const message = videoReq.prompt === void 0 ? "Missing required parameter: 'prompt'" : "Invalid type for parameter: 'prompt' must be a non-empty string";
		writeErrorResponse(res, 400, JSON.stringify({
			code: "invalid_request",
			error: message
		}));
		return;
	}
	if (videoReq.model !== void 0 && (typeof videoReq.model !== "string" || !videoReq.model)) {
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: parsedBody,
			response: {
				status: 400,
				fixture: null
			}
		});
		writeErrorResponse(res, 400, JSON.stringify({
			code: "invalid_request",
			error: "Invalid type for parameter: 'model' must be a non-empty string"
		}));
		return;
	}
	const syntheticReq = {
		model: videoReq.model ?? DEFAULT_GROK_VIDEO_MODEL,
		messages: [{
			role: "user",
			content: videoReq.prompt
		}],
		_endpointType: "video",
		_videoProvider: "grok",
		_context: getContext(req)
	};
	const testId = getTestId(req);
	const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(fixtures, syntheticReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
	if (fixture) {
		journal.incrementFixtureMatchCount(fixture, fixtures, testId);
		defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
	} else {
		const snippet = videoReq.prompt.slice(0, 80);
		defaults.logger.debug(`No fixture matched for request (model=${syntheticReq.model}, msg="${snippet}")`);
	}
	if (applyChaos(res, fixture, defaults.chaos, req.headers, journal, {
		method,
		path,
		headers: flattenHeaders(req.headers),
		body: syntheticReq
	}, fixture ? "fixture" : resolveStrictMode(defaults.strict, req.headers) ? "internal" : defaults.record?.providers.grok ? "proxy" : "internal", defaults.registry, defaults.logger)) return;
	if (!fixture) {
		if (resolveStrictMode(defaults.strict, req.headers)) {
			const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);
			defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));
			journal.add({
				method,
				path,
				headers: flattenHeaders(req.headers),
				body: syntheticReq,
				response: {
					status: 503,
					fixture: null,
					...strictOverrideField(defaults.strict, req.headers)
				}
			});
			writeErrorResponse(res, 503, JSON.stringify({
				code: "no_fixture_match",
				error: strictMessage
			}));
			return;
		}
		if (defaults.record) {
			if (await proxyGrokVideoSubmit({
				req,
				res,
				raw,
				syntheticReq,
				record: defaults.record,
				journal,
				defaults,
				jobs,
				method,
				path
			}) === "handled") return;
		}
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: syntheticReq,
			response: {
				status: 404,
				fixture: null,
				...strictOverrideField(defaults.strict, req.headers)
			}
		});
		writeErrorResponse(res, 404, JSON.stringify({
			code: "not_found",
			error: "No fixture matched"
		}));
		return;
	}
	const worldGeneration = jobs.generation;
	const response = await resolveResponse(fixture, syntheticReq);
	if (isErrorResponse(response)) {
		if (res.destroyed || res.writableEnded) return;
		const status = response.status ?? 500;
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: syntheticReq,
			response: {
				status,
				fixture
			}
		});
		writeErrorResponse(res, status, serializeErrorResponse(response), { retryAfter: response.retryAfter });
		return;
	}
	if (!isVideoResponse(response)) {
		if (res.destroyed || res.writableEnded) return;
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: syntheticReq,
			response: {
				status: 500,
				fixture
			}
		});
		writeErrorResponse(res, 500, JSON.stringify({ error: {
			message: "Fixture response is not a video type",
			type: "server_error"
		} }));
		return;
	}
	if (res.destroyed || res.writableEnded) return;
	journal.add({
		method,
		path,
		headers: flattenHeaders(req.headers),
		body: syntheticReq,
		response: {
			status: 200,
			fixture
		}
	});
	const fixtureStatus = response.video.status;
	if (fixtureStatus === "processing") defaults.logger.warn(`Video fixture has status "processing" — treated as completed for /v1/videos/generations jobs`);
	else if (fixtureStatus !== "completed" && fixtureStatus !== "failed") defaults.logger.warn(`Video fixture has unknown status "${fixtureStatus}" — treating as completed for /v1/videos/generations jobs`);
	const requestId = crypto.randomUUID();
	const progression = resolveProgression(defaults.grokVideo);
	const job = {
		kind: "replay",
		requestId,
		status: "pending",
		pollCount: 0,
		pollsBeforeInProgress: progression.pollsBeforeInProgress,
		pollsBeforeCompleted: progression.pollsBeforeCompleted,
		video: { ...response.video }
	};
	if (progression.pollsBeforeCompleted === 0) job.status = terminalStatus(job);
	if (jobs.generation === worldGeneration) jobs.set(`${testId}:${requestId}`, job);
	else defaults.logger.warn(`Grok video submit resolved after a fixtures reset — not inserting job ${requestId} into the new world (its polls will 404)`);
	res.writeHead(200, { "Content-Type": "application/json" });
	res.end(JSON.stringify({ request_id: requestId + testIdSuffix(testId, "?") }));
}
async function handleGrokVideoStatus(req, res, id, fixtures, journal, defaults, setCorsHeaders, grokJobs, videoStates) {
	const testId = getTestId(req);
	const key = `${testId}:${id}`;
	const job = grokJobs.get(key);
	if (!job) {
		handleVideoStatus(req, res, id, journal, defaults, setCorsHeaders, videoStates);
		return;
	}
	setCorsHeaders(res);
	const path = req.url ?? `/v1/videos/${id}`;
	const method = req.method ?? "GET";
	if (applyChaos(res, null, defaults.chaos, req.headers, journal, {
		method,
		path,
		headers: flattenHeaders(req.headers),
		body: null
	}, "internal", defaults.registry, defaults.logger)) return;
	if (job.kind === "record") {
		if (resolveStrictMode(defaults.strict, req.headers)) {
			defaults.logger.error(`STRICT: video job ${id} is proxied live upstream (record mode) — refusing the upstream poll`);
			journal.add({
				method,
				path,
				headers: flattenHeaders(req.headers),
				body: null,
				response: {
					status: 503,
					fixture: null,
					...strictOverrideField(defaults.strict, req.headers)
				}
			});
			writeErrorResponse(res, 503, JSON.stringify({
				code: "no_fixture_match",
				error: `Strict mode: video job ${id} is proxied live upstream (record mode) — nothing reaches an upstream under strict mode`
			}));
			return;
		}
		await proxyGrokVideoRecordPoll({
			req,
			res,
			job,
			key,
			testId,
			fixtures,
			journal,
			defaults,
			jobs: grokJobs,
			method,
			path
		});
		return;
	}
	if (res.destroyed || res.writableEnded) return;
	advanceJob(job);
	grokJobs.set(key, job);
	journal.add({
		method,
		path,
		headers: flattenHeaders(req.headers),
		body: null,
		response: {
			status: 200,
			fixture: null
		}
	});
	const body = serializeGrokReplay(job, defaults);
	res.writeHead(200, { "Content-Type": "application/json" });
	res.end(JSON.stringify(body));
}
/**
* Serialize a replay job into the Grok wire body. STORED `video.status` is the
* 3-value VideoResponse union; the wire `status` is derived here
* (completed → "done"). cost_in_usd_ticks is reconstructed from the stored USD
* `video.cost` (round(cost * 1e10)).
*/
function serializeGrokReplay(job, defaults) {
	const body = {
		request_id: job.requestId,
		status: grokWireStatus(job.status),
		progress: grokProgress(job)
	};
	if (job.status === "completed") {
		body.video = {
			url: job.video.url || GROK_DEFAULT_VIDEO_URL,
			duration: job.video.duration ?? 0
		};
		body.usage = { cost_in_usd_ticks: Math.round((job.video.cost ?? 0) * GROK_USD_TICKS_PER_USD) };
	} else if (job.status === "failed") {
		if (job.video.error === "" && !job.emptyErrorWarned) {
			job.emptyErrorWarned = true;
			defaults.logger.warn(`Video fixture for job ${job.requestId} has an empty error message — using the default`);
		}
		body.code = "generation_failed";
		body.error = job.video.error || "Video generation failed";
	}
	return body;
}
const GROK_VIDEO_SUBMIT_PATH = "/v1/videos/generations";
/**
* Proxy an unmatched Grok video submit to the configured upstream and answer
* the client with a mock-rewritten `{ request_id }`. The upstream lifecycle is
* then driven interactively by the client's own polls. Returns "no_upstream"
* when record mode has no grok provider URL — the caller falls through to 404.
*/
async function proxyGrokVideoSubmit(args) {
	const { req, res, raw, syntheticReq, record, journal, defaults, jobs, method, path } = args;
	const upstreamBase = record.providers.grok;
	if (!upstreamBase) {
		defaults.logger.warn(`No upstream URL configured for provider "grok" — cannot proxy`);
		return "no_upstream";
	}
	const proxyError = (msg) => {
		defaults.logger.error(`Grok video submit proxy failed: ${msg}`);
		if (res.destroyed || res.writableEnded) return "handled";
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: syntheticReq,
			response: {
				status: 502,
				fixture: null,
				source: "proxy",
				...strictOverrideField(defaults.strict, req.headers)
			}
		});
		writeErrorResponse(res, 502, JSON.stringify({
			code: "proxy_error",
			error: `Proxy to upstream failed: ${msg}`
		}));
		return "handled";
	};
	let submitUrl;
	try {
		submitUrl = resolveUpstreamUrl(upstreamBase, GROK_VIDEO_SUBMIT_PATH);
		new URL(upstreamBase).origin;
	} catch (err) {
		return proxyError(`Invalid upstream URL: ${upstreamBase} (${err instanceof Error ? err.message : String(err)})`);
	}
	defaults.logger.warn(`NO FIXTURE MATCH — proxying video submit to ${upstreamBase}${GROK_VIDEO_SUBMIT_PATH}`);
	const worldGeneration = jobs.generation;
	let fetched;
	try {
		const upstreamRes = await fetch(submitUrl, {
			method: "POST",
			headers: buildForwardHeaders(req),
			body: raw,
			signal: upstreamTimeoutSignal(record)
		});
		fetched = {
			status: upstreamRes.status,
			contentType: upstreamRes.headers.get("content-type"),
			text: await readEnvelopeText(upstreamRes, record)
		};
	} catch (err) {
		return proxyError(err instanceof Error ? err.message : "Unknown proxy error");
	}
	if (fetched.status === 401 || fetched.status === 403) {
		defaults.logger.warn(`Upstream rejected the video submit (${fetched.status}) — relaying the upstream status`);
		if (res.destroyed || res.writableEnded) return "handled";
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: syntheticReq,
			response: {
				status: fetched.status,
				fixture: null,
				source: "proxy",
				...strictOverrideField(defaults.strict, req.headers)
			}
		});
		res.writeHead(fetched.status, { "Content-Type": fetched.contentType ?? "application/json" });
		res.end(fetched.text);
		return "handled";
	}
	let upstreamRequestId;
	{
		if (fetched.status < 200 || fetched.status >= 300) return proxyError(`Submit ${fetched.status}: ${fetched.text.slice(0, 200)}`);
		let parsed;
		try {
			parsed = JSON.parse(fetched.text);
		} catch {
			return proxyError(`Submit returned non-JSON: ${fetched.text.slice(0, 200)}`);
		}
		if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return proxyError("Submit response is not a JSON object");
		const envelope = parsed;
		upstreamRequestId = String(envelope.request_id ?? "").trim();
		if (!upstreamRequestId) return proxyError("Submit response missing request_id");
	}
	const upstreamPollingUrl = resolveUpstreamUrl(upstreamBase, `/v1/videos/${encodeURIComponent(upstreamRequestId)}`).toString();
	const testId = getTestId(req);
	const matchRequest = defaults.requestTransform ? defaults.requestTransform(syntheticReq) : syntheticReq;
	const requestId = crypto.randomUUID();
	const job = {
		kind: "record",
		requestId,
		status: "pending",
		upstreamRequestId,
		upstreamPollingUrl,
		match: buildFixtureMatch(matchRequest, record)
	};
	if (jobs.generation === worldGeneration) jobs.set(`${testId}:${requestId}`, job);
	else defaults.logger.warn(`Grok video submit for upstream request ${upstreamRequestId} completed after a fixtures reset — not inserting the job into the new world (its polls will 404)`);
	if (res.destroyed || res.writableEnded) return "handled";
	journal.add({
		method,
		path,
		headers: flattenHeaders(req.headers),
		body: syntheticReq,
		response: {
			status: 200,
			fixture: null,
			source: "proxy",
			...strictOverrideField(defaults.strict, req.headers)
		}
	});
	res.writeHead(200, { "Content-Type": "application/json" });
	res.end(JSON.stringify({ request_id: requestId + testIdSuffix(testId, "?") }));
	return "handled";
}
/**
* Proxy a status poll for a record-mode job 1:1 to the upstream and relay the
* result with the mock requestId substituted. On `done` the rewritten body is
* relayed IMMEDIATELY and an eager capture runs (synchronously here — there is
* NO byte download, only the url/duration/cost from the terminal poll body —
* but structured to mirror openrouter-video's terminal path). `failed`
* persists a failed fixture synchronously. `expired` (and any unrepresentable
* status) pass through with a warn and keep proxying. Every post-await map
* mutation is identity-guarded. Each successful proxied poll refreshes the TTL.
*/
async function proxyGrokVideoRecordPoll(args) {
	const { req, res, job, key, testId, fixtures, journal, defaults, jobs, method, path } = args;
	const logger = defaults.logger;
	const journalProxy = (status) => {
		journal.add({
			method,
			path,
			headers: flattenHeaders(req.headers),
			body: null,
			response: {
				status,
				fixture: null,
				source: "proxy",
				...strictOverrideField(defaults.strict, req.headers)
			}
		});
	};
	const proxyError = (msg) => {
		logger.error(`Grok video poll proxy failed: ${msg}`);
		if (res.destroyed || res.writableEnded) return;
		journalProxy(502);
		writeErrorResponse(res, 502, JSON.stringify({
			code: "proxy_error",
			error: `Proxy to upstream failed: ${msg}`
		}));
	};
	const record = defaults.record;
	if (!record) {
		proxyError("record mode is no longer configured for an in-flight record job");
		return;
	}
	let fetched;
	try {
		const upstreamRes = await fetch(job.upstreamPollingUrl, {
			headers: buildForwardHeaders(req),
			signal: upstreamTimeoutSignal(record)
		});
		fetched = {
			status: upstreamRes.status,
			contentType: upstreamRes.headers.get("content-type"),
			text: await readEnvelopeText(upstreamRes, record)
		};
	} catch (err) {
		proxyError(err instanceof Error ? err.message : "Unknown proxy error");
		return;
	}
	if (fetched.status === 401 || fetched.status === 403) {
		logger.warn(`Upstream rejected the status poll for job ${job.upstreamRequestId} (${fetched.status}) — relaying the upstream status`);
		if (res.destroyed || res.writableEnded) return;
		journalProxy(fetched.status);
		res.writeHead(fetched.status, { "Content-Type": fetched.contentType ?? "application/json" });
		res.end(fetched.text);
		return;
	}
	let upstreamBody;
	{
		if (fetched.status < 200 || fetched.status >= 300) {
			proxyError(`Status ${fetched.status}: ${fetched.text.slice(0, 200)}`);
			return;
		}
		let parsed;
		try {
			parsed = JSON.parse(fetched.text);
		} catch {
			proxyError(`Status returned non-JSON: ${fetched.text.slice(0, 200)}`);
			return;
		}
		if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
			proxyError("Status response is not a JSON object");
			return;
		}
		upstreamBody = parsed;
	}
	const relayBody = {
		...upstreamBody,
		request_id: job.requestId
	};
	const relayJson = (body) => {
		if (res.destroyed || res.writableEnded) return;
		journalProxy(200);
		res.writeHead(200, { "Content-Type": "application/json" });
		res.end(JSON.stringify(body));
	};
	const upstreamStatus = String(upstreamBody.status ?? "");
	if (upstreamStatus === "pending" || upstreamStatus === "in_progress") {
		if (jobs.get(key) === job) {
			if (!job.capturing && job.status !== "completed" && job.status !== "failed") job.status = upstreamStatus === "in_progress" ? "in_progress" : "pending";
			jobs.set(key, job);
		}
		relayJson(relayBody);
		return;
	}
	if (upstreamStatus === "done") {
		if (record.proxyOnly) {
			if (jobs.get(key) === job) {
				job.status = "completed";
				jobs.set(key, job);
			}
			relayJson(relayBody);
			return;
		}
		if (job.capturing || jobs.get(key) !== job) {
			if (jobs.get(key) === job) jobs.set(key, job);
			relayJson(relayBody);
			return;
		}
		job.capturing = true;
		job.status = "completed";
		jobs.set(key, job);
		relayJson(relayBody);
		captureGrokVideoRecordFixture({
			job,
			key,
			testId,
			fixtures,
			defaults,
			jobs,
			record,
			upstreamBody
		});
		return;
	}
	if (upstreamStatus === "failed") {
		if (record.proxyOnly) {
			if (jobs.get(key) === job) {
				job.status = "failed";
				jobs.set(key, job);
			}
			relayJson(relayBody);
			return;
		}
		if (jobs.get(key) !== job) {
			relayJson(relayBody);
			return;
		}
		const rawError = upstreamBody.error;
		let error;
		if (typeof rawError === "string" && rawError) error = rawError;
		else if (rawError !== null && typeof rawError === "object" && !Array.isArray(rawError)) {
			const message = rawError.message;
			if (typeof message === "string" && message) error = message;
		}
		if (error === void 0 && rawError !== void 0 && rawError !== null) logger.warn(`Upstream video job ${job.upstreamRequestId} failed with an unusable error value (${JSON.stringify(rawError).slice(0, 100)}) — recording the fixture without an error message (replay serves the default)`);
		const video = {
			id: job.upstreamRequestId,
			status: "failed",
			...error !== void 0 ? { error } : {}
		};
		const persistResult = persistFixture({
			record,
			providerKey: "grok",
			testId,
			fixture: {
				match: job.match,
				response: { video }
			},
			fixtures,
			logger
		});
		if (persistResult.kind === "failed" && !res.headersSent) res.setHeader("X-AIMock-Record-Error", sanitizeHeaderValue(persistResult.error));
		jobs.set(key, {
			kind: "replay",
			requestId: job.requestId,
			status: "failed",
			pollCount: 0,
			pollsBeforeInProgress: 0,
			pollsBeforeCompleted: 0,
			video: { ...video }
		});
		relayJson(relayBody);
		return;
	}
	logger.warn(`Upstream video job ${job.upstreamRequestId} reported status "${upstreamStatus}" — not representable in fixture video.status; passing through without recording`);
	if (jobs.get(key) === job) jobs.set(key, job);
	relayJson(relayBody);
}
/**
* Capture a completed Grok record job into a fixture (PERSIST only — NO byte
* download): builds `video` from the terminal poll body (id = upstream
* request_id, status = stored "completed", url/duration/cost), persists it, and
* mutates the map entry into a terminal replay job. World-generation guard
* before persist (a fixtures reset mid-flight invalidates the world).
*/
function captureGrokVideoRecordFixture(args) {
	const { job, key, testId, fixtures, defaults, jobs, record, upstreamBody } = args;
	const logger = defaults.logger;
	try {
		const upstreamVideo = upstreamBody.video !== null && typeof upstreamBody.video === "object" && !Array.isArray(upstreamBody.video) ? upstreamBody.video : void 0;
		const rawUrl = upstreamVideo?.url;
		const url = typeof rawUrl === "string" && rawUrl ? rawUrl : GROK_DEFAULT_VIDEO_URL;
		const rawDuration = upstreamVideo?.duration;
		const duration = typeof rawDuration === "number" ? rawDuration : void 0;
		const rawTicks = (upstreamBody.usage !== null && typeof upstreamBody.usage === "object" && !Array.isArray(upstreamBody.usage) ? upstreamBody.usage : void 0)?.cost_in_usd_ticks;
		const cost = typeof rawTicks === "number" ? rawTicks / GROK_USD_TICKS_PER_USD : void 0;
		const video = {
			id: job.upstreamRequestId,
			status: "completed",
			url,
			...duration !== void 0 ? { duration } : {},
			...cost !== void 0 ? { cost } : {}
		};
		if (jobs.get(key) !== job) {
			logger.warn(`Grok video capture for job ${job.upstreamRequestId} discarded: the job map no longer holds this job (fixtures reset or TTL eviction) — nothing persisted`);
			return;
		}
		persistFixture({
			record,
			providerKey: "grok",
			testId,
			fixture: {
				match: job.match,
				response: { video }
			},
			fixtures,
			logger
		});
		if (jobs.get(key) === job) jobs.set(key, {
			kind: "replay",
			requestId: job.requestId,
			status: "completed",
			pollCount: 0,
			pollsBeforeInProgress: 0,
			pollsBeforeCompleted: 0,
			video: { ...video }
		});
	} catch (err) {
		const msg = err instanceof Error ? err.message : String(err);
		logger.error(`Grok video capture for job ${job.upstreamRequestId} failed unexpectedly (${msg}) — fixture not persisted; the job keeps proxying live`);
	} finally {
		job.capturing = false;
	}
}

//#endregion
export { GROK_VIDEO_MAX_ENTRIES, GrokVideoJobMap, handleGrokVideoCreate, handleGrokVideoStatus };
//# sourceMappingURL=grok-video.js.map