import { updateAsyncLogContext } from "../../utils/logger/context.js";
import { createLogger } from "../../utils/logger/index.js";
import { init_logger } from "../../utils/logger.js";
import { MAX_WEBCHAT_ATTACHMENT_FILE_BYTES } from "../chat-limits.js";
import { resolveWebchatSessionKey } from "../resolve-webchat-session-key.js";
import { stringifySSEData } from "./sse-json.js";
import { streamSSE } from "hono/streaming";
//#region src/gateway/hono/sse.ts
init_logger();
const log = createLogger("Hono:SSE");
const activeConnections = /* @__PURE__ */ new Map();
function isValidAgentRequest(body) {
	if (!body || typeof body !== "object") return false;
	const b = body;
	const hasMessage = typeof b.message === "string";
	const hasAttachments = Array.isArray(b.attachments) && b.attachments.length > 0;
	return hasMessage || hasAttachments;
}
/** Max base64 character length that can decode to `MAX_WEBCHAT_ATTACHMENT_FILE_BYTES`. */
function maxBase64CharsForBinary(maxBinaryBytes) {
	return 4 * Math.ceil(maxBinaryBytes / 3);
}
/**
* POST /api/agent — Send a message to the agent, stream response via SSE.
*
* Request body: { message, channel?, chatId?, attachments? }
* Accept: text/event-stream → SSE stream
* Accept: application/json → wait for full response, return JSON
*
* SSE events:
*   event: status   — { status, runId }
*   event: user_message — { timestamp, content?, attachments? } (user turn accepted, before agent tokens)
*   event: user_transcript — { text, attachments? } (voice STT complete, before agent tokens)
*   event: token    — { content }
*   event: error    — { content }
*   event: result   — { ok, payload: { status, summary } }
*/
function createAgentSSEHandler(config) {
	const { service } = config;
	return async (c) => {
		const body = await c.req.json().catch(() => null);
		if (!isValidAgentRequest(body)) return c.json({
			ok: false,
			error: {
				code: "BAD_REQUEST",
				message: "Missing required field: message or attachments"
			}
		}, 400);
		const { message, channel = "webchat", attachments, thinking } = body;
		const clientCreatedAtMs = typeof body.clientCreatedAtMs === "number" && Number.isFinite(body.clientCreatedAtMs) ? body.clientCreatedAtMs : void 0;
		const newSession = Boolean(body.newSession);
		const cfg = service.currentConfig;
		const resolved = resolveWebchatSessionKey({
			cfg,
			sessionKey: typeof body.sessionKey === "string" ? body.sessionKey : void 0,
			chatId: typeof body.chatId === "string" ? body.chatId : void 0,
			newSession
		});
		if (resolved.ok === false) return c.json({
			ok: false,
			error: {
				code: "BAD_REQUEST",
				message: resolved.error
			}
		}, 400);
		const chatId = resolved.sessionKey;
		updateAsyncLogContext({ sessionId: String(chatId) });
		if (Array.isArray(attachments)) {
			const maxDataChars = maxBase64CharsForBinary(MAX_WEBCHAT_ATTACHMENT_FILE_BYTES);
			for (const a of attachments) {
				if (!a || typeof a !== "object") continue;
				const data = a.data;
				if (typeof data === "string" && data.length > maxDataChars) return c.json({
					ok: false,
					error: {
						code: "BAD_REQUEST",
						message: `Attachment exceeds maximum size (${MAX_WEBCHAT_ATTACHMENT_FILE_BYTES} bytes)`
					}
				}, 400);
			}
		}
		const wantSSE = (c.req.header("Accept") || "").includes("text/event-stream");
		const clientAbort = new AbortController();
		const raw = c.req.raw;
		if (channel !== "webchat") if (raw.signal.aborted) clientAbort.abort();
		else raw.signal.addEventListener("abort", () => clientAbort.abort(), { once: true });
		if (!wantSSE) {
			const jsonSessionKey = channel === "webchat" ? chatId : void 0;
			const generator = service.runAgent(message, channel, chatId, attachments, thinking, {
				signal: clientAbort.signal,
				...clientCreatedAtMs !== void 0 ? { clientCreatedAtMs } : {}
			});
			try {
				let finalResult;
				const tokens = [];
				while (true) {
					const { done, value } = await generator.next();
					if (done) {
						finalResult = value;
						break;
					}
					const chunk = value;
					if (chunk.type === "token" && chunk.content) tokens.push(chunk.content);
				}
				return c.json({
					ok: true,
					payload: {
						...finalResult,
						content: tokens.join(""),
						...jsonSessionKey !== void 0 ? {
							sessionKey: jsonSessionKey,
							key: jsonSessionKey
						} : {}
					}
				});
			} catch (error) {
				log.error({ err: error }, "Agent run failed (JSON mode)");
				return c.json({
					ok: false,
					error: {
						code: "INTERNAL_ERROR",
						message: error instanceof Error ? error.message : "Unknown error"
					}
				}, 500);
			}
		}
		c.header("X-Accel-Buffering", "no");
		return streamSSE(c, async (stream) => {
			if (channel !== "webchat") stream.onAbort(() => {
				clientAbort.abort();
			});
			const generator = service.runAgent(message, channel, chatId, attachments, thinking, {
				signal: clientAbort.signal,
				...clientCreatedAtMs !== void 0 ? { clientCreatedAtMs } : {}
			});
			let eventId = 0;
			try {
				while (true) {
					const { done, value } = await generator.next();
					if (done) {
						await stream.writeSSE({
							id: String(++eventId),
							event: "result",
							data: JSON.stringify({
								ok: true,
								payload: value
							})
						});
						break;
					}
					const chunk = value;
					await stream.writeSSE({
						id: String(++eventId),
						event: chunk.type || "message",
						data: stringifySSEData(chunk)
					});
				}
			} catch (error) {
				log.error({ err: error }, "Agent run failed (SSE mode)");
				await stream.writeSSE({
					id: String(++eventId),
					event: "error",
					data: JSON.stringify({
						ok: false,
						error: {
							code: "INTERNAL_ERROR",
							message: error instanceof Error ? error.message : "Unknown error"
						}
					})
				});
			}
		});
	};
}
/**
* POST /api/agent/resume — Re-attach to an in-progress agent run via SSE.
*
* Request body: { runId, chatId }
* The relay replays all buffered events from the beginning and then live-tails
* until the run completes.
*
* SSE events are identical to those from POST /api/agent.
*/
function createAgentResumeHandler(config) {
	const { service } = config;
	return async (c) => {
		const body = await c.req.json().catch(() => null);
		if (!body || typeof body !== "object") return c.json({
			ok: false,
			error: {
				code: "BAD_REQUEST",
				message: "Invalid JSON body"
			}
		}, 400);
		const { runId, chatId: resumeChatId } = body;
		if (typeof resumeChatId === "string" && resumeChatId.trim()) updateAsyncLogContext({ sessionId: resumeChatId.trim() });
		if (!runId || typeof runId !== "string") return c.json({
			ok: false,
			error: {
				code: "BAD_REQUEST",
				message: "Missing required field: runId"
			}
		}, 400);
		if (!service.runRelay.hasRun(runId)) return c.json({
			ok: false,
			error: {
				code: "NOT_FOUND",
				message: "Run not found or already expired"
			}
		}, 404);
		c.header("X-Accel-Buffering", "no");
		return streamSSE(c, async (stream) => {
			let eventId = 0;
			try {
				for await (const event of service.runRelay.subscribe(runId)) await stream.writeSSE({
					id: String(++eventId),
					event: event.type || "message",
					data: stringifySSEData(event)
				});
				await stream.writeSSE({
					id: String(++eventId),
					event: "result",
					data: JSON.stringify({
						ok: true,
						payload: {
							status: "ok",
							summary: "Resumed run completed"
						}
					})
				});
			} catch (error) {
				log.error({
					err: error,
					runId
				}, "Resume stream failed");
				await stream.writeSSE({
					id: String(++eventId),
					event: "error",
					data: JSON.stringify({
						ok: false,
						error: {
							code: "INTERNAL_ERROR",
							message: error instanceof Error ? error.message : "Unknown error"
						}
					})
				});
			}
		});
	};
}
/**
* POST /api/send — Send a message through a channel (non-streaming).
*/
function createSendHandler(config) {
	const { service } = config;
	return async (c) => {
		const body = await c.req.json().catch(() => ({}));
		const channel = body.channel;
		const chatId = body.chatId;
		const content = body.content;
		if (!channel || !chatId || !content) return c.json({
			ok: false,
			error: {
				code: "BAD_REQUEST",
				message: "Missing required fields: channel, chatId, content"
			}
		}, 400);
		updateAsyncLogContext({ sessionId: String(chatId) });
		try {
			const result = await service.sendMessage(channel, chatId, content);
			return c.json({
				ok: true,
				payload: result
			});
		} catch (error) {
			log.error({ err: error }, "Send failed");
			return c.json({
				ok: false,
				error: {
					code: "INTERNAL_ERROR",
					message: error instanceof Error ? error.message : "Unknown error"
				}
			}, 500);
		}
	};
}
/**
* GET /api/events — Server-pushed event stream (SSE).
*
* The client opens this long-lived connection to receive:
*   - channel status changes
*   - config reload notifications
*   - cron execution results
*   - any other server-initiated events
*
* Supports Last-Event-ID for reconnection.
* Enforces maximum connection limit to prevent DoS.
*/
function createEventsSSEHandler(config) {
	const { service } = config;
	const maxConnections = config.maxSseConnections ?? 100;
	return async (c) => {
		if (activeConnections.size >= maxConnections) {
			log.warn({
				current: activeConnections.size,
				max: maxConnections
			}, "SSE connection limit reached");
			return c.json({
				ok: false,
				error: {
					code: "TOO_MANY_CONNECTIONS",
					message: "Maximum SSE connections exceeded"
				}
			}, 503);
		}
		const lastEventId = c.req.header("Last-Event-ID") || void 0;
		const sessionId = c.req.header("X-Session-Id") || c.req.query("sessionId") || crypto.randomUUID();
		updateAsyncLogContext({ sessionId: String(sessionId) });
		const abortController = new AbortController();
		activeConnections.set(sessionId, abortController);
		return streamSSE(c, async (stream) => {
			let aborted = false;
			await stream.writeSSE({
				id: "0",
				event: "connected",
				data: JSON.stringify({ sessionId })
			});
			const cleanup = service.subscribe(sessionId, async (event) => {
				if (aborted) return;
				try {
					await stream.writeSSE({
						id: event.id,
						event: event.type,
						data: JSON.stringify(event.payload)
					});
				} catch {}
			});
			if (lastEventId) {
				const missed = service.getEventsSince(sessionId, lastEventId);
				for (const event of missed) await stream.writeSSE({
					id: event.id,
					event: event.type,
					data: JSON.stringify(event.payload)
				});
			}
			const keepAlive = setInterval(async () => {
				if (aborted) {
					clearInterval(keepAlive);
					return;
				}
				try {
					await stream.writeSSE({
						event: "ping",
						data: ""
					});
				} catch {
					clearInterval(keepAlive);
				}
			}, 3e4);
			await new Promise((resolve) => {
				stream.onAbort(() => {
					aborted = true;
					clearInterval(keepAlive);
					cleanup();
					activeConnections.delete(sessionId);
					log.debug({ sessionId }, "Event stream disconnected");
					resolve();
				});
			});
		});
	};
}
//#endregion
export { createAgentResumeHandler, createAgentSSEHandler, createEventsSSEHandler, createSendHandler };

//# sourceMappingURL=sse.js.map