"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.setPendingAudioStep = setPendingAudioStep;
exports.handleMediaStreamConnection = handleMediaStreamConnection;
const path_1 = require("path");
const ws_1 = __importDefault(require("ws"));
const esm_1 = require("../../esm");
const logger_1 = __importDefault(require("../../logger"));
const audio_utils_phone_1 = require("../../providers/phone/audio-utils-phone");
const call_data_storage_1 = require("../../providers/phone/call-data-storage");
const constants_1 = require("../../providers/phone/constants");
const call_billing_1 = require("../../providers/phone/call-billing");
const pendingAudioSteps = new Map();
function setPendingAudioStep(callSid, audioUrl, stepIndex, nextStepUrl, shouldListen = true, startAfterSeconds = null, consecutiveSteps = []) {
    pendingAudioSteps.set(callSid, {
        audioUrl,
        stepIndex,
        nextStepUrl,
        shouldListen,
        startAfterSeconds,
        consecutiveSteps,
    });
    logger_1.default.info(`[Media Stream] Stored pending audio step for call ${callSid}, shouldListen: ${shouldListen}, startAfter: ${startAfterSeconds}, consecutive steps: ${consecutiveSteps.length}`);
}
/**
 * Handle WebSocket connection for Twilio Media Stream
 * Full implementation with OpenAI Realtime API integration
 */
function handleMediaStreamConnection(ws, req) {
    logger_1.default.info('[Media Stream] Client connected');
    logger_1.default.info(`[Media Stream] WebSocket URL: ${req.url}`);
    logger_1.default.info(`[Media Stream] Headers: ${JSON.stringify(req.headers)}`);
    // Parse query parameters from URL
    // req.url should contain the path and query string (e.g., "/api/phone/media-stream?outbound=true&callSid=xxx")
    let isOutboundCall = false;
    let callSid = null;
    let audioStepIndex = null;
    let audioUrl = null;
    let nextStepUrl = null;
    if (req.url) {
        try {
            // Try parsing as a full URL first (in case it includes protocol)
            let url;
            if (req.url.startsWith('http://') ||
                req.url.startsWith('https://') ||
                req.url.startsWith('ws://') ||
                req.url.startsWith('wss://')) {
                url = new URL(req.url);
            }
            else {
                // Parse as relative URL with host from headers
                const host = req.headers.host || 'localhost';
                const protocol = req.headers['x-forwarded-proto'] === 'https' ? 'https' : 'http';
                url = new URL(req.url, `${protocol}://${host}`);
            }
            isOutboundCall = url.searchParams.get('outbound') === 'true';
            callSid = url.searchParams.get('callSid');
            audioStepIndex = url.searchParams.get('audioStep');
            audioUrl = url.searchParams.get('audioUrl');
            nextStepUrl = url.searchParams.get('nextStepUrl');
            logger_1.default.info(`[Media Stream] Successfully parsed URL: ${url.toString()}`);
            logger_1.default.info(`[Media Stream] All search params: ${JSON.stringify(Object.fromEntries(url.searchParams))}`);
        }
        catch (error) {
            logger_1.default.error(`[Media Stream] Error parsing URL: ${error.message}`);
            logger_1.default.error(`[Media Stream] Raw req.url: ${req.url}`);
            // Fallback: try manual parsing of query string
            if (req.url.includes('?')) {
                const queryString = req.url.split('?')[1];
                const params = new URLSearchParams(queryString);
                isOutboundCall = params.get('outbound') === 'true';
                callSid = params.get('callSid');
                audioStepIndex = params.get('audioStep');
                audioUrl = params.get('audioUrl');
                nextStepUrl = params.get('nextStepUrl');
                logger_1.default.info(`[Media Stream] Fallback parsing successful from query string`);
            }
        }
    }
    logger_1.default.info(`[Media Stream] Parsed params: ${JSON.stringify({
        isOutboundCall,
        callSid,
        audioStepIndex,
        audioUrl,
        nextStepUrl,
    })}`);
    // Connection-specific state
    let streamSid = null;
    let latestMediaTimestamp = 0;
    let lastAssistantItem = null;
    let markQueue = [];
    let responseStartTimestampTwilio = null;
    let connectionClosed = false;
    let isStreamingAudio = false;
    let waitingForUserResponseAfterAudio = false;
    let nextStepUrlAfterAudio = null;
    // Timer for startAfter interruption (will be set when user starts speaking)
    let startAfterTimer = null;
    let startAfterSecondsValue = null; // Store startAfter value
    let consecutiveSteps = []; // Consecutive steps to execute
    // Track all active timeouts for cleanup
    const activeTimeouts = new Set();
    // Create a reference object for latestMediaTimestamp for use in async functions
    const latestMediaTimestampRef = { current: 0 };
    // Billing tracking state
    let totalInputAudioBytes = 0;
    let totalOutputAudioBytes = 0;
    // Get call length from call data if available (async, will update if found)
    let callLengthSeconds = 180; // Default 3 minutes
    if (callSid) {
        // Load call data asynchronously and update call length if found
        (0, call_data_storage_1.getCallData)(callSid)
            .then((callData) => {
            if (callData && callData.totalCallLength) {
                callLengthSeconds = callData.totalCallLength;
                logger_1.default.info(`[Media Stream] Updated call length from call data: ${callLengthSeconds} seconds`);
            }
        })
            .catch((err) => {
            logger_1.default.warn(`Failed to get call data for call length: ${err}`);
        });
    }
    // Initialize OpenAI WebSocket connection
    const { OPENAI_API_KEY } = process.env;
    if (!OPENAI_API_KEY) {
        logger_1.default.error('[Media Stream] OPENAI_API_KEY not set in environment');
        try {
            if (ws.readyState === ws_1.default.OPEN || ws.readyState === ws_1.default.CONNECTING) {
                ws.close();
            }
        }
        catch (closeErr) {
            logger_1.default.warn(`[Media Stream] Error closing connection (no API key): ${closeErr.message}`);
        }
        return;
    }
    const openAiWs = new ws_1.default(`wss://api.openai.com/v1/realtime?model=gpt-realtime&temperature=${constants_1.TEMPERATURE}`, {
        headers: {
            Authorization: `Bearer ${OPENAI_API_KEY}`,
        },
    });
    // Track if session has been initialized (we'll initialize after receiving start event with callSid)
    let sessionInitialized = false;
    // Helper function to create timeout with tracking
    const createTrackedTimeout = (callback, delay) => {
        const timeout = setTimeout(() => {
            activeTimeouts.delete(timeout);
            if (!connectionClosed) {
                callback();
            }
        }, delay);
        activeTimeouts.add(timeout);
        return timeout;
    };
    // Helper function to clear all timeouts
    const clearAllTimeouts = () => {
        activeTimeouts.forEach((timeout) => clearTimeout(timeout));
        activeTimeouts.clear();
    };
    // Set call timeout (tracked automatically in activeTimeouts)
    createTrackedTimeout(() => {
        logger_1.default.info(`[Media Stream] Call timeout reached (${callLengthSeconds} seconds) - disconnecting`);
        connectionClosed = true;
        if (ws.readyState === ws_1.default.OPEN) {
            try {
                ws.close();
            }
            catch (closeErr) {
                logger_1.default.warn(`[Media Stream] Error closing connection (call timeout): ${closeErr.message}`);
            }
        }
        if (openAiWs.readyState === ws_1.default.OPEN) {
            openAiWs.close();
        }
    }, callLengthSeconds * 1000);
    // Send initial conversation item if AI talks first
    const sendInitialConversationItem = () => {
        const initialConversationItem = {
            type: 'conversation.item.create',
            item: {
                type: 'message',
                role: 'user',
                content: [
                    {
                        type: 'input_text',
                        text: 'Greet the user with "Hello there! I am an AI voice assistant powered by Twilio and the OpenAI Realtime API. You can ask me for facts, jokes, or anything you can imagine. How can I help you?"',
                    },
                ],
            },
        };
        if (constants_1.SHOW_TIMING_MATH) {
            logger_1.default.info(`Sending initial conversation item: ${JSON.stringify(initialConversationItem)}`);
        }
        try {
            if (openAiWs.readyState === ws_1.default.OPEN && !connectionClosed) {
                openAiWs.send(JSON.stringify(initialConversationItem));
                openAiWs.send(JSON.stringify({ type: 'response.create' }));
            }
        }
        catch (err) {
            logger_1.default.warn(`[Media Stream] Error sending initial conversation item: ${err.message || err}`);
            if (err.message?.includes('EOF') || err.code === 'EPIPE') {
                connectionClosed = true;
            }
        }
    };
    // Control initial session with OpenAI
    const initializeSession = async () => {
        logger_1.default.info(`[Media Stream] initializeSession called with callSid: ${callSid}, isOutboundCall: ${isOutboundCall}`);
        // Get system prompt from call data if available, otherwise use default
        let systemPromptToUse = constants_1.SYSTEM_MESSAGE;
        if (callSid) {
            try {
                const callData = await (0, call_data_storage_1.getCallData)(callSid);
                logger_1.default.info(`[Media Stream] Call data lookup for ${callSid}: ${callData ? 'found' : 'not found'}`);
                if (callData && callData.systemPrompt) {
                    systemPromptToUse = callData.systemPrompt;
                    logger_1.default.info('[Media Stream] Using custom system prompt from call data');
                }
            }
            catch (err) {
                logger_1.default.warn(`Failed to get call data for system prompt: ${err}`);
            }
        }
        else {
            logger_1.default.warn('[Media Stream] callSid is null - cannot retrieve call data');
        }
        const sessionUpdate = {
            type: 'session.update',
            session: {
                type: 'realtime',
                model: 'gpt-realtime',
                output_modalities: ['audio'],
                audio: {
                    input: { format: { type: 'audio/pcmu' }, turn_detection: { type: 'server_vad' } },
                    output: { format: { type: 'audio/pcmu' }, voice: constants_1.VOICE },
                },
                instructions: systemPromptToUse,
            },
        };
        logger_1.default.info(`Sending session update: ${JSON.stringify(sessionUpdate)}`);
        try {
            if (openAiWs.readyState === ws_1.default.OPEN && !connectionClosed) {
                openAiWs.send(JSON.stringify(sessionUpdate));
            }
        }
        catch (err) {
            logger_1.default.warn(`[Media Stream] Error sending session update: ${err.message || err}`);
            if (err.message?.includes('EOF') || err.code === 'EPIPE') {
                connectionClosed = true;
            }
        }
        // Determine if AI should speak first
        // Priority: 1. Call data flag (aiSpeaksFirst), 2. Query parameter (isOutboundCall)
        let shouldAiSpeakFirst = isOutboundCall; // Default to query parameter
        logger_1.default.info(`[Media Stream] Initial shouldAiSpeakFirst from isOutboundCall: ${shouldAiSpeakFirst}`);
        if (callSid) {
            try {
                const callData = await (0, call_data_storage_1.getCallData)(callSid);
                if (callData && callData.aiSpeaksFirst !== undefined) {
                    shouldAiSpeakFirst = callData.aiSpeaksFirst;
                    logger_1.default.info(`[Media Stream] Using aiSpeaksFirst flag from call data: ${shouldAiSpeakFirst}`);
                }
                else {
                    logger_1.default.info(`[Media Stream] Call data for ${callSid} does not have aiSpeaksFirst flag, using isOutboundCall: ${isOutboundCall}`);
                }
            }
            catch (err) {
                logger_1.default.warn(`Failed to get call data for aiSpeaksFirst: ${err}`);
            }
        }
        else {
            logger_1.default.warn(`[Media Stream] callSid is null, using isOutboundCall (${isOutboundCall}) as fallback`);
        }
        // Check if this is an audio-only step (not a realtime step)
        // For audio steps, we should NOT send initial greeting - just stream the audio file
        // Check pendingAudioSteps to see if this is an audio step
        let isAudioStep = false;
        if (callSid) {
            const audioStepInfo = pendingAudioSteps.get(callSid);
            if (audioStepInfo && audioStepInfo.audioUrl) {
                isAudioStep = true;
                logger_1.default.info(`[Media Stream] Detected audio step from pendingAudioSteps for call ${callSid}`);
            }
        }
        if (shouldAiSpeakFirst && !isAudioStep) {
            // Only send greeting for realtime steps, not audio steps
            logger_1.default.info('[Media Stream] AI will speak first - sending initial greeting');
            sendInitialConversationItem();
        }
        else if (isAudioStep) {
            logger_1.default.info('[Media Stream] Audio step detected - skipping initial greeting (audio file will play instead)');
        }
        else {
            logger_1.default.info('[Media Stream] Waiting for user to speak first');
        }
    };
    // Handle interruption when the caller's speech starts
    const handleSpeechStartedEvent = () => {
        // Set up startAfter timer if specified and we're waiting for user response
        if (startAfterSecondsValue !== null &&
            startAfterSecondsValue > 0 &&
            waitingForUserResponseAfterAudio &&
            nextStepUrlAfterAudio &&
            startAfterTimer === null) {
            logger_1.default.info(`[Media Stream] User started speaking. Setting startAfter timer for ${startAfterSecondsValue} seconds.`);
            const redirectUrl = nextStepUrlAfterAudio; // Capture the URL before timer expires
            startAfterTimer = setTimeout(() => {
                if (connectionClosed) {
                    return;
                }
                logger_1.default.info(`[Media Stream] StartAfter timer expired (${startAfterSecondsValue}s). Interrupting user and redirecting.`);
                waitingForUserResponseAfterAudio = false;
                nextStepUrlAfterAudio = null;
                startAfterTimer = null;
                if (ws.readyState === ws_1.default.OPEN && !connectionClosed) {
                    try {
                        const stopEvent = { event: 'stop', streamSid };
                        ws.send(JSON.stringify(stopEvent));
                        logger_1.default.info('[Media Stream] Sent stop event to Twilio (startAfter)');
                    }
                    catch (err) {
                        logger_1.default.warn(`[Media Stream] Error sending stop event (startAfter): ${err.message}`);
                    }
                    try {
                        if (ws.readyState === ws_1.default.OPEN || ws.readyState === ws_1.default.CONNECTING) {
                            ws.close();
                        }
                    }
                    catch (closeErr) {
                        logger_1.default.warn(`[Media Stream] Error closing connection (startAfter): ${closeErr.message}`);
                    }
                    logger_1.default.info(`[Media Stream] Stream connection closed (startAfter). Twilio will redirect to: ${redirectUrl}`);
                }
            }, startAfterSecondsValue * 1000);
        }
        if (markQueue.length > 0 && responseStartTimestampTwilio != null) {
            const elapsedTime = latestMediaTimestamp - responseStartTimestampTwilio;
            if (constants_1.SHOW_TIMING_MATH) {
                logger_1.default.info(`Calculating elapsed time for truncation: ${latestMediaTimestamp} - ${responseStartTimestampTwilio} = ${elapsedTime}ms`);
            }
            if (lastAssistantItem) {
                const truncateEvent = {
                    type: 'conversation.item.truncate',
                    item_id: lastAssistantItem,
                    content_index: 0,
                    audio_end_ms: elapsedTime,
                };
                if (constants_1.SHOW_TIMING_MATH) {
                    logger_1.default.info(`Sending truncation event: ${JSON.stringify(truncateEvent)}`);
                }
                try {
                    if (openAiWs.readyState === ws_1.default.OPEN && !connectionClosed) {
                        openAiWs.send(JSON.stringify(truncateEvent));
                    }
                }
                catch (err) {
                    logger_1.default.warn(`[Media Stream] Error sending truncation event: ${err.message || err}`);
                    if (err.message?.includes('EOF') || err.code === 'EPIPE') {
                        connectionClosed = true;
                    }
                }
            }
            try {
                if (ws.readyState === ws_1.default.OPEN && !connectionClosed) {
                    ws.send(JSON.stringify({
                        event: 'clear',
                        streamSid,
                    }));
                }
            }
            catch (err) {
                logger_1.default.warn(`[Media Stream] Error sending clear event: ${err.message || err}`);
                if (err.message?.includes('EOF') || err.code === 'EPIPE') {
                    connectionClosed = true;
                }
            }
            // Reset
            markQueue = [];
            lastAssistantItem = null;
            responseStartTimestampTwilio = null;
        }
    };
    // Send mark messages to Media Streams
    const sendMark = (streamSid) => {
        if (streamSid && !connectionClosed) {
            try {
                if (ws.readyState === ws_1.default.OPEN) {
                    const markEvent = {
                        event: 'mark',
                        streamSid,
                        mark: { name: 'responsePart' },
                    };
                    ws.send(JSON.stringify(markEvent));
                    markQueue.push('responsePart');
                }
            }
            catch (err) {
                logger_1.default.warn(`[Media Stream] Error sending mark event: ${err.message || err}`);
                if (err.message?.includes('EOF') || err.code === 'EPIPE') {
                    connectionClosed = true;
                }
            }
        }
    };
    // Open event for OpenAI WebSocket
    openAiWs.on('open', () => {
        logger_1.default.info('Connected to the OpenAI Realtime API');
        // If we already have callSid from query params, initialize immediately
        // Otherwise, wait for Twilio start event to get callSid
        if (callSid) {
            logger_1.default.info('[Media Stream] callSid available from query params, initializing session');
            sessionInitialized = true;
            setTimeout(() => {
                if (!connectionClosed) {
                    initializeSession().catch((err) => {
                        logger_1.default.error(`Error in initializeSession: ${err}`);
                    });
                }
            }, 100);
        }
        else {
            logger_1.default.info('[Media Stream] Waiting for start event to get callSid before initializing session');
        }
    });
    // Listen for messages from the OpenAI WebSocket
    openAiWs.on('message', (data) => {
        if (connectionClosed) {
            return;
        }
        try {
            const response = JSON.parse(data.toString());
            if (constants_1.LOG_EVENT_TYPES.includes(response.type)) {
                logger_1.default.info(`Received event: ${response.type} ${JSON.stringify(response)}`);
            }
            if (response.type === 'response.output_audio.delta' && response.delta && !connectionClosed) {
                if (!streamSid) {
                    logger_1.default.warn('[Media Stream] Received audio delta but streamSid is not set yet');
                    return;
                }
                // Track output audio bytes (PCMU is 8kHz, 1 byte per sample)
                // Each delta is base64 encoded PCMU audio
                try {
                    const audioBytes = Buffer.from(response.delta, 'base64').length;
                    totalOutputAudioBytes += audioBytes;
                }
                catch (err) {
                    logger_1.default.warn(`[Media Stream] Error tracking output audio: ${err}`);
                }
                const audioDelta = {
                    event: 'media',
                    streamSid,
                    media: { payload: response.delta },
                };
                try {
                    if (ws.readyState === ws_1.default.OPEN && !connectionClosed) {
                        ws.send(JSON.stringify(audioDelta));
                    }
                }
                catch (err) {
                    logger_1.default.warn(`[Media Stream] Error sending audio delta: ${err.message || err}`);
                    if (err.message?.includes('EOF') || err.code === 'EPIPE') {
                        connectionClosed = true;
                    }
                }
                // First delta from a new response starts the elapsed time counter
                if (!responseStartTimestampTwilio) {
                    responseStartTimestampTwilio = latestMediaTimestamp;
                    if (constants_1.SHOW_TIMING_MATH) {
                        logger_1.default.info(`Setting start timestamp for new response: ${responseStartTimestampTwilio}ms`);
                    }
                }
                if (response.item_id) {
                    lastAssistantItem = response.item_id;
                }
                sendMark(streamSid);
            }
            // Track usage when OpenAI sends response.done event
            if (response.type === 'response.done' && response.response?.usage) {
                const usage = response.response.usage;
                logger_1.default.info(`[Media Stream] Received usage data from OpenAI: ${JSON.stringify(usage)}`);
                // Calculate audio seconds from bytes (PCMU at 8kHz: 1 byte per sample, 8000 samples per second)
                const inputAudioSeconds = totalInputAudioBytes / 8000;
                const outputAudioSeconds = totalOutputAudioBytes / 8000;
                // Track usage in billing storage (stored in Redis or in-memory fallback)
                if (callSid) {
                    (0, call_billing_1.trackOpenAIRealtimeUsage)(callSid, {
                        input_tokens: usage.input_tokens || 0,
                        output_tokens: usage.output_tokens || 0,
                        input_audio_seconds: usage.input_audio_seconds || inputAudioSeconds || 0,
                        output_audio_seconds: usage.output_audio_seconds || outputAudioSeconds || 0,
                    }).catch((error) => {
                        logger_1.default.warn(`[Media Stream] Error tracking Realtime usage: ${error}`);
                    });
                }
            }
            if (response.type === 'input_audio_buffer.speech_started') {
                handleSpeechStartedEvent();
            }
            // When user finishes speaking after an audio step, close stream and redirect
            if ((response.type === 'input_audio_buffer.committed' ||
                response.type === 'input_audio_buffer.speech_stopped') &&
                waitingForUserResponseAfterAudio &&
                nextStepUrlAfterAudio) {
                // Clear startAfter timer if user finished speaking before timeout
                if (startAfterTimer) {
                    clearTimeout(startAfterTimer);
                    startAfterTimer = null;
                }
                logger_1.default.info(`[Media Stream] User finished speaking after audio step. Closing stream and redirecting to: ${nextStepUrlAfterAudio}`);
                waitingForUserResponseAfterAudio = false;
                const redirectUrl = nextStepUrlAfterAudio;
                nextStepUrlAfterAudio = null;
                // Close the WebSocket connection to allow Twilio to continue to the next step
                setTimeout(() => {
                    try {
                        if (connectionClosed) {
                            return;
                        }
                        // Double-check connection state before attempting any operations
                        if (!connectionClosed && ws.readyState === ws_1.default.OPEN) {
                            // Send a stop event to Twilio before closing
                            try {
                                const stopEvent = {
                                    event: 'stop',
                                    streamSid,
                                };
                                ws.send(JSON.stringify(stopEvent));
                                logger_1.default.info('[Media Stream] Sent stop event to Twilio');
                            }
                            catch (err) {
                                logger_1.default.warn(`[Media Stream] Error sending stop event: ${err.message || err}`);
                            }
                        }
                        // Close the connection if still open
                        if (!connectionClosed && (ws.readyState === ws_1.default.OPEN || ws.readyState === ws_1.default.CONNECTING)) {
                            try {
                                ws.close();
                            }
                            catch (closeErr) {
                                logger_1.default.warn(`[Media Stream] Error closing connection: ${closeErr.message || closeErr}`);
                            }
                        }
                        logger_1.default.info(`[Media Stream] Stream connection closed. Twilio will redirect to: ${redirectUrl}`);
                    }
                    catch (error) {
                        // Catch any unexpected errors to prevent crash
                        logger_1.default.error(`[Media Stream] Unexpected error in timeout handler: ${error.message || error}`);
                    }
                }, 500); // Small delay to ensure all events are processed
            }
        }
        catch (error) {
            logger_1.default.error(`Error processing OpenAI message: ${error.message}. Raw message: ${data}`);
        }
    });
    // Try to get audio step info from query params or pendingAudioSteps
    let storedAudioUrl = audioUrl || null;
    let storedNextStepUrl = nextStepUrl || null;
    let audioStepIndexNum = audioStepIndex
        ? Number.parseInt(audioStepIndex, 10)
        : null;
    let shouldListenAfterAudio = true; // Default to listening
    // Always check pendingAudioSteps to get consecutiveSteps and other metadata
    if (callSid) {
        const audioStepInfo = pendingAudioSteps.get(callSid);
        if (audioStepInfo) {
            // Only override storedAudioUrl if it's not already set
            if (!storedAudioUrl) {
                storedAudioUrl = audioStepInfo.audioUrl;
            }
            // Only override storedNextStepUrl if it's not already set
            if (!storedNextStepUrl) {
                storedNextStepUrl = audioStepInfo.nextStepUrl;
            }
            // Only override audioStepIndexNum if it's not already set
            if (audioStepIndexNum === null) {
                audioStepIndexNum = audioStepInfo.stepIndex;
            }
            // Always update these from pendingAudioSteps as they contain important metadata
            shouldListenAfterAudio = audioStepInfo.shouldListen ?? true;
            startAfterSecondsValue = audioStepInfo.startAfterSeconds || null;
            consecutiveSteps = audioStepInfo.consecutiveSteps || [];
            logger_1.default.info(`[Media Stream] Found audio step from pendingAudioSteps for call ${callSid}, shouldListen: ${shouldListenAfterAudio}, startAfter: ${startAfterSecondsValue}, consecutive steps: ${consecutiveSteps.length}`);
        }
    }
    // Handle incoming messages from Twilio
    ws.on('message', (message) => {
        if (connectionClosed) {
            return;
        }
        try {
            const data = JSON.parse(message.toString());
            switch (data.event) {
                case 'media':
                    latestMediaTimestamp = data.media.timestamp;
                    latestMediaTimestampRef.current = latestMediaTimestamp;
                    if (constants_1.SHOW_TIMING_MATH) {
                        logger_1.default.info(`Received media message with timestamp: ${latestMediaTimestamp}ms`);
                    }
                    // Track input audio bytes (PCMU is 8kHz, 1 byte per sample)
                    // Only track when not streaming audio files (real-time conversation)
                    if (!isStreamingAudio && data.media.payload) {
                        try {
                            const audioBytes = Buffer.from(data.media.payload, 'base64').length;
                            totalInputAudioBytes += audioBytes;
                        }
                        catch (err) {
                            logger_1.default.warn(`[Media Stream] Error tracking input audio: ${err}`);
                        }
                    }
                    // Don't send audio to OpenAI during audio file streaming (only for real-time conversation)
                    if (openAiWs.readyState === ws_1.default.OPEN && !connectionClosed && !isStreamingAudio) {
                        try {
                            const audioAppend = {
                                type: 'input_audio_buffer.append',
                                audio: data.media.payload,
                            };
                            openAiWs.send(JSON.stringify(audioAppend));
                        }
                        catch (err) {
                            logger_1.default.warn(`[Media Stream] Error sending audio to OpenAI: ${err.message || err}`);
                            if (err.message?.includes('EOF') || err.code === 'EPIPE') {
                                connectionClosed = true;
                            }
                        }
                    }
                    break;
                case 'start':
                    streamSid = data.start?.streamSid || null;
                    // Extract callSid from start event if not already available from query params
                    const startCallSid = data.start?.callSid || null;
                    if (startCallSid) {
                        // Update callSid if we didn't have it before
                        if (!callSid) {
                            callSid = startCallSid;
                            logger_1.default.info(`[Media Stream] Extracted callSid from start event: ${callSid}`);
                        }
                        // Update call length if available
                        (0, call_data_storage_1.getCallData)(startCallSid)
                            .then((callData) => {
                            if (callData && callData.totalCallLength) {
                                callLengthSeconds = callData.totalCallLength;
                                logger_1.default.info(`[Media Stream] Updated call length from call data: ${callLengthSeconds} seconds`);
                            }
                        })
                            .catch((err) => {
                            logger_1.default.warn(`Failed to get call data for call length update: ${err}`);
                        });
                        // Check pendingAudioSteps again now that we have callSid
                        // Always check to get consecutiveSteps and other metadata, even if storedAudioUrl is already set
                        if (startCallSid) {
                            const audioStepInfo = pendingAudioSteps.get(startCallSid);
                            if (audioStepInfo) {
                                // Only override if not already set
                                if (!storedAudioUrl) {
                                    storedAudioUrl = audioStepInfo.audioUrl;
                                }
                                if (!storedNextStepUrl) {
                                    storedNextStepUrl = audioStepInfo.nextStepUrl;
                                }
                                if (audioStepIndexNum === null) {
                                    audioStepIndexNum = audioStepInfo.stepIndex;
                                }
                                // Always update these from pendingAudioSteps as they contain important metadata
                                shouldListenAfterAudio = audioStepInfo.shouldListen ?? true;
                                startAfterSecondsValue = audioStepInfo.startAfterSeconds || null;
                                consecutiveSteps = audioStepInfo.consecutiveSteps || [];
                                logger_1.default.info(`[Media Stream] Found audio step from pendingAudioSteps for call ${startCallSid} (after start event), shouldListen: ${shouldListenAfterAudio}, startAfter: ${startAfterSecondsValue}, consecutive steps: ${consecutiveSteps.length}`);
                            }
                        }
                        // Now initialize the session with the correct callSid
                        // But first check if this is an audio step - if so, we'll skip the greeting
                        if (!sessionInitialized && openAiWs.readyState === ws_1.default.OPEN) {
                            logger_1.default.info('[Media Stream] Initializing session now that we have callSid');
                            sessionInitialized = true;
                            // Re-check storedAudioUrl after getting callSid from start event
                            // Always check to get consecutiveSteps and other metadata
                            if (startCallSid) {
                                const audioStepInfo = pendingAudioSteps.get(startCallSid);
                                if (audioStepInfo) {
                                    // Only override if not already set
                                    if (!storedAudioUrl) {
                                        storedAudioUrl = audioStepInfo.audioUrl;
                                    }
                                    if (!storedNextStepUrl) {
                                        storedNextStepUrl = audioStepInfo.nextStepUrl;
                                    }
                                    // Always update these from pendingAudioSteps as they contain important metadata
                                    shouldListenAfterAudio = audioStepInfo.shouldListen ?? true;
                                    startAfterSecondsValue = audioStepInfo.startAfterSeconds || null;
                                    consecutiveSteps = audioStepInfo.consecutiveSteps || [];
                                    audioStepIndexNum = audioStepInfo.stepIndex;
                                    logger_1.default.info(`[Media Stream] Found audio step from pendingAudioSteps for call ${startCallSid} (before initializing session)`);
                                }
                            }
                            setTimeout(() => {
                                initializeSession().catch((err) => {
                                    logger_1.default.error(`Error in initializeSession: ${err}`);
                                });
                            }, 100);
                        }
                    }
                    logger_1.default.info(`Incoming stream has started ${streamSid}`);
                    logger_1.default.info(`[Media Stream] Stream SID: ${streamSid} Ready to receive/send media`);
                    logger_1.default.info(`[Media Stream] Call SID from start event: ${startCallSid || 'not provided'}`);
                    // Reset timestamps on stream start
                    responseStartTimestampTwilio = null;
                    latestMediaTimestamp = 0;
                    latestMediaTimestampRef.current = 0;
                    // If this is an audio file step OR a listening step (no audio URL), handle accordingly
                    if (streamSid && !isStreamingAudio) {
                        if (storedAudioUrl) {
                            // Audio file step - stream the audio
                            logger_1.default.info(`[Media Stream] Audio file step - stream connected, will stream audio: ${storedAudioUrl}`);
                            isStreamingAudio = true;
                        }
                        else {
                            // Listening step (next step) - just listen, don't stream audio
                            logger_1.default.info(`[Media Stream] Listening step - stream connected, will listen to client response`);
                            // Initialize session but don't send greeting - wait for user to speak
                            if (!sessionInitialized && openAiWs.readyState === ws_1.default.OPEN) {
                                sessionInitialized = true;
                                setTimeout(() => {
                                    initializeSession().catch((err) => {
                                        logger_1.default.error(`Error in initializeSession: ${err}`);
                                    });
                                }, 100);
                            }
                            // Set up listening flags
                            if (callSid && storedNextStepUrl) {
                                waitingForUserResponseAfterAudio = true;
                                nextStepUrlAfterAudio = storedNextStepUrl;
                                logger_1.default.info(`[Media Stream] Waiting for user to speak. Will redirect to: ${storedNextStepUrl} after user finishes speaking`);
                                // Set up startAfter timer if specified
                                if (startAfterSecondsValue !== null && startAfterSecondsValue > 0) {
                                    logger_1.default.info(`[Media Stream] startAfter enabled: will interrupt client after ${startAfterSecondsValue} seconds of speaking`);
                                }
                                // Clean up pending step
                                if (audioStepIndexNum !== null) {
                                    pendingAudioSteps.delete(callSid);
                                    logger_1.default.info(`[Media Stream] Cleaned up pending step for call ${callSid}`);
                                }
                                // Set a timeout in case user doesn't speak - advance after 10 seconds
                                setTimeout(() => {
                                    try {
                                        if (connectionClosed) {
                                            return;
                                        }
                                        if (waitingForUserResponseAfterAudio && nextStepUrlAfterAudio) {
                                            logger_1.default.info(`[Media Stream] Timeout waiting for user response. Advancing to next step: ${nextStepUrlAfterAudio}`);
                                            waitingForUserResponseAfterAudio = false;
                                            const redirectUrl = nextStepUrlAfterAudio;
                                            nextStepUrlAfterAudio = null;
                                            // Double-check connection state before attempting any operations
                                            if (!connectionClosed && ws.readyState === ws_1.default.OPEN) {
                                                try {
                                                    const stopEvent = {
                                                        event: 'stop',
                                                        streamSid,
                                                    };
                                                    ws.send(JSON.stringify(stopEvent));
                                                    logger_1.default.info('[Media Stream] Sent stop event to Twilio (timeout)');
                                                }
                                                catch (err) {
                                                    logger_1.default.warn(`[Media Stream] Error sending stop event: ${err.message || err}`);
                                                }
                                            }
                                            // Close connection if still open
                                            if (!connectionClosed && (ws.readyState === ws_1.default.OPEN || ws.readyState === ws_1.default.CONNECTING)) {
                                                try {
                                                    ws.close();
                                                }
                                                catch (closeErr) {
                                                    logger_1.default.warn(`[Media Stream] Error closing connection (timeout): ${closeErr.message || closeErr}`);
                                                }
                                            }
                                            logger_1.default.info(`[Media Stream] Stream connection closed (timeout). Twilio will redirect to: ${redirectUrl}`);
                                        }
                                    }
                                    catch (error) {
                                        // Catch any unexpected errors to prevent crash
                                        logger_1.default.error(`[Media Stream] Unexpected error in timeout handler: ${error.message || error}`);
                                    }
                                }, 10000); // 10 second timeout
                            }
                            break; // Don't proceed to audio streaming
                        }
                        // Capture timestamp immediately
                        const audioStreamStartTimestamp = latestMediaTimestamp || 0;
                        latestMediaTimestampRef.current = audioStreamStartTimestamp;
                        logger_1.default.info(`[Media Stream] Captured audio stream start timestamp: ${audioStreamStartTimestamp}ms (latestMediaTimestamp: ${latestMediaTimestamp})`);
                        // Wait for stream to be fully ready (1 second to ensure Twilio is ready)
                        setTimeout(async () => {
                            try {
                                if (connectionClosed) {
                                    return;
                                }
                                const startTimestamp = latestMediaTimestampRef.current;
                                logger_1.default.info(`[Media Stream] Starting audio file streaming, streamSid: ${streamSid}`);
                                logger_1.default.info(`[Media Stream] Audio stream start timestamp: ${startTimestamp}ms`);
                                // Stream audio file through the media stream
                                try {
                                    const tempDir = (0, path_1.join)((0, esm_1.getDirectory)(), 'temp');
                                    // Stream the first audio file
                                    await (0, audio_utils_phone_1.streamAudioFile)(ws, streamSid, storedAudioUrl, latestMediaTimestampRef, () => connectionClosed, tempDir);
                                    logger_1.default.info('[Media Stream] First audio file streaming completed');
                                    // Execute consecutive steps in sequence (includes pre-audio steps)
                                    if (consecutiveSteps.length > 0) {
                                        logger_1.default.info(`[Media Stream] Executing ${consecutiveSteps.length} consecutive steps (including pre-audio steps)`);
                                        for (const consecutiveStep of consecutiveSteps) {
                                            if (connectionClosed) {
                                                logger_1.default.info('[Media Stream] Connection closed during consecutive steps execution');
                                                break;
                                            }
                                            if (consecutiveStep.type === 'audio') {
                                                // Stream another audio file
                                                logger_1.default.info(`[Media Stream] Streaming consecutive audio: ${consecutiveStep.value}`);
                                                await (0, audio_utils_phone_1.streamAudioFile)(ws, streamSid, consecutiveStep.value, latestMediaTimestampRef, () => connectionClosed, tempDir);
                                                logger_1.default.info(`[Media Stream] Consecutive audio file streaming completed`);
                                            }
                                            else if (consecutiveStep.type === 'delay') {
                                                // Add a delay
                                                const delaySeconds = Number.parseInt(consecutiveStep.value, 10);
                                                if (!Number.isNaN(delaySeconds) && delaySeconds > 0) {
                                                    logger_1.default.info(`[Media Stream] Adding delay of ${delaySeconds} seconds`);
                                                    // Send silent chunks for the delay duration
                                                    const silentChunk = Buffer.alloc(160, 0xff).toString('base64'); // 20ms of silence
                                                    const numChunks = Math.ceil((delaySeconds * 1000) / 20); // Number of 20ms chunks
                                                    for (let i = 0; i < numChunks; i++) {
                                                        if (connectionClosed) {
                                                            break;
                                                        }
                                                        const mediaEvent = {
                                                            event: 'media',
                                                            streamSid,
                                                            media: { payload: silentChunk },
                                                        };
                                                        if (ws.readyState === ws_1.default.OPEN) {
                                                            ws.send(JSON.stringify(mediaEvent));
                                                        }
                                                        await new Promise((resolve) => setTimeout(resolve, 20));
                                                    }
                                                    logger_1.default.info(`[Media Stream] Delay of ${delaySeconds} seconds completed`);
                                                }
                                            }
                                            else if (consecutiveStep.type === 'keyboard') {
                                                // Send DTMF tones via TwiML - we can't send DTMF through media stream
                                                // Log it for now - ideally keyboard steps should be handled before media stream
                                                logger_1.default.info(`[Media Stream] Keyboard step encountered: ${consecutiveStep.value} (DTMF should be sent via TwiML before media stream)`);
                                                // Note: Keyboard steps in pre-audio position might not work perfectly
                                                // as they need to be sent via TwiML, not media stream
                                            }
                                        }
                                        logger_1.default.info('[Media Stream] All consecutive steps completed');
                                    }
                                    isStreamingAudio = false;
                                    latestMediaTimestamp = latestMediaTimestampRef.current;
                                    if (storedNextStepUrl) {
                                        logger_1.default.info(`[Media Stream] Sequence complete. Next step URL: ${storedNextStepUrl}, shouldListen: ${shouldListenAfterAudio}`);
                                    }
                                    // Only listen for user response if shouldListenAfterAudio is true
                                    if (shouldListenAfterAudio) {
                                        // After audio plays, keep stream open to listen for user response
                                        // We'll close and redirect when user finishes speaking
                                        logger_1.default.info('[Media Stream] Audio playback complete. Stream will remain open to capture user response.');
                                        // Set up a flag to track that we're waiting for user response after audio
                                        // This will be used to close the stream when user finishes speaking
                                        if (callSid && storedNextStepUrl) {
                                            waitingForUserResponseAfterAudio = true;
                                            nextStepUrlAfterAudio = storedNextStepUrl;
                                            logger_1.default.info(`[Media Stream] Waiting for user to speak. Will redirect to: ${storedNextStepUrl} after user finishes speaking`);
                                            // Set up startAfter timer if specified
                                            if (startAfterSecondsValue !== null && startAfterSecondsValue > 0) {
                                                logger_1.default.info(`[Media Stream] startAfter enabled: will interrupt client after ${startAfterSecondsValue} seconds of speaking`);
                                                // Timer will be started when user starts speaking (in speech_started event)
                                            }
                                            // Clean up pending audio step after streaming (but keep stream open)
                                            if (audioStepIndexNum !== null) {
                                                pendingAudioSteps.delete(callSid);
                                                logger_1.default.info(`[Media Stream] Cleaned up pending audio step for call ${callSid}`);
                                            }
                                            // Set a timeout in case user doesn't speak - advance after 10 seconds
                                            setTimeout(() => {
                                                try {
                                                    if (connectionClosed) {
                                                        return;
                                                    }
                                                    if (waitingForUserResponseAfterAudio && nextStepUrlAfterAudio) {
                                                        logger_1.default.info(`[Media Stream] Timeout waiting for user response. Advancing to next step: ${nextStepUrlAfterAudio}`);
                                                        waitingForUserResponseAfterAudio = false;
                                                        const redirectUrl = nextStepUrlAfterAudio;
                                                        nextStepUrlAfterAudio = null;
                                                        // Double-check connection state before attempting any operations
                                                        if (!connectionClosed && ws.readyState === ws_1.default.OPEN) {
                                                            try {
                                                                const stopEvent = {
                                                                    event: 'stop',
                                                                    streamSid,
                                                                };
                                                                ws.send(JSON.stringify(stopEvent));
                                                                logger_1.default.info('[Media Stream] Sent stop event to Twilio (timeout)');
                                                            }
                                                            catch (err) {
                                                                logger_1.default.warn(`[Media Stream] Error sending stop event: ${err.message || err}`);
                                                            }
                                                        }
                                                        // Close connection if still open
                                                        if (!connectionClosed && (ws.readyState === ws_1.default.OPEN || ws.readyState === ws_1.default.CONNECTING)) {
                                                            try {
                                                                ws.close();
                                                            }
                                                            catch (closeErr) {
                                                                logger_1.default.warn(`[Media Stream] Error closing connection (timeout): ${closeErr.message || closeErr}`);
                                                            }
                                                        }
                                                        logger_1.default.info(`[Media Stream] Stream connection closed (timeout). Twilio will redirect to: ${redirectUrl}`);
                                                    }
                                                }
                                                catch (error) {
                                                    // Catch any unexpected errors to prevent crash
                                                    logger_1.default.error(`[Media Stream] Unexpected error in timeout handler: ${error.message || error}`);
                                                }
                                            }, 10000); // 10 second timeout
                                        }
                                    }
                                    else {
                                        // Don't listen - just close and redirect immediately
                                        logger_1.default.info('[Media Stream] Sequence complete. Not listening for user response (no "next" step). Closing stream.');
                                        // Clean up pending audio step
                                        if (audioStepIndexNum !== null && callSid) {
                                            pendingAudioSteps.delete(callSid);
                                            logger_1.default.info(`[Media Stream] Cleaned up pending audio step for call ${callSid}`);
                                        }
                                        // Close stream and redirect immediately
                                        if (storedNextStepUrl) {
                                            setTimeout(() => {
                                                try {
                                                    if (connectionClosed) {
                                                        return;
                                                    }
                                                    // Double-check connection state before attempting any operations
                                                    if (!connectionClosed && ws.readyState === ws_1.default.OPEN) {
                                                        try {
                                                            const stopEvent = {
                                                                event: 'stop',
                                                                streamSid,
                                                            };
                                                            ws.send(JSON.stringify(stopEvent));
                                                            logger_1.default.info('[Media Stream] Sent stop event to Twilio (no listening)');
                                                        }
                                                        catch (err) {
                                                            logger_1.default.warn(`[Media Stream] Error sending stop event: ${err.message || err}`);
                                                        }
                                                    }
                                                    // Close connection if still open
                                                    if (!connectionClosed && (ws.readyState === ws_1.default.OPEN || ws.readyState === ws_1.default.CONNECTING)) {
                                                        try {
                                                            ws.close();
                                                        }
                                                        catch (closeErr) {
                                                            logger_1.default.warn(`[Media Stream] Error closing connection (no listening): ${closeErr.message || closeErr}`);
                                                        }
                                                    }
                                                    logger_1.default.info(`[Media Stream] Stream connection closed (no listening). Twilio will redirect to: ${storedNextStepUrl}`);
                                                }
                                                catch (error) {
                                                    // Catch any unexpected errors to prevent crash
                                                    logger_1.default.error(`[Media Stream] Unexpected error in timeout handler (no listening): ${error.message || error}`);
                                                }
                                            }, 500);
                                        }
                                    }
                                }
                                catch (error) {
                                    logger_1.default.error(`[Media Stream] Error streaming audio file: ${error.message}`);
                                    isStreamingAudio = false;
                                }
                            }
                            catch (outerError) {
                                // Catch any unexpected errors in the timeout callback
                                logger_1.default.error(`[Media Stream] Unexpected error in audio streaming timeout: ${outerError.message || outerError}`);
                            }
                        }, 1000); // Wait 1 second for stream to be fully ready
                    }
                    break;
                case 'mark':
                    if (markQueue.length > 0) {
                        markQueue.shift();
                    }
                    break;
                case 'stop':
                    logger_1.default.info('[Media Stream] Received stop event from Twilio - call was terminated');
                    connectionClosed = true;
                    // Clear all timeouts immediately
                    clearAllTimeouts();
                    if (startAfterTimer) {
                        clearTimeout(startAfterTimer);
                        startAfterTimer = null;
                    }
                    // Close OpenAI WebSocket if still open
                    if (openAiWs.readyState === ws_1.default.OPEN) {
                        try {
                            openAiWs.close();
                        }
                        catch (err) {
                            logger_1.default.warn(`[Media Stream] Error closing OpenAI WebSocket on stop: ${err.message || err}`);
                        }
                    }
                    break;
                default:
                    logger_1.default.info(`Received non-media event: ${data.event}`);
                    break;
            }
        }
        catch (error) {
            logger_1.default.error(`Error parsing message: ${error.message}. Message: ${message}`);
        }
    });
    // Handle connection close
    ws.on('close', () => {
        logger_1.default.info('Client disconnected.');
        connectionClosed = true;
        // Clear all timeouts
        clearAllTimeouts();
        if (startAfterTimer) {
            clearTimeout(startAfterTimer);
            startAfterTimer = null;
        }
        // Close OpenAI WebSocket if still open
        if (openAiWs.readyState === ws_1.default.OPEN) {
            openAiWs.close();
        }
    });
    // Handle WebSocket errors - suppress expected errors when connection is closed
    ws.on('error', (error) => {
        // Suppress expected errors when connection is already closed (EOF, EPIPE, ECONNRESET)
        const errorCode = error.code;
        const errorMessage = error.message || '';
        if (errorMessage.includes('write EOF') ||
            errorMessage.includes('EOF') ||
            errorCode === 'EPIPE' ||
            errorCode === 'ECONNRESET' ||
            errorCode === 'WSAECONNRESET') {
            // These are expected when the connection closes - just set the flag and clean up
            connectionClosed = true;
            clearAllTimeouts();
            if (startAfterTimer) {
                clearTimeout(startAfterTimer);
                startAfterTimer = null;
            }
            return; // Don't log, just return silently
        }
        // Log other unexpected errors
        logger_1.default.error(`[Media Stream] WebSocket error: ${error.message}`);
        connectionClosed = true;
        // Clear all timeouts on error
        clearAllTimeouts();
        if (startAfterTimer) {
            clearTimeout(startAfterTimer);
            startAfterTimer = null;
        }
    });
    // Handle OpenAI WebSocket close and errors
    openAiWs.on('close', () => {
        logger_1.default.info('Disconnected from the OpenAI Realtime API');
        connectionClosed = true;
        // Clear all timeouts
        clearAllTimeouts();
        if (startAfterTimer) {
            clearTimeout(startAfterTimer);
            startAfterTimer = null;
        }
        // Final billing update when OpenAI connection closes
        if (callSid && (totalInputAudioBytes > 0 || totalOutputAudioBytes > 0)) {
            // Calculate audio seconds from bytes (PCMU at 8kHz: 1 byte per sample, 8000 samples per second)
            const inputAudioSeconds = totalInputAudioBytes / 8000;
            const outputAudioSeconds = totalOutputAudioBytes / 8000;
            // Update any remaining usage
            (0, call_billing_1.trackOpenAIRealtimeUsage)(callSid, {
                input_tokens: 0,
                output_tokens: 0,
                input_audio_seconds: inputAudioSeconds,
                output_audio_seconds: outputAudioSeconds,
            }).catch((error) => {
                logger_1.default.warn(`[Media Stream] Error tracking final Realtime usage on OpenAI close: ${error}`);
            });
        }
    });
    openAiWs.on('error', (error) => {
        // Suppress expected errors when connection is already closed
        const errorCode = error.code;
        const errorMessage = error.message || '';
        if (errorMessage.includes('write EOF') ||
            errorMessage.includes('EOF') ||
            errorCode === 'EPIPE' ||
            errorCode === 'ECONNRESET' ||
            errorCode === 'WSAECONNRESET') {
            // These are expected when the connection closes - just set the flag and clean up
            connectionClosed = true;
            clearAllTimeouts();
            if (startAfterTimer) {
                clearTimeout(startAfterTimer);
                startAfterTimer = null;
            }
            return; // Don't log, just return silently
        }
        // Log other unexpected errors
        logger_1.default.error(`Error in the OpenAI WebSocket: ${error.message}`);
        connectionClosed = true;
        // Clear all timeouts on error
        clearAllTimeouts();
        if (startAfterTimer) {
            clearTimeout(startAfterTimer);
            startAfterTimer = null;
        }
    });
}
//# sourceMappingURL=media-stream-handler.js.map