"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.outboundCallRealtimeController = exports.outboundCallStepCompleteController = exports.outboundCallAnsweredController = void 0;
const twilio_1 = __importDefault(require("twilio"));
const logger_1 = __importDefault(require("../../../logger"));
const call_data_storage_1 = require("../../../providers/phone/call-data-storage");
const media_stream_handler_1 = require("../../websocket/media-stream-handler");
// Helper function to escape XML special characters
function escapeXml(unsafe) {
    if (!unsafe) {
        return '';
    }
    return unsafe
        .toString()
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&apos;');
}
// Helper to extract string from various types
const extractString = (value) => {
    if (!value) {
        return undefined;
    }
    if (typeof value === 'string') {
        return value;
    }
    if (Array.isArray(value)) {
        return typeof value[0] === 'string' ? value[0] : String(value[0]);
    }
    return String(value);
};
/**
 * Webhook handler for when an advanced outbound call is answered
 * This endpoint receives the Twilio webhook and returns TwiML to control the call
 * Based on the working Fastify implementation
 */
const outboundCallAnsweredController = async (req, res) => {
    logger_1.default.info(`[Advanced Call] Received ${req.method} request to ${req.url}`);
    logger_1.default.info(`[Advanced Call] Headers: ${JSON.stringify(req.headers)}`);
    logger_1.default.info(`[Advanced Call] Body: ${JSON.stringify(req.body)}`);
    logger_1.default.info(`[Advanced Call] Query: ${JSON.stringify(req.query)}`);
    try {
        // Log request details for debugging
        logger_1.default.info(`[Advanced Call] Webhook received: method=${req.method}, url=${req.url}`);
        logger_1.default.info(`[Advanced Call] Headers: ${JSON.stringify(req.headers)}`);
        logger_1.default.info(`[Advanced Call] Body: ${JSON.stringify(req.body)}`);
        logger_1.default.info(`[Advanced Call] Query: ${JSON.stringify(req.query)}`);
        // Twilio sends CallSid in the request body (form data) - check all possible locations
        // Handle arrays (when Express parses duplicate query parameters)
        let callSid;
        const bodyCallSid = req.body?.CallSid || req.body?.callSid;
        const queryCallSid = req.query?.CallSid || req.query?.callSid;
        // Extract from body (could be string or array)
        if (bodyCallSid) {
            callSid = extractString(bodyCallSid);
        }
        // Extract from query (could be string or array or ParsedQs)
        else if (queryCallSid) {
            callSid = extractString(queryCallSid);
        }
        const stepIndex = Number.parseInt(req.query?.step || '0', 10);
        if (!callSid) {
            logger_1.default.error('[Advanced Call] CallSid not found in request');
            logger_1.default.error(`[Advanced Call] Full request body: ${JSON.stringify(req.body, null, 2)}`);
            logger_1.default.error(`[Advanced Call] Full request query: ${JSON.stringify(req.query, null, 2)}`);
            // Try one more time with raw body access
            if (req.body && typeof req.body === 'object') {
                const rawCallSid = req.body.CallSid || req.body['CallSid'] || req.body.callSid;
                if (rawCallSid) {
                    callSid = Array.isArray(rawCallSid) ? rawCallSid[0] : rawCallSid;
                }
            }
            if (!callSid) {
                // Return valid TwiML even when accessed directly (for testing/debugging)
                const twimlResponse = `<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Say>Error: Call SID not found. This endpoint is only accessible via Twilio.</Say>
    <Hangup/>
</Response>`;
                res.type('text/xml; charset=utf-8').send(twimlResponse);
                return;
            }
        }
        // Normalize callSid: ensure it's a string, trim whitespace, remove duplicates if comma-separated
        if (typeof callSid === 'string') {
            callSid = callSid.trim();
            // If callSid contains comma (duplicated), take the first part
            if (callSid.includes(',')) {
                const parts = callSid
                    .split(',')
                    .map((p) => p.trim())
                    .filter(Boolean);
                callSid = parts[0];
                if (parts.length > 1) {
                    logger_1.default.warn(`[Advanced Call] CallSid appeared duplicated in string, using first part: ${callSid}`);
                }
            }
        }
        else {
            // If somehow callSid is not a string at this point, convert it
            callSid = String(callSid).trim();
        }
        logger_1.default.info(`[Advanced Call] Processing call ${callSid}, step ${stepIndex}`);
        // Log all stored call SIDs for debugging
        const allCallSids = await (0, call_data_storage_1.getAllCallSids)();
        logger_1.default.info(`[Advanced Call] Currently stored call SIDs: ${JSON.stringify(allCallSids)}`);
        const callData = await (0, call_data_storage_1.getCallData)(callSid);
        // If not found, try to find by partial match (in case of formatting differences)
        if (!callData && callSid) {
            // This would require iterating through all call data, which we can't do easily
            // For now, just log the error
            logger_1.default.error(`[Advanced Call] Call data not found for SID: ${callSid}`);
        }
        logger_1.default.info(`[Advanced Call] Call data found: ${callData ? 'Yes' : 'No'}`);
        if (callData) {
            logger_1.default.info(`[Advanced Call] Steps in call data: ${JSON.stringify(callData.steps)}`);
        }
        // Determine shouldAiSpeakFirst from callData if available
        const shouldAiSpeakFirst = callData?.aiSpeaksFirst === undefined ? true : callData.aiSpeaksFirst; // Default to true for outbound calls
        // Helper function to build WebSocket URL
        const buildStreamUrl = (includeCallSid = true) => {
            const { TWILIO_BASE_URL } = process.env;
            let host = req.headers.host;
            let protocol = 'https'; // Default to HTTPS for Twilio
            if (TWILIO_BASE_URL) {
                try {
                    const baseUrlObj = new URL(TWILIO_BASE_URL);
                    host = baseUrlObj.host;
                    protocol = baseUrlObj.protocol.replace(':', '');
                }
                catch {
                    logger_1.default.warn('[Advanced Call] Invalid TWILIO_BASE_URL, using request headers');
                    protocol =
                        req.headers['x-forwarded-proto'] || (req.secure ? 'https' : 'http');
                }
            }
            else {
                protocol = req.headers['x-forwarded-proto'] || (req.secure ? 'https' : 'http');
            }
            const wsProtocol = protocol === 'https' ? 'wss' : 'ws';
            const params = new URLSearchParams();
            // Only add outbound=true if shouldAiSpeakFirst is true
            if (shouldAiSpeakFirst) {
                params.append('outbound', 'true');
            }
            // Always include callSid if available and requested
            if (includeCallSid && callSid) {
                params.append('callSid', callSid);
            }
            const queryString = params.toString();
            return `${wsProtocol}://${host}/api/phone/media-stream${queryString ? `?${queryString}` : ''}`;
        };
        if (!callData || !callData.steps || callData.steps.length === 0) {
            logger_1.default.info('[Advanced Call] No steps found, going directly to real-time audio');
            // No steps, go directly to real-time audio
            const streamUrl = buildStreamUrl(true);
            logger_1.default.info(`[Advanced Call] Direct stream URL: ${streamUrl}`);
            const twimlResponse = `<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Connect>
        <Stream url="${escapeXml(streamUrl)}"/>
    </Connect>
</Response>`;
            res.type('text/xml; charset=utf-8').send(twimlResponse);
            return;
        }
        logger_1.default.info(`[Advanced Call] Total steps: ${callData.steps.length}, Current step: ${stepIndex}`);
        // If we've completed all steps, connect to media stream
        if (stepIndex >= callData.steps.length) {
            logger_1.default.info('[Advanced Call] All steps completed, connecting to media stream');
            const streamUrl = buildStreamUrl(true);
            logger_1.default.info(`[Advanced Call] Final stream URL: ${streamUrl}`);
            const twimlResponse = `<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Connect>
        <Stream url="${escapeXml(streamUrl)}"/>
    </Connect>
</Response>`;
            res.type('text/xml; charset=utf-8').send(twimlResponse);
            return;
        }
        const step = callData.steps[stepIndex];
        logger_1.default.info(`[Advanced Call] Executing step ${stepIndex}: ${JSON.stringify(step)}`);
        const { TWILIO_BASE_URL } = process.env;
        const baseUrl = TWILIO_BASE_URL || req.headers.origin || `https://${req.headers.host}`;
        const nextStepUrl = `${baseUrl}/api/phone/outbound-call-advanced-answered?CallSid=${encodeURIComponent(callSid)}&step=${stepIndex + 1}`;
        // Build TwiML response properly
        let twimlResponse = '<?xml version="1.0" encoding="UTF-8"?>\n<Response>';
        // Add overall delay before the first step (step 0) for outbound calls
        // This gives the called party time to answer and prepare
        if (stepIndex === 0) {
            const overallDelay = callData?.overallDelay || 5; // Default to 5 seconds if not set
            logger_1.default.info(`[Advanced Call] Adding ${overallDelay}-second delay before first step`);
            twimlResponse += `\n    <Pause length="${overallDelay}"/>`;
        }
        switch (step.type) {
            case 'keyboard':
                // Execute keyboard step normally - steps before "next" are executed sequentially
                const digits = step.value || '';
                logger_1.default.info(`[Advanced Call] Sending DTMF digits: ${digits}`);
                if (digits) {
                    // Use Play with digits attribute for DTMF - sanitize and escape
                    const sanitizedDigits = digits.replace(/[^0-9*#wW]/g, '');
                    twimlResponse += `\n    <Play digits="${sanitizedDigits}"/>`;
                }
                // Move to next step after a short pause (allow time for DTMF to be sent)
                twimlResponse += `\n    <Pause length="2"/>`;
                twimlResponse += `\n    <Redirect method="GET">${escapeXml(nextStepUrl)}</Redirect>`;
                break;
            case 'audio':
                // Audio files will be downloaded, converted to PCMU, and streamed through media stream
                // This ensures they are recorded in the call recordings
                const audioUrl = step.value || '';
                logger_1.default.info(`[Advanced Call] Audio file step - will stream through media stream: ${audioUrl}`);
                if (audioUrl && callSid) {
                    // Collect consecutive steps (delay, keyboard, startAfter, audio) BEFORE this audio step
                    // These are steps that were skipped and should be executed before this audio
                    const preAudioSteps = [];
                    // Look backwards to find any delay/keyboard/startAfter steps that were skipped
                    // (They would have redirected to this audio step)
                    for (let i = stepIndex - 1; i >= 0; i--) {
                        const prevStep = callData.steps[i];
                        if (prevStep.type === 'delay' && prevStep.value) {
                            const delaySeconds = Number.parseInt(prevStep.value, 10);
                            if (!Number.isNaN(delaySeconds) && delaySeconds > 0) {
                                preAudioSteps.unshift({ type: 'delay', value: prevStep.value, index: i });
                            }
                        }
                        else if (prevStep.type === 'keyboard' && prevStep.value) {
                            preAudioSteps.unshift({ type: 'keyboard', value: prevStep.value, index: i });
                        }
                        else if (prevStep.type === 'startAfter' && prevStep.value) {
                            const seconds = Number.parseInt(prevStep.value, 10);
                            if (!Number.isNaN(seconds) && seconds > 0) {
                                // startAfter is handled separately, don't add to preAudioSteps
                            }
                        }
                        else if (prevStep.type === 'audio' ||
                            prevStep.type === 'realtime' ||
                            prevStep.type === 'next') {
                            // Stop looking backwards when we hit a different step type
                            break;
                        }
                    }
                    // Collect consecutive steps (audio, delay, keyboard, startAfter) AFTER this audio step until "next" or realtime
                    const consecutiveSteps = [];
                    let shouldListenAfterSequence = false;
                    let startAfterSecondsValue = null;
                    let stepAfterSequence = stepIndex + 1;
                    // Look ahead to find consecutive steps until "next" or realtime
                    for (let i = stepIndex + 1; i < callData.steps.length; i++) {
                        const currentStep = callData.steps[i];
                        if (currentStep.type === 'next') {
                            // Found "next" step - we should listen after playing all consecutive steps
                            shouldListenAfterSequence = true;
                            stepAfterSequence = i + 1; // Step after "next"
                            logger_1.default.info(`[Advanced Call] Found "next" step at index ${i} - will listen after sequence`);
                            break;
                        }
                        else if (currentStep.type === 'realtime') {
                            // Realtime step - stop collecting, will be handled separately
                            stepAfterSequence = i;
                            logger_1.default.info(`[Advanced Call] Found realtime step at index ${i} - stopping sequence collection`);
                            break;
                        }
                        else if (currentStep.type === 'audio' && currentStep.value) {
                            consecutiveSteps.push({ type: 'audio', value: currentStep.value, index: i });
                        }
                        else if (currentStep.type === 'delay' && currentStep.value) {
                            const delaySeconds = Number.parseInt(currentStep.value, 10);
                            if (!Number.isNaN(delaySeconds) && delaySeconds > 0) {
                                consecutiveSteps.push({ type: 'delay', value: currentStep.value, index: i });
                            }
                        }
                        else if (currentStep.type === 'keyboard' && currentStep.value) {
                            consecutiveSteps.push({ type: 'keyboard', value: currentStep.value, index: i });
                        }
                        else if (currentStep.type === 'startAfter' && currentStep.value) {
                            const seconds = Number.parseInt(currentStep.value, 10);
                            if (!Number.isNaN(seconds) && seconds > 0) {
                                startAfterSecondsValue = seconds;
                                // Don't add startAfter to consecutiveSteps, it's handled separately
                            }
                        }
                        else {
                            // Unknown or invalid step type, stop collecting
                            stepAfterSequence = i;
                            break;
                        }
                    }
                    // Combine pre-audio steps and consecutive steps
                    const allSteps = [...preAudioSteps, ...consecutiveSteps];
                    // Check for startAfter in pre-audio steps too
                    for (let i = stepIndex - 1; i >= 0; i--) {
                        const prevStep = callData.steps[i];
                        if (prevStep.type === 'startAfter' && prevStep.value) {
                            const seconds = Number.parseInt(prevStep.value, 10);
                            if (!Number.isNaN(seconds) && seconds > 0) {
                                startAfterSecondsValue = seconds;
                                break;
                            }
                        }
                        else if (prevStep.type === 'audio' ||
                            prevStep.type === 'realtime' ||
                            prevStep.type === 'next') {
                            break;
                        }
                    }
                    const actualNextStepUrl = `${baseUrl}/api/phone/outbound-call-advanced-answered?CallSid=${encodeURIComponent(callSid)}&step=${stepAfterSequence}`;
                    logger_1.default.info(`[Advanced Call] Collected ${preAudioSteps.length} pre-audio steps and ${consecutiveSteps.length} consecutive steps (total: ${allSteps.length}). Will ${shouldListenAfterSequence ? 'listen' : 'continue'} after sequence. Next step: ${stepAfterSequence}`);
                    // Store audio step info BEFORE Twilio connects to WebSocket
                    // Include all steps (pre-audio + consecutive) to be executed in sequence
                    (0, media_stream_handler_1.setPendingAudioStep)(callSid, audioUrl, stepIndex, actualNextStepUrl, shouldListenAfterSequence, startAfterSecondsValue, allSteps);
                    logger_1.default.info(`[Advanced Call] Stored pending audio step for call ${callSid}, step ${stepIndex}, shouldListen: ${shouldListenAfterSequence}, startAfter: ${startAfterSecondsValue}, consecutive steps: ${consecutiveSteps.length}`);
                    // Build stream URL (query params won't be available in WebSocket, but we store them separately)
                    const streamUrl = buildStreamUrl(true);
                    twimlResponse += `\n    <Connect>
        <Stream url="${escapeXml(streamUrl)}"/>
    </Connect>
    <Redirect method="GET">${escapeXml(actualNextStepUrl)}</Redirect>`;
                    // Note: After the stream closes, Twilio will continue to the redirect
                }
                else {
                    // No audio URL, just move to next step
                    twimlResponse += `\n    <Redirect method="GET">${escapeXml(nextStepUrl)}</Redirect>`;
                }
                break;
            case 'realtime':
                // Connect to real-time audio stream
                const streamUrl = buildStreamUrl(true);
                logger_1.default.info(`[Advanced Call] Connecting to real-time audio: ${streamUrl}`);
                // Escape XML special characters in URL
                twimlResponse += `\n    <Connect>
        <Stream url="${escapeXml(streamUrl)}"/>
    </Connect>`;
                break;
            case 'delay':
                // Execute delay step normally - steps before "next" are executed sequentially
                const delaySeconds = step.value ? Number.parseInt(step.value, 10) : 2; // Default to 2 seconds if not provided
                const validDelay = !Number.isNaN(delaySeconds) && delaySeconds > 0 ? delaySeconds : 2;
                logger_1.default.info(`[Advanced Call] Adding delay of ${validDelay} seconds`);
                twimlResponse += `\n    <Pause length="${validDelay}"/>`;
                // Move to next step after delay
                twimlResponse += `\n    <Redirect method="GET">${escapeXml(nextStepUrl)}</Redirect>`;
                break;
            default:
                // Unknown step type, skip to next
                twimlResponse += `\n    <Redirect method="GET">${escapeXml(nextStepUrl)}</Redirect>`;
        }
        twimlResponse += '\n</Response>';
        logger_1.default.info(`[Advanced Call] Generated TwiML: ${twimlResponse}`);
        res.type('text/xml; charset=utf-8').send(twimlResponse);
    }
    catch (error) {
        logger_1.default.error(`Error in outbound call answered webhook: ${error.message}`);
        const twimlResponse = `<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Say>An application error has occurred. Goodbye.</Say>
    <Hangup/>
</Response>`;
        res.type('text/xml; charset=utf-8').send(twimlResponse);
    }
};
exports.outboundCallAnsweredController = outboundCallAnsweredController;
/**
 * Webhook handler for step completion (keyboard/audio steps)
 * This is no longer needed with the redirect-based approach, but kept for compatibility
 */
const outboundCallStepCompleteController = async (req, res) => {
    // This endpoint is no longer used with the redirect-based approach
    // All step progression is handled via redirects in the main controller
    const twiml = new twilio_1.default.twiml.VoiceResponse();
    twiml.hangup();
    res.type('text/xml');
    res.send(twiml.toString());
};
exports.outboundCallStepCompleteController = outboundCallStepCompleteController;
/**
 * Webhook handler for realtime conversation
 * This is no longer needed as realtime is handled via media stream
 */
const outboundCallRealtimeController = async (req, res) => {
    // This endpoint is no longer used - realtime is handled via media stream WebSocket
    const twiml = new twilio_1.default.twiml.VoiceResponse();
    twiml.hangup();
    res.type('text/xml');
    res.send(twiml.toString());
};
exports.outboundCallRealtimeController = outboundCallRealtimeController;
//# sourceMappingURL=outbound-call-answered.js.map