import { stream as honoStream } from 'hono/streaming';
import { context as otelContext, ROOT_CONTEXT } from '@opentelemetry/api';
import { StructuredError } from '@agentuity/core';
import { tagRoute } from './_route-meta';
/**
 * Error thrown when sse() is called without a handler function.
 */
const SSEHandlerMissingError = StructuredError('SSEHandlerMissingError', 'An SSE handler function is required. Use sse(handler) or sse({ output: schema }, handler).');
/**
 * Context variable key for stream completion promise.
 * Used by middleware to defer session/thread saving until stream completes.
 * @internal
 */
export const STREAM_DONE_PROMISE_KEY = '_streamDonePromise';
/**
 * Context variable key to indicate this is a streaming response.
 * @internal
 */
export const IS_STREAMING_RESPONSE_KEY = '_isStreamingResponse';
/**
 * Format an SSE message according to the SSE specification.
 * @see https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
 */
function formatSSEMessage(message) {
    let text = '';
    if (message.event) {
        text += `event: ${message.event}\n`;
    }
    if (message.id) {
        text += `id: ${message.id}\n`;
    }
    if (typeof message.retry === 'number') {
        text += `retry: ${message.retry}\n`;
    }
    // Data can be multiline - each line needs its own "data:" prefix
    const dataLines = message.data.split(/\r?\n/);
    for (const line of dataLines) {
        text += `data: ${line}\n`;
    }
    // SSE messages are terminated by a blank line
    text += '\n';
    return text;
}
export function sse(handlerOrOptions, maybeHandler) {
    // Determine if first arg is options or handler
    const handler = typeof handlerOrOptions === 'function' ? handlerOrOptions : maybeHandler;
    // Validate handler is provided - catches sse({ output }) without handler
    if (!handler) {
        throw new SSEHandlerMissingError();
    }
    // Note: options.output is captured for type inference but not used at runtime
    // The CLI extracts this during build to generate typed route registries
    const sseHandler = (c) => {
        // Track stream completion for deferred session/thread saving
        // This promise resolves when the stream closes (normally or via abort)
        let resolveDone;
        let rejectDone;
        const donePromise = new Promise((resolve, reject) => {
            resolveDone = resolve;
            rejectDone = reject;
        });
        // Prevent unhandled rejection warnings if no middleware consumes donePromise.
        // The error is still propagated via the rejection for middleware that awaits it.
        donePromise.catch(() => {
            // Intentionally empty - error is logged in runInContext catch block
        });
        // Idempotent function to mark stream as completed
        let isDone = false;
        const markDone = (error) => {
            if (isDone)
                return;
            isDone = true;
            if (error && rejectDone) {
                rejectDone(error);
            }
            else if (resolveDone) {
                resolveDone();
            }
        };
        // Expose completion tracking to middleware
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        c.set(STREAM_DONE_PROMISE_KEY, donePromise);
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        c.set(IS_STREAMING_RESPONSE_KEY, true);
        // Set SSE-specific headers
        c.header('Content-Type', 'text/event-stream');
        c.header('Cache-Control', 'no-cache');
        c.header('Connection', 'keep-alive');
        // Use honoStream instead of honoStreamSSE.
        // honoStream uses a fire-and-forget async IIFE pattern that returns the Response
        // immediately while the handler runs in the background. This is critical for
        // compatibility with AI SDK's generateText/generateObject which use fetch()
        // internally. With honoStreamSSE, the callback is awaited before returning,
        // which causes "ReadableStream has already been used" errors when fetch
        // response streams are consumed in the same async chain.
        // See: https://github.com/agentuity/sdk/issues/471
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        return honoStream(c, async (s) => {
            const encoder = new TextEncoder();
            // Track if user registered an onAbort callback
            let userAbortCallback;
            // Internal function to write an SSE message
            const writeSSEInternal = async (message) => {
                const formatted = formatSSEMessage(message);
                await s.write(encoder.encode(formatted));
            };
            const wrappedStream = {
                write: async (data) => {
                    if (typeof data === 'string' ||
                        typeof data === 'number' ||
                        typeof data === 'boolean') {
                        return writeSSEInternal({ data: String(data) });
                    }
                    else if (typeof data === 'object' && data !== null) {
                        return writeSSEInternal(data);
                    }
                    return writeSSEInternal({ data: String(data) });
                },
                writeSSE: writeSSEInternal,
                onAbort: (callback) => {
                    userAbortCallback = callback;
                    s.onAbort(() => {
                        try {
                            callback();
                        }
                        finally {
                            // Mark stream as done on abort
                            markDone();
                        }
                    });
                },
                close: () => {
                    try {
                        s.close?.();
                    }
                    finally {
                        // Mark stream as done on close
                        markDone();
                    }
                },
            };
            // Always register internal abort handler if user doesn't register one
            // This ensures we track completion even if user doesn't call onAbort
            s.onAbort(() => {
                if (!userAbortCallback) {
                    // Only mark done if user didn't register their own handler
                    // (their handler wrapper already calls markDone)
                    markDone();
                }
            });
            const runInContext = async () => {
                try {
                    await handler(c, wrappedStream);
                    markDone();
                }
                catch (err) {
                    // Log error but don't rethrow - would be unhandled rejection
                    c.var.logger?.error?.('SSE handler error:', err);
                    markDone(err);
                }
            };
            // IMPORTANT: We run in ROOT_CONTEXT (no active OTEL span) to avoid a Bun bug
            // where OTEL-instrumented fetch conflicts with streaming responses.
            // This causes "ReadableStream has already been used" errors when AI SDK's
            // generateText/generateObject (which use fetch + stream.tee() internally)
            // are called inside SSE handlers. Running without an active span makes
            // our OTEL fetch wrapper use the original unpatched fetch.
            // See: https://github.com/agentuity/sdk/issues/471
            // See: https://github.com/oven-sh/bun/issues/24766
            await otelContext.with(ROOT_CONTEXT, runInContext);
        });
    };
    return tagRoute(sseHandler, { type: 'sse' });
}
//# sourceMappingURL=sse.js.map