import { DEFAULT_TEST_ID } from "./constants.js";
import { flattenHeaders, formatToMime, generateToolCallId, isAudioResponse, isContentWithToolCallsResponse, isErrorResponse, isTextResponse, isToolCallResponse, resolveFixtureBlocks, resolveResponse, resolveStrictMode, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
import "./journal.js";
import { matchFixtureDiagnostic } from "./router.js";
import { calculateDelay, delay } from "./sse-writer.js";
import { createInterruptionSignal } from "./interruption.js";

//#region src/ws-gemini-live.ts
const WS_PATH = "/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent";
/**
* Map HTTP status codes to gRPC error codes.
* Gemini Live uses gRPC codes, not HTTP status codes.
*/
function httpToGrpc(httpCode) {
	switch (httpCode) {
		case 400: return 3;
		case 401: return 16;
		case 403: return 7;
		case 404: return 5;
		case 409: return 10;
		case 429: return 8;
		case 501: return 12;
		case 503: return 14;
		default: return 13;
	}
}
/**
* Convert Gemini Live turns into ChatMessage[] for fixture matching.
*/
function geminiTurnsToMessages(turns, logger) {
	const messages = [];
	for (const turn of turns) {
		const role = turn.role ?? "user";
		if (role === "user") {
			const funcResponses = turn.parts.filter((p) => p.functionResponse);
			const textParts = turn.parts.filter((p) => p.text !== void 0 && !p.thought);
			if (funcResponses.length > 0) {
				for (let i = 0; i < funcResponses.length; i++) {
					const fr = funcResponses[i].functionResponse;
					messages.push({
						role: "tool",
						content: typeof fr.response === "string" ? fr.response : JSON.stringify(fr.response),
						tool_call_id: fr.id ?? generateToolCallId()
					});
				}
				if (textParts.length > 0) messages.push({
					role: "user",
					content: textParts.map((p) => p.text).join("")
				});
			} else {
				const text = textParts.map((p) => p.text).join("");
				messages.push({
					role: "user",
					content: text
				});
			}
		} else if (role === "model") {
			const funcCalls = turn.parts.filter((p) => p.functionCall);
			const textParts = turn.parts.filter((p) => p.text !== void 0 && !p.thought);
			if (funcCalls.length > 0) {
				const text = textParts.map((p) => p.text).join("");
				messages.push({
					role: "assistant",
					content: text || null,
					tool_calls: funcCalls.map((p) => ({
						id: generateToolCallId(),
						type: "function",
						function: {
							name: p.functionCall.name,
							arguments: JSON.stringify(p.functionCall.args ?? {})
						}
					}))
				});
			} else {
				const text = textParts.map((p) => p.text).join("");
				messages.push({
					role: "assistant",
					content: text
				});
			}
		} else logger?.warn(`[gemini-live] skipping turn with unrecognized role: ${role}`);
	}
	return messages;
}
/**
* Convert toolResponse messages into ChatMessage[] for fixture matching.
*/
function toolResponseToMessages(toolResponse) {
	return toolResponse.functionResponses.map((fr) => ({
		role: "tool",
		content: typeof fr.response === "string" ? fr.response : JSON.stringify(fr.response),
		tool_call_id: fr.id ?? generateToolCallId()
	}));
}
/**
* Convert Gemini tool definitions to ChatCompletion ToolDefinition[].
*/
function convertTools(geminiTools) {
	if (!geminiTools || geminiTools.length === 0) return [];
	return geminiTools.flatMap((t) => t.functionDeclarations ?? []).map((d) => ({
		type: "function",
		function: {
			name: d.name,
			description: d.description,
			parameters: d.parameters
		}
	}));
}
function handleWebSocketGeminiLive(ws, fixtures, journal, defaults) {
	const { logger } = defaults;
	const session = {
		setupDone: false,
		model: defaults.model,
		tools: [],
		conversationHistory: []
	};
	let pending = Promise.resolve();
	ws.on("message", (raw) => {
		pending = pending.then(() => processMessage(raw, ws, fixtures, journal, defaults, session).catch((err) => {
			const msg = err instanceof Error ? err.message : "Internal error";
			logger.error(`WebSocket Gemini Live error: ${msg}`);
			try {
				ws.send(JSON.stringify({ error: {
					code: 13,
					message: msg,
					status: "INTERNAL"
				} }));
			} catch (sendErr) {
				defaults.logger.debug(`Failed to send error to client: ${sendErr instanceof Error ? sendErr.message : "unknown"}`);
			}
		}));
	});
}
async function processMessage(raw, ws, fixtures, journal, defaults, session) {
	let parsed;
	try {
		parsed = JSON.parse(raw);
	} catch (parseErr) {
		const detail = parseErr instanceof Error ? parseErr.message : "unknown";
		ws.send(JSON.stringify({ error: {
			code: 3,
			message: `Malformed JSON: ${detail}`,
			status: "INVALID_ARGUMENT"
		} }));
		return;
	}
	const setupMsg = parsed.setup ?? parsed.config;
	if (setupMsg) {
		session.setupDone = true;
		session.model = setupMsg.model ?? defaults.model;
		session.tools = convertTools(setupMsg.tools);
		ws.send(JSON.stringify({ setupComplete: {} }));
		return;
	}
	if (!session.setupDone) {
		ws.send(JSON.stringify({ error: {
			code: 9,
			message: "Setup required",
			status: "FAILED_PRECONDITION"
		} }));
		return;
	}
	let newMessages;
	if (parsed.clientContent) {
		if (!parsed.clientContent.turns || !Array.isArray(parsed.clientContent.turns)) {
			ws.send(JSON.stringify({ error: {
				code: 3,
				message: "Missing 'turns' in clientContent",
				status: "INVALID_ARGUMENT"
			} }));
			return;
		}
		newMessages = geminiTurnsToMessages(parsed.clientContent.turns, defaults.logger);
	} else if (parsed.toolResponse) {
		if (!parsed.toolResponse.functionResponses || !Array.isArray(parsed.toolResponse.functionResponses)) {
			ws.send(JSON.stringify({ error: {
				code: 3,
				message: "Missing 'functionResponses' in toolResponse",
				status: "INVALID_ARGUMENT"
			} }));
			return;
		}
		newMessages = toolResponseToMessages(parsed.toolResponse);
	} else {
		ws.send(JSON.stringify({ error: {
			code: 3,
			message: "Expected clientContent or toolResponse",
			status: "INVALID_ARGUMENT"
		} }));
		return;
	}
	const geminiContextHeader = defaults.upgradeHeaders?.["x-aimock-context"];
	const geminiContext = typeof geminiContextHeader === "string" ? geminiContextHeader : Array.isArray(geminiContextHeader) && geminiContextHeader.length > 0 ? geminiContextHeader[0] : void 0;
	const completionReq = {
		model: session.model,
		messages: [...session.conversationHistory, ...newMessages],
		stream: true,
		tools: session.tools.length > 0 ? session.tools : void 0,
		_endpointType: "chat",
		_context: geminiContext
	};
	const testId = defaults.testId ?? DEFAULT_TEST_ID;
	const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(fixtures, completionReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
	const path = WS_PATH;
	if (fixture) journal.incrementFixtureMatchCount(fixture, fixtures, testId);
	if (!fixture) {
		if (resolveStrictMode(defaults.strict, defaults.upgradeHeaders)) {
			const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);
			defaults.logger.error(strictNoMatchLogLine("WS", path, skippedBySequenceOrTurn));
			journal.add({
				method: "WS",
				path,
				headers: flattenHeaders(defaults.upgradeHeaders ?? {}),
				body: completionReq,
				response: {
					status: 503,
					fixture: null,
					...strictOverrideField(defaults.strict, defaults.upgradeHeaders)
				}
			});
			ws.close(1008, strictMessage);
			return;
		}
		journal.add({
			method: "WS",
			path,
			headers: flattenHeaders(defaults.upgradeHeaders ?? {}),
			body: completionReq,
			response: {
				status: 404,
				fixture: null,
				...strictOverrideField(defaults.strict, defaults.upgradeHeaders)
			}
		});
		ws.send(JSON.stringify({ error: {
			code: 5,
			message: "No fixture matched",
			status: "NOT_FOUND"
		} }));
		return;
	}
	session.conversationHistory.push(...newMessages);
	const response = await resolveResponse(fixture, completionReq);
	const latency = fixture.latency ?? defaults.latency;
	const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);
	if (isErrorResponse(response)) {
		const status = response.status ?? 500;
		journal.add({
			method: "WS",
			path,
			headers: flattenHeaders(defaults.upgradeHeaders ?? {}),
			body: completionReq,
			response: {
				status,
				fixture
			}
		});
		ws.send(JSON.stringify({ error: {
			code: httpToGrpc(status),
			message: response.error.message,
			status: response.error.type ?? "INTERNAL"
		} }));
		return;
	}
	if (isAudioResponse(response)) {
		journal.add({
			method: "WS",
			path,
			headers: flattenHeaders(defaults.upgradeHeaders ?? {}),
			body: completionReq,
			response: {
				status: 200,
				fixture
			}
		});
		const audioResp = response;
		let mimeType;
		let data;
		if (typeof audioResp.audio === "string") {
			mimeType = formatToMime(audioResp.format ?? "mp3");
			data = audioResp.audio;
		} else {
			mimeType = audioResp.audio.contentType ?? "audio/mpeg";
			data = audioResp.audio.b64Json;
		}
		ws.send(JSON.stringify({ serverContent: {
			modelTurn: { parts: [{ inlineData: {
				mimeType,
				data
			} }] },
			turnComplete: true
		} }));
		session.conversationHistory.push({
			role: "assistant",
			content: "[audio]"
		});
		return;
	}
	if (isContentWithToolCallsResponse(response)) {
		const journalEntry = journal.add({
			method: "WS",
			path,
			headers: flattenHeaders(defaults.upgradeHeaders ?? {}),
			body: completionReq,
			response: {
				status: 200,
				fixture
			}
		});
		if (response.blocks && response.blocks.length > 0) {
			const resolvedBlocks = resolveFixtureBlocks(response.blocks);
			const interruption = createInterruptionSignal(fixture);
			const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
			const { recordedTimings } = fixture;
			let chunkIndex = 0;
			let interrupted = false;
			let historyContent = "";
			const historyToolCalls = [];
			outer: for (const block of resolvedBlocks) if (block.type === "toolCall") {
				if (ws.isClosed) break;
				const tcDelay = calculateDelay(chunkIndex, void 0, latency, recordedTimings, replaySpeed);
				if (tcDelay > 0) await delay(tcDelay, interruption?.signal);
				if (interruption?.signal.aborted) {
					interrupted = true;
					break;
				}
				if (ws.isClosed) break;
				const resolvedId = block.id ?? generateToolCallId();
				let argsObj;
				try {
					argsObj = JSON.parse(block.arguments || "{}");
				} catch {
					defaults.logger.warn(`Malformed JSON in fixture tool call arguments for "${block.name}": ${block.arguments}`);
					argsObj = {};
				}
				try {
					ws.send(JSON.stringify({ toolCall: { functionCalls: [{
						name: block.name,
						args: argsObj,
						id: resolvedId
					}] } }));
				} catch (err) {
					defaults.logger.debug("[gemini-live] send failed during blocks streaming, closing", err);
					break;
				}
				chunkIndex++;
				historyToolCalls.push({
					id: resolvedId,
					type: "function",
					function: {
						name: block.name,
						arguments: block.arguments
					}
				});
				interruption?.tick();
				if (interruption?.signal.aborted) {
					interrupted = true;
					break;
				}
			} else {
				const text = block.text;
				historyContent += text;
				if (text.length === 0) continue;
				for (let i = 0; i < text.length; i += chunkSize) {
					if (ws.isClosed) break outer;
					const chunkDelay = calculateDelay(chunkIndex, void 0, latency, recordedTimings, replaySpeed);
					if (chunkDelay > 0) await delay(chunkDelay, interruption?.signal);
					if (interruption?.signal.aborted) {
						interrupted = true;
						break outer;
					}
					if (ws.isClosed) break outer;
					try {
						ws.send(JSON.stringify({ serverContent: { modelTurn: { parts: [{ text: text.slice(i, i + chunkSize) }] } } }));
					} catch (err) {
						defaults.logger.debug("[gemini-live] send failed during blocks streaming, closing", err);
						break outer;
					}
					chunkIndex++;
					interruption?.tick();
					if (interruption?.signal.aborted) {
						interrupted = true;
						break outer;
					}
				}
			}
			if (interrupted) {
				ws.destroy();
				journalEntry.response.interrupted = true;
				journalEntry.response.interruptReason = interruption?.reason();
				interruption?.cleanup();
				return;
			}
			interruption?.cleanup();
			if (!ws.isClosed) ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
			session.conversationHistory.push({
				role: "assistant",
				content: historyContent || null,
				...historyToolCalls.length > 0 ? { tool_calls: historyToolCalls } : {}
			});
			return;
		}
		const content = response.content ?? "";
		const chunkList = [];
		for (let i = 0; i < content.length; i += chunkSize) chunkList.push(content.slice(i, i + chunkSize));
		const interruption = createInterruptionSignal(fixture);
		const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
		const { recordedTimings } = fixture;
		let interrupted = false;
		if (content.length === 0) {
			if (!ws.isClosed) ws.send(JSON.stringify({ serverContent: { modelTurn: { parts: [{ text: "" }] } } }));
		} else for (let i = 0; i < chunkList.length; i++) {
			if (ws.isClosed) break;
			const chunkDelay = calculateDelay(i, void 0, latency, recordedTimings, replaySpeed);
			if (chunkDelay > 0) await delay(chunkDelay, interruption?.signal);
			if (interruption?.signal.aborted) {
				interrupted = true;
				break;
			}
			if (ws.isClosed) break;
			try {
				ws.send(JSON.stringify({ serverContent: { modelTurn: { parts: [{ text: chunkList[i] }] } } }));
			} catch (err) {
				defaults.logger.debug("[gemini-live] send failed during text streaming, closing", err);
				break;
			}
			interruption?.tick();
			if (interruption?.signal.aborted) {
				interrupted = true;
				break;
			}
		}
		if (interrupted) {
			ws.destroy();
			journalEntry.response.interrupted = true;
			journalEntry.response.interruptReason = interruption?.reason();
			interruption?.cleanup();
			return;
		}
		const resolvedToolCalls = (response.toolCalls ?? []).map((tc) => ({
			...tc,
			resolvedId: tc.id ?? generateToolCallId()
		}));
		if (!ws.isClosed) {
			const tcDelay = calculateDelay(chunkList.length, void 0, latency, recordedTimings, replaySpeed);
			if (tcDelay > 0) await delay(tcDelay, interruption?.signal);
			if (interruption?.signal.aborted) {
				ws.destroy();
				journalEntry.response.interrupted = true;
				journalEntry.response.interruptReason = interruption?.reason();
				interruption?.cleanup();
				return;
			}
			const functionCalls = resolvedToolCalls.map((tc) => {
				let argsObj;
				try {
					argsObj = JSON.parse(tc.arguments || "{}");
				} catch {
					defaults.logger.warn(`Malformed JSON in fixture tool call arguments for "${tc.name}": ${tc.arguments}`);
					argsObj = {};
				}
				return {
					name: tc.name,
					args: argsObj,
					id: tc.resolvedId
				};
			});
			ws.send(JSON.stringify({ toolCall: { functionCalls } }));
			interruption?.tick();
		}
		if (interruption?.signal.aborted) {
			ws.destroy();
			journalEntry.response.interrupted = true;
			journalEntry.response.interruptReason = interruption?.reason();
			interruption?.cleanup();
			return;
		}
		interruption?.cleanup();
		if (!ws.isClosed) ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
		session.conversationHistory.push({
			role: "assistant",
			content: content || null,
			tool_calls: resolvedToolCalls.map((tc) => ({
				id: tc.resolvedId,
				type: "function",
				function: {
					name: tc.name,
					arguments: tc.arguments
				}
			}))
		});
		return;
	}
	if (isTextResponse(response)) {
		const journalEntry = journal.add({
			method: "WS",
			path,
			headers: flattenHeaders(defaults.upgradeHeaders ?? {}),
			body: completionReq,
			response: {
				status: 200,
				fixture
			}
		});
		const content = response.content;
		if (content.length === 0) {
			if (ws.isClosed) return;
			ws.send(JSON.stringify({ serverContent: { modelTurn: { parts: [{ text: "" }] } } }));
			ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
			return;
		}
		const chunks = [];
		for (let i = 0; i < content.length; i += chunkSize) chunks.push(content.slice(i, i + chunkSize));
		const interruption = createInterruptionSignal(fixture);
		const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
		const { recordedTimings } = fixture;
		let interrupted = false;
		for (let i = 0; i < chunks.length; i++) {
			if (ws.isClosed) break;
			const chunkDelay = calculateDelay(i, void 0, latency, recordedTimings, replaySpeed);
			if (chunkDelay > 0) await delay(chunkDelay, interruption?.signal);
			if (interruption?.signal.aborted) {
				interrupted = true;
				break;
			}
			if (ws.isClosed) break;
			try {
				ws.send(JSON.stringify({ serverContent: { modelTurn: { parts: [{ text: chunks[i] }] } } }));
			} catch (err) {
				defaults.logger.debug("[gemini-live] send failed during text streaming, closing", err);
				break;
			}
			interruption?.tick();
			if (interruption?.signal.aborted) {
				interrupted = true;
				break;
			}
		}
		if (interrupted) {
			ws.destroy();
			journalEntry.response.interrupted = true;
			journalEntry.response.interruptReason = interruption?.reason();
			interruption?.cleanup();
			return;
		}
		interruption?.cleanup();
		if (!ws.isClosed) ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
		session.conversationHistory.push({
			role: "assistant",
			content
		});
		return;
	}
	if (isToolCallResponse(response)) {
		const journalEntry = journal.add({
			method: "WS",
			path,
			headers: flattenHeaders(defaults.upgradeHeaders ?? {}),
			body: completionReq,
			response: {
				status: 200,
				fixture
			}
		});
		const interruption = createInterruptionSignal(fixture);
		const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
		const { recordedTimings } = fixture;
		if (ws.isClosed) {
			interruption?.cleanup();
			return;
		}
		const tcDelay = calculateDelay(0, void 0, latency, recordedTimings, replaySpeed);
		if (tcDelay > 0) await delay(tcDelay, interruption?.signal);
		if (interruption?.signal.aborted) {
			ws.destroy();
			journalEntry.response.interrupted = true;
			journalEntry.response.interruptReason = interruption?.reason();
			interruption?.cleanup();
			return;
		}
		if (ws.isClosed) {
			interruption?.cleanup();
			return;
		}
		const resolvedToolCalls = (response.toolCalls ?? []).map((tc) => ({
			...tc,
			resolvedId: tc.id ?? generateToolCallId()
		}));
		const functionCalls = resolvedToolCalls.map((tc) => {
			let argsObj;
			try {
				argsObj = JSON.parse(tc.arguments || "{}");
			} catch {
				defaults.logger.warn(`Malformed JSON in fixture tool call arguments for "${tc.name}": ${tc.arguments}`);
				argsObj = {};
			}
			return {
				name: tc.name,
				args: argsObj,
				id: tc.resolvedId
			};
		});
		ws.send(JSON.stringify({ toolCall: { functionCalls } }));
		interruption?.tick();
		if (interruption?.signal.aborted) {
			ws.destroy();
			journalEntry.response.interrupted = true;
			journalEntry.response.interruptReason = interruption?.reason();
			interruption?.cleanup();
			return;
		}
		interruption?.cleanup();
		if (!ws.isClosed) ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
		session.conversationHistory.push({
			role: "assistant",
			content: null,
			tool_calls: resolvedToolCalls.map((tc) => ({
				id: tc.resolvedId,
				type: "function",
				function: {
					name: tc.name,
					arguments: tc.arguments
				}
			}))
		});
		return;
	}
	journal.add({
		method: "WS",
		path,
		headers: flattenHeaders(defaults.upgradeHeaders ?? {}),
		body: completionReq,
		response: {
			status: 500,
			fixture
		}
	});
	ws.send(JSON.stringify({ error: {
		code: 13,
		message: "Fixture response did not match any known type",
		status: "INTERNAL"
	} }));
}

//#endregion
export { handleWebSocketGeminiLive };
//# sourceMappingURL=ws-gemini-live.js.map