import { flattenHeaders, getContext, getTestId, isAudioResponse, isErrorResponse, isJSONResponse, resolveResponse, resolveStrictMode, serializeErrorResponse, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
import { matchFixtureDiagnostic } from "./router.js";
import { writeErrorResponse } from "./sse-writer.js";
import { resolveUpstreamUrl } from "./url.js";
import { applyProviderAuth } from "./provider-auth.js";
import { buildFixtureMatch, buildForwardHeaders, clampTimeout, persistFixture, proxyAndRecord, sanitizeHeaderValue } from "./recorder.js";
import { audioToFalFile } from "./fal-audio.js";
import crypto from "node:crypto";

//#region src/fal.ts
const FAL_QUEUE_MAX_ENTRIES = 1e4;
const FAL_QUEUE_TTL_MS = 36e5;
/**
* Per-testId queue state for the general fal handler. Mirrors FalJobMap from
* fal-audio.ts but stores arbitrary JSON payloads instead of audio file
* objects, so it can serve any fal model (image, video, motion, music, etc.).
*/
var FalQueueStateMap = class {
	entries = /* @__PURE__ */ new Map();
	get(key) {
		const entry = this.entries.get(key);
		if (!entry) return void 0;
		if (Date.now() - entry.createdAt > FAL_QUEUE_TTL_MS) {
			this.entries.delete(key);
			return;
		}
		return entry.job;
	}
	set(key, job) {
		this.entries.set(key, {
			job,
			createdAt: Date.now()
		});
		if (this.entries.size > FAL_QUEUE_MAX_ENTRIES) {
			const excess = this.entries.size - FAL_QUEUE_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();
	}
	get size() {
		return this.entries.size;
	}
};
const falQueueStates = new FalQueueStateMap();
function extractExtension(url, fallback) {
	const segment = url.split("?")[0].split("#")[0].split("/").pop() ?? "";
	const fileName = segment.length > 0 ? segment : "";
	const dotIdx = fileName.lastIndexOf(".");
	return {
		fileName,
		ext: dotIdx >= 0 ? fileName.slice(dotIdx + 1).toLowerCase() : fallback
	};
}
function imageItemToFalImage(item, index) {
	const url = item.url ?? `https://mock.fal.media/files/generated_image_${index}.png`;
	const { ext } = extractExtension(url, "png");
	return {
		url,
		width: 1024,
		height: 1024,
		content_type: ext === "jpg" || ext === "jpeg" ? "image/jpeg" : `image/${ext}`
	};
}
/**
* Translate an `ImageResponse` fixture into fal's image envelope shape:
* `{ images: [...], timings, seed, has_nsfw_concepts, prompt }`.
* Used by `LLMock.onFalImage` to keep callers from re-deriving the wire shape.
*/
function imageResponseToFalJson(response) {
	const images = (response.images ?? (response.image ? [response.image] : [])).map((item, i) => imageItemToFalImage(item, i));
	return {
		images,
		timings: { inference: 0 },
		seed: 0,
		has_nsfw_concepts: images.map(() => false),
		prompt: ""
	};
}
/**
* Translate a `VideoResponse` fixture into fal's video envelope shape:
* `{ video: { url, content_type, file_name, file_size }, seed }`.
*/
function videoResponseToFalJson(response) {
	const url = response.video.url ?? "https://mock.fal.media/files/generated_video.mp4";
	const { fileName, ext } = extractExtension(url, "mp4");
	return {
		video: {
			url,
			content_type: `video/${ext}`,
			file_name: fileName || "generated_video.mp4",
			file_size: 0
		},
		seed: 0
	};
}
function resolveProgression(config) {
	const sanitize = (value) => typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : void 0;
	const explicitInProgress = sanitize(config?.pollsBeforeInProgress);
	const explicitCompleted = sanitize(config?.pollsBeforeCompleted);
	const pollsBeforeInProgress = explicitInProgress ?? 0;
	let pollsBeforeCompleted;
	if (explicitCompleted !== void 0) pollsBeforeCompleted = Math.max(pollsBeforeInProgress, explicitCompleted);
	else if (explicitInProgress !== void 0) pollsBeforeCompleted = pollsBeforeInProgress + 1;
	else pollsBeforeCompleted = 0;
	return {
		pollsBeforeInProgress,
		pollsBeforeCompleted
	};
}
/**
* Mutates a job in place to advance its state on a status/result poll.
* IN_QUEUE → IN_PROGRESS → COMPLETED based on poll-count thresholds. No-op
* once COMPLETED or CANCELLED.
*/
function advanceJob(job) {
	if (job.status === "COMPLETED" || job.status === "CANCELLED") return;
	job.pollCount += 1;
	if (job.status === "IN_QUEUE" && job.pollCount >= job.pollsBeforeInProgress) {
		job.status = "IN_PROGRESS";
		job.logs.push({
			timestamp: (/* @__PURE__ */ new Date()).toISOString(),
			level: "INFO",
			message: "Job started processing."
		});
	} else if (job.pollCount >= job.pollsBeforeCompleted) {
		job.status = "COMPLETED";
		job.completedAt = Date.now();
		job.logs.push({
			timestamp: (/* @__PURE__ */ new Date()).toISOString(),
			level: "INFO",
			message: "Job completed."
		});
	}
}
function queuePosition(job) {
	if (job.status !== "IN_QUEUE") return 0;
	return Math.max(0, job.pollsBeforeInProgress - job.pollCount);
}
function statusResponseBody(job) {
	const body = {
		status: job.status,
		request_id: job.requestId,
		response_url: `https://${FAL_HOSTS.queue}/${job.modelId}/requests/${job.requestId}`,
		logs: job.logs
	};
	if (job.status === "IN_QUEUE" || job.status === "IN_PROGRESS") body.queue_position = queuePosition(job);
	if (job.status === "COMPLETED" && job.completedAt != null) body.metrics = { inference_time: (job.completedAt - job.submittedAt) / 1e3 };
	return body;
}
const FAL_HOSTS = {
	queue: "queue.fal.run",
	sync: "fal.run",
	storage: "rest.fal.ai",
	storageAlpha: "rest.alpha.fal.ai",
	gateway: "gateway.fal.ai"
};
const FAL_REQUEST_ID_HEADER = "x-fal-request-id";
const FAL_BILLABLE_UNITS_HEADER = "x-fal-billable-units";
/**
* Read an optional `billableUnits` off a resolved fixture response. Returns the
* finite numeric value or `null` (absent, non-numeric, or non-finite) — the
* `x-fal-billable-units` header is emitted only for a non-null result.
*/
function extractBillableUnits(response) {
	if (response && typeof response === "object" && "billableUnits" in response) {
		const value = response.billableUnits;
		if (typeof value === "number" && Number.isFinite(value)) return value;
	}
	return null;
}
const QUEUE_REQUESTS_RE = /^(.+)\/requests\/([^/]+)(\/status|\/cancel)?$/;
const STORAGE_INITIATE_PATH = "/storage/upload/initiate";
function stripFalPrefix(pathname) {
	const stripped = pathname.replace(/^\/fal/, "");
	return stripped.length > 0 ? stripped : "/";
}
function extractPromptFromBody(body) {
	if (!body || typeof body !== "object") return "";
	const obj = body;
	if (typeof obj.prompt === "string") return obj.prompt;
	if (typeof obj.text === "string") return obj.text;
	const input = obj.input;
	if (input && typeof input === "object") {
		const inputObj = input;
		if (typeof inputObj.prompt === "string") return inputObj.prompt;
		if (typeof inputObj.text === "string") return inputObj.text;
	}
	return "";
}
function parseFalPath(stripped) {
	if (!stripped.startsWith("/")) return null;
	const trimmed = stripped.replace(/^\/+/, "");
	if (!trimmed) return null;
	const m = QUEUE_REQUESTS_RE.exec(`/${trimmed}`);
	if (m) {
		const modelId = m[1].replace(/^\/+/, "");
		const action = m[3] === "/status" ? "status" : m[3] === "/cancel" ? "cancel" : "result";
		return {
			modelId,
			requestId: m[2],
			action
		};
	}
	return { modelId: trimmed };
}
function classifyRoute(req, pathname, targetHost) {
	const stripped = stripFalPrefix(pathname);
	if (targetHost === FAL_HOSTS.storage || targetHost === FAL_HOSTS.storageAlpha) {
		if (req.method === "POST" && stripped === STORAGE_INITIATE_PATH) return {
			kind: "storage",
			targetHost
		};
		return null;
	}
	const parsed = parseFalPath(stripped);
	if (!parsed) return null;
	if (targetHost === FAL_HOSTS.queue) {
		if (parsed.requestId) {
			if (parsed.action === "status" && req.method === "GET") return {
				kind: "queue-status",
				modelId: parsed.modelId,
				requestId: parsed.requestId,
				targetHost
			};
			if (parsed.action === "cancel" && req.method === "PUT") return {
				kind: "queue-cancel",
				modelId: parsed.modelId,
				requestId: parsed.requestId,
				targetHost
			};
			if (parsed.action === "result" && req.method === "GET") return {
				kind: "queue-result",
				modelId: parsed.modelId,
				requestId: parsed.requestId,
				targetHost
			};
			return null;
		}
		if (req.method === "POST") return {
			kind: "queue-submit",
			modelId: parsed.modelId,
			targetHost
		};
		return null;
	}
	if (targetHost === FAL_HOSTS.sync) {
		if (req.method === "POST" && parsed.modelId) return {
			kind: "sync-run",
			modelId: parsed.modelId,
			targetHost
		};
		return null;
	}
	return null;
}
/**
* General fal.ai handler. Routes by `x-fal-target-host` header (the convention
* used by `@fal-ai/client`'s server-side requestMiddleware workaround for the
* fact that `proxyUrl` is browser-only).
*
* Returns `"passthrough"` when the request does not look like a host-mirrored
* fal call, so the caller can fall back to the legacy `/fal/queue/...` and
* `/fal/run/...` audio routes.
*/
async function handleFal(req, res, body, pathname, fixtures, defaults, journal) {
	const targetHostHeader = req.headers["x-fal-target-host"];
	const targetHost = Array.isArray(targetHostHeader) ? targetHostHeader[0] : targetHostHeader;
	if (!targetHost) return "passthrough";
	const route = classifyRoute(req, pathname, targetHost);
	if (!route) return "passthrough";
	const testId = getTestId(req);
	const stateKey = (id) => `${testId}:${id}`;
	switch (route.kind) {
		case "queue-status": {
			const job = falQueueStates.get(stateKey(route.requestId));
			if (!job) {
				respondNotFound(req, res, pathname, journal, route.requestId);
				return "handled";
			}
			advanceJob(job);
			writeJson(req, res, 200, statusResponseBody(job), pathname, journal, { [FAL_REQUEST_ID_HEADER]: job.requestId });
			return "handled";
		}
		case "queue-result": {
			const job = falQueueStates.get(stateKey(route.requestId));
			if (!job) {
				respondNotFound(req, res, pathname, journal, route.requestId);
				return "handled";
			}
			advanceJob(job);
			const resultHeaders = { [FAL_REQUEST_ID_HEADER]: job.requestId };
			if (job.status !== "COMPLETED") {
				writeJson(req, res, 202, statusResponseBody(job), pathname, journal, resultHeaders);
				return "handled";
			}
			if (job.billableUnits != null) resultHeaders[FAL_BILLABLE_UNITS_HEADER] = String(job.billableUnits);
			writeJson(req, res, 200, job.result, pathname, journal, resultHeaders);
			return "handled";
		}
		case "queue-cancel": {
			const job = falQueueStates.get(stateKey(route.requestId));
			if (!job) {
				journal.add({
					method: req.method ?? "PUT",
					path: pathname,
					headers: flattenHeaders(req.headers),
					body: null,
					response: {
						status: 404,
						fixture: null
					}
				});
				res.writeHead(404, { "Content-Type": "application/json" });
				res.end(JSON.stringify({ status: "NOT_FOUND" }));
				return "handled";
			}
			if (job.status === "COMPLETED") {
				journal.add({
					method: req.method ?? "PUT",
					path: pathname,
					headers: flattenHeaders(req.headers),
					body: null,
					response: {
						status: 400,
						fixture: null
					}
				});
				res.writeHead(400, { "Content-Type": "application/json" });
				res.end(JSON.stringify({ status: "ALREADY_COMPLETED" }));
				return "handled";
			}
			if (job.status === "CANCELLED") {
				journal.add({
					method: req.method ?? "PUT",
					path: pathname,
					headers: flattenHeaders(req.headers),
					body: null,
					response: {
						status: 200,
						fixture: null
					}
				});
				res.writeHead(200, { "Content-Type": "application/json" });
				res.end(JSON.stringify({ status: "CANCELLED" }));
				return "handled";
			}
			job.status = "CANCELLED";
			job.logs.push({
				timestamp: (/* @__PURE__ */ new Date()).toISOString(),
				level: "INFO",
				message: "Job cancelled."
			});
			journal.add({
				method: req.method ?? "PUT",
				path: pathname,
				headers: flattenHeaders(req.headers),
				body: null,
				response: {
					status: 200,
					fixture: null
				}
			});
			res.writeHead(200, { "Content-Type": "application/json" });
			res.end(JSON.stringify({ status: "CANCELLED" }));
			return "handled";
		}
		case "storage": {
			let filename = "upload.bin";
			try {
				const parsed = body ? JSON.parse(body) : {};
				if (typeof parsed.filename === "string") filename = parsed.filename;
				if (typeof parsed.file_name === "string") filename = parsed.file_name;
			} catch {}
			const fileId = crypto.randomUUID();
			writeJson(req, res, 200, {
				upload_url: `https://${route.targetHost}/storage/upload/${fileId}`,
				file_url: `https://${route.targetHost}/files/${fileId}/${filename}`
			}, pathname, journal);
			return "handled";
		}
		case "queue-submit":
		case "sync-run": {
			const modelId = route.modelId;
			let parsedBody;
			try {
				parsedBody = parseBody(body);
			} catch (err) {
				const detail = err instanceof Error ? err.message : "Invalid JSON body";
				journal.add({
					method: req.method ?? "POST",
					path: pathname,
					headers: flattenHeaders(req.headers),
					body: null,
					response: {
						status: 400,
						fixture: null
					}
				});
				res.writeHead(400, { "Content-Type": "application/json" });
				res.end(JSON.stringify({ error: {
					message: detail,
					type: "invalid_request_error",
					code: "invalid_json"
				} }));
				return "handled";
			}
			const syntheticReq = {
				model: modelId,
				messages: [{
					role: "user",
					content: extractPromptFromBody(parsedBody) || JSON.stringify(parsedBody ?? {})
				}],
				_endpointType: "fal",
				_context: getContext(req)
			};
			const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(fixtures, syntheticReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
			if (!fixture) {
				if (resolveStrictMode(defaults.strict, req.headers)) {
					const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);
					defaults.logger.error(strictNoMatchLogLine(req.method ?? "POST", pathname, skippedBySequenceOrTurn));
					journal.add({
						method: req.method ?? "POST",
						path: pathname,
						headers: flattenHeaders(req.headers),
						body: syntheticReq,
						response: {
							status: 503,
							fixture: null,
							...strictOverrideField(defaults.strict, req.headers)
						}
					});
					res.writeHead(503, { "Content-Type": "application/json" });
					res.end(JSON.stringify({ error: {
						message: strictMessage,
						type: "invalid_request_error",
						code: "no_fixture_match"
					} }));
					return "handled";
				}
				if (defaults.record) {
					const effectiveDefaults = withFalUpstream(defaults, route.targetHost);
					if (route.kind === "queue-submit") {
						if (await proxyAndRecordFalQueueSubmit({
							req,
							res,
							syntheticReq,
							modelId,
							pathname,
							strippedPath: stripFalPrefix(pathname),
							body,
							fixtures,
							defaults: effectiveDefaults,
							stateKey,
							journal
						}) === "handled") return "handled";
					} else {
						const outcome = await proxyAndRecord(req, res, syntheticReq, "fal", stripFalPrefix(pathname), fixtures, effectiveDefaults, body);
						if (outcome === "handled_by_hook") return "handled";
						if (outcome !== "not_configured") {
							journal.add({
								method: req.method ?? "POST",
								path: pathname,
								headers: flattenHeaders(req.headers),
								body: syntheticReq,
								response: {
									status: res.statusCode ?? 200,
									fixture: null,
									source: "proxy"
								}
							});
							return "handled";
						}
					}
				}
				journal.add({
					method: req.method ?? "POST",
					path: pathname,
					headers: flattenHeaders(req.headers),
					body: syntheticReq,
					response: {
						status: 404,
						fixture: null,
						...strictOverrideField(defaults.strict, req.headers)
					}
				});
				res.writeHead(404, { "Content-Type": "application/json" });
				res.end(JSON.stringify({ error: {
					message: "No fixture matched",
					type: "invalid_request_error",
					code: "no_fixture_match"
				} }));
				return "handled";
			}
			journal.incrementFixtureMatchCount(fixture, fixtures, testId);
			const response = await resolveResponse(fixture, syntheticReq);
			if (isErrorResponse(response)) {
				const status = response.status ?? 500;
				journal.add({
					method: req.method ?? "POST",
					path: pathname,
					headers: flattenHeaders(req.headers),
					body: syntheticReq,
					response: {
						status,
						fixture
					}
				});
				writeErrorResponse(res, status, serializeErrorResponse(response), { retryAfter: response.retryAfter });
				return "handled";
			}
			let payload;
			if (isJSONResponse(response)) payload = response.json;
			else if (isAudioResponse(response)) payload = audioToFalFile(response);
			else {
				journal.add({
					method: req.method ?? "POST",
					path: pathname,
					headers: flattenHeaders(req.headers),
					body: syntheticReq,
					response: {
						status: 500,
						fixture
					}
				});
				res.writeHead(500, { "Content-Type": "application/json" });
				res.end(JSON.stringify({ error: {
					message: "Fixture response is not JSON or audio for fal endpoint",
					type: "server_error"
				} }));
				return "handled";
			}
			if (route.kind === "sync-run") {
				journal.add({
					method: req.method ?? "POST",
					path: pathname,
					headers: flattenHeaders(req.headers),
					body: syntheticReq,
					response: {
						status: 200,
						fixture
					}
				});
				res.writeHead(200, { "Content-Type": "application/json" });
				res.end(JSON.stringify(payload));
				return "handled";
			}
			const requestId = crypto.randomUUID();
			const progression = resolveProgression(defaults.falQueue);
			const now = Date.now();
			const initialStatus = progression.pollsBeforeCompleted === 0 ? "COMPLETED" : "IN_QUEUE";
			const job = {
				requestId,
				modelId,
				status: initialStatus,
				result: payload,
				billableUnits: extractBillableUnits(response),
				pollCount: 0,
				pollsBeforeInProgress: progression.pollsBeforeInProgress,
				pollsBeforeCompleted: progression.pollsBeforeCompleted,
				submittedAt: now,
				completedAt: initialStatus === "COMPLETED" ? now : null,
				logs: [{
					timestamp: new Date(now).toISOString(),
					level: "INFO",
					message: "Job enqueued."
				}],
				createdAt: now
			};
			falQueueStates.set(stateKey(requestId), job);
			const envelope = {
				request_id: requestId,
				response_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}`,
				status_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}/status`,
				cancel_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}/cancel`,
				queue_position: queuePosition(job)
			};
			journal.add({
				method: req.method ?? "POST",
				path: pathname,
				headers: flattenHeaders(req.headers),
				body: syntheticReq,
				response: {
					status: 200,
					fixture
				}
			});
			res.writeHead(200, { "Content-Type": "application/json" });
			res.end(JSON.stringify(envelope));
			return "handled";
		}
	}
}
function parseBody(raw) {
	if (!raw.trim()) return null;
	try {
		return JSON.parse(raw);
	} catch (err) {
		const detail = err instanceof Error ? err.message : "unknown";
		throw new Error(`Malformed JSON: ${detail}`);
	}
}
const DEFAULT_FAL_POLL_INTERVAL_MS = 1e3;
const DEFAULT_FAL_TIMEOUT_MS = 9e5;
const DEFAULT_FAL_FETCH_TIMEOUT_MS = 3e4;
/**
* Walk a fal-shaped queue protocol upstream: POST submit, poll status until
* COMPLETED, GET final result. Returns the parsed final body and the result
* response's billed quantity so the caller can persist the fixture and seed
* local queue state.
*
* Decoupled from the route layer so the legacy `/fal/queue/submit/{model}`
* audio path (`fal-audio.ts`) can reuse the same logic.
*/
async function walkFalQueue(args) {
	const { upstreamBase, submitPath, body, headers, pollIntervalMs = DEFAULT_FAL_POLL_INTERVAL_MS, timeoutMs = DEFAULT_FAL_TIMEOUT_MS, upstreamTimeoutMs, fallbackStatusPath, fallbackResultPath, logger, builtinKey } = args;
	applyProviderAuth(headers, resolveUpstreamUrl(upstreamBase, submitPath), "fal", builtinKey);
	const deadline = Date.now() + timeoutMs;
	const perFetchTimeoutMs = clampTimeout(upstreamTimeoutMs, DEFAULT_FAL_FETCH_TIMEOUT_MS);
	const fetchSignal = () => AbortSignal.timeout(Math.max(1, Math.min(perFetchTimeoutMs, deadline - Date.now())));
	const submitUrl = resolveUpstreamUrl(upstreamBase, submitPath);
	const submitRes = await fetch(submitUrl, {
		method: "POST",
		headers,
		body,
		signal: fetchSignal()
	});
	const submitText = await submitRes.text();
	if (!submitRes.ok) throw new Error(`Submit ${submitRes.status}: ${submitText.slice(0, 200)}`);
	const submitJson = parseJsonOrThrow(submitText, "Submit");
	if (submitJson === null || typeof submitJson !== "object" || Array.isArray(submitJson)) throw new Error("Submit response is not a JSON object");
	const env = submitJson;
	const upstreamRequestId = String(env.request_id ?? "").trim();
	if (!upstreamRequestId) throw new Error("Submit response missing request_id");
	const upstreamOrigin = submitUrl.origin;
	const adoptSameOrigin = (value, label, fallbackPath) => {
		if (typeof value === "string" && value) try {
			const parsed = new URL(value);
			if (parsed.origin === upstreamOrigin) return parsed;
			logger?.warn(`Upstream ${label} origin ${parsed.origin} differs from the upstream origin ${upstreamOrigin} — using the constructed canonical path instead`);
		} catch {
			logger?.warn(`Upstream ${label} is not a valid URL (${value.slice(0, 100)}) — using the constructed canonical path instead`);
		}
		return resolveUpstreamUrl(upstreamBase, fallbackPath);
	};
	const statusUrl = adoptSameOrigin(env.status_url, "status_url", fallbackStatusPath(upstreamRequestId));
	const resultUrl = adoptSameOrigin(env.response_url, "response_url", fallbackResultPath(upstreamRequestId));
	while (true) {
		if (Date.now() > deadline) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);
		const statusRes = await fetch(statusUrl, {
			headers,
			signal: fetchSignal()
		});
		const statusText = await statusRes.text();
		if (!statusRes.ok) throw new Error(`Status ${statusRes.status}: ${statusText.slice(0, 200)}`);
		const statusParsed = parseJsonOrThrow(statusText, "Status");
		if (statusParsed === null || typeof statusParsed !== "object" || Array.isArray(statusParsed)) throw new Error("Status response is not a JSON object");
		const statusJson = statusParsed;
		const s = String(statusJson.status ?? "");
		if (s === "COMPLETED") break;
		if (s === "FAILED" || s === "ERROR" || s === "CANCELLED") throw new Error(`Upstream job terminated with status ${s}`);
		const remaining = deadline - Date.now();
		if (remaining <= 0) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);
		const sleep = Math.min(pollIntervalMs, remaining);
		await new Promise((r) => setTimeout(r, Math.max(0, sleep)));
	}
	const resultRes = await fetch(resultUrl, {
		headers,
		signal: fetchSignal()
	});
	const resultText = await resultRes.text();
	if (!resultRes.ok) throw new Error(`Result ${resultRes.status}: ${resultText.slice(0, 200)}`);
	return {
		body: parseJsonOrThrow(resultText, "Result"),
		billableUnits: parseBillableUnitsHeader(resultRes.headers.get(FAL_BILLABLE_UNITS_HEADER))
	};
}
/**
* Parse the upstream `x-fal-billable-units` header into a finite number, or
* `null` when absent/unparseable — mirrors the consumer-side parse so a
* recorded fixture's captured units match what the live adapter would have read.
*/
function parseBillableUnitsHeader(raw) {
	if (raw == null) return null;
	const value = Number(raw.trim());
	return Number.isFinite(value) ? value : null;
}
async function proxyAndRecordFalQueueSubmit(args) {
	const { req, res, syntheticReq, modelId, pathname, strippedPath, body, fixtures, defaults, stateKey, journal } = args;
	const record = defaults.record;
	if (!record) return "no_upstream";
	const upstreamBase = record.providers.fal;
	if (!upstreamBase) {
		defaults.logger.warn(`No upstream URL configured for provider "fal" — cannot proxy`);
		return "no_upstream";
	}
	defaults.logger.warn(`NO FIXTURE MATCH — walking fal queue at ${upstreamBase}${strippedPath}`);
	let finalBody;
	let billableUnits;
	try {
		const walk = await walkFalQueue({
			upstreamBase,
			submitPath: strippedPath,
			body,
			headers: buildForwardHeaders(req),
			builtinKey: record.providerKeys?.fal,
			pollIntervalMs: record.fal?.pollIntervalMs,
			timeoutMs: record.fal?.timeoutMs,
			upstreamTimeoutMs: record.upstreamTimeoutMs,
			fallbackStatusPath: (id) => `${modelId}/requests/${id}/status`,
			fallbackResultPath: (id) => `${modelId}/requests/${id}`,
			logger: defaults.logger
		});
		finalBody = walk.body;
		billableUnits = walk.billableUnits;
	} catch (err) {
		const msg = err instanceof Error ? err.message : "Unknown queue-walk error";
		defaults.logger.error(`fal queue-walk proxy failed: ${msg}`);
		if (res.destroyed || res.writableEnded) return "handled";
		journal.add({
			method: req.method ?? "POST",
			path: pathname,
			headers: flattenHeaders(req.headers),
			body: syntheticReq,
			response: {
				status: 502,
				fixture: null,
				source: "proxy"
			}
		});
		res.writeHead(502, { "Content-Type": "application/json" });
		res.end(JSON.stringify({ error: {
			message: `Proxy to upstream failed: ${msg}`,
			type: "proxy_error"
		} }));
		return "handled";
	}
	const fixture = {
		match: buildFixtureMatch(defaults.requestTransform ? defaults.requestTransform(syntheticReq) : syntheticReq, record),
		response: {
			json: finalBody,
			status: 200,
			...billableUnits != null ? { billableUnits } : {}
		}
	};
	const persistResult = persistFixture({
		record,
		providerKey: "fal",
		testId: getTestId(req),
		fixture,
		fixtures,
		logger: defaults.logger
	});
	if (persistResult.kind === "failed" && !res.headersSent) res.setHeader("X-AIMock-Record-Error", sanitizeHeaderValue(persistResult.error));
	const newRequestId = crypto.randomUUID();
	const progression = resolveProgression(defaults.falQueue);
	const now = Date.now();
	const initialStatus = progression.pollsBeforeCompleted === 0 ? "COMPLETED" : "IN_QUEUE";
	const job = {
		requestId: newRequestId,
		modelId,
		status: initialStatus,
		result: finalBody,
		billableUnits,
		pollCount: 0,
		pollsBeforeInProgress: progression.pollsBeforeInProgress,
		pollsBeforeCompleted: progression.pollsBeforeCompleted,
		submittedAt: now,
		completedAt: initialStatus === "COMPLETED" ? now : null,
		logs: [{
			timestamp: new Date(now).toISOString(),
			level: "INFO",
			message: "Job enqueued."
		}],
		createdAt: now
	};
	falQueueStates.set(stateKey(newRequestId), job);
	const envelope = {
		request_id: newRequestId,
		response_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}`,
		status_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}/status`,
		cancel_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}/cancel`,
		queue_position: queuePosition(job)
	};
	if (res.destroyed || res.writableEnded) return "handled";
	journal.add({
		method: req.method ?? "POST",
		path: pathname,
		headers: flattenHeaders(req.headers),
		body: syntheticReq,
		response: {
			status: 200,
			fixture: null,
			source: "proxy"
		}
	});
	res.writeHead(200, { "Content-Type": "application/json" });
	res.end(JSON.stringify(envelope));
	return "handled";
}
function parseJsonOrThrow(text, label) {
	try {
		return JSON.parse(text);
	} catch {
		throw new Error(`${label} returned non-JSON: ${text.slice(0, 200)}`);
	}
}
function withFalUpstream(defaults, targetHost) {
	if (!defaults.record) return defaults;
	if (defaults.record.providers.fal) return defaults;
	return {
		...defaults,
		record: {
			...defaults.record,
			providers: {
				...defaults.record.providers,
				fal: `https://${targetHost}`
			}
		}
	};
}
function writeJson(req, res, status, payload, pathname, journal, extraHeaders) {
	journal.add({
		method: req.method ?? "GET",
		path: pathname,
		headers: flattenHeaders(req.headers),
		body: null,
		response: {
			status,
			fixture: null
		}
	});
	res.writeHead(status, {
		"Content-Type": "application/json",
		...extraHeaders
	});
	res.end(JSON.stringify(payload));
}
function respondNotFound(req, res, pathname, journal, requestId) {
	journal.add({
		method: req.method ?? "GET",
		path: pathname,
		headers: flattenHeaders(req.headers),
		body: null,
		response: {
			status: 404,
			fixture: null
		}
	});
	res.writeHead(404, { "Content-Type": "application/json" });
	res.end(JSON.stringify({ error: {
		message: `Request ${requestId} not found`,
		type: "not_found"
	} }));
}

//#endregion
export { FalQueueStateMap, falQueueStates, handleFal, imageResponseToFalJson, resolveProgression, videoResponseToFalJson, walkFalQueue };
//# sourceMappingURL=fal.js.map