/**
 * Real-time Streaming Support
 *
 * Provides dedicated streaming capabilities including Server-Sent Events (SSE),
 * WebSocket connections, and async iterators for real-time data streaming.
 *
 * @module @neurolink/client/streaming
 */
import { logger } from "../utils/logger.js";
import { combineSignals, sleep } from "./httpClient.js";
// =============================================================================
// SSE Client
// =============================================================================
/**
 * Server-Sent Events (SSE) Client
 *
 * Provides a robust SSE connection with automatic reconnection,
 * event parsing, and async iterator support.
 *
 * @example Basic usage
 * ```typescript
 * const sse = new SSEClient('https://api.example.com/stream');
 *
 * sse.on('message', (data) => console.log(data));
 * sse.on('error', (error) => console.error(error));
 *
 * await sse.connect({ body: { prompt: 'Hello' } });
 * ```
 *
 * @example Async iterator usage
 * ```typescript
 * const sse = new SSEClient('https://api.example.com/stream');
 *
 * for await (const event of sse.events({ body: { prompt: 'Hello' } })) {
 *   if (event.type === 'text') {
 *     console.log(event.content);
 *   }
 * }
 * ```
 */
export class SSEClient {
    url;
    options;
    state = "disconnected";
    abortController = null;
    reconnectAttempts = 0;
    eventHandlers = new Map();
    constructor(url, options = {}) {
        this.url = url;
        this.options = {
            autoReconnect: options.autoReconnect ?? false,
            reconnectDelay: options.reconnectDelay ?? 1000,
            maxReconnectAttempts: options.maxReconnectAttempts ?? 5,
            ...options,
        };
    }
    /**
     * Connect to SSE endpoint
     */
    async connect(requestOptions = {}) {
        if (this.state === "connected" || this.state === "connecting") {
            return;
        }
        this.state = "connecting";
        this.abortController = new AbortController();
        logger.debug(`[SSEClient] Connecting to ${this.url}`);
        // Combine signals if external signal provided
        const signal = this.options.signal
            ? combineSignals(this.options.signal, this.abortController.signal)
            : this.abortController.signal;
        try {
            const response = await fetch(this.url, {
                method: requestOptions.body ? "POST" : "GET",
                headers: {
                    "Content-Type": "application/json",
                    Accept: "text/event-stream",
                    ...this.options.headers,
                    ...requestOptions.headers,
                },
                credentials: this.options.credentials,
                body: requestOptions.body
                    ? JSON.stringify(requestOptions.body)
                    : undefined,
                signal,
            });
            if (!response.ok) {
                const error = await response.json().catch(() => ({
                    code: "SSE_ERROR",
                    message: `HTTP ${response.status}`,
                    status: response.status,
                }));
                logger.debug(`[SSEClient] Connection failed: HTTP ${response.status}`);
                throw error;
            }
            this.state = "connected";
            this.reconnectAttempts = 0;
            this.emit("connected");
            logger.debug(`[SSEClient] Connected to ${this.url}`);
            await this.processStream(response);
        }
        catch (error) {
            if (error.name === "AbortError") {
                this.state = "disconnected";
                logger.debug("[SSEClient] Connection aborted");
                return;
            }
            this.state = "error";
            this.emit("error", error);
            logger.debug("[SSEClient] Connection error", {
                message: error.message,
            });
            if (this.options.autoReconnect && this.shouldReconnect()) {
                await this.reconnect(requestOptions);
            }
        }
    }
    /**
     * Disconnect from SSE endpoint
     */
    disconnect() {
        logger.debug(`[SSEClient] Disconnecting from ${this.url}`);
        this.abortController?.abort();
        this.state = "disconnected";
        this.emit("disconnected");
    }
    /**
     * Process the SSE stream
     */
    async processStream(response) {
        const reader = response.body?.getReader();
        if (!reader) {
            throw new Error("Response body is not readable");
        }
        const decoder = new TextDecoder();
        let buffer = "";
        try {
            while (true) {
                const { done, value } = await reader.read();
                if (done) {
                    this.state = "disconnected";
                    this.emit("disconnected");
                    break;
                }
                buffer += decoder.decode(value, { stream: true });
                const lines = buffer.split("\n");
                buffer = lines.pop() ?? "";
                for (const line of lines) {
                    if (line.startsWith("data: ")) {
                        const data = line.slice(6);
                        if (data === "[DONE]") {
                            this.emit("done", { content: "" });
                            continue;
                        }
                        try {
                            const event = JSON.parse(data);
                            this.handleEvent(event);
                        }
                        catch {
                            // Ignore parse errors
                        }
                    }
                    else if (line.startsWith("event: ")) {
                        // Handle named events
                        const eventName = line.slice(7);
                        this.emit("event-type", eventName);
                    }
                    else if (line.startsWith("id: ")) {
                        // Handle event IDs
                        const eventId = line.slice(4);
                        this.emit("event-id", eventId);
                    }
                    else if (line.startsWith("retry: ")) {
                        // Handle retry directive
                        const retryMs = parseInt(line.slice(7), 10);
                        if (!isNaN(retryMs)) {
                            this.options.reconnectDelay = retryMs;
                        }
                    }
                }
            }
        }
        finally {
            reader.releaseLock();
        }
    }
    /**
     * Handle a stream event
     */
    handleEvent(event) {
        this.emit("message", event);
        switch (event.type) {
            case "text":
                if (event.content) {
                    this.emit("text", event.content);
                }
                break;
            case "tool-call":
                if (event.toolCall) {
                    this.emit("tool-call", event.toolCall);
                }
                break;
            case "tool-result":
                if (event.toolResult) {
                    this.emit("tool-result", event.toolResult);
                }
                break;
            case "error":
                if (event.error) {
                    this.emit("error", event.error);
                }
                break;
            case "metadata":
                if (event.metadata) {
                    this.emit("metadata", event.metadata);
                }
                break;
            case "audio":
                if (event.audio) {
                    this.emit("audio", event.audio);
                }
                break;
            case "thinking":
                if (event.thinking) {
                    this.emit("thinking", event.thinking);
                }
                break;
            case "done":
                this.emit("done", { content: "" });
                break;
        }
    }
    /**
     * Check if should attempt reconnection
     */
    shouldReconnect() {
        return this.reconnectAttempts < (this.options.maxReconnectAttempts ?? 5);
    }
    /**
     * Attempt reconnection
     */
    async reconnect(requestOptions) {
        this.state = "reconnecting";
        this.reconnectAttempts++;
        this.emit("reconnecting", this.reconnectAttempts);
        const delayMs = (this.options.reconnectDelay ?? 1000) * this.reconnectAttempts;
        logger.debug(`[SSEClient] Reconnecting to ${this.url} (attempt ${this.reconnectAttempts}, delay ${delayMs}ms)`);
        await sleep(delayMs);
        await this.connect(requestOptions);
    }
    /**
     * Register event handler
     */
    on(event, callback) {
        let handlers = this.eventHandlers.get(event);
        if (!handlers) {
            handlers = new Set();
            this.eventHandlers.set(event, handlers);
        }
        handlers.add(callback);
    }
    /**
     * Remove event handler
     */
    off(event, callback) {
        this.eventHandlers.get(event)?.delete(callback);
    }
    /**
     * Emit event
     */
    emit(event, ...args) {
        const handlers = this.eventHandlers.get(event);
        if (handlers) {
            handlers.forEach((handler) => handler(...args));
        }
    }
    /**
     * Get current connection state
     */
    getState() {
        return this.state;
    }
    /**
     * Create async iterator for events
     *
     * @example
     * ```typescript
     * for await (const event of sse.events({ body: { prompt: 'Hello' } })) {
     *   console.log(event);
     * }
     * ```
     */
    async *events(requestOptions = {}) {
        const events = [];
        let done = false;
        let error = null;
        let resolver = null;
        const onMessage = (event) => {
            events.push(event);
            resolver?.();
        };
        const onDone = () => {
            done = true;
            resolver?.();
        };
        const onError = (err) => {
            error = err;
            resolver?.();
        };
        this.on("message", onMessage);
        this.on("done", onDone);
        this.on("error", onError);
        try {
            // Start connection in background
            this.connect(requestOptions).catch((err) => {
                error = err;
                resolver?.();
            });
            while (!done && !error) {
                if (events.length > 0) {
                    const nextEvent = events.shift();
                    if (!nextEvent) {
                        continue;
                    }
                    yield nextEvent;
                }
                else {
                    await new Promise((resolve) => {
                        resolver = resolve;
                    });
                }
            }
            // Yield remaining events
            while (events.length > 0) {
                const nextEvent = events.shift();
                if (!nextEvent) {
                    continue;
                }
                yield nextEvent;
            }
            if (error) {
                throw error;
            }
        }
        finally {
            this.off("message", onMessage);
            this.off("done", onDone);
            this.off("error", onError);
        }
    }
}
// =============================================================================
// WebSocket Streaming Client
// =============================================================================
/**
 * WebSocket Streaming Client
 *
 * Provides WebSocket-based streaming with automatic reconnection,
 * heartbeat, and message handling.
 *
 * @example
 * ```typescript
 * const ws = new WebSocketStreamingClient({
 *   url: 'wss://api.example.com/ws',
 *   autoReconnect: true,
 * });
 *
 * ws.on('message', (data) => console.log(data));
 *
 * await ws.connect();
 * ws.send({ type: 'chat', content: 'Hello' });
 * ```
 */
export class WebSocketStreamingClient {
    options;
    ws = null;
    state = "disconnected";
    reconnectAttempts = 0;
    heartbeatInterval = null;
    eventHandlers = new Map();
    constructor(options) {
        this.options = {
            autoReconnect: options.autoReconnect ?? true,
            reconnectInterval: options.reconnectInterval ?? 5000,
            maxReconnectAttempts: options.maxReconnectAttempts ?? 10,
            heartbeatInterval: options.heartbeatInterval ?? 30000,
            ...options,
        };
    }
    /**
     * Connect to WebSocket server
     */
    async connect() {
        if (typeof WebSocket === "undefined") {
            throw new Error("WebSocket is not available. Please use a polyfill for non-browser environments.");
        }
        if (this.state === "connected" || this.state === "connecting") {
            return;
        }
        logger.debug(`[WebSocketClient] Connecting to ${this.options.url}`);
        return new Promise((resolve, reject) => {
            this.state = "connecting";
            try {
                this.ws = new WebSocket(this.options.url, this.options.protocols);
                this.ws.onopen = () => {
                    this.state = "connected";
                    this.reconnectAttempts = 0;
                    this.startHeartbeat();
                    this.emit("connected");
                    logger.debug(`[WebSocketClient] Connected to ${this.options.url}`);
                    resolve();
                };
                this.ws.onmessage = (event) => {
                    try {
                        const data = JSON.parse(event.data);
                        this.handleMessage(data);
                    }
                    catch {
                        this.emit("raw-message", event.data);
                    }
                };
                this.ws.onclose = (event) => {
                    this.state = "disconnected";
                    this.stopHeartbeat();
                    this.emit("disconnected", { code: event.code, reason: event.reason });
                    logger.debug("[WebSocketClient] Disconnected", {
                        code: event.code,
                        reason: event.reason,
                    });
                    if (this.options.autoReconnect && !event.wasClean) {
                        this.attemptReconnect();
                    }
                };
                this.ws.onerror = (event) => {
                    this.emit("error", event);
                    logger.debug("[WebSocketClient] Connection error");
                    if (this.state === "connecting") {
                        reject(new Error("WebSocket connection failed"));
                    }
                };
            }
            catch (error) {
                this.state = "disconnected";
                logger.debug("[WebSocketClient] Connection error", {
                    message: error.message,
                });
                reject(error);
            }
        });
    }
    /**
     * Disconnect from WebSocket server
     */
    disconnect() {
        logger.debug(`[WebSocketClient] Disconnecting from ${this.options.url}`);
        this.stopHeartbeat();
        if (this.ws) {
            this.ws.close(1000, "Client disconnect");
            this.ws = null;
        }
        this.state = "disconnected";
    }
    /**
     * Send message to server
     */
    send(data) {
        if (this.ws?.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify(data));
        }
        else {
            throw new Error("WebSocket is not connected");
        }
    }
    /**
     * Send message and wait for response
     */
    async request(data, timeout = 30000) {
        return new Promise((resolve, reject) => {
            const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
            const timeoutId = setTimeout(() => {
                this.off("response", responseHandler);
                reject(new Error(`Request timed out after ${timeout}ms`));
            }, timeout);
            const responseHandler = (data) => {
                const response = data;
                if (response.requestId === requestId) {
                    clearTimeout(timeoutId);
                    this.off("response", responseHandler);
                    resolve(response.data);
                }
            };
            this.on("response", responseHandler);
            this.send({ ...data, requestId });
        });
    }
    /**
     * Handle incoming message
     */
    handleMessage(data) {
        this.emit("message", data);
        // Handle typed messages
        if (typeof data === "object" && data !== null && "type" in data) {
            const typedData = data;
            this.emit(typedData.type, typedData);
        }
    }
    /**
     * Start heartbeat
     */
    startHeartbeat() {
        if (!this.options.heartbeatInterval) {
            return;
        }
        this.heartbeatInterval = setInterval(() => {
            if (this.ws?.readyState === WebSocket.OPEN) {
                this.send({ type: "ping", timestamp: Date.now() });
            }
        }, this.options.heartbeatInterval);
    }
    /**
     * Stop heartbeat
     */
    stopHeartbeat() {
        if (this.heartbeatInterval) {
            clearInterval(this.heartbeatInterval);
            this.heartbeatInterval = null;
        }
    }
    /**
     * Attempt reconnection
     */
    async attemptReconnect() {
        if (this.reconnectAttempts >= (this.options.maxReconnectAttempts ?? 10) ||
            this.state === "reconnecting") {
            this.emit("reconnect-failed", this.reconnectAttempts);
            logger.debug("[WebSocketClient] Max reconnect attempts reached", {
                attempts: this.reconnectAttempts,
            });
            return;
        }
        this.state = "reconnecting";
        this.reconnectAttempts++;
        this.emit("reconnecting", this.reconnectAttempts);
        const delayMs = (this.options.reconnectInterval ?? 5000) *
            Math.min(this.reconnectAttempts, 5);
        logger.debug(`[WebSocketClient] Reconnecting (attempt ${this.reconnectAttempts}, delay ${delayMs}ms)`);
        await sleep(delayMs);
        try {
            await this.connect();
        }
        catch {
            // Reconnection failed, will be retried on next disconnect
        }
    }
    /**
     * Register event handler
     */
    on(event, callback) {
        let handlers = this.eventHandlers.get(event);
        if (!handlers) {
            handlers = new Set();
            this.eventHandlers.set(event, handlers);
        }
        handlers.add(callback);
    }
    /**
     * Remove event handler
     */
    off(event, callback) {
        this.eventHandlers.get(event)?.delete(callback);
    }
    /**
     * Emit event
     */
    emit(event, ...args) {
        const handlers = this.eventHandlers.get(event);
        if (handlers) {
            handlers.forEach((handler) => handler(args.length === 1 ? args[0] : args));
        }
    }
    /**
     * Get current connection state
     */
    getState() {
        return this.state;
    }
    /**
     * Create async iterator for messages
     */
    async *messages() {
        const messageQueue = [];
        let resolver = null;
        let disconnected = false;
        const onMessage = (data) => {
            messageQueue.push(data);
            resolver?.();
        };
        const onDisconnect = () => {
            disconnected = true;
            resolver?.();
        };
        this.on("message", onMessage);
        this.on("disconnected", onDisconnect);
        try {
            while (!disconnected) {
                if (messageQueue.length > 0) {
                    const nextMessage = messageQueue.shift();
                    if (nextMessage === undefined) {
                        continue;
                    }
                    yield nextMessage;
                }
                else {
                    await new Promise((resolve) => {
                        resolver = resolve;
                    });
                }
            }
            // Yield remaining messages
            while (messageQueue.length > 0) {
                const nextMessage = messageQueue.shift();
                if (nextMessage === undefined) {
                    continue;
                }
                yield nextMessage;
            }
        }
        finally {
            this.off("message", onMessage);
            this.off("disconnected", onDisconnect);
        }
    }
}
/**
 * Streaming Client Factory
 *
 * Creates streaming clients for real-time communication with NeuroLink API.
 *
 * @example SSE streaming
 * ```typescript
 * const client = createStreamingClient({
 *   baseUrl: 'https://api.example.com',
 *   apiKey: 'your-key',
 *   transport: 'sse',
 * });
 *
 * const result = await client.stream({
 *   input: { text: 'Hello' },
 *   callbacks: {
 *     onText: (text) => console.log(text),
 *   },
 * });
 * ```
 *
 * @example WebSocket streaming
 * ```typescript
 * const client = createStreamingClient({
 *   baseUrl: 'https://api.example.com',
 *   apiKey: 'your-key',
 *   transport: 'websocket',
 * });
 *
 * await client.connect();
 * const result = await client.stream({
 *   input: { text: 'Hello' },
 * });
 * ```
 */
export function createStreamingClient(config) {
    const { baseUrl, apiKey, token, headers = {}, transport = "sse" } = config;
    const authHeaders = {
        ...headers,
    };
    if (apiKey) {
        authHeaders["X-API-Key"] = apiKey;
    }
    if (token) {
        authHeaders["Authorization"] = `Bearer ${token}`;
    }
    if (transport === "websocket") {
        const wsUrl = baseUrl.replace(/^http/, "ws") + "/ws";
        const wsClient = new WebSocketStreamingClient({
            url: wsUrl,
            autoReconnect: true,
        });
        return {
            connect: () => wsClient.connect(),
            disconnect: () => wsClient.disconnect(),
            stream: async (options) => {
                const { callbacks, ...requestOptions } = options;
                return new Promise((resolve, reject) => {
                    let fullContent = "";
                    const toolCalls = [];
                    const toolResults = [];
                    const messageHandler = (data) => {
                        const event = data;
                        switch (event.type) {
                            case "text":
                                if (event.content) {
                                    fullContent += event.content;
                                    callbacks?.onText?.(event.content);
                                }
                                break;
                            case "tool-call":
                                if (event.toolCall) {
                                    toolCalls.push(event.toolCall);
                                    callbacks?.onToolCall?.(event.toolCall);
                                }
                                break;
                            case "tool-result":
                                if (event.toolResult) {
                                    toolResults.push(event.toolResult);
                                    callbacks?.onToolResult?.(event.toolResult);
                                }
                                break;
                            case "error":
                                if (event.error) {
                                    callbacks?.onError?.(event.error);
                                    reject(event.error);
                                }
                                break;
                            case "done": {
                                wsClient.off("message", messageHandler);
                                const result = {
                                    content: fullContent,
                                    toolCalls,
                                    toolResults,
                                };
                                callbacks?.onDone?.(result);
                                resolve(result);
                                break;
                            }
                        }
                    };
                    wsClient.on("message", messageHandler);
                    wsClient.send({
                        type: "stream",
                        ...requestOptions,
                    });
                });
            },
            send: (data) => wsClient.send(data),
            on: (event, callback) => wsClient.on(event, callback),
            off: (event, callback) => wsClient.off(event, callback),
            getState: () => wsClient.getState(),
        };
    }
    // SSE transport — track connection state across stream calls
    let sseState = "disconnected";
    return {
        connect: () => {
            sseState = "connected";
            return Promise.resolve();
        },
        disconnect: () => {
            sseState = "disconnected";
        },
        stream: async (options) => {
            const { callbacks, ...requestOptions } = options;
            const sseUrl = `${baseUrl}/api/stream`;
            const sse = new SSEClient(sseUrl, {
                headers: authHeaders,
            });
            sseState = "connecting";
            return new Promise((resolve, reject) => {
                let fullContent = "";
                const toolCalls = [];
                const toolResults = [];
                sse.on("connected", () => {
                    sseState = "connected";
                });
                sse.on("text", (text) => {
                    fullContent += text;
                    callbacks?.onText?.(text);
                });
                sse.on("tool-call", (toolCall) => {
                    toolCalls.push(toolCall);
                    callbacks?.onToolCall?.(toolCall);
                });
                sse.on("tool-result", (toolResult) => {
                    toolResults.push(toolResult);
                    callbacks?.onToolResult?.(toolResult);
                });
                sse.on("metadata", (metadata) => {
                    callbacks?.onMetadata?.(metadata);
                });
                sse.on("audio", (audio) => {
                    callbacks?.onAudio?.(audio);
                });
                sse.on("thinking", (thinking) => {
                    callbacks?.onThinking?.(thinking);
                });
                sse.on("error", (error) => {
                    sseState = "disconnected";
                    callbacks?.onError?.(error);
                    reject(error);
                });
                sse.on("done", () => {
                    sseState = "disconnected";
                    const result = {
                        content: fullContent,
                        toolCalls,
                        toolResults,
                    };
                    callbacks?.onDone?.(result);
                    resolve(result);
                });
                sse.on("disconnected", () => {
                    sseState = "disconnected";
                });
                sse.connect({ body: requestOptions });
            });
        },
        send: () => {
            throw new Error("SSE transport does not support sending messages");
        },
        on: () => { },
        off: () => { },
        getState: () => sseState,
    };
}
// =============================================================================
// Async Stream Utilities
// =============================================================================
/**
 * Create an async iterable from streaming response
 *
 * @example
 * ```typescript
 * const stream = createAsyncStream(fetch('/api/stream', { method: 'POST' }));
 *
 * for await (const event of stream) {
 *   console.log(event);
 * }
 * ```
 */
export async function* createAsyncStream(responsePromise) {
    const response = await responsePromise;
    if (!response.ok) {
        const error = await response.json().catch(() => ({
            code: "STREAM_ERROR",
            message: `HTTP ${response.status}`,
            status: response.status,
        }));
        throw error;
    }
    const reader = response.body?.getReader();
    if (!reader) {
        throw new Error("Response body is not readable");
    }
    const decoder = new TextDecoder();
    let buffer = "";
    try {
        while (true) {
            const { done, value } = await reader.read();
            if (done) {
                break;
            }
            buffer += decoder.decode(value, { stream: true });
            const lines = buffer.split("\n");
            buffer = lines.pop() ?? "";
            for (const line of lines) {
                if (line.startsWith("data: ")) {
                    const data = line.slice(6);
                    if (data === "[DONE]") {
                        return;
                    }
                    try {
                        const event = JSON.parse(data);
                        yield event;
                    }
                    catch {
                        // Ignore parse errors
                    }
                }
            }
        }
    }
    finally {
        reader.releaseLock();
    }
}
/**
 * Collect streaming events into a single result
 *
 * @example
 * ```typescript
 * const result = await collectStream(
 *   createAsyncStream(fetch('/api/stream', { method: 'POST' }))
 * );
 * console.log(result.content);
 * ```
 */
export async function collectStream(stream) {
    let content = "";
    const toolCalls = [];
    const toolResults = [];
    let finishReason;
    let usage;
    let metadata;
    for await (const event of stream) {
        switch (event.type) {
            case "text":
                if (event.content) {
                    content += event.content;
                }
                break;
            case "tool-call":
                if (event.toolCall) {
                    toolCalls.push(event.toolCall);
                }
                break;
            case "tool-result":
                if (event.toolResult) {
                    toolResults.push(event.toolResult);
                }
                break;
            case "metadata":
                metadata = { ...metadata, ...event.metadata };
                if (event.metadata?.finishReason) {
                    finishReason = event.metadata.finishReason;
                }
                if (event.metadata?.usage) {
                    usage = event.metadata.usage;
                }
                break;
        }
    }
    return {
        content,
        toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
        toolResults: toolResults.length > 0 ? toolResults : undefined,
        finishReason,
        usage,
        metadata,
    };
}
// Types are exported at their definition sites above and via imports
