import { extractOverrides, flattenHeaders, generateToolCallId, getContext, getTestId, isContentWithToolCallsResponse, isErrorResponse, isTextResponse, isToolCallResponse, resolveFixtureBlocks, resolveResponse, resolveStrictMode, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
import { matchFixtureDiagnostic } from "./router.js";
import { calculateDelay, delay, writeErrorResponse } from "./sse-writer.js";
import { createInterruptionSignal } from "./interruption.js";
import { applyChaos } from "./chaos.js";
import { proxyAndRecord } from "./recorder.js";

//#region src/gemini-interactions.ts
/** Step types whose payload is a tool/agent result keyed by call_id. */
const STEP_RESULT_TYPES = new Set([
	"function_result",
	"code_execution_result",
	"url_context_result",
	"google_search_result",
	"google_maps_result",
	"mcp_server_tool_result",
	"file_search_result"
]);
/** All recognized top-level Step types (used as the Step[] discriminator). */
const STEP_TYPES = new Set([
	"user_input",
	"model_output",
	...STEP_RESULT_TYPES
]);
function geminiInteractionsToCompletionRequest(req) {
	const messages = [];
	const model = req.model ?? "gemini-2.5-flash";
	if (req.system_instruction) messages.push({
		role: "system",
		content: req.system_instruction
	});
	if (req.input !== void 0) {
		if (typeof req.input === "string") messages.push({
			role: "user",
			content: req.input
		});
		else if (Array.isArray(req.input)) {
			const firstItem = req.input[0];
			const isStepArray = !!firstItem && !("role" in firstItem) && typeof firstItem.type === "string" && STEP_TYPES.has(firstItem.type);
			if (firstItem && "role" in firstItem) for (const turn of req.input) {
				const role = turn.role === "model" ? "assistant" : turn.role;
				const blocks = turn.content ?? turn.parts;
				if (!blocks || blocks.length === 0) {
					if (role === "user" || role === "assistant") messages.push({
						role,
						content: ""
					});
					continue;
				}
				const funcCallParts = blocks.filter((p) => p.type === "function_call");
				const funcResultParts = blocks.filter((p) => p.type === "function_result");
				const textParts = blocks.filter((p) => p.type === "text");
				if (funcCallParts.length > 0) {
					const textContent = textParts.map((p) => p.text ?? "").join("");
					messages.push({
						role: "assistant",
						content: textContent || null,
						tool_calls: funcCallParts.map((p) => ({
							id: p.id ?? p.call_id ?? generateToolCallId(),
							type: "function",
							function: {
								name: p.name ?? "",
								arguments: JSON.stringify(p.arguments ?? {})
							}
						}))
					});
				} else if (funcResultParts.length > 0) {
					for (const part of funcResultParts) {
						const resultValue = part.result ?? part.output;
						messages.push({
							role: "tool",
							content: typeof resultValue === "string" ? resultValue : JSON.stringify(resultValue ?? ""),
							tool_call_id: part.call_id ?? part.id ?? ""
						});
					}
					if (textParts.length > 0) {
						const text = textParts.map((p) => p.text ?? "").join("");
						if (text) messages.push({
							role: "user",
							content: text
						});
					}
				} else {
					const text = textParts.map((p) => p.text ?? "").join("");
					if (role === "user" || role === "assistant" || role === "system") messages.push({
						role,
						content: text
					});
				}
			}
			else if (isStepArray) {
				for (const step of req.input) if (step.type === "user_input") {
					const text = (step.content ?? []).filter((p) => p.type === "text").map((p) => p.text ?? "").join("");
					messages.push({
						role: "user",
						content: text
					});
				} else if (step.type === "model_output") {
					const blocks = step.content ?? [];
					const funcCallParts = blocks.filter((p) => p.type === "function_call");
					const textContent = blocks.filter((p) => p.type === "text").map((p) => p.text ?? "").join("");
					if (funcCallParts.length > 0) messages.push({
						role: "assistant",
						content: textContent || null,
						tool_calls: funcCallParts.map((p) => ({
							id: p.id ?? p.call_id ?? generateToolCallId(),
							type: "function",
							function: {
								name: p.name ?? "",
								arguments: JSON.stringify(p.arguments ?? {})
							}
						}))
					});
					else messages.push({
						role: "assistant",
						content: textContent
					});
				} else if (STEP_RESULT_TYPES.has(step.type)) {
					const resultValue = step.result ?? step.output;
					messages.push({
						role: "tool",
						content: typeof resultValue === "string" ? resultValue : JSON.stringify(resultValue ?? ""),
						tool_call_id: step.call_id ?? step.id ?? ""
					});
				}
			} else {
				const text = req.input.filter((p) => p.type === "text").map((p) => p.text ?? "").join("");
				messages.push({
					role: "user",
					content: text || ""
				});
			}
		}
	}
	let tools;
	if (req.tools && req.tools.length > 0) {
		const funcTools = req.tools.filter((t) => t.type === "function");
		if (funcTools.length > 0) tools = funcTools.map((t) => ({
			type: "function",
			function: {
				name: t.name,
				description: t.description,
				parameters: t.parameters
			}
		}));
	}
	return {
		model,
		messages,
		stream: req.stream !== false,
		temperature: req.generation_config?.temperature,
		max_tokens: req.generation_config?.max_output_tokens,
		tools
	};
}
let interactionCounter = 0;
function resetInteractionCounter() {
	interactionCounter = 0;
}
function nextInteractionId() {
	return `aimock-int-${interactionCounter++}`;
}
function interactionsUsage(overrides) {
	if (!overrides?.usage) return {
		total_input_tokens: 0,
		total_output_tokens: 0,
		total_tokens: 0
	};
	const input = overrides.usage.input_tokens ?? overrides.usage.prompt_tokens ?? overrides.usage.promptTokenCount ?? 0;
	const output = overrides.usage.output_tokens ?? overrides.usage.completion_tokens ?? overrides.usage.candidatesTokenCount ?? 0;
	return {
		total_input_tokens: input,
		total_output_tokens: output,
		total_tokens: overrides.usage.total_tokens ?? overrides.usage.totalTokenCount ?? input + output
	};
}
function buildInteractionsTextResponse(content, model, interactionId, overrides) {
	return {
		id: interactionId,
		status: "completed",
		model: overrides?.model ?? model,
		role: "model",
		output_text: content,
		steps: [{
			type: "model_output",
			content: [{
				type: "text",
				text: content
			}]
		}],
		usage: interactionsUsage(overrides)
	};
}
function buildFunctionCallStep(tc, logger) {
	let argsObj;
	try {
		argsObj = JSON.parse(tc.arguments || "{}");
	} catch {
		logger.warn(`Malformed JSON in fixture tool call arguments for "${tc.name}": ${tc.arguments}`);
		argsObj = {};
	}
	return {
		type: "function_call",
		id: tc.id || generateToolCallId(),
		name: tc.name,
		arguments: argsObj
	};
}
function buildInteractionsToolCallResponse(toolCalls, model, interactionId, logger, overrides) {
	return {
		id: interactionId,
		status: "requires_action",
		model: overrides?.model ?? model,
		role: "model",
		steps: toolCalls.map((tc) => buildFunctionCallStep(tc, logger)),
		usage: interactionsUsage(overrides)
	};
}
function buildInteractionsContentWithToolCallsResponse(content, toolCalls, model, interactionId, logger, overrides, blocks) {
	const steps = [];
	let outputText = "";
	if (blocks && blocks.length > 0) {
		const ordered = resolveFixtureBlocks(blocks);
		for (const block of ordered) if (block.type === "text") {
			steps.push({
				type: "model_output",
				content: [{
					type: "text",
					text: block.text
				}]
			});
			outputText += block.text;
		} else steps.push(buildFunctionCallStep({
			name: block.name,
			arguments: block.arguments,
			id: block.id
		}, logger));
	} else {
		steps.push({
			type: "model_output",
			content: [{
				type: "text",
				text: content
			}]
		});
		outputText = content;
		for (const tc of toolCalls) steps.push(buildFunctionCallStep(tc, logger));
	}
	return {
		id: interactionId,
		status: "requires_action",
		model: overrides?.model ?? model,
		role: "model",
		output_text: outputText,
		steps,
		usage: interactionsUsage(overrides)
	};
}
function buildInteractionsErrorResponse(message, code) {
	return { error: {
		code: code ?? "INVALID_ARGUMENT",
		message
	} };
}
let eventIdCounter = 0;
function resetEventIdCounter() {
	eventIdCounter = 0;
}
function nextEventId() {
	return `evt_${++eventIdCounter}`;
}
function buildInteractionsTextSSEEvents(content, interactionId, chunkSize, overrides) {
	const events = [];
	events.push({
		event_type: "interaction.created",
		interaction: {
			id: interactionId,
			status: "in_progress"
		},
		event_id: nextEventId()
	});
	events.push({
		event_type: "step.start",
		index: 0,
		step: { type: "model_output" },
		event_id: nextEventId()
	});
	if (content.length === 0) events.push({
		event_type: "step.delta",
		index: 0,
		delta: {
			type: "text",
			text: ""
		},
		event_id: nextEventId()
	});
	else for (let i = 0; i < content.length; i += chunkSize) {
		const slice = content.slice(i, i + chunkSize);
		events.push({
			event_type: "step.delta",
			index: 0,
			delta: {
				type: "text",
				text: slice
			},
			event_id: nextEventId()
		});
	}
	events.push({
		event_type: "step.stop",
		index: 0,
		event_id: nextEventId()
	});
	events.push({
		event_type: "interaction.completed",
		interaction: {
			id: interactionId,
			status: "completed",
			usage: interactionsUsage(overrides)
		},
		event_id: nextEventId()
	});
	return events;
}
function buildInteractionsToolCallSSEEvents(toolCalls, interactionId, logger, overrides) {
	const events = [];
	events.push({
		event_type: "interaction.created",
		interaction: {
			id: interactionId,
			status: "in_progress"
		},
		event_id: nextEventId()
	});
	for (let idx = 0; idx < toolCalls.length; idx++) {
		const tc = toolCalls[idx];
		let argsObj;
		try {
			argsObj = JSON.parse(tc.arguments || "{}");
		} catch {
			logger.warn(`Malformed JSON in fixture tool call arguments for "${tc.name}": ${tc.arguments}`);
			argsObj = {};
		}
		events.push({
			event_type: "step.start",
			index: idx,
			step: {
				type: "function_call",
				id: tc.id || generateToolCallId(),
				name: tc.name,
				arguments: {}
			},
			event_id: nextEventId()
		});
		events.push({
			event_type: "step.delta",
			index: idx,
			delta: {
				type: "arguments_delta",
				arguments: JSON.stringify(argsObj)
			},
			event_id: nextEventId()
		});
		events.push({
			event_type: "step.stop",
			index: idx,
			event_id: nextEventId()
		});
	}
	events.push({
		event_type: "interaction.completed",
		interaction: {
			id: interactionId,
			status: "requires_action",
			usage: interactionsUsage(overrides)
		},
		event_id: nextEventId()
	});
	return events;
}
function pushTextStepEvents(events, index, content, chunkSize) {
	events.push({
		event_type: "step.start",
		index,
		step: { type: "model_output" },
		event_id: nextEventId()
	});
	if (content.length === 0) events.push({
		event_type: "step.delta",
		index,
		delta: {
			type: "text",
			text: ""
		},
		event_id: nextEventId()
	});
	else for (let i = 0; i < content.length; i += chunkSize) {
		const slice = content.slice(i, i + chunkSize);
		events.push({
			event_type: "step.delta",
			index,
			delta: {
				type: "text",
				text: slice
			},
			event_id: nextEventId()
		});
	}
	events.push({
		event_type: "step.stop",
		index,
		event_id: nextEventId()
	});
}
function pushFunctionCallStepEvents(events, index, tc, logger) {
	let argsObj;
	try {
		argsObj = JSON.parse(tc.arguments || "{}");
	} catch {
		logger.warn(`Malformed JSON in fixture tool call arguments for "${tc.name}": ${tc.arguments}`);
		argsObj = {};
	}
	events.push({
		event_type: "step.start",
		index,
		step: {
			type: "function_call",
			id: tc.id || generateToolCallId(),
			name: tc.name,
			arguments: {}
		},
		event_id: nextEventId()
	});
	events.push({
		event_type: "step.delta",
		index,
		delta: {
			type: "arguments_delta",
			arguments: JSON.stringify(argsObj)
		},
		event_id: nextEventId()
	});
	events.push({
		event_type: "step.stop",
		index,
		event_id: nextEventId()
	});
}
function buildInteractionsContentWithToolCallsSSEEvents(content, toolCalls, interactionId, chunkSize, logger, overrides, blocks) {
	const events = [];
	events.push({
		event_type: "interaction.created",
		interaction: {
			id: interactionId,
			status: "in_progress"
		},
		event_id: nextEventId()
	});
	if (blocks && blocks.length > 0) {
		const ordered = resolveFixtureBlocks(blocks);
		let idx = 0;
		for (const block of ordered) {
			if (block.type === "text") pushTextStepEvents(events, idx, block.text, chunkSize);
			else pushFunctionCallStepEvents(events, idx, {
				name: block.name,
				arguments: block.arguments,
				id: block.id
			}, logger);
			idx += 1;
		}
	} else {
		pushTextStepEvents(events, 0, content, chunkSize);
		for (let i = 0; i < toolCalls.length; i++) pushFunctionCallStepEvents(events, i + 1, toolCalls[i], logger);
	}
	events.push({
		event_type: "interaction.completed",
		interaction: {
			id: interactionId,
			status: "requires_action",
			usage: interactionsUsage(overrides)
		},
		event_id: nextEventId()
	});
	return events;
}
async function writeGeminiInteractionsSSEStream(res, events, optionsOrLatency) {
	const opts = typeof optionsOrLatency === "number" ? { latency: optionsOrLatency } : optionsOrLatency ?? {};
	const latency = opts.latency ?? 0;
	const profile = opts.streamingProfile;
	const { recordedTimings, replaySpeed } = opts;
	const signal = opts.signal;
	const onChunkSent = opts.onChunkSent;
	if (res.writableEnded) return true;
	res.setHeader("Content-Type", "text/event-stream");
	res.setHeader("Cache-Control", "no-cache");
	res.setHeader("Connection", "keep-alive");
	let chunkIndex = 0;
	for (const event of events) {
		const chunkDelay = calculateDelay(chunkIndex, profile, latency, recordedTimings, replaySpeed);
		if (chunkDelay > 0) await delay(chunkDelay, signal);
		if (signal?.aborted) return false;
		if (res.writableEnded) return true;
		res.write(`data: ${JSON.stringify(event)}\n\n`);
		if (event.event_type === "step.delta") {
			onChunkSent?.();
			chunkIndex++;
		}
		if (signal?.aborted) return false;
	}
	if (!res.writableEnded) res.end();
	return true;
}
async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaults, setCorsHeaders) {
	const { logger } = defaults;
	setCorsHeaders(res);
	const urlPath = req.url ?? "/v1beta/interactions";
	let interactionsReq;
	try {
		interactionsReq = JSON.parse(raw);
	} catch (parseErr) {
		const detail = parseErr instanceof Error ? parseErr.message : "unknown";
		journal.add({
			method: req.method ?? "POST",
			path: urlPath,
			headers: flattenHeaders(req.headers),
			body: null,
			response: {
				status: 400,
				fixture: null
			}
		});
		writeErrorResponse(res, 400, JSON.stringify(buildInteractionsErrorResponse(`Malformed JSON body: ${detail}`, "INVALID_ARGUMENT")));
		return;
	}
	const completionReq = geminiInteractionsToCompletionRequest(interactionsReq);
	completionReq._endpointType = "chat";
	completionReq._context = getContext(req);
	const streaming = interactionsReq.stream !== false;
	const model = completionReq.model;
	const testId = getTestId(req);
	const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(fixtures, completionReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
	if (fixture) journal.incrementFixtureMatchCount(fixture, fixtures, testId);
	if (applyChaos(res, fixture, defaults.chaos, req.headers, journal, {
		method: req.method ?? "POST",
		path: urlPath,
		headers: flattenHeaders(req.headers),
		body: completionReq
	}, fixture ? "fixture" : "proxy", defaults.registry, defaults.logger)) return;
	if (!fixture) {
		if (resolveStrictMode(defaults.strict, req.headers)) {
			const strictStatus = 503;
			const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);
			logger.error(strictNoMatchLogLine(req.method ?? "POST", urlPath, skippedBySequenceOrTurn));
			journal.add({
				method: req.method ?? "POST",
				path: urlPath,
				headers: flattenHeaders(req.headers),
				body: completionReq,
				response: {
					status: strictStatus,
					fixture: null,
					...strictOverrideField(defaults.strict, req.headers)
				}
			});
			writeErrorResponse(res, strictStatus, JSON.stringify(buildInteractionsErrorResponse(strictMessage, "UNAVAILABLE")));
			return;
		}
		if (defaults.record) {
			const outcome = await proxyAndRecord(req, res, completionReq, "gemini-interactions", urlPath, fixtures, defaults, raw);
			if (outcome === "handled_by_hook") return;
			if (outcome !== "not_configured") {
				journal.add({
					method: req.method ?? "POST",
					path: urlPath,
					headers: flattenHeaders(req.headers),
					body: completionReq,
					response: {
						status: res.statusCode ?? 200,
						fixture: null,
						source: "proxy"
					}
				});
				return;
			}
		}
		journal.add({
			method: req.method ?? "POST",
			path: urlPath,
			headers: flattenHeaders(req.headers),
			body: completionReq,
			response: {
				status: 404,
				fixture: null,
				...strictOverrideField(defaults.strict, req.headers)
			}
		});
		writeErrorResponse(res, 404, JSON.stringify(buildInteractionsErrorResponse("No fixture matched", "NOT_FOUND")));
		return;
	}
	const response = await resolveResponse(fixture, completionReq);
	const latency = fixture.latency ?? defaults.latency;
	const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);
	const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
	if (isErrorResponse(response)) {
		const status = response.status ?? 500;
		journal.add({
			method: req.method ?? "POST",
			path: urlPath,
			headers: flattenHeaders(req.headers),
			body: completionReq,
			response: {
				status,
				fixture
			}
		});
		writeErrorResponse(res, status, JSON.stringify(buildInteractionsErrorResponse(response.error.message, response.error.type ?? "ERROR")), { retryAfter: response.retryAfter });
		return;
	}
	const interactionId = nextInteractionId();
	if (isContentWithToolCallsResponse(response)) {
		if (response.webSearches?.length) logger.warn("webSearches in fixture response are not supported for Gemini Interactions API — ignoring");
		const overrides = extractOverrides(response);
		const journalEntry = journal.add({
			method: req.method ?? "POST",
			path: urlPath,
			headers: flattenHeaders(req.headers),
			body: completionReq,
			response: {
				status: 200,
				fixture
			}
		});
		if (!streaming) {
			const body = buildInteractionsContentWithToolCallsResponse(response.content ?? "", response.toolCalls ?? [], model, interactionId, logger, overrides, response.blocks);
			res.writeHead(200, { "Content-Type": "application/json" });
			res.end(JSON.stringify(body));
		} else {
			const events = buildInteractionsContentWithToolCallsSSEEvents(response.content ?? "", response.toolCalls ?? [], interactionId, chunkSize, logger, overrides, response.blocks);
			const interruption = createInterruptionSignal(fixture);
			if (!await writeGeminiInteractionsSSEStream(res, events, {
				latency,
				streamingProfile: fixture.streamingProfile,
				recordedTimings: fixture.recordedTimings,
				replaySpeed,
				signal: interruption?.signal,
				onChunkSent: interruption?.tick
			})) {
				if (!res.writableEnded) res.destroy();
				journalEntry.response.interrupted = true;
				journalEntry.response.interruptReason = interruption?.reason();
			}
			interruption?.cleanup();
		}
		return;
	}
	if (isTextResponse(response)) {
		if (response.webSearches?.length) logger.warn("webSearches in fixture response are not supported for Gemini Interactions API — ignoring");
		const overrides = extractOverrides(response);
		const journalEntry = journal.add({
			method: req.method ?? "POST",
			path: urlPath,
			headers: flattenHeaders(req.headers),
			body: completionReq,
			response: {
				status: 200,
				fixture
			}
		});
		if (!streaming) {
			const body = buildInteractionsTextResponse(response.content, model, interactionId, overrides);
			res.writeHead(200, { "Content-Type": "application/json" });
			res.end(JSON.stringify(body));
		} else {
			const events = buildInteractionsTextSSEEvents(response.content, interactionId, chunkSize, overrides);
			const interruption = createInterruptionSignal(fixture);
			if (!await writeGeminiInteractionsSSEStream(res, events, {
				latency,
				streamingProfile: fixture.streamingProfile,
				recordedTimings: fixture.recordedTimings,
				replaySpeed,
				signal: interruption?.signal,
				onChunkSent: interruption?.tick
			})) {
				if (!res.writableEnded) res.destroy();
				journalEntry.response.interrupted = true;
				journalEntry.response.interruptReason = interruption?.reason();
			}
			interruption?.cleanup();
		}
		return;
	}
	if (isToolCallResponse(response)) {
		if (response.webSearches?.length) logger.warn("webSearches in fixture response are not supported for Gemini Interactions API — ignoring");
		const overrides = extractOverrides(response);
		const journalEntry = journal.add({
			method: req.method ?? "POST",
			path: urlPath,
			headers: flattenHeaders(req.headers),
			body: completionReq,
			response: {
				status: 200,
				fixture
			}
		});
		if (!streaming) {
			const body = buildInteractionsToolCallResponse(response.toolCalls, model, interactionId, logger, overrides);
			res.writeHead(200, { "Content-Type": "application/json" });
			res.end(JSON.stringify(body));
		} else {
			const events = buildInteractionsToolCallSSEEvents(response.toolCalls, interactionId, logger, overrides);
			const interruption = createInterruptionSignal(fixture);
			if (!await writeGeminiInteractionsSSEStream(res, events, {
				latency,
				streamingProfile: fixture.streamingProfile,
				recordedTimings: fixture.recordedTimings,
				replaySpeed,
				signal: interruption?.signal,
				onChunkSent: interruption?.tick
			})) {
				if (!res.writableEnded) res.destroy();
				journalEntry.response.interrupted = true;
				journalEntry.response.interruptReason = interruption?.reason();
			}
			interruption?.cleanup();
		}
		return;
	}
	journal.add({
		method: req.method ?? "POST",
		path: urlPath,
		headers: flattenHeaders(req.headers),
		body: completionReq,
		response: {
			status: 500,
			fixture
		}
	});
	writeErrorResponse(res, 500, JSON.stringify(buildInteractionsErrorResponse("Fixture response did not match any known type", "INTERNAL")));
}

//#endregion
export { geminiInteractionsToCompletionRequest, handleGeminiInteractions, resetEventIdCounter, resetInteractionCounter };
//# sourceMappingURL=gemini-interactions.js.map