/**
 * Realtime Voice API Infrastructure
 *
 * Base handler and processor for realtime voice communication.
 * Supports bidirectional audio streaming with providers like OpenAI and Gemini.
 *
 * @module voice/RealtimeVoiceAPI
 */
import { logger } from "../utils/logger.js";
import { RealtimeError } from "./errors.js";
import { DEFAULT_REALTIME_CONFIG, REALTIME_ERROR_CODES, } from "../types/index.js";
import { ErrorCategory, ErrorSeverity } from "../constants/enums.js";
/**
 * Realtime Processor class for orchestrating realtime voice operations
 *
 * Provides a unified interface for realtime voice across multiple providers.
 *
 * @example
 * ```typescript
 * // Register a handler (typically done in providerRegistry.ts on startup)
 * RealtimeProcessor.registerHandler('openai-realtime', openaiHandler);
 *
 * // Connect to a session — the first arg is the registered handler key,
 * // and `config.provider` must match the same key.
 * const session = await RealtimeProcessor.connect('openai-realtime', {
 *   provider: 'openai-realtime',
 *   voice: 'alloy',
 *   systemPrompt: 'You are a helpful assistant.'
 * });
 *
 * // Send audio
 * await RealtimeProcessor.sendAudio('openai-realtime', audioBuffer);
 *
 * // Disconnect
 * await RealtimeProcessor.disconnect('openai-realtime');
 * ```
 */
