/**
 * VMO AI Voice Panel - Jarvis-Style Floating Chat
 * Full voice conversation with AI-powered speech recognition and text-to-speech
 */

class VMOVoicePanel {
    constructor() {
        this.panel = document.getElementById('vmo-ai-voice');
        this.chatHistory = document.getElementById('vmo-voice-chat-history');
        this.statusText = document.getElementById('vmo-voice-status');
        this.collapseBtn = document.getElementById('vmo-voice-collapse');
        this.textBtn = document.getElementById('vmo-voice-text-btn');
        this.micBtn = document.getElementById('vmo-voice-mic-btn');
        this.uploadBtn = document.getElementById('vmo-voice-upload-btn'); // ✅ NEW: Upload button
        // ✅ REMOVED: this.speechBtn (button deleted - was duplicate)
        this.startBtn = document.getElementById('vmo-voice-start-btn');
    this.listeningModal = document.getElementById('vmo-listening-modal');
        this.listeningTranscript = document.getElementById('vmo-listening-transcript');
        this.listeningText = document.querySelector('#vmo-listening-modal .vmo-listening-text');
    // Optional close button inside listening modal
    this.listeningCloseBtn = document.getElementById('vmo-listening-close');
        this.muteBtn = document.getElementById('vmo-listening-mute');
    // Optional settings button in header
    this.settingsBtn = document.getElementById('vmo-voice-settings');
        
        this.isCollapsed = false;
        this.isActive = false;
        this.isListening = false;
    this.isSpeaking = false;
    this.isProcessing = false; // suppresses auto-listen while awaiting TTS
        this.isMicMuted = !!window.vmoMicMuted;
        this._autoRestartEnabled = true;
        this.messages = [];
    this.stagedAttachments = [];
    this.uploadedFiles = [];
        
        // Speech Recognition (Web Speech API for listening to user)
        this.recognition = null;
        // Determine a sensible default language from page or browser
        this.defaultLang = (document && document.documentElement && document.documentElement.lang)
            ? document.documentElement.lang
            : (navigator.language || 'en-US');
        this.initSpeechRecognition();
        
        // OpenAI TTS for AI voice responses
        this.currentAudio = null;
        this.isMuted = !!window.vmoMuteTTS;
        
        this.init();

        // Listen for global mic revoke requests
        try {
            window.addEventListener('vmo:mic-revoke', (e) => {
                const from = e?.detail?.from;
                // If this module owns the mic, stop recognition immediately
                if (from === 'voice-panel' || (!from && this.isListening)) {
                    try { this.recognition && this.recognition.stop(); } catch(_) {}
                }
            });
        } catch(_) {}

        // Privacy: allow other modules (consultation) to hard-stop voice completely
        try {
            window.addEventListener('vmo:voice-hard-stop', (e) => {
                const from = e?.detail?.from;
                if (from === 'consultation') {
                    this.endVoiceSession();
                }
            });
        } catch (_) {}
    }
    
    initSpeechRecognition() {
        // Check if browser supports Web Speech API
        const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
        if (SpeechRecognition) {
            this.recognition = new SpeechRecognition();
            // Continuous mode to allow silence-based gating
            this.recognition.continuous = true;
            this.recognition.interimResults = true;
            // Use site/browser language as default
            try {
                this.recognition.lang = this.defaultLang || 'en-US';
            } catch (_) {
                this.recognition.lang = 'en-US';
            }
            this.recognition.maxAlternatives = 1;

            // Fallback-only endpointing: give the user a short thinking pause
            // before Jarvis responds. Realtime mode uses the same 5.5s window.
            this.silenceTimeoutMs = 5500;
            this._silenceTimer = null;
            this._transcriptBuffer = '';
            this._interimText = '';
            
            // ✅ SELF-LOOP PREVENTION: Track when TTS started to ignore any speech during playback
            this._ttsStartedAt = 0;
            this._ttsMinDurationMs = 500; // Minimum time to ignore speech after TTS starts
            this._ttsEndCooldownMs = 250; // Minimal fallback cooldown after TTS ends
            this._ttsEndedAt = 0;
            
            this.recognition.onstart = () => {
                console.log('🎤 Speech recognition started');
                this.isListening = true;
                this.updateStatus('Listening...');
                this.showListeningModal('listening');
                // Claim mic ownership
                try { window.VMOAudioCoordinator && window.VMOAudioCoordinator.requestMic('voice-panel'); } catch(_) {}
            };
            
            this.recognition.onresult = (event) => {
                // ✅ CRITICAL SELF-LOOP PREVENTION: Ignore ALL speech while AI is speaking
                // This prevents the AI from hearing its own TTS output
                if (this.isSpeaking || this.isProcessing) {
                    console.log('🔇 Ignoring speech input - AI is speaking or processing');
                    return;
                }
                
                // ✅ COOLDOWN CHECK: Also ignore speech shortly after TTS ends
                // TTS audio may still be resonating/echoing
                if (this._ttsEndedAt && (Date.now() - this._ttsEndedAt < this._ttsEndCooldownMs)) {
                    console.log('🔇 Ignoring speech input - TTS cooldown period');
                    return;
                }
                
                // ✅ Check coordinator for global TTS state (with stale-state safety)
                if (window.VMOAudioCoordinator && window.VMOAudioCoordinator.isSpeaking) {
                    // Safety: if no audio is ACTUALLY playing, the flag is stale — reset it
                    const anyAudioPlaying = Array.from(document.querySelectorAll('audio')).some(a => !a.paused && !a.ended);
                    const synthSpeaking = window.speechSynthesis && window.speechSynthesis.speaking;
                    if (!anyAudioPlaying && !synthSpeaking && !this.currentAudio) {
                        console.warn('⚠️ Coordinator isSpeaking was stale — resetting');
                        window.VMOAudioCoordinator.isSpeaking = false;
                        if (window.VMOAudioCoordinator.onTTSStop) window.VMOAudioCoordinator.onTTSStop();
                    } else {
                        console.log('🔇 Ignoring speech input - VMOAudioCoordinator reports TTS active');
                        return;
                    }
                }
                
                // Only handle the current result; interim updates should not be permanently appended
                const res = event.results && event.results[event.resultIndex];
                if (!res) return;
                const t = (res[0] && res[0].transcript) ? res[0].transcript : '';

                // ✅ BARGE-IN: If AI is streaming, abort the stream so user can interrupt
                if (t.trim() && this._streamController) {
                    console.log('🗣️ Barge-in: user spoke while AI was streaming — aborting stream');
                    try { this._streamController.abort(); } catch(_) {}
                }

                if (res.isFinal) {
                    this._transcriptBuffer += t;
                    this._interimText = '';
                } else {
                    this._interimText = t;
                }

                // ✅ LIVE-TO-INPUT: Show transcription directly in the chat input box
                const fullText = (this._transcriptBuffer + (this._interimText || ''));
                this._populateInputBox(fullText);

                // ✅ LIVE TRANSCRIPT: Show user's words in real-time in the listening modal
                // (Same approach as vmo-intelligent-consultation.js uses)
                if (fullText.trim()) {
                    this.updateLiveTranscript(fullText, 'user');
                }

                // Reset silence timer — behavior depends on mode
                if (this._silenceTimer) clearTimeout(this._silenceTimer);
                const delay = this.silenceTimeoutMs;
                this._silenceTimer = setTimeout(() => {
                    const finalText = (this._transcriptBuffer + (this._interimText || '')).trim();
                    this._transcriptBuffer = '';
                    this._interimText = '';
                    this.clearLiveTranscript();
                    if (finalText) {
                        try { this.recognition.stop(); } catch(_) {}
                        if (this.isActive) {
                            // Voice conversation mode: auto-send (listen → think → respond → listen)
                            this.sendMessage(finalText);
                        } else {
                            // Speech-to-text mode: populate input for user review & manual send
                            this._populateInputBox(finalText);
                            this.hideListeningModal();
                            this.updateStatus('Ready — review & send');
                        }
                    }
                }, delay);
            };
            
            this.recognition.onerror = (event) => {
                console.error('Speech recognition error:', event.error);
                this.isListening = false;
                // Keep modal visible during active voice session to avoid flicker
                if (!this.isActive) this.hideListeningModal();
                if (event.error === 'no-speech') {
                    // Friendly retry for no-speech timeouts
                    this.updateStatus('Didn\'t catch that. Listening…');
                    if (this.isActive && this._autoRestartEnabled && !this.isSpeaking && !this.isProcessing) {
                        setTimeout(() => this.startVoiceInput(), 500);
                    }
                } else if (event.error === 'aborted') {
                    // Ignorable (often happens on quick restarts)
                    if (this.isActive && this._autoRestartEnabled && !this.isSpeaking && !this.isProcessing) {
                        setTimeout(() => this.startVoiceInput(), 300);
                    }
                } else if (event.error === 'not-allowed' || event.error === 'service-not-allowed') {
                    this.updateStatus('Mic permission blocked. Please allow microphone access.');
                    alert('Microphone permission is blocked. Please allow mic access in your browser settings and reload.');
                } else {
                    this.updateStatus('Error: ' + event.error);
                    if (this.isActive && this._autoRestartEnabled && !this.isSpeaking && !this.isProcessing) {
                        setTimeout(() => this.startVoiceInput(), 800);
                    }
                }
            };
            
            this.recognition.onend = () => {
                console.log('🎤 Speech recognition ended');
                this.isListening = false;
                // Keep modal visible during active voice session so UI remains stable
                if (!this.isActive) this.hideListeningModal();
                // Release mic ownership
                try { window.VMOAudioCoordinator && window.VMOAudioCoordinator.releaseMic('voice-panel'); } catch(_) {}
                // Auto-restart if session active and we aren't speaking
                if (this.isActive && this._autoRestartEnabled && !this.isSpeaking && !this.isProcessing) {
                    setTimeout(() => this.startVoiceInput(), 300);
                }
            };

            // Diagnostics: track audio lifecycle
            this.recognition.onaudiostart = () => console.log('🎤 Audio capturing started');
            this.recognition.onaudioend = () => console.log('🎤 Audio capturing ended');
            this.recognition.onsoundstart = () => console.log('🎤 Sound detected');
            this.recognition.onsoundend = () => console.log('🎤 Sound ended');
            
            console.log('✅ Speech recognition initialized');
        } else {
            console.warn('⚠️ Web Speech API not supported in this browser');
        }
    }

    // Warm up the microphone to trigger permission prompt and stabilize input
    async micWarmup() {
        if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) return;
        try {
            const stream = await navigator.mediaDevices.getUserMedia({
                audio: {
                    echoCancellation: true,
                    noiseSuppression: true,
                    autoGainControl: true
                }
            });
            // Immediately stop tracks; this is only to unlock permissions and wake audio path
            setTimeout(() => stream.getTracks().forEach(t => t.stop()), 100);
        } catch (err) {
            console.warn('Mic warmup failed or permission denied:', err);
        }
    }
    
    async init() {
        console.log('🎤 VMO Voice Panel: Initializing...');
        
        // Set up event listeners
        this.collapseBtn?.addEventListener('click', () => this.toggleCollapse());
        this.textBtn?.addEventListener('click', () => this.activateTextChat());
        this.micBtn?.addEventListener('click', () => this.startVoiceInput());
        this.uploadBtn?.addEventListener('click', () => this.openFileUpload()); // ✅ NEW: Upload button handler
        // ✅ REMOVED: this.speechBtn (button deleted - was duplicate)
        this.startBtn?.addEventListener('click', () => this.startVoiceSession());
        // Close button inside listening modal ends the appropriate session
        this.listeningCloseBtn?.addEventListener('click', (e) => {
            e.stopPropagation();
            const owner = (this.listeningModal && this.listeningModal.dataset && this.listeningModal.dataset.owner) || 'voice-panel';
            if (owner === 'consultation') {
                // Consultation will handle this in its own handler; avoid double-processing
                return;
            }
            this.endVoiceSession();
        });
        // Settings button opens voice selection popover
        this.settingsBtn?.addEventListener('click', (e) => {
            e.stopPropagation();
            this.openSettingsPopover();
        });
        // Enable dragging the panel by its header
        this.enableDrag();
        // Load saved preferred Jarvis voice
        try {
            const saved = localStorage.getItem('vmoJarvisVoice');
            if (saved) window.vmoSelectedVoice = saved;
            else if (!window.vmoSelectedVoice) window.vmoSelectedVoice = 'ash';
        } catch(_) { if (!window.vmoSelectedVoice) window.vmoSelectedVoice = 'ash'; }
        
        // Load chat history from database first, then show greeting if first time
        await this.loadChatHistory();
        
        // Enable buttons
    if (this.textBtn) this.textBtn.disabled = false;
    if (this.micBtn) this.micBtn.disabled = false;
    if (this.startBtn) this.startBtn.disabled = false;
        
        console.log('✅ VMO Voice Panel: Ready');
        // Mute toggle (Jarvis only; consultation intercepts when it owns the modal)
        // ✅ FIX: Mute now explicitly controls the Microphone (User Input)
        this.muteBtn?.addEventListener('click', (e) => {
            e.stopPropagation();
            const owner = (this.listeningModal && this.listeningModal.dataset && this.listeningModal.dataset.owner) || 'voice-panel';
            if (owner === 'consultation') return; // consultation owns this click

            this.setMicMuted(!window.vmoMicMuted, { restartFallback: true });
        });
        
        // ═══════════════════════════════════════════════════════════════════════════════
        // ✅ BARGE-IN ZONE: Clicking the circular animation or modal body stops AI speech
        // This gives users a large target to tap/click to interrupt
        // ═══════════════════════════════════════════════════════════════════════════════
        const modalContent = this.listeningModal?.querySelector('.vmo-listening-content');
        if (modalContent) {
            modalContent.addEventListener('click', (e) => {
                // Don't trigger on button clicks (they have their own handlers)
                if (e.target.tagName === 'BUTTON') return;
                
                // Only trigger if AI is speaking
                if (this.isSpeaking) {
                    console.log('🔊 BARGE-IN via modal tap');
                    this.stopAllSpeaking();
                    if (!this._realtimeSession) this.startVoiceInput(true); // Manual fallback barge-in
                }
            });
        }
        
        // ✅ KEYBOARD BARGE-IN: Press Escape to stop AI speech and start listening
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape' && this.isActive && this.isSpeaking) {
                console.log('🔊 BARGE-IN via Escape key');
                e.preventDefault();
                e.stopPropagation();
                this.stopAllSpeaking();
                if (!this._realtimeSession) this.startVoiceInput(true); // Manual fallback barge-in
            }
        });
        
        // ✅ GLOBAL BARGE-IN EVENT: Other code can trigger this
        window.addEventListener('vmo:voice-barge-in', () => {
            if (this.isSpeaking) {
                console.log('🔊 BARGE-IN via global event');
                this.stopAllSpeaking();
                if (!this._realtimeSession) this.startVoiceInput(true);
            }
        });
    }
    
    /**
     * ═══════════════════════════════════════════════════════════════════════════════
     * FORCE-STOP ALL SPEAKING - Guaranteed barge-in support
     * This method immediately stops all audio output from any source
     * ═══════════════════════════════════════════════════════════════════════════════
     */
    stopAllSpeaking() {
        console.log('🛑 FORCE STOPPING ALL AUDIO');

        // Realtime voice uses a live WebRTC sink, not the fallback audio element.
        // Canceling there preserves the active mic session for true barge-in.
        if (this._realtimeSession && typeof this._realtimeSession.cancelResponse === 'function') {
            try { this._realtimeSession.cancelResponse(); } catch(_) {}
        }
        
        // 1. Stop our own audio element
        if (this.currentAudio) {
            try { 
                this.currentAudio.pause(); 
                this.currentAudio.currentTime = 0;
                this.currentAudio.src = '';
            } catch(_) {}
            this.currentAudio = null;
        }
        
        // 2. Stop browser speech synthesis
        try {
            if (window.speechSynthesis) {
                window.speechSynthesis.cancel();
            }
        } catch(_) {}
        
        // 3. Stop any global audio via coordinator
        try {
            if (window.VMOAudioCoordinator) {
                if (window.VMOAudioCoordinator.stopAllTTS) {
                    window.VMOAudioCoordinator.stopAllTTS('barge-in', 'voice-panel');
                }
                if (window.VMOAudioCoordinator.currentAudio) {
                    try { window.VMOAudioCoordinator.currentAudio.pause(); } catch(_) {}
                }
                window.VMOAudioCoordinator.isSpeaking = false;
                if (window.VMOAudioCoordinator.onTTSStop) {
                    window.VMOAudioCoordinator.onTTSStop();
                }
            }
        } catch(_) {}
        
        // 4. Stop global vmoCurrentAudio
        try {
            if (window.vmoCurrentAudio) {
                window.vmoCurrentAudio.pause();
                window.vmoCurrentAudio = null;
            }
        } catch(_) {}
        
        // 5. Nuclear option: pause ALL audio elements on page
        try {
            document.querySelectorAll('audio').forEach(audio => {
                if (this._realtimeSession && audio.id === 'vmo-realtime-audio') return;
                try { audio.pause(); } catch(_) {}
            });
        } catch(_) {}
        
        // Reset state
        this.isSpeaking = false;
        this.isProcessing = false;
        this._ttsEndedAt = Date.now();
        this.clearLiveTranscript();
        this.updateStatus('Ready');
        if (this.isActive) this.setModalPhase('listening');
    }

    setMicMuted(muted, options = {}) {
        const shouldMute = !!muted;
        window.vmoMicMuted = shouldMute;
        this.isMicMuted = shouldMute;

        if (this._realtimeSession) {
            try {
                if (shouldMute && typeof this._realtimeSession.mute === 'function') this._realtimeSession.mute();
                if (!shouldMute && typeof this._realtimeSession.unmute === 'function') this._realtimeSession.unmute();
            } catch(_) {}
        }

        if (shouldMute) {
            console.log('🔇 Jarvis Mute: Microphone muted by user');
            if (this.recognition) {
                try { this.recognition.abort(); } catch(_) {}
                try { this.recognition.stop(); } catch(_) {}
                this.isListening = false;
            }
            if (this._silenceTimer) { try { clearTimeout(this._silenceTimer); } catch(_) {} this._silenceTimer = null; }
            this._transcriptBuffer = '';
            this._interimText = '';
            this.clearLiveTranscript();
            this.updateStatus('Microphone muted');
        } else {
            console.log('🔊 Jarvis Unmute: Microphone unmuted by user');
            this.updateStatus(this._realtimeSession ? 'Listening…' : 'Microphone active');
            if (options.restartFallback && !this._realtimeSession && this.isActive && !this.isSpeaking && !this.isProcessing) {
                this.startVoiceInput(false);
            }
        }

        this.applyMute();
    }

    applyMute() {
        // Sync button label/state (Jarvis context)
        if (this.muteBtn) {
            const muted = !!window.vmoMicMuted;
            this.muteBtn.textContent = muted ? 'Unmute Mic' : 'Mute Mic';
            this.muteBtn.setAttribute('aria-pressed', muted ? 'true' : 'false');
            this.muteBtn.title = muted ? 'Unmute Microphone' : 'Mute Microphone';
            // Optional: visual indication
            this.muteBtn.style.color = muted ? '#ff4444' : '#00ffff';
            this.muteBtn.style.borderColor = muted ? '#ff4444' : '#00ffff';
        }
        
        // Notify listeners (no owner tag here; consultation ignores these)
        try { window.dispatchEvent(new CustomEvent('vmo:mute-changed', { detail: { muted: !!window.vmoMicMuted } })); } catch(_) {}
    }
    
    toggleCollapse() {
        this.isCollapsed = !this.isCollapsed;
        this.panel?.classList.toggle('collapsed');
        this.collapseBtn.textContent = this.isCollapsed ? '+' : '−';
    }
    
    async loadChatHistory() {
        console.log('📚 Loading chat history from database...');
        
        try {
            const response = await fetch(vmo_ajax.ajaxurl, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                },
                body: new URLSearchParams({
                    action: 'vmo_load_chat_history',
                    chat_type: 'jarvis', // Only load Jarvis conversations
                    security: vmo_ajax.nonce
                })
            });
            
            const data = await response.json();
            
            if (data.success && data.data.messages) {
                const messages = data.data.messages;
                console.log(`✅ Loaded ${messages.length} messages from database`);
                
                // Clear any existing messages
                this.messages = [];
                
                // Add all messages to chat history
                messages.forEach(msg => {
                    const sender = msg.role === 'user' ? 'user' : 'ai';
                    this.addMessage(msg.content || '', sender, false); // false = don't save to DB (already there)
                    // Normalize internal message shape to { text, sender }
                    if (typeof msg.content === 'string' && msg.content.trim() !== '') {
                        this.messages.push({
                            text: msg.content,
                            sender: sender,
                            time: new Date()
                        });
                    }
                });
                
                // If no messages, show greeting
                if (messages.length === 0) {
                    await this.loadGreeting();
                }
                
                this.updateStatus('Ready');
            } else {
                console.error('Failed to load chat history:', data);
                await this.loadGreeting(); // Fallback to greeting
            }
        } catch (error) {
            console.error('Error loading chat history:', error);
            await this.loadGreeting(); // Fallback to greeting
        }
    }
    
    async loadGreeting() {
        // Just show a simple greeting without making an API call
        this.addMessage('Hello! I\'m VMO, your AI business assistant. Click the text button to start chatting, or use voice input!', 'ai', false);
        this.updateStatus('Ready');
    }
    
    activateTextChat() {
        // Focus on the text input area if it exists, or create one
        let textInput = document.getElementById('vmo-voice-text-input');
        
        if (!textInput) {
            // Create text input area dynamically
            const inputContainer = document.createElement('div');
            inputContainer.className = 'vmo-voice-text-input-container';
            inputContainer.innerHTML = `
                <textarea 
                    id="vmo-voice-text-input" 
                    class="vmo-voice-text-input" 
                    placeholder="Type your message to VMO..."
                    rows="3"
                ></textarea>
                <button id="vmo-voice-send-text" class="vmo-voice-send-btn" title="Send Message">
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
                        <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
                    </svg>
                </button>
            `;
            
            // Insert before the input controls
            const inputControls = document.querySelector('.vmo-voice-input-controls');
            if (inputControls) {
                inputControls.parentNode.insertBefore(inputContainer, inputControls);
            }
            
            textInput = document.getElementById('vmo-voice-text-input');
            const sendBtn = document.getElementById('vmo-voice-send-text');
            
            // Bind send button
            sendBtn?.addEventListener('click', () => {
                const message = textInput.value.trim();
                if (message || this.hasStagedAttachments()) {
                    this.sendMessage(message);
                    textInput.value = '';
                }
            });
            
            // Bind Enter key (Shift+Enter for new line)
            textInput?.addEventListener('keypress', (e) => {
                if (e.key === 'Enter' && !e.shiftKey) {
                    e.preventDefault();
                    const message = textInput.value.trim();
                    if (message || this.hasStagedAttachments()) {
                        this.sendMessage(message);
                        textInput.value = '';
                    }
                }
            });
        }
        
        // Focus the input
        textInput?.focus();
    }
    
    /**
     * ✅ Populate the Jarvis chat input box with transcribed text.
     * Creates the input if it doesn't exist, updates it live during dictation.
     */
    _populateInputBox(text) {
        let textInput = document.getElementById('vmo-voice-text-input');
        if (!textInput) {
            this.activateTextChat();
            textInput = document.getElementById('vmo-voice-text-input');
        }
        if (textInput) {
            textInput.value = text;
            textInput.focus();
            // Auto-resize: scroll to bottom of textarea so latest text is visible
            textInput.scrollTop = textInput.scrollHeight;
        }
    }
    
    hasStagedAttachments() {
        return Array.isArray(this.stagedAttachments) && this.stagedAttachments.length > 0;
    }

    getUploadSessionId() {
        if (window.vmoCurrentConsultationSessionId) {
            return String(window.vmoCurrentConsultationSessionId);
        }

        try {
            const stored = sessionStorage.getItem('vmo_jarvis_upload_session_id');
            if (stored) return stored;

            const generated = 'jarvis_' + Date.now() + '_' + Math.random().toString(36).slice(2, 10);
            sessionStorage.setItem('vmo_jarvis_upload_session_id', generated);
            return generated;
        } catch (_) {
            return 'jarvis_' + Date.now();
        }
    }

    getAttachmentKind(fileOrPayload) {
        const mime = String(fileOrPayload?.type || fileOrPayload?.media_type || '').toLowerCase();
        const name = String(fileOrPayload?.name || fileOrPayload?.filename || fileOrPayload?.title || fileOrPayload?.url || '').toLowerCase();

        if (mime.startsWith('video/') || /\.(mp4|webm|ogg|mov|m4v)$/i.test(name)) return 'video';
        if (mime.startsWith('audio/') || /\.(mp3|wav|m4a|aac|oga)$/i.test(name)) return 'audio';
        if (mime.startsWith('image/') || /\.(png|jpe?g|gif|webp|svg|avif)$/i.test(name)) return 'image';
        return 'file';
    }

    formatFileSize(bytes) {
        const size = Number(bytes || 0);
        if (!size) return '';
        if (size < 1024) return size + ' B';
        if (size < 1024 * 1024) return Math.round(size / 1024) + ' KB';
        return (size / (1024 * 1024)).toFixed(size >= 10 * 1024 * 1024 ? 0 : 1) + ' MB';
    }

    stageFilesForAttachment(files) {
        const incoming = Array.from(files || []);
        if (!incoming.length) return;

        if (!Array.isArray(this.stagedAttachments)) this.stagedAttachments = [];

        incoming.forEach(file => {
            const exists = this.stagedAttachments.some(item => {
                const stagedFile = item.file || item;
                return stagedFile.name === file.name && stagedFile.size === file.size && stagedFile.lastModified === file.lastModified;
            });

            if (exists) return;

            const kind = this.getAttachmentKind(file);
            this.stagedAttachments.push({
                file,
                kind,
                name: file.name,
                size: file.size,
                type: file.type,
                previewUrl: kind === 'image' || kind === 'video' ? URL.createObjectURL(file) : ''
            });
        });

        this.renderStagedAttachments();
        this.activateTextChat();
        this.updateStatus(this.stagedAttachments.length === 1 ? '1 file attached. Add an instruction.' : this.stagedAttachments.length + ' files attached. Add an instruction.');
    }

    removeStagedAttachment(index) {
        if (!Array.isArray(this.stagedAttachments)) return;
        const removed = this.stagedAttachments.splice(index, 1)[0];
        if (removed?.previewUrl) {
            try { URL.revokeObjectURL(removed.previewUrl); } catch (_) {}
        }
        this.renderStagedAttachments();
        this.updateStatus(this.hasStagedAttachments() ? 'Attachment ready. Add an instruction.' : 'Ready');
    }

    clearStagedAttachments() {
        if (Array.isArray(this.stagedAttachments)) {
            this.stagedAttachments.forEach(item => {
                if (item?.previewUrl) {
                    try { URL.revokeObjectURL(item.previewUrl); } catch (_) {}
                }
            });
        }
        this.stagedAttachments = [];
        this.renderStagedAttachments();
    }

    renderStagedAttachments() {
        let preview = document.getElementById('vmo-voice-attachment-preview');
        const inputControls = document.querySelector('.vmo-voice-input-controls');
        const attachments = Array.isArray(this.stagedAttachments) ? this.stagedAttachments : [];

        if (!attachments.length) {
            if (preview) preview.remove();
            return;
        }

        if (!preview) {
            preview = document.createElement('div');
            preview.id = 'vmo-voice-attachment-preview';
            preview.className = 'vmo-voice-attachment-preview';
            if (inputControls && inputControls.parentNode) {
                inputControls.parentNode.insertBefore(preview, inputControls);
            } else if (this.panel) {
                this.panel.appendChild(preview);
            }
        }

        preview.innerHTML = '';

        attachments.forEach((item, index) => {
            const chip = document.createElement('div');
            chip.className = 'vmo-voice-attachment-chip';

            const thumb = document.createElement('div');
            thumb.className = 'vmo-voice-attachment-thumb';

            if (item.kind === 'image' && item.previewUrl) {
                const img = document.createElement('img');
                img.src = item.previewUrl;
                img.alt = '';
                thumb.appendChild(img);
            } else if (item.kind === 'video' && item.previewUrl) {
                const video = document.createElement('video');
                video.src = item.previewUrl;
                video.muted = true;
                thumb.appendChild(video);
            } else {
                thumb.textContent = item.kind === 'audio' ? 'Audio' : 'File';
            }

            const label = document.createElement('div');
            label.className = 'vmo-voice-attachment-label';
            label.innerHTML = `<strong>${this.escapeHtml(item.name || 'Attachment')}</strong><span>${this.formatFileSize(item.size)}</span>`;

            const remove = document.createElement('button');
            remove.type = 'button';
            remove.className = 'vmo-voice-attachment-remove';
            remove.setAttribute('aria-label', 'Remove attachment');
            remove.textContent = '×';
            remove.addEventListener('click', () => this.removeStagedAttachment(index));

            chip.appendChild(thumb);
            chip.appendChild(label);
            chip.appendChild(remove);
            preview.appendChild(chip);
        });
    }

    escapeHtml(value) {
        return String(value || '')
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;');
    }

    getAttachmentDisplayText(text, attachments) {
        const message = String(text || '').trim();
        const names = (attachments || []).map(item => item.name || item.file?.name || 'attachment').join(', ');
        if (!names) return message;
        return message ? `${message}\n\nAttached: ${names}` : `Attached: ${names}`;
    }

    normalizeUploadedAttachment(payload, originalFile) {
        const data = payload || {};
        const url = data.url || data.media_url || data.source_url || data.guid || '';
        const filename = data.filename || data.title || data.name || originalFile?.name || (url ? url.split('/').pop() : 'Attachment');
        const mime = data.type || data.media_type || originalFile?.type || '';
        const kind = this.getAttachmentKind({ type: mime, filename, url });

        return {
            id: data.id || data.attachment_id || data.attachmentId || data.media_id || data.mediaId || '',
            url,
            filename,
            title: data.title || filename,
            type: kind,
            mime,
            size: originalFile?.size || 0
        };
    }

    getUploadedAttachmentContext(message, uploadedFiles) {
        const base = String(message || '').trim() || 'Please review the attached file(s).';
        if (!uploadedFiles || !uploadedFiles.length) return base;

        const attachmentLines = uploadedFiles.map((file, index) => {
            return `${index + 1}. ${file.title || file.filename || 'Attachment'} (${file.mime || file.type || 'file'}): ${file.url}`;
        }).join('\n');

        return `${base}\n\nAttached files for context:\n${attachmentLines}\n\nUse these attachments as evidence/reference unless I explicitly ask you to insert them into the website preview.`;
    }

    shouldUseAttachmentsForWebsite(prompt) {
        const text = String(prompt || '').toLowerCase();
        const action = /\b(insert|place|put|add|apply|use|set|make|turn|create|replace|show|display)\b/i.test(text);
        const websiteTarget = /\b(website|webpage|page|site|section|hero|banner|background|backdrop|parallax|gallery|carousel|slider|iframe|preview|live preview|web page)\b/i.test(text);
        const referenceOnly = /\b(error|issue|bug|problem|screenshot|reference|explain|why|what is|diagnose|debug|broken|failed)\b/i.test(text);

        return action && websiteTarget && !(referenceOnly && !/\b(insert|place|put|add|apply|set|replace|gallery|carousel|slider|background|hero|parallax)\b/i.test(text));
    }

    async uploadStagedAttachments(attachments) {
        const uploaded = [];
        for (const item of attachments) {
            const file = item.file || item;
            const result = await this.uploadFile(file);
            uploaded.push(result);
        }
        return uploaded;
    }

    // ✅ NEW: Open file upload dialog for Jarvis AI chat
    openFileUpload() {
        console.log('📎 Opening file upload for Jarvis AI chat...');
        
        // Create a hidden file input if it doesn't exist
        let fileInput = document.getElementById('vmo-voice-file-input');
        if (!fileInput) {
            fileInput = document.createElement('input');
            fileInput.type = 'file';
            fileInput.id = 'vmo-voice-file-input';
            fileInput.accept = 'image/*,video/*,audio/*,.pdf,.doc,.docx,.txt,.csv,.json,.zip';
            fileInput.multiple = true;
            fileInput.style.display = 'none';
            document.body.appendChild(fileInput);
            
            // Handle file selection
            fileInput.addEventListener('change', (e) => {
                const files = Array.from(e.target.files);
                if (files.length === 0) return;
                this.stageFilesForAttachment(files);
                
                // Clear the input for next use
                fileInput.value = '';
            });
        }
        
        // Trigger file picker
        fileInput.click();
    }
    
    // ✅ NEW: Upload a file to the server
    async uploadFile(file) {
        const formData = new FormData();
        formData.append('media', file);
        formData.append('action', 'vmo_upload_consultation_media');
        formData.append('session_id', this.getUploadSessionId());
        formData.append('context', 'jarvis_voice_attachment');
        
        // Get nonce from various sources
        const nonce = window.vmo_builder_nonce || 
                      window.vmoBuilderNonce || 
                      (window.vmo_ajax && window.vmo_ajax.nonce) ||
                      (window.vmoAjax && (window.vmoAjax.nonce || window.vmoAjax.security)) || '';
        formData.append('security', nonce);
                formData.append('nonce', nonce);
                formData.append('_ajax_nonce', nonce);
        
        // Get ajax URL
        const ajaxUrl = (window.vmo_ajax && window.vmo_ajax.ajaxurl) || 
                        (window.vmoAjax && (window.vmoAjax.ajaxurl || window.vmoAjax.ajax_url || window.vmoAjax.url)) ||
                        (window.ajaxurl) ||
                        '/wp-admin/admin-ajax.php';
        
        const response = await fetch(ajaxUrl, {
            method: 'POST',
            body: formData
        });
        
        const result = await response.json();
        
        if (result.success && result.data) {
            const uploaded = this.normalizeUploadedAttachment(result.data, file);
            if (!uploaded.url) {
                throw new Error('Upload succeeded but no media URL was returned');
            }

            console.log('✅ File uploaded:', uploaded.filename);
            
            // Store the uploaded file info
            if (!this.uploadedFiles) this.uploadedFiles = [];
            this.uploadedFiles.push(uploaded);
            
            return uploaded;
        } else {
            throw new Error(result.data?.message || 'Upload failed');
        }
    }

    formatAssistantError(message) {
        const cleaned = String(message || '').replace(/^Error:\s*/i, '').trim();
        if (!cleaned) {
            return 'Sorry, I couldn\'t complete that just now. Please try again.';
        }
        if (
            /^sorry\b/i.test(cleaned) ||
            /^jarvis\b/i.test(cleaned) ||
            /^the consultation assistant\b/i.test(cleaned) ||
            /^insufficient tokens\b/i.test(cleaned)
        ) {
            return cleaned;
        }
        return 'Sorry, I couldn\'t complete that just now. Please try again.';
    }

    isJarvisOnboardingReady() {
        return !!(window.vmo_ajax && window.vmo_ajax.jarvis_onboarding_ready);
    }

    getConsultationStartMessage() {
        return 'Before I build or change anything, please click the red Business Wizard button at the top to begin your Business Consultation. I can still answer questions about VMO and help you find your way around the plugin, but the consultation has to create the connected business foundation before I build, refine, or set up the website and business systems.';
    }

    shouldRoutePreConsultationToWizard(text, pendingAttachments) {
        const hasAttachments = Array.isArray(pendingAttachments) && pendingAttachments.length > 0;
        const normalized = String(text || '').toLowerCase().replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
        if (!normalized && hasAttachments) return true;

        const patterns = [
            /\b(build|create|generate|make|design|draft|produce|start|launch|publish)\b.{0,90}\b(my|our|me|us|website|web\s*site|site|landing\s*page|homepage|home\s*page|store|shop|ecommerce|e-commerce)\b/i,
            /\b(website|web\s*site|site|landing\s*page|homepage|home\s*page|page|section|hero|header|footer|testimonial|testimonials|portfolio|services|preview)\b.{0,90}\b(add|change|edit|update|replace|remove|refine|improve|redesign|rewrite|insert|move|resize)\b/i,
            /\b(add|change|edit|update|replace|remove|refine|improve|redesign|rewrite|insert|move|resize)\b.{0,90}\b(website|web\s*site|site|landing\s*page|homepage|home\s*page|page|section|hero|header|footer|testimonial|testimonials|portfolio|services|preview)\b/i,
            /\b(set\s*up|setup|connect|provision|configure|activate|turn\s*on|install)\b.{0,90}\b(business|website|web\s*site|email|mailbox|phone|sms|seo|payment|payments|stripe|shipping|domain|hosting|crm|kpi|dashboard)\b/i,
            /\b(read|compose|reply|send|save|draft)\b.{0,90}\b(email|emails|mail|message|messages)\b/i
        ];

        return patterns.some((pattern) => pattern.test(normalized));
    }

    focusConsultationButton() {
        const btn = document.getElementById('vmo-business-wizard-btn');
        if (!btn) return;
        try { btn.focus({ preventScroll: false }); } catch (_) { try { btn.focus(); } catch (_) {} }
        const oldShadow = btn.style.boxShadow;
        const oldTransform = btn.style.transform;
        btn.style.boxShadow = '0 0 0 4px rgba(239,68,68,.25), 0 0 22px rgba(239,68,68,.45)';
        btn.style.transform = 'translateY(-1px)';
        setTimeout(() => {
            btn.style.boxShadow = oldShadow;
            btn.style.transform = oldTransform;
        }, 1800);
    }
    
    async sendMessage(text) {
        const originalText = String(text || '').trim();
        const pendingAttachments = Array.isArray(this.stagedAttachments) ? this.stagedAttachments.slice() : [];
        const userDisplayText = this.getAttachmentDisplayText(originalText, pendingAttachments);

        if (!originalText && !pendingAttachments.length) {
            this.updateStatus('Type a message or attach a file first.');
            return;
        }

        // Add user message to chat
        this.addMessage(userDisplayText, 'user');

        const owner = (this.listeningModal && this.listeningModal.dataset && this.listeningModal.dataset.owner) || '';
        if (owner !== 'consultation' && !this.isJarvisOnboardingReady() && this.shouldRoutePreConsultationToWizard(originalText, pendingAttachments)) {
            const guidance = this.getConsultationStartMessage();
            this.addMessage(guidance, 'ai');
            this.focusConsultationButton();
            this.updateStatus('Business Consultation required');
            this.isProcessing = false;
            return;
        }
        
        // Update status
        this.updateStatus(pendingAttachments.length ? 'Preparing attached file(s)...' : 'VMO is thinking...');
        // If in voice session, reflect phase in modal
        if (this.isActive) this.setModalPhase('thinking');
        // Prevent premature auto-listen while we wait for AI + TTS
        this.isProcessing = true;

        // ✅ SAFETY WATCHDOG: Auto-reset isProcessing after 30s to prevent permanent freeze
        const processingWatchdog = setTimeout(() => {
            if (this.isProcessing) {
                console.warn('⚠️ Processing watchdog fired — resetting isProcessing after 30s');
                this.isProcessing = false;
                this.updateStatus('Request timed out');
                this.addMessage('Sorry, the request took too long. Please try again.', 'ai');
                if (this.isActive && !this.isSpeaking) {
                    setTimeout(() => this.startVoiceInput(false), 500);
                }
            }
        }, 30000);
        
        try {
            let messageForAI = originalText;

            if (pendingAttachments.length) {
                let uploadedAttachments = [];

                this.updateStatus(pendingAttachments.length === 1 ? 'Uploading attached file...' : 'Uploading attached files...');

                try {
                    uploadedAttachments = await this.uploadStagedAttachments(pendingAttachments);
                } catch (uploadError) {
                    console.error('File upload error:', uploadError);
                    this.updateStatus('File upload failed');
                    this.addMessage('File upload failed. The attachment is still staged, so you can try sending again.', 'system', false);
                    this.isProcessing = false;
                    if (this.isActive) {
                        setTimeout(() => {
                            if (this.isActive && !this.isSpeaking && !this.isProcessing) this.startVoiceInput(false);
                        }, 1500);
                    }
                    return;
                }

                this.clearStagedAttachments();

                if (this.shouldUseAttachmentsForWebsite(originalText) && typeof window.vmoApplyMediaInstructionToPreview === 'function') {
                    const instruction = originalText || 'Insert the attached media into the website preview.';
                    const applied = window.vmoApplyMediaInstructionToPreview(instruction, uploadedAttachments, { clearPending: true });

                    this.addMessage(applied
                        ? 'Done. I applied the attached media to the website preview.'
                        : 'I uploaded the file, but I could not apply it to the current website preview. Please make sure a generated page is loaded first.', 'ai');
                    this.updateStatus(applied ? 'Ready' : 'Could not apply media');
                    this.isProcessing = false;
                    return;
                }

                messageForAI = this.getUploadedAttachmentContext(originalText, uploadedAttachments);

                for (let i = this.messages.length - 1; i >= 0; i--) {
                    if (this.messages[i] && this.messages[i].sender === 'user') {
                        this.messages[i].text = messageForAI;
                        break;
                    }
                }
            }

            if (owner !== 'consultation' && await this.tryHandleJarvisEmailOperatorCommand(messageForAI)) {
                clearTimeout(processingWatchdog);
                return;
            }

            this.updateStatus('VMO is thinking...');

            // Build conversation history for context (normalize shapes and filter out empties)
            const chatHistory = this.messages
                .map(msg => {
                    const content = typeof msg.text === 'string' ? msg.text : (typeof msg.content === 'string' ? msg.content : '');
                    const role = (msg.sender === 'user' || msg.role === 'user') ? 'user' : 'assistant';
                    if (!content || typeof content !== 'string' || content.trim() === '') return null;
                    return { role, content: content.trim() };
                })
                .filter(Boolean);
            
            // ✅ STREAMING + INTERRUPTION: Use AbortController for both timeout and barge-in
            if (this._streamController) { try { this._streamController.abort(); } catch(_){} }
            this._streamController = new AbortController();
            const fetchTimeout = setTimeout(() => {
                if (this._streamController) this._streamController.abort();
            }, 60000); // 60s timeout for streaming

            const params = new URLSearchParams();
            params.set('action', 'vmo_conversation_chat');
            const chatType = owner === 'consultation' ? 'consultant' : 'jarvis';
            params.set('chat_type', chatType);
            const sid = (typeof window !== 'undefined' && window.vmoCurrentConsultationSessionId) ? String(window.vmoCurrentConsultationSessionId) : '';
            if (sid) params.set('session_id', sid);
            try {
                const lang = (this.recognition && this.recognition.lang) ? this.recognition.lang : (this.defaultLang || navigator.language || '');
                if (lang) params.set('lang', lang);
            } catch (_) {}
            params.set('chat', JSON.stringify(chatHistory));
            params.set('security', vmo_ajax.nonce);
            params.set('stream', '1'); // ✅ Request SSE streaming

            const response = await fetch(vmo_ajax.ajaxurl, {
                method: 'POST',
                signal: this._streamController.signal,
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: params
            });
            
            clearTimeout(fetchTimeout);

            const contentType = response.headers.get('Content-Type') || '';

            if (contentType.includes('text/event-stream')) {
                // ══ STREAMING PATH ══
                // Create placeholder AI message bubble and fill it token-by-token
                const msgEl = this._addStreamBubble();
                let fullText = '';
                let streamError = '';
                let streamErrorDetail = '';
                const reader = response.body.getReader();
                const decoder = new TextDecoder();
                let sseBuffer = '';

                try {
                    while (true) {
                        const { done, value } = await reader.read();
                        if (done) break;
                        sseBuffer += decoder.decode(value, { stream: true });
                        const lines = sseBuffer.split('\n');
                        sseBuffer = lines.pop(); // keep incomplete line
                        for (const line of lines) {
                            const trimmed = line.trim();
                            if (!trimmed || !trimmed.startsWith('data: ')) continue;
                            try {
                                const evt = JSON.parse(trimmed.substring(6));
                                if (evt.t) {
                                    fullText += evt.t;
                                    if (msgEl) msgEl.textContent = fullText;
                                    if (this.chatHistory) this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
                                } else if (evt.error) {
                                    streamError = evt.error;
                                    streamErrorDetail = evt.detail || '';
                                }
                                // evt.done handled after loop
                            } catch (pe) {
                                if (pe.message && !pe.message.includes('JSON')) throw pe;
                            }
                        }
                    }
                } catch (streamErr) {
                    if (streamErr.name === 'AbortError') {
                        // Interrupted by user barge-in — keep partial text
                        console.log('🔇 Stream interrupted by user (barge-in)');
                        this._partialAIResponse = fullText;
                    } else {
                        throw streamErr;
                    }
                }

                if (streamError && !fullText) {
                    if (streamErrorDetail) {
                        console.error('Jarvis stream detail:', streamErrorDetail);
                    }
                    this.addMessage(this.formatAssistantError(streamError), 'ai');
                    this.updateStatus('Error');
                    this.isProcessing = false;
                    if (this.isActive) {
                        setTimeout(() => {
                            if (this.isActive && !this.isSpeaking && !this.isProcessing) this.startVoiceInput(false);
                        }, 1500);
                    }
                    return;
                }

                if (fullText) {
                    this.messages.push({ text: fullText, sender: 'ai', time: new Date() });
                }
                if (this.isActive && fullText) {
                    if (this.recognition && this.isListening) {
                        try { this.recognition.stop(); this.isListening = false; } catch(_) {}
                    }
                    await this.speakWithOpenAI(fullText);
                } else {
                    this.updateStatus('Ready');
                    this.isProcessing = false;
                }
            } else {
                // ══ FALLBACK: non-streaming JSON response ══
                const data = await response.json();
                if (data.success && data.data && data.data.response) {
                    this.addMessage(data.data.response, 'ai');
                    if (data.data.consultation_required) {
                        this.focusConsultationButton();
                        this.updateStatus('Business Consultation required');
                        this.isProcessing = false;
                        return;
                    }
                    if (this.isActive) {
                        if (this.recognition && this.isListening) {
                            try { this.recognition.stop(); this.isListening = false; } catch(_) {}
                        }
                        await this.speakWithOpenAI(data.data.response);
                    } else {
                        this.updateStatus('Ready');
                        this.isProcessing = false;
                    }
                } else {
                    let errorMsg = '';
                    if (data.data && data.data.message) {
                        errorMsg = data.data.message;
                        if (data.data.detail) {
                            console.error('Jarvis request detail:', data.data.detail);
                        }
                    } else if (data.message) {
                        errorMsg = data.message;
                    } else {
                        errorMsg = 'Sorry, I couldn\'t complete that just now. Please try again.';
                    }
                    this.addMessage(this.formatAssistantError(errorMsg), 'ai');
                    this.updateStatus('Error');
                    this.isProcessing = false;
                    if (this.isActive) {
                        setTimeout(() => {
                            if (this.isActive && !this.isSpeaking && !this.isProcessing) this.startVoiceInput(false);
                        }, 1500);
                    }
                }
            }
        } catch (error) {
            console.error('Failed to send message:', error);
            const isTimeout = error.name === 'AbortError';
            if (isTimeout && this._partialAIResponse) {
                // Barge-in: partial response already rendered, don't show error
                this.isProcessing = false;
            } else {
                const fallbackMsg = isTimeout
                    ? 'Sorry, the request timed out. Please try again.'
                    : 'Sorry, I couldn\'t complete that just now. Please try again.';
                this.addMessage(fallbackMsg, 'ai');
                this.updateStatus(isTimeout ? 'Request timed out' : 'Request error');
                this.isProcessing = false;
            }
            if (this.isActive) {
                setTimeout(() => {
                    if (this.isActive && !this.isSpeaking && !this.isProcessing) this.startVoiceInput(false);
                }, 1500);
            }
        } finally {
            clearTimeout(processingWatchdog);
            this._streamController = null;
        }
    }

    async tryHandleJarvisEmailOperatorCommand(text) {
        const command = String(text || '').trim();
        if (!command || !/\b(email|emails|inbox|mail|message|messages|reply|draft)\b/i.test(command)) {
            return false;
        }
        if (/^\s*(write|compose|create|send)\s+(?:an?\s+)?email\s*$/i.test(command)) {
            return false;
        }

        try {
            const response = await fetch(vmo_ajax.ajaxurl, {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: new URLSearchParams({
                    action: 'vmo_jarvis_command',
                    security: vmo_ajax.nonce,
                    command: command
                })
            });
            const payload = await response.json();
            if (!payload || !payload.success || !payload.data || payload.data.type !== 'email_operator') {
                return false;
            }

            const data = payload.data;
            if (data.tab && typeof switchTab === 'function') {
                switchTab(data.tab, null);
            }
            if (data.operation === 'draft_reply' && typeof window.vmoSwitchMailFolder === 'function') {
                window.vmoSwitchMailFolder('drafts');
            } else if (data.operation === 'send_reply' && typeof window.vmoSwitchMailFolder === 'function') {
                window.vmoSwitchMailFolder('sent');
            } else if ((data.operation === 'sync_inbox' || data.operation === 'mark_done' || data.operation === 'list_emails' || data.operation === 'read_email') && typeof window.vmoSwitchMailFolder === 'function') {
                window.vmoSwitchMailFolder('inbox');
            }

            const spoken = this.formatJarvisEmailOperatorText(data);
            this.addMessage(spoken, 'ai');
            if (this.isActive) {
                await this.speakWithOpenAI(spoken);
            } else {
                this.updateStatus('Ready');
                this.isProcessing = false;
            }
            return true;
        } catch (error) {
            console.warn('Jarvis email operator command failed:', error);
            return false;
        }
    }

    formatJarvisEmailOperatorText(data) {
        let text = String((data && data.message) || 'Email command completed.');
        if (Array.isArray(data.emails) && data.emails.length) {
            const lines = data.emails.slice(0, 5).map(email => {
                return 'Email ' + (email.index || '') + ' from ' + (email.from_email || 'unknown') + ': ' + (email.subject || '(No subject)') + '. ' + (email.snippet || '');
            });
            text += '\n' + lines.join('\n');
        }
        if (data.email) {
            text += '\nFrom ' + (data.email.from_email || 'unknown') + ': ' + (data.email.subject || '(No subject)') + '. ' + (data.email.body || data.email.snippet || '');
        }
        if (data.draft_preview) {
            text += '\nDraft preview: ' + data.draft_preview;
        }
        return text;
    }
    
    async startVoiceInput(isManualBargeIn = true) {
        // ✅ MUTE GUARD: If muted, never capture microphone audio.
        if (window.vmoMicMuted) {
            console.log('🔇 Voice input blocked - Microphone is muted');
            this.updateStatus('Microphone muted');
            this.applyMute();
            return;
        }

        if (!this.recognition) {
            alert('Speech recognition is not supported in your browser. Please use Chrome, Edge, or Safari.');
            return;
        }
        
        // ✅ SELF-LOOP PREVENTION: Only block AUTOMATIC restarts, not manual barge-in
        // Manual barge-in (user clicking mic) should ALWAYS work
        if (!isManualBargeIn) {
            // This is an automatic restart after TTS - apply cooldown checks
            if (this.isSpeaking) {
                console.log('🔇 Auto-restart blocked - AI is speaking');
                return;
            }
            
            if (this._ttsEndedAt && (Date.now() - this._ttsEndedAt < (this._ttsEndCooldownMs || 1500))) {
                console.log('🔇 Auto-restart blocked - TTS cooldown period');
                return;
            }
            
            if (window.VMOAudioCoordinator && window.VMOAudioCoordinator.isSpeaking) {
                console.log('🔇 Auto-restart blocked - VMOAudioCoordinator reports TTS active');
                return;
            }
        }
        
        if (this.isListening) {
            // Stop listening
            this.recognition.stop();
            this.isListening = false;
            this.hideListeningModal();
            this.updateStatus('Stopped listening');
        } else {
            // ═══════════════════════════════════════════════════════════════════════════════
            // ✅ GUARANTEED BARGE-IN: Force-stop ALL audio immediately
            // This ensures the user can ALWAYS interrupt the AI
            // ═══════════════════════════════════════════════════════════════════════════════
            console.log('🔊 BARGE-IN: Forcing stop of all audio');
            
            // 1. Stop our own audio element
            if (this.currentAudio) {
                try { 
                    this.currentAudio.pause(); 
                    this.currentAudio.currentTime = 0;
                    this.currentAudio.src = '';
                } catch(_) {}
                this.currentAudio = null;
            }
            
            // 2. Stop browser speech synthesis (fallback TTS)
            try {
                if (window.speechSynthesis) {
                    window.speechSynthesis.cancel();
                }
            } catch(_) {}
            
            // 3. Stop any global audio via coordinator
            try {
                if (window.VMOAudioCoordinator) {
                    if (window.VMOAudioCoordinator.stopAllTTS) {
                        window.VMOAudioCoordinator.stopAllTTS('barge-in', 'voice-panel');
                    }
                    // Also try to stop any audio elements it tracks
                    if (window.VMOAudioCoordinator.currentAudio) {
                        try { window.VMOAudioCoordinator.currentAudio.pause(); } catch(_) {}
                    }
                }
            } catch(_) {}
            
            // 4. Stop global vmoCurrentAudio if exists
            try {
                if (window.vmoCurrentAudio) {
                    window.vmoCurrentAudio.pause();
                    window.vmoCurrentAudio = null;
                }
            } catch(_) {}
            
            // 5. Force all audio elements on page to pause (nuclear option)
            try {
                document.querySelectorAll('audio').forEach(audio => {
                    if (this._realtimeSession && audio.id === 'vmo-realtime-audio') return;
                    try { audio.pause(); } catch(_) {}
                });
            } catch(_) {}
            
            // Clear state
            this.isSpeaking = false;
            this.isProcessing = false;
            this._ttsEndedAt = Date.now();
            this.clearLiveTranscript();
            
            // ═══════════════════════════════════════════════════════════════════════════════
            // Now request mic - but DON'T let coordinator block us on barge-in
            // ═══════════════════════════════════════════════════════════════════════════════
            if (window.VMOAudioCoordinator) {
                // Force-request mic with barge-in flag
                try {
                    // First release any existing mic claims
                    window.VMOAudioCoordinator.releaseMic('voice-panel');
                    window.VMOAudioCoordinator.releaseMic('consultation');
                } catch(_) {}
                
                // Mark coordinator as not speaking (we just stopped everything)
                try {
                    if (window.VMOAudioCoordinator.onTTSStop) {
                        window.VMOAudioCoordinator.onTTSStop();
                    }
                    window.VMOAudioCoordinator.isSpeaking = false;
                } catch(_) {}
                
                // Now request mic - should always succeed after clearing state
                const ok = window.VMOAudioCoordinator.requestMic('voice-panel', { bargeIn: true });
                if (!ok) {
                    // If still blocked, just proceed anyway for barge-in
                    console.warn('⚠️ Coordinator blocked mic request, but proceeding with barge-in anyway');
                }
            }
            
            // Start listening
            try {
                await this.micWarmup();
                this.recognition.start();
            } catch (error) {
                console.error('Failed to start recognition:', error);
                this.updateStatus('Error starting voice input');
                try { window.VMOAudioCoordinator && window.VMOAudioCoordinator.releaseMic('voice-panel'); } catch(_) {}
            }
        }
    }
    
    processSpeechInput(transcript) {
        console.log('📝 Transcript:', transcript);
        
        // ✅ RACE CONDITION FIX: Immediately flag processing to prevent double-submissions
        if (this.isActive && this.isProcessing) return;

        // ✅ SELF-LOOP PREVENTION: Don't process speech if AI is speaking or processing
        if (this.isSpeaking || this.isProcessing) {
            console.log('🔇 Ignoring speech input in processSpeechInput - AI is speaking or processing');
            return;
        }
        
        // ✅ COOLDOWN CHECK: Don't process speech too soon after TTS ends
        if (this._ttsEndedAt && (Date.now() - this._ttsEndedAt < (this._ttsEndCooldownMs || 1500))) {
            console.log('🔇 Ignoring speech input in processSpeechInput - TTS cooldown period');
            return;
        }
        
        // Keep modal visible during active voice session to avoid flicker
        if (!this.isActive) this.hideListeningModal();
        
        if (transcript.trim()) {
            // ✅ ALWAYS populate the input box — user reviews, edits, and clicks Send
            this._populateInputBox(transcript.trim());
        }
    }
    
    async speakWithOpenAI(text) {
        // Stop any current audio
        if (this.currentAudio) {
            try { this.currentAudio.pause(); } catch(_) {}
            this.currentAudio = null;
        }
        
        // ✅ CRITICAL SELF-LOOP PREVENTION: Force-stop recognition before TTS
        // Also abort any pending recognition to ensure clean state
        if (this.recognition) {
            try {
                this.recognition.abort(); // More aggressive than stop()
                this.isListening = false;
            } catch(_) {}
            try {
                this.recognition.stop();
                this.isListening = false;
            } catch(_) {}
        }
        
        // Keep modal visible during active voice session (speaking phase follows)
        if (!this.isActive) this.hideListeningModal();
        
        // Clear any pending silence timers and transcript buffers
        if (this._silenceTimer) {
            clearTimeout(this._silenceTimer);
            this._silenceTimer = null;
        }
        this._transcriptBuffer = '';
        this._interimText = '';
        
        // ✅ SELF-LOOP PREVENTION: Mark TTS start time
        this._ttsStartedAt = Date.now();
        this._ttsEndedAt = 0; // Reset end time
        
    this.isSpeaking = true;
    // Transition guard complete: moving into Speaking
    this.isProcessing = false;
    this.updateStatus('VMO is speaking...');
    // Update modal to Speaking phase
    if (this.isActive) this.setModalPhase('speaking');

    // ✅ SAFETY WATCHDOG: Force-reset isSpeaking after 60s max
    // Prevents permanent freeze if audio never fires onended/onerror
    if (this._speakingWatchdog) clearTimeout(this._speakingWatchdog);
    this._speakingWatchdog = setTimeout(() => {
        if (this.isSpeaking) {
            console.warn('⚠️ Speaking watchdog fired — resetting isSpeaking after 60s');
            this.isSpeaking = false;
            this._ttsEndedAt = Date.now();
            this.clearLiveTranscript();
            try { window.VMOAudioCoordinator && window.VMOAudioCoordinator.onTTSStop(); } catch(_) {}
            this.updateStatus('Ready');
            if (this.isActive) {
                this.setModalPhase('listening');
                setTimeout(() => {
                    if (this.isActive && !this.isSpeaking && !this.isProcessing) {
                        this.startVoiceInput(false);
                    }
                }, 500);
            }
        }
    }, 60000);
    
        // ✅ LIVE TRANSCRIPT: Show AI text while speaking (will be cleared when done)
        // Show first ~100 chars with ellipsis if longer
        const displayText = text.length > 100 ? text.substring(0, 100) + '...' : text;
        this.updateLiveTranscript(displayText, 'ai');
        this._currentSpeakingText = text; // Store full text for progressive reveal
        
        // Notify coordinator that TTS is starting
        try { 
            window.VMOAudioCoordinator && window.VMOAudioCoordinator.onTTSStart('voice-panel'); 
        } catch(_) {}
        
        try {
            // ✅ DOUBLE-CHECK: Ensure we are ABSOLUTELY not listening while speaking
            if (this.recognition) {
                try { this.recognition.abort(); } catch(_) {}
                try { this.recognition.stop(); } catch(_) {}
                this.isListening = false;
            }
            if (this._silenceTimer) { try { clearTimeout(this._silenceTimer); } catch(_) {} this._silenceTimer = null; }

            // Ensure the TTS client is available; retry briefly if still loading
            const ensureTts = () => new Promise((resolve, reject) => {
                if (typeof window.vmoSpeak === 'function') return resolve();
                const start = Date.now();
                const timer = setInterval(() => {
                    if (typeof window.vmoSpeak === 'function') {
                        clearInterval(timer);
                        resolve();
                    } else if (Date.now() - start > 3000) { // wait up to 3s
                        clearInterval(timer);
                        reject(new Error('TTS client not available'));
                    }
                }, 150);
            });

            await ensureTts();

            if (typeof window.vmoSpeak !== 'function') {
                throw new Error('TTS system not loaded');
            }

            // Use the REST API TTS
            const voice = window.vmoSelectedVoice || 'ash';
            this.currentAudio = await window.vmoSpeak(text, voice);
            // Apply global mute preference
            if (this.currentAudio) {
                try {
                    this.currentAudio.muted = !!window.vmoMuteTTS;
                    this.currentAudio.volume = window.vmoMuteTTS ? 0 : 1;
                } catch(_) {}
            }

            if (!this.currentAudio) {
                throw new Error('TTS failed to generate audio');
            }

            // Set up event handlers
            this.currentAudio.onended = () => {
                console.log('🔊 TTS playback ended');
                this.isSpeaking = false;
                if (this._speakingWatchdog) { clearTimeout(this._speakingWatchdog); this._speakingWatchdog = null; }
                
                // ✅ SELF-LOOP PREVENTION: Mark TTS end time for cooldown
                this._ttsEndedAt = Date.now();
                
                // ✅ LIVE TRANSCRIPT: Clear when AI finishes speaking
                this.clearLiveTranscript();
                this._currentSpeakingText = null;
                
                // Notify coordinator that TTS has stopped
                try { 
                    window.VMOAudioCoordinator && window.VMOAudioCoordinator.onTTSStop(); 
                } catch(_) {}
                
                this.updateStatus('Ready');
                if (this.isActive) this.setModalPhase('listening');

                // If voice session is active, automatically start listening again
                // ✅ INCREASED DELAY: Wait longer before restarting to ensure TTS audio has fully stopped
                if (this.isActive) {
                    const cooldownDelay = this._ttsEndCooldownMs || 1500;
                    setTimeout(() => {
                        // Double-check we're still in active mode and not speaking
                        if (this.isActive && !this.isSpeaking && !this.isProcessing) {
                            console.log('🎤 Auto-restarting voice input after TTS (with cooldown)');
                            this.startVoiceInput(false); // false = automatic restart, not manual barge-in
                        }
                    }, cooldownDelay);
                }
            };

            this.currentAudio.onerror = (error) => {
                console.error('❌ Audio playback error:', error);
                this.isSpeaking = false;
                if (this._speakingWatchdog) { clearTimeout(this._speakingWatchdog); this._speakingWatchdog = null; }
                
                // ✅ SELF-LOOP PREVENTION: Mark TTS end time for cooldown
                this._ttsEndedAt = Date.now();
                
                // ✅ LIVE TRANSCRIPT: Clear on error too
                this.clearLiveTranscript();
                this._currentSpeakingText = null;
                
                // Notify coordinator that TTS has stopped
                try { 
                    window.VMOAudioCoordinator && window.VMOAudioCoordinator.onTTSStop(); 
                } catch(_) {}
                
                this.updateStatus('Audio error');
                if (this.isActive) this.setModalPhase('listening');
                
                // Still resume listening on error if in active mode
                // ✅ INCREASED DELAY: Use cooldown period
                if (this.isActive) {
                    const cooldownDelay = this._ttsEndCooldownMs || 1500;
                    setTimeout(() => {
                        if (this.isActive && !this.isSpeaking && !this.isProcessing) {
                            this.startVoiceInput(false); // false = automatic restart
                        }
                    }, cooldownDelay);
                }
            };
            
        } catch (error) {
            console.error('❌ TTS error:', error);
            if (this._speakingWatchdog) { clearTimeout(this._speakingWatchdog); this._speakingWatchdog = null; }
            // Notify coordinator that TTS has stopped (even on error)
            try { 
                window.VMOAudioCoordinator && window.VMOAudioCoordinator.onTTSStop(); 
            } catch(_) {}
            
            // Fallback to browser speech synthesis if available
            if ('speechSynthesis' in window) {
                try {
                    const utter = new SpeechSynthesisUtterance(text);
                    const voices = window.speechSynthesis.getVoices();
                    // Try to match current recognition/browser language
                    const langPref = (this.recognition && this.recognition.lang) ? this.recognition.lang : (this.defaultLang || navigator.language || 'en-US');
                    utter.lang = langPref;
                    const preferred = voices.find(v => v.lang === langPref)
                        || voices.find(v => v.lang && v.lang.toLowerCase().startsWith((langPref || 'en').toLowerCase().split('-')[0] + '-'))
                        || voices.find(v => v.name.includes('Samantha') || (v.lang && v.lang.startsWith('en-')));
                    if (preferred) utter.voice = preferred;
                    utter.onend = () => {
                        console.log('🔊 Browser TTS ended');
                        this.isSpeaking = false;
                        
                        // ✅ SELF-LOOP PREVENTION: Mark TTS end time for cooldown
                        this._ttsEndedAt = Date.now();
                        
                        // ✅ Clear transcript on browser TTS end
                        this.clearLiveTranscript();
                        
                        try { 
                            window.VMOAudioCoordinator && window.VMOAudioCoordinator.onTTSStop(); 
                        } catch(_) {}
                        this.updateStatus('Ready');
                        
                        // ✅ FIX: Use cooldown delay and pass false for auto-restart
                        if (this.isActive) {
                            const cooldownDelay = this._ttsEndCooldownMs || 1500;
                            setTimeout(() => {
                                if (this.isActive && !this.isSpeaking && !this.isProcessing) {
                                    this.startVoiceInput(false); // false = automatic restart
                                }
                            }, cooldownDelay);
                        }
                    };
                    utter.onerror = () => {
                        console.error('❌ Browser TTS error');
                        this.isSpeaking = false;
                        
                        // ✅ SELF-LOOP PREVENTION: Mark TTS end time for cooldown
                        this._ttsEndedAt = Date.now();
                        
                        // ✅ Clear transcript on browser TTS error
                        this.clearLiveTranscript();
                        
                        try { 
                            window.VMOAudioCoordinator && window.VMOAudioCoordinator.onTTSStop(); 
                        } catch(_) {}
                        this.updateStatus('Audio error');
                        
                        // ✅ FIX: Use cooldown delay and pass false for auto-restart
                        if (this.isActive) {
                            const cooldownDelay = this._ttsEndCooldownMs || 1500;
                            setTimeout(() => {
                                if (this.isActive && !this.isSpeaking && !this.isProcessing) {
                                    this.startVoiceInput(false); // false = automatic restart
                                }
                            }, cooldownDelay);
                        }
                    };
                    window.speechSynthesis.speak(utter);
                    return;
                } catch (e) {
                    console.warn('⚠️  Browser TTS fallback failed:', e);
                }
            }
            this.isSpeaking = false;
            this.updateStatus('Error with voice playback');
            
            // Resume listening even if TTS failed
            if (this.isActive) {
                setTimeout(() => {
                    this.startVoiceInput();
                }, 1000);
            }
        }
        // Keep mic mute UI separate from TTS output mute.
        this.applyMute();
        // Apply to current audio if any
        if (this.currentAudio) {
            try {
                this.currentAudio.muted = !!window.vmoMuteTTS;
                this.currentAudio.volume = window.vmoMuteTTS ? 0 : 1;
            } catch (_) {}
        }
    }
    
    startVoiceSession() {
        // ─── Realtime path (preferred) ──────────────────────────────────────────
        // OpenAI Realtime API gives us full-duplex audio + native barge-in.
        // The legacy Web Speech + TTS path below is kept as a fallback for
        // browsers without WebRTC or when the Realtime token mint fails.
        // [VMO_REALTIME_VOICE_PANEL_HOOK]
        if (!this.isActive && global_VMORealtime_isUsable() && !this._realtimeDisabled) {
            this._startRealtimeSession();
            return;
        }
        if (this.isActive) {
            this.endVoiceSession();
            return;
        }
        if (!this.recognition) {
            alert('Voice chat requires speech recognition support. Please use Chrome, Edge, or Safari.');
            return;
        }
        
        if (!this.isActive) {
            this.isActive = true;
            this._autoRestartEnabled = true;
            this.updateStatus('Voice chat active');
            if (this.startBtn) {
                this.startBtn.innerHTML = '<span class="vmo-voice-start-icon">⏸️</span><span class="vmo-voice-start-text">Stop Voice</span>';
            }
            // Attempt to unlock audio playback gate on some browsers (Safari/iOS)
            try {
                const AC = window.AudioContext || window.webkitAudioContext;
                if (AC) {
                    if (!window._vmoAudioCtx) window._vmoAudioCtx = new AC();
                    window._vmoAudioCtx.resume().catch(() => {});
                }
            } catch(_) {}
            
            // Add system message
            this.addMessage('Voice chat started. Speak naturally!', 'system');
            
            // Keep the listening modal visible throughout the session
            this.showListeningModal('listening');
            // Start listening
            this.startVoiceInput();
        } else {
            this.endVoiceSession();
        }
    }
    
    // (removed duplicate show/hide modal methods; see unified versions below)
    
    addMessage(text, sender, saveToDb = true) {
        const messageDiv = document.createElement('div');
        messageDiv.className = `vmo-voice-message ${sender}`;
        
        const contentDiv = document.createElement('div');
        contentDiv.className = 'vmo-voice-message-content';
        contentDiv.textContent = text;
        
        const timeDiv = document.createElement('div');
        timeDiv.className = 'vmo-voice-message-time';
        timeDiv.textContent = new Date().toLocaleTimeString();
        
        messageDiv.appendChild(contentDiv);
        messageDiv.appendChild(timeDiv);
        
        this.chatHistory?.appendChild(messageDiv);
        
        // Scroll to bottom
        if (this.chatHistory) {
            this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
        }
        
        // Store message (don't store system messages, and only save if requested)
        if (sender !== 'system' && saveToDb) {
            this.messages.push({ text, sender, time: new Date() });
        }
    }
    
    /**
     * Create an empty AI message bubble for streaming and return its content element.
     * Tokens are appended to the returned element as they arrive.
     */
    _addStreamBubble() {
        const messageDiv = document.createElement('div');
        messageDiv.className = 'vmo-voice-message ai';

        const contentDiv = document.createElement('div');
        contentDiv.className = 'vmo-voice-message-content';
        contentDiv.textContent = ''; // starts empty

        const timeDiv = document.createElement('div');
        timeDiv.className = 'vmo-voice-message-time';
        timeDiv.textContent = new Date().toLocaleTimeString();

        messageDiv.appendChild(contentDiv);
        messageDiv.appendChild(timeDiv);

        this.chatHistory?.appendChild(messageDiv);
        if (this.chatHistory) this.chatHistory.scrollTop = this.chatHistory.scrollHeight;

        return contentDiv;
    }

    updateStatus(text) {
        if (this.statusText) {
            this.statusText.textContent = text;
        }
    }
    
    // Phase helper: listening | thinking | speaking
    setModalPhase(phase = 'listening') {
        if (!this.listeningModal) return;
        this.listeningModal.classList.remove('listening', 'thinking', 'speaking');
        this.listeningModal.classList.add(phase);
        if (this.listeningText) {
            if (phase === 'speaking') this.listeningText.textContent = 'Speaking...';
            else if (phase === 'thinking') this.listeningText.textContent = 'Processing...';
            else this.listeningText.textContent = 'Listening...';
        }
    }

    showListeningModal(phaseOrTranscript = '') {
        if (this.listeningModal) {
            // Ensure modal is at top-level to avoid stacking-context traps
            try {
                if (document.body && this.listeningModal.parentElement !== document.body) {
                    document.body.appendChild(this.listeningModal);
                }
            } catch(_) {}
            this.listeningModal.style.display = 'flex';
            // Mark ownership to Jarvis by default
            this.listeningModal.dataset.owner = 'voice-panel';
            // Hide consultation-only upload affordance when Jarvis owns modal
            const uploadBtn = document.getElementById('vmo-listening-upload');
            if (uploadBtn) uploadBtn.style.display = 'none';
            // Accept either a phase string or a transcript string
            const validPhase = ['listening','thinking','speaking'];
            if (typeof phaseOrTranscript === 'string' && validPhase.includes(phaseOrTranscript)) {
                this.setModalPhase(phaseOrTranscript);
            } else {
                // Treat as transcript text and default to listening phase
                this.setModalPhase('listening');
                if (this.listeningTranscript) {
                    this.listeningTranscript.textContent = phaseOrTranscript || '';
                }
            }
        }
    }
    
    hideListeningModal() {
        if (this.listeningModal) {
            this.listeningModal.style.display = 'none';
            if (this.listeningTranscript) {
                this.listeningTranscript.textContent = '';
            }
            // Clear ownership when hidden
            try { delete this.listeningModal.dataset.owner; } catch(_) {}
        }
    }

    /**
     * ✅ LIVE TRANSCRIPT: Update the transcript display in real-time
     * Shows what the user or AI is currently saying in the listening modal
     * @param {string} text - The text to display
     * @param {string} speaker - 'user' or 'ai' to style accordingly
     */
    updateLiveTranscript(text, speaker = 'user') {
        if (!this.listeningTranscript) {
            this.listeningTranscript = document.getElementById('vmo-listening-transcript');
        }
        
        if (this.listeningTranscript && text) {
            // Style based on speaker
            const prefix = speaker === 'ai' ? '🤖 ' : '🎤 ';
            this.listeningTranscript.textContent = prefix + text;
            this.listeningTranscript.style.opacity = '1';
            
            // Add visual distinction for AI vs user
            if (speaker === 'ai') {
                this.listeningTranscript.style.color = '#4CAF50'; // Green for AI
                this.listeningTranscript.style.fontStyle = 'normal';
            } else {
                this.listeningTranscript.style.color = '#fff'; // White for user
                this.listeningTranscript.style.fontStyle = 'italic';
            }
        }
    }

    /**
     * ✅ LIVE TRANSCRIPT: Clear the transcript display
     * Called when speech/message is complete
     */
    clearLiveTranscript() {
        if (!this.listeningTranscript) {
            this.listeningTranscript = document.getElementById('vmo-listening-transcript');
        }
        
        if (this.listeningTranscript) {
            this.listeningTranscript.textContent = '';
            this.listeningTranscript.style.opacity = '0';
        }
        
        // Reset buffers
        this._transcriptBuffer = '';
        this._interimText = '';
    }

    endVoiceSession() {
        // Stop voice chat completely across Realtime, fallback TTS, and Web Speech.
        this._autoRestartEnabled = false;
        this.isActive = false;
        this.isSpeaking = false;
        this.isProcessing = false;

        if (this._streamController) {
            try { this._streamController.abort(); } catch(_) {}
            this._streamController = null;
        }
        if (this._silenceTimer) { try { clearTimeout(this._silenceTimer); } catch(_) {} this._silenceTimer = null; }
        if (this._speakingWatchdog) { try { clearTimeout(this._speakingWatchdog); } catch(_) {} this._speakingWatchdog = null; }

        this._stopRealtimeSession();
        this.updateStatus('Voice chat stopped');
        if (this.startBtn) {
            this.startBtn.innerHTML = '<span class="vmo-voice-start-icon">🎤</span><span class="vmo-voice-start-text">Start Voice</span>';
        }
        // Stop any ongoing audio
        if (this.currentAudio) {
            try { this.currentAudio.pause(); } catch(_) {}
            try { this.currentAudio.currentTime = 0; } catch(_) {}
            try { this.currentAudio.removeAttribute('src'); } catch(_) {}
            try { this.currentAudio.load(); } catch(_) {}
            this.currentAudio = null;
        }
        try { window.speechSynthesis && window.speechSynthesis.cancel(); } catch(_) {}
        try {
            if (window.vmoCurrentAudio) {
                window.vmoCurrentAudio.pause();
                window.vmoCurrentAudio = null;
            }
        } catch(_) {}
        try { window.VMOAudioCoordinator && window.VMOAudioCoordinator.stopAllTTS && window.VMOAudioCoordinator.stopAllTTS('end-voice-session', 'voice-panel'); } catch(_) {}
        // Stop listening
        if (this.recognition) {
            try { this.recognition.abort(); } catch(_) {}
            try { this.recognition.stop(); } catch(_) {}
            this.isListening = false;
        }
        this._transcriptBuffer = '';
        this._interimText = '';
        this._partialAIResponse = '';
        this.clearLiveTranscript();
        window.vmoMicMuted = false;
        this.isMicMuted = false;
        this.applyMute();
        // Hide modal explicitly
        this.hideListeningModal();
        // Release mic
        try { window.VMOAudioCoordinator && window.VMOAudioCoordinator.releaseMic('voice-panel'); } catch(_) {}
    }

    openSettingsPopover() {
        // Build a lightweight popover for selecting the Jarvis voice
        let pop = document.getElementById('vmo-voice-settings-popover');
        if (pop) { pop.remove(); }
        pop = document.createElement('div');
        pop.id = 'vmo-voice-settings-popover';
        pop.style.cssText = 'position:fixed; z-index:999999; background:#111; color:#fff; border:1px solid #333; border-radius:8px; padding:12px; box-shadow:0 8px 24px rgba(0,0,0,0.35); max-width:240px;';
        // Position near settings button
        const btnRect = this.settingsBtn?.getBoundingClientRect();
        const x = (btnRect?.left || 20);
        const y = (btnRect?.bottom || 60) + 8;
        pop.style.left = Math.min(window.innerWidth - 260, Math.max(8, x - 200)) + 'px';
        pop.style.top = Math.min(window.innerHeight - 200, y) + 'px';
        pop.innerHTML = `
          <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
            <strong>Voice Settings</strong>
            <button id="vmo-voice-settings-close" style="background:none;border:none;color:#888;cursor:pointer;font-size:16px;">✕</button>
          </div>
          <div id="vmo-voice-settings-list" style="display:grid;gap:6px;margin-bottom:10px;">
            <div style="color:#aaa;">Loading voices…</div>
          </div>
          <div style="display:flex;gap:8px;justify-content:flex-end;">
            <button id="vmo-voice-save" style="background:#2e7d32;color:#fff;border:none;border-radius:4px;padding:6px 10px;cursor:pointer;">Save</button>
          </div>
        `;
        document.body.appendChild(pop);
        const close = () => { try { pop.remove(); } catch(_) {} };
        pop.querySelector('#vmo-voice-settings-close')?.addEventListener('click', close);

        const listEl = pop.querySelector('#vmo-voice-settings-list');
        const current = window.vmoSelectedVoice || localStorage.getItem('vmoJarvisVoice') || 'ash';
        const renderList = (voices) => {
            if (!voices || !voices.length) {
                listEl.innerHTML = '<div style="color:#aaa;">No voices available.</div>';
                return;
            }
            listEl.innerHTML = '';
            voices.forEach(v => {
                const row = document.createElement('label');
                row.style.cssText = 'display:flex;align-items:center;gap:8px;cursor:pointer;';
                const input = document.createElement('input');
                input.type = 'radio';
                input.name = 'vmo-voice-choice';
                input.value = v.id;
                if (v.id === current) input.checked = true;
                const span = document.createElement('span');
                span.textContent = `${v.name} (${v.id})`;
                row.appendChild(input);
                row.appendChild(span);
                listEl.appendChild(row);
            });
        };

        // Load from API
        (async () => {
            try {
                const voices = (typeof window.vmoGetVoices === 'function') ? await window.vmoGetVoices() : [];
                renderList(voices);
            } catch (e) {
                listEl.innerHTML = '<div style="color:#f99;">Failed to load voices.</div>';
            }
        })();

        pop.querySelector('#vmo-voice-save')?.addEventListener('click', () => {
            const sel = pop.querySelector('input[name="vmo-voice-choice"]:checked');
            const val = sel?.value || 'ash';
            window.vmoSelectedVoice = val;
            try { localStorage.setItem('vmoJarvisVoice', val); } catch(_) {}
            this.addMessage(`Jarvis voice set to: ${val}`, 'system');
            close();
        });
    }

    enableDrag() {
        const header = this.panel?.querySelector('.vmo-voice-header');
        const panel = this.panel;
        if (!header || !panel) return;
        let dragging = false;
        let startX = 0, startY = 0;
        let origLeft = 0, origTop = 0;
        const onMouseMove = (e) => {
            if (!dragging) return;
            const dx = e.clientX - startX;
            const dy = e.clientY - startY;
            let left = origLeft + dx;
            let top = origTop + dy;
            // Clamp within viewport
            left = Math.min(window.innerWidth - panel.offsetWidth, Math.max(0, left));
            top = Math.min(window.innerHeight - panel.offsetHeight, Math.max(0, top));
            panel.style.left = left + 'px';
            panel.style.top = top + 'px';
            panel.style.bottom = 'auto';
        };
        const onMouseUp = () => {
            dragging = false;
            document.removeEventListener('mousemove', onMouseMove);
            document.removeEventListener('mouseup', onMouseUp);
        };
        header.addEventListener('mousedown', (e) => {
            // Ignore clicks on buttons in the header (collapse/settings)
            if (e.target.closest('button')) return;
            dragging = true;
            const rect = panel.getBoundingClientRect();
            startX = e.clientX;
            startY = e.clientY;
            // Compute current left/top from rect
            origLeft = rect.left;
            origTop = rect.top;
            // Ensure fixed positioning
            panel.style.position = 'fixed';
            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
        });
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // REALTIME (WebRTC) VOICE PATH — preferred over Web Speech + TTS
    // Native server-side VAD + barge-in. See assets/vmo-realtime.js.
    // ═══════════════════════════════════════════════════════════════════════════
    _startRealtimeSession() {
        if (this._realtimeSession) return;
        try {
            this.isActive = true;
            this._autoRestartEnabled = false;            // Realtime owns the loop
            this.updateStatus('Connecting to Jarvis…');
            if (this.startBtn) {
                this.startBtn.innerHTML = '<span class="vmo-voice-start-icon">⏸️</span><span class="vmo-voice-start-text">Stop Voice</span>';
            }
            this.addMessage('Voice chat started (Realtime). Speak naturally — interrupt anytime.', 'system');
            this.showListeningModal('listening');

            const voice = window.vmoSelectedVoice || (function(){ try { return localStorage.getItem('vmoJarvisVoice'); } catch(_) { return null; } })() || 'ash';
            const sess = new window.VMORealtimeSession({ context: 'jarvis', voice: voice, muted: !!window.vmoMicMuted });
            this._realtimeSession = sess;
            window.VMORealtimeSession.shared = sess;

            let aiBubble = null;
            sess.on('status', (s) => {
                if (s === 'live')      { this.updateStatus(window.vmoMicMuted ? 'Microphone muted' : 'Listening…'); this.isSpeaking = false; }
                else if (s === 'speaking')  { this.updateStatus('Jarvis is speaking…'); this.isSpeaking = true; }
                else if (s === 'listening') { this.updateStatus(window.vmoMicMuted ? 'Microphone muted' : 'Listening…'); this.isSpeaking = false; }
                else if (s === 'ended')     { this.updateStatus('Voice chat stopped'); }
                else if (s === 'error')     { this.updateStatus('Voice error'); }
            });
            sess.on('ready', () => {
                if (window.vmoMicMuted) this.setMicMuted(true, { restartFallback: false });
                else this.applyMute();
            });
            sess.on('barge_in', () => {
                // User cut Jarvis off — close any in-flight assistant bubble
                aiBubble = null;
                this.isSpeaking = false;
            });
            sess.on('transcript.user', ({ text, final }) => {
                this.updateLiveTranscript(text, 'user');
                if (final && text && text.trim()) {
                    this.clearLiveTranscript();
                    this.addMessage(text.trim(), 'user');
                }
            });
            sess.on('transcript.assistant', ({ text, final }) => {
                if (!aiBubble) aiBubble = this._addStreamBubble();
                if (aiBubble) aiBubble.textContent = text;
                if (final) {
                    if (text && text.trim()) {
                        this.messages.push({ text: text.trim(), sender: 'ai', time: new Date() });
                    }
                    aiBubble = null;
                }
            });
            sess.on('error', (err) => {
                console.warn('[VMOVoicePanel] Realtime error, falling back', err);
                this._realtimeDisabled = true;
                this._realtimeSession = null;
                window.VMORealtimeSession.shared = null;
                this.isActive = false;
                this.addMessage('Voice connection failed. Using fallback mode.', 'system');
                // Try the legacy Web Speech path
                this.startVoiceSession();
            });

            sess.start().catch(() => { /* error handler above already fired */ });
        } catch (e) {
            console.warn('[VMOVoicePanel] Realtime start threw, falling back', e);
            this._realtimeDisabled = true;
            this._realtimeSession = null;
            this.isActive = false;
            this.startVoiceSession();
        }
    }

    _stopRealtimeSession() {
        if (!this._realtimeSession) return;
        try { this._realtimeSession.stop(); } catch (_) {}
        if (window.VMORealtimeSession && window.VMORealtimeSession.shared === this._realtimeSession) {
            window.VMORealtimeSession.shared = null;
        }
        this._realtimeSession = null;
    }
}

// Helper used by VMOVoicePanel to decide whether the Realtime path is available.
function global_VMORealtime_isUsable() {
    return !!(window.VMORealtimeSession &&
              typeof window.VMORealtimeSession.isSupported === 'function' &&
              window.VMORealtimeSession.isSupported());
}

// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
    if (document.getElementById('vmo-ai-voice')) {
        if (window.vmoVoicePanel && window.vmoVoicePanel instanceof VMOVoicePanel) {
            console.warn('⚠️ VMO Voice Panel already initialized; skipping duplicate init');
            return;
        }
        window.vmoVoicePanel = new VMOVoicePanel();
        console.log('✅ VMO Voice Panel initialized');
    }
});

// Close listening modal on click
document.addEventListener('click', (e) => {
    const modal = document.getElementById('vmo-listening-modal');
    if (modal && e.target === modal) {
        if (window.vmoVoicePanel) {
            // During an active session, ignore outside clicks to prevent accidental close
            if (!window.vmoVoicePanel.isActive) window.vmoVoicePanel.hideListeningModal();
        }
    }
});