export class RealtimeProcessor {
    /**
     * Handler registry mapping provider names to Realtime handlers
     */
    static handlers = new Map();
    /**
     * Active sessions by provider
     */
    static sessions = new Map();
    /**
     * Register a Realtime handler for a specific provider
     *
     * @param providerName - Provider identifier (e.g., 'openai', 'gemini')
     * @param handler - Realtime handler implementation
     */
    static registerHandler(providerName, handler) {
        if (!providerName) {
            throw new Error("Provider name is required");
        }
        if (!handler) {
            throw new Error("Handler is required");
        }
        const normalizedName = providerName.toLowerCase();
        if (this.handlers.has(normalizedName)) {
            logger.warn(`[RealtimeProcessor] Overwriting existing handler for provider: ${normalizedName}`);
        }
        this.handlers.set(normalizedName, handler);
        logger.debug(`[RealtimeProcessor] Registered Realtime handler for provider: ${normalizedName}`);
    }
    /**
     * Get a registered Realtime handler by provider name.
     *
     * Exposed publicly so module-level auto-registration code can reuse an
     * already-registered primary handler when backfilling its aliases.
     */
    static getHandler(providerName) {
        const normalizedName = providerName.toLowerCase();
        return this.handlers.get(normalizedName);
    }
    /**
     * Check if a provider is supported
     */
    static supports(providerName) {
        if (!providerName) {
            return false;
        }
        const normalizedName = providerName.toLowerCase();
        return this.handlers.has(normalizedName);
    }
    /**
     * Get list of all registered providers
     */
    static getProviders() {
        return Array.from(this.handlers.keys());
    }
    /**
     * Connect to a realtime session
     *
     * @param provider - Provider identifier
     * @param config - Session configuration
     * @param handlers - Event handlers
     * @returns Session information
     */
    static async connect(provider, config, handlers) {
        const handler = this.getHandler(provider);
        if (!handler) {
            throw RealtimeError.providerNotSupported(provider, Array.from(this.handlers.keys()));
        }
        if (!handler.isConfigured()) {
            throw RealtimeError.providerNotConfigured(provider);
        }
        // Check for existing session
        if (handler.isConnected()) {
            throw RealtimeError.sessionAlreadyActive(provider);
        }
        // Merge with defaults
        const mergedConfig = {
            ...DEFAULT_REALTIME_CONFIG,
            ...config,
        };
        // Register event handlers if provided
        if (handlers) {
            handler.on(handlers);
        }
        try {
            logger.debug(`[RealtimeProcessor] Connecting to provider: ${provider}`);
            const session = await handler.connect(mergedConfig);
            this.sessions.set(provider.toLowerCase(), session);
            logger.info(`[RealtimeProcessor] Connected to ${provider} session: ${session.id}`);
            return session;
        }
        catch (err) {
            if (handlers) {
                handler.off();
            }
            if (err instanceof RealtimeError) {
                throw err;
            }
            const errorMessage = err instanceof Error ? err.message : String(err || "Unknown error");
            throw RealtimeError.connectionFailed(errorMessage, provider, err instanceof Error ? err : undefined);
        }
    }
    /**
     * Disconnect from a realtime session
     *
     * @param provider - Provider identifier
     */
    static async disconnect(provider) {
        const handler = this.getHandler(provider);
        if (!handler) {
            throw RealtimeError.providerNotSupported(provider, Array.from(this.handlers.keys()));
        }
        if (!handler.isConnected()) {
            logger.warn(`[RealtimeProcessor] No active session for provider: ${provider}`);
            return;
        }
        try {
            await handler.disconnect();
            this.sessions.delete(provider.toLowerCase());
            handler.off();
            logger.info(`[RealtimeProcessor] Disconnected from ${provider}`);
        }
        catch (err) {
            if (err instanceof RealtimeError) {
                throw err;
            }
            const errorMessage = err instanceof Error ? err.message : String(err || "Unknown error");
            throw RealtimeError.protocolError(`Disconnect failed: ${errorMessage}`, provider, err instanceof Error ? err : undefined);
        }
    }
    /**
     * Send audio to a realtime session
     *
     * @param provider - Provider identifier
     * @param audio - Audio data
     */
    static async sendAudio(provider, audio) {
        const handler = this.getHandler(provider);
        if (!handler) {
            throw RealtimeError.providerNotSupported(provider, Array.from(this.handlers.keys()));
        }
        if (!handler.isConnected()) {
            throw RealtimeError.sessionNotActive(provider);
        }
        try {
            await handler.sendAudio(audio);
        }
        catch (err) {
            if (err instanceof RealtimeError) {
                throw err;
            }
            const errorMessage = err instanceof Error ? err.message : String(err || "Unknown error");
            throw RealtimeError.audioStreamError(errorMessage, provider);
        }
    }
    /**
     * Send text to a realtime session
     *
     * @param provider - Provider identifier
     * @param text - Text to send
     */
    static async sendText(provider, text) {
        const handler = this.getHandler(provider);
        if (!handler) {
            throw RealtimeError.providerNotSupported(provider, Array.from(this.handlers.keys()));
        }
        if (!handler.isConnected()) {
            throw RealtimeError.sessionNotActive(provider);
        }
        if (!handler.sendText) {
            throw new RealtimeError({
                code: REALTIME_ERROR_CODES.PROTOCOL_ERROR,
                message: `Provider "${provider}" does not support text input`,
                category: ErrorCategory.VALIDATION,
                severity: ErrorSeverity.MEDIUM,
                context: { provider },
            });
        }
        // Normalize provider exceptions into RealtimeError so callers see a
        // consistent error taxonomy across sendAudio/sendText/triggerResponse/
        // cancelResponse — previously raw provider errors leaked from the
        // text/control paths while sendAudio wrapped them (CodeRabbit review).
        try {
            await handler.sendText(text);
        }
        catch (err) {
            if (err instanceof RealtimeError) {
                throw err;
            }
            throw new RealtimeError({
                code: REALTIME_ERROR_CODES.PROTOCOL_ERROR,
                message: `sendText failed: ${err instanceof Error ? err.message : String(err)}`,
                category: ErrorCategory.NETWORK,
                severity: ErrorSeverity.MEDIUM,
                retriable: true,
                context: { provider },
                originalError: err instanceof Error ? err : undefined,
            });
        }
    }
    /**
     * Trigger a response from the model (manual turn detection)
     *
     * @param provider - Provider identifier
     */
    static async triggerResponse(provider) {
        const handler = this.getHandler(provider);
        if (!handler) {
            throw RealtimeError.providerNotSupported(provider, Array.from(this.handlers.keys()));
        }
        if (!handler.isConnected()) {
            throw RealtimeError.sessionNotActive(provider);
        }
        if (handler.triggerResponse) {
            try {
                await handler.triggerResponse();
            }
            catch (err) {
                if (err instanceof RealtimeError) {
                    throw err;
                }
                throw new RealtimeError({
                    code: REALTIME_ERROR_CODES.PROTOCOL_ERROR,
                    message: `triggerResponse failed: ${err instanceof Error ? err.message : String(err)}`,
                    category: ErrorCategory.NETWORK,
                    severity: ErrorSeverity.MEDIUM,
                    retriable: true,
                    context: { provider },
                    originalError: err instanceof Error ? err : undefined,
                });
            }
        }
    }
    /**
     * Cancel the current response
     *
     * @param provider - Provider identifier
     */
    static async cancelResponse(provider) {
        const handler = this.getHandler(provider);
        if (!handler) {
            throw RealtimeError.providerNotSupported(provider, Array.from(this.handlers.keys()));
        }
        if (!handler.isConnected()) {
            return; // Nothing to cancel
        }
        if (handler.cancelResponse) {
            try {
                await handler.cancelResponse();
            }
            catch (err) {
                if (err instanceof RealtimeError) {
                    throw err;
                }
                throw new RealtimeError({
                    code: REALTIME_ERROR_CODES.PROTOCOL_ERROR,
                    message: `cancelResponse failed: ${err instanceof Error ? err.message : String(err)}`,
                    category: ErrorCategory.NETWORK,
                    severity: ErrorSeverity.MEDIUM,
                    retriable: true,
                    context: { provider },
                    originalError: err instanceof Error ? err : undefined,
                });
            }
        }
    }
    /**
     * Get current session for a provider
     *
     * @param provider - Provider identifier
     * @returns Session or null
     */
    static getSession(provider) {
        const handler = this.getHandler(provider);
        return handler?.getSession() ?? null;
    }
    /**
     * Check if a provider has an active session
     *
     * @param provider - Provider identifier
     */
    static isConnected(provider) {
        const handler = this.getHandler(provider);
        return handler?.isConnected() ?? false;
    }
    /**
     * Get supported formats for a provider
     *
     * @param provider - Provider identifier
     */
    static getSupportedFormats(provider) {
        const handler = this.getHandler(provider);
        return handler?.getSupportedFormats() ?? [];
    }
    /**
     * Clear all handlers and sessions (for testing)
     */
    static clearHandlers() {
        // Disconnect all active sessions
        for (const [provider] of this.sessions) {
            const handler = this.handlers.get(provider);
            if (handler?.isConnected()) {
                handler.disconnect().catch(() => {
                    // Ignore errors during cleanup
                });
            }
        }
        this.handlers.clear();
        this.sessions.clear();
        logger.debug("[RealtimeProcessor] Cleared all handlers and sessions");
    }
}
/**
 * Base Realtime Handler with common functionality
 *
 * Providers can extend this class for common behavior.
 */
export class BaseRealtimeHandler {
    session = null;
    eventHandlers = null;
    state = "disconnected";
    isConnected() {
        return this.state === "connected";
    }
    getSession() {
        return this.session;
    }
    on(handlers) {
        this.eventHandlers = handlers;
    }
    off() {
        this.eventHandlers = null;
    }
    /**
     * Emit state change event
     */
    emitStateChange(newState) {
        this.state = newState;
        if (this.session) {
            this.session.state = newState;
            this.session.lastActivityAt = new Date();
        }
        this.eventHandlers?.onStateChange?.(newState);
    }
    /**
     * Emit audio event
     */
    emitAudio(chunk) {
        this.eventHandlers?.onAudio?.(chunk);
    }
    /**
     * Emit transcript event
     */
    emitTranscript(text, isFinal) {
        this.eventHandlers?.onTranscript?.(text, isFinal);
    }
    /**
     * Emit text event
     */
    emitText(text, isFinal) {
        this.eventHandlers?.onText?.(text, isFinal);
    }
    /**
     * Emit function call event
     */
    async emitFunctionCall(name, args) {
        if (this.eventHandlers?.onFunctionCall) {
            return this.eventHandlers.onFunctionCall(name, args);
        }
        return undefined;
    }
    /**
     * Emit error event
     */
    emitError(error) {
        this.eventHandlers?.onError?.(error);
    }
    /**
     * Emit turn start event
     */
    emitTurnStart() {
        this.eventHandlers?.onTurnStart?.();
    }
    /**
     * Emit turn end event
     */
    emitTurnEnd() {
        this.eventHandlers?.onTurnEnd?.();
    }
    /**
     * Create a session object
     */
    createSession(id, config) {
        return {
            id,
            state: "connected",
            provider: this.name,
            model: config.model,
            createdAt: new Date(),
            lastActivityAt: new Date(),
            config,
        };
    }
}
//# sourceMappingURL=RealtimeVoiceAPI.js.map