console.log('voice.js loaded');

class VoiceChannel {
    constructor(ws, settings) {
        // ws is ignored now as we use PHP signaling, but kept for compatibility
        this.settings = settings || { 
            mode: 'vox', 
            pttKey: 'v', 
            voxThreshold: 0.1,
            inputDevice: 'default',
            outputDevice: 'default',
            inputVolume: parseFloat(localStorage.getItem('voice_input_volume') || 1.0),
            outputVolume: parseFloat(localStorage.getItem('voice_output_volume') || 1.0),
            echoCancellation: true,
            noiseSuppression: true,
            advancedFilters: true
        };
        console.log('VoiceChannel constructor called with settings:', this.settings);
        this.localStream = null;
        this.processedStream = null;
        this.analysisStream = null;
        this.peers = {}; // userId -> RTCPeerConnection
        this.participants = {}; // userId -> {name}
        this.currentChannelId = null;
        this.myPeerId = null;
        this.pollInterval = null;
        this.canSpeak = true;
        this.remoteAudios = {}; // userId -> Audio element
        this.isSelfMuted = false;
        this.isDeafened = false;
        
        this.peerStates = {}; // userId -> { makingOffer, ignoreOffer }
        
        this.whisperSettings = []; // from DB
        this.whisperPeers = new Set(); // active whisper target peer_ids
        this.isWhispering = false;
        this.whisperListeners = [];
        
        this.audioContext = null;
        this.analyser = null;
        this.microphone = null;
        this.inputGainNode = null;
        this.scriptProcessor = null;
        this.rnnoiseNode = null;
        this.rnnoiseLoaded = false;
        this.rnnoiseWasmBinary = null;
        
        this.isTalking = false;
        this.pttPressed = false;
        this.voxActive = false;
        this.lastVoiceTime = 0;
        this.voxHoldTime = 400; 
        
        this.speakingUsers = new Set();

        this.setupPTTListeners();
        this.loadWhisperSettings();
        
        setTimeout(() => {
            const savedChannelId = sessionStorage.getItem('activeVoiceChannel');
            const savedPeerId = sessionStorage.getItem('activeVoicePeerId');
            if (savedChannelId) {
                console.log('Auto-rejoining voice channel:', savedChannelId);
                if (savedPeerId) this.myPeerId = savedPeerId;
                this.join(savedChannelId, true);
            }
        }, 200);
    }

    set whisperParamètres(val) {
        this.whisperSettings = val;
    }

    getAudioConstraints() {
        const useAdvanced = this.settings.advancedFilters !== false;
        const ns = !!this.settings.noiseSuppression;
        const ec = !!this.settings.echoCancellation;

        return {
            echoCancellation: ec,
            noiseSuppression: ns,
            autoGainControl: useAdvanced,
            googEchoCancellation: ec,
            googAutoGainControl: useAdvanced,
            googNoiseSuppression: ns,
            googHighpassFilter: useAdvanced,
            googTypingNoiseDetection: true,
            googAudioMirroring: false,
            googNoiseReduction: ns,
            googAutoGainControl2: useAdvanced,
            channelCount: 1,
            sampleRate: 48000,
            sampleSize: 16,
            latency: 0.005
        };
    }

    setupPTTListeners() {
        window.addEventListener('keydown', (e) => {
            if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
            if (this.settings.mode === 'ptt') {
                const isMatch = e.key.toLowerCase() === this.settings.pttKey.toLowerCase() || 
                               (e.code && e.code.toLowerCase() === this.settings.pttKey.toLowerCase()) ||
                               (this.settings.pttKey === '0' && e.code === 'Numpad0');
                if (isMatch) {
                    if (!this.pttPressed) {
                        this.pttPressed = true;
                        this.updateMuteState();
                    }
                    return;
                }
            }
            this.whisperSettings.forEach(w => {
                if (e.key.toLowerCase() === w.whisper_key.toLowerCase()) {
                    this.startWhisper(w);
                }
            });
        });
        window.addEventListener('keyup', (e) => {
            if (this.settings.mode === 'ptt') {
                const isMatch = e.key.toLowerCase() === this.settings.pttKey.toLowerCase() || 
                               (e.code && e.code.toLowerCase() === this.settings.pttKey.toLowerCase()) ||
                               (this.settings.pttKey === '0' && e.code === 'Numpad0');
                if (isMatch) {
                    this.pttPressed = false;
                    this.updateMuteState();
                    return;
                }
            }
            this.whisperSettings.forEach(w => {
                if (e.key.toLowerCase() === w.whisper_key.toLowerCase()) {
                    this.stopWhisper(w);
                }
            });
        });
    }

    async loadWhisperSettings() {
        try {
            const resp = await fetch('api_v1_voice.php?action=get_whispers');
            const data = await resp.json();
            if (data.success) {
                this.whisperSettings = data.whispers;
            }
        } catch (e) {
            console.error('Failed to load whispers:', e);
        }
    }

    setupWhisperListeners() {
        this.loadWhisperSettings();
    }

    async startWhisper(config) {
        if (this.isWhispering) return;
        try {
            const resp = await fetch(`api_v1_voice.php?action=find_whisper_targets&target_type=${config.target_type}&target_id=${config.target_id}`);
            const data = await resp.json();
            if (data.success && data.targets.length > 0) {
                this.isWhispering = true;
                this.whisperPeers.clear();
                for (const target of data.targets) {
                    if (target.peer_id === this.myPeerId) continue;
                    this.whisperPeers.add(target.peer_id);
                    if (!this.peers[target.peer_id]) {
                        this.createPeerConnection(target.peer_id, true);
                    }
                }
                this.updateMuteState();
            }
        } catch (e) {
            console.error('Whisper start error:', e);
        }
    }

    stopWhisper(config) {
        if (!this.isWhispering) return;
        this.isWhispering = false;
        this.whisperPeers.clear();
        this.updateMuteState();
    }

    async join(channelId, isAutoRejoin = false) {
        if (this.currentChannelId === channelId && !isAutoRejoin) return;
        if (this.currentChannelId && this.currentChannelId != channelId) this.leave();

        this.currentChannelId = channelId;
        sessionStorage.setItem('activeVoiceChannel', channelId);

        try {
            const constraints = { audio: this.getAudioConstraints(), video: false };
            console.log('Requesting mic with constraints:', constraints);
            try {
                this.localStream = await navigator.mediaDevices.getUserMedia(constraints);
            } catch (err) {
                console.warn('GUM with constraints failed, falling back to basic audio', err);
                this.localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
            }

            this.setMute(false);
            await this.setupVOX();

            const url = `api_v1_voice.php?action=join&room=${channelId}&name=${encodeURIComponent(window.currentUsername || 'Unknown')}${this.myPeerId ? '&peer_id='+this.myPeerId : ''}`;
            const resp = await fetch(url);
            const data = await resp.json();
            
            if (data.success) {
                this.myPeerId = data.peer_id;
                this.canSpeak = data.can_speak !== false;
                sessionStorage.setItem('activeVoicePeerId', this.myPeerId);
                this.startPolling();
                this.updateVoiceUI();
            }
        } catch (e) {
            console.error('Failed to join voice:', e);
            alert('Microphone access required. Error: ' + e.message);
            this.currentChannelId = null;
        }
    }

    startPolling() {
        if (this.pollInterval) clearInterval(this.pollInterval);
        this.pollInterval = setInterval(() => this.poll(), 500);
        this.poll();
    }

    async poll() {
        if (!this.myPeerId || !this.currentChannelId) return;
        try {
            const resp = await fetch(`api_v1_voice.php?action=poll&room=${this.currentChannelId}&peer_id=${this.myPeerId}&is_muted=${this.isSelfMuted ? 1 : 0}&is_deafened=${this.isDeafened ? 1 : 0}`);
            const data = await resp.json();
            if (data.success) {
                this.canSpeak = data.can_speak !== false;
                const oldPs = Object.keys(this.participants);
                this.participants = data.participants;
                const newPs = Object.keys(this.participants);
                newPs.forEach(pid => {
                    if (pid !== this.myPeerId && !this.peers[pid]) this.createPeerConnection(pid, true);
                });
                oldPs.forEach(pid => {
                    if (!this.participants[pid] && this.peers[pid] && !this.whisperPeers.has(pid) && !this.speakingUsers.has(pid)) {
                        this.peers[pid].close();
                        delete this.peers[pid];
                        if (this.remoteAudios[pid]) {
                            this.remoteAudios[pid].pause();
                            this.remoteAudios[pid].srcObject = null;
                            this.remoteAudios[pid].remove();
                            delete this.remoteAudios[pid];
                        }
                    }
                });
                if (data.signals && data.signals.length > 0) {
                    for (const sig of data.signals) await this.handleSignaling(sig);
                }
                this.updateVoiceUI();
            }
        } catch (e) { console.error('Polling error:', e); }
    }

    async sendSignal(to, data) {
        if (!this.myPeerId || !this.currentChannelId) return;
        await fetch(`api_v1_voice.php?action=signal&room=${this.currentChannelId}&peer_id=${this.myPeerId}&to=${to}&data=${encodeURIComponent(JSON.stringify(data))}`);
    }

    createPeerConnection(userId, isOfferor) {
        if (this.peers[userId]) return this.peers[userId];
        if (!this.peerStates[userId]) this.peerStates[userId] = { makingOffer: false, ignoreOffer: false };

        const pc = new RTCPeerConnection({
            iceServers: [
                { urls: 'stun:stun.l.google.com:19302' },
                { urls: 'stun:stun1.l.google.com:19302' },
                { urls: 'stun:stun2.l.google.com:19302' }
            ]
        });
        this.peers[userId] = pc;

        pc.onnegotiationneeded = async () => {
            try {
                this.peerStates[userId].makingOffer = true;
                await pc.setLocalDescription();
                this.sendSignal(userId, { type: 'offer', offer: pc.localDescription });
            } catch (err) { console.error('onnegotiationneeded error:', err); }
            finally { this.peerStates[userId].makingOffer = false; }
        };

        const streamToUse = this.processedStream || this.localStream;
        if (streamToUse) {
            streamToUse.getTracks().forEach(track => pc.addTrack(track, streamToUse));
        }

        pc.onicecandidate = (event) => {
            if (event.candidate) this.sendSignal(userId, { type: 'ice_candidate', candidate: event.candidate });
        };

        pc.ontrack = (event) => {
            const stream = event.streams[0] || new MediaStream([event.track]);
            if (this.audioContext && this.audioContext.state === 'suspended') this.audioContext.resume();
            if (this.remoteAudios[userId]) {
                this.remoteAudios[userId].pause();
                this.remoteAudios[userId].srcObject = null;
                this.remoteAudios[userId].remove();
            }
            const remoteAudio = new Audio();
            remoteAudio.autoplay = true;
            remoteAudio.style.display = 'none';
            remoteAudio.srcObject = stream;
            remoteAudio.muted = this.isDeafened;
            remoteAudio.volume = this.settings.outputVolume || 1.0;
            if (this.settings.outputDevice !== 'default' && typeof remoteAudio.setSinkId === 'function') {
                remoteAudio.setSinkId(this.settings.outputDevice);
            }
            document.body.appendChild(remoteAudio);
            this.remoteAudios[userId] = remoteAudio;
            remoteAudio.play().catch(e => console.warn('Autoplay prevented:', userId, e));
        };

        if (isOfferor && pc.signalingState === 'stable') pc.onnegotiationneeded();
        return pc;
    }

    async handleSignaling(sig) {
        const from = sig.from;
        const data = sig.data;
        try {
            switch (data.type) {
                case 'offer': await this.handleOffer(from, data.offer); break;
                case 'answer': await this.handleAnswer(from, data.answer); break;
                case 'ice_candidate': await this.handleCandidate(from, data.candidate); break;
                case 'voice_speaking': this.updateSpeakingUI(data.user_id, data.speaking, data.is_whisper); break;
            }
        } catch (err) { console.error('Signaling error:', err); }
    }

    async handleOffer(from, offer) {
        const pc = this.createPeerConnection(from, false);
        const state = this.peerStates[from];
        const offerCollision = (offer.type === "offer") && (state.makingOffer || pc.signalingState !== "stable");
        const isPolite = this.myPeerId > from;
        state.ignoreOffer = !isPolite && offerCollision;
        if (state.ignoreOffer) return;
        await pc.setRemoteDescription(new RTCSessionDescription(offer));
        if (offer.type === "offer") {
            await pc.setLocalDescription();
            this.sendSignal(from, { type: 'answer', answer: pc.localDescription });
        }
    }

    async handleAnswer(from, answer) {
        const pc = this.peers[from];
        if (pc) await pc.setRemoteDescription(new RTCSessionDescription(answer));
    }

    async handleCandidate(from, candidate) {
        const pc = this.peers[from];
        try { if (pc) await pc.addIceCandidate(new RTCIceCandidate(candidate)); } catch (err) {}
    }

    async setupVOX() {
        if (!this.localStream) return;
        try {
            if (!this.audioContext) {
                this.audioContext = new (window.AudioContext || window.webkitAudioContext)({ 
                    sampleRate: 48000,
                    latencyHint: 'interactive'
                });
            }
            if (this.audioContext.state === 'suspended') await this.audioContext.resume();

            if (this.settings.noiseSuppression && !this.rnnoiseLoaded) {
                try {
                    console.log('Loading RNNoise module and WASM...');
                    const version = Date.now();
                    await this.audioContext.audioWorklet.addModule(`assets/js/rnnoise-processor.js?v=${version}`);
                    const resp = await fetch(`assets/js/rnnoise.wasm?v=${version}`);
                    this.rnnoiseWasmBinary = await resp.arrayBuffer();
                    this.rnnoiseLoaded = true;
                    console.log('RNNoise loaded successfully');
                } catch (e) { console.error('Failed to load RNNoise:', e); }
            }

            // Cleanup old nodes
            [this.scriptProcessor, this.rnnoiseNode, this.inputGainNode, this.microphone].forEach(node => {
                if (node) { try { node.disconnect(); } catch(e) {} }
            });

            this.microphone = this.audioContext.createMediaStreamSource(this.localStream);
            let lastNode = this.microphone;

            // 1. Noise Suppression (RNNoise) FIRST - so it processes non-amplified signal
            if (this.rnnoiseLoaded && this.settings.noiseSuppression) {
                console.log('Activating RNNoise suppression node (PRE-GAIN)');
                this.rnnoiseNode = new AudioWorkletNode(this.audioContext, 'rnnoise-processor');
                this.rnnoiseNode.port.postMessage({ type: 'INIT', wasmBinary: this.rnnoiseWasmBinary });
                this.rnnoiseNode.port.postMessage({ type: 'SET_ENABLED', enabled: true });
                lastNode.connect(this.rnnoiseNode);
                lastNode = this.rnnoiseNode;
            }

            // 2. Input Gain (Amplification) AFTER Noise Suppression
            this.inputGainNode = this.audioContext.createGain();
            this.inputGainNode.gain.value = this.isSelfMuted ? 0 : (this.settings.inputVolume || 1.0);
            lastNode.connect(this.inputGainNode);
            lastNode = this.inputGainNode;

            // Destination for processed stream
            const destination = this.audioContext.createMediaStreamDestination();
            lastNode.connect(destination);
            this.processedStream = destination.stream;

            // Update peer tracks
            Object.values(this.peers).forEach(pc => {
                const sender = pc.getSenders().find(s => s.track && s.track.kind === 'audio');
                if (sender) {
                    console.log('Replacing track for peer to use processed (denoised) stream');
                    sender.replaceTrack(this.processedStream.getAudioTracks()[0]);
                }
            });

            // Analysis for VOX (on the FINAL processed signal)
            this.analyser = this.audioContext.createAnalyser();
            this.analyser.fftSize = 1024; 
            this.analyser.smoothingTimeConstant = 0.3;
            this.scriptProcessor = this.audioContext.createScriptProcessor(2048, 1, 1);
            
            lastNode.connect(this.analyser);
            this.analyser.connect(this.scriptProcessor);
            
            const silence = this.audioContext.createGain();
            silence.gain.value = 0;
            this.scriptProcessor.connect(silence);
            silence.connect(this.audioContext.destination);
            
            const buffer = new Float32Array(this.analyser.fftSize);
            this.scriptProcessor.onaudioprocess = () => {
                this.analyser.getFloatTimeDomainData(buffer);
                let sum = 0;
                for (let i = 0; i < buffer.length; i++) sum += buffer[i] * buffer[i];
                const rms = Math.sqrt(sum / buffer.length);
                this.currentVolume = Math.min(1.0, rms * 3); 
                
                if (this.settings.mode !== 'vox') { this.voxActive = false; return; }
                if (this.currentVolume > this.settings.voxThreshold) {
                    this.lastVoiceTime = Date.now();
                    if (!this.voxActive) { this.voxActive = true; this.updateMuteState(); }
                } else {
                    if (this.voxActive && Date.now() - this.lastVoiceTime > this.voxHoldTime) {
                        this.voxActive = false; this.updateMuteState();
                    }
                }
            };
        } catch (e) { console.error('Failed setupVOX:', e); }
    }

    getVolume() { return this.currentVolume || 0; }

    updateMuteState() {
        if (!this.localStream) return;
        let shouldTalk = (this.settings.mode === 'ptt') ? this.pttPressed : this.voxActive;
        if (this.canSpeak === false) shouldTalk = false;
        if (this.isWhispering) shouldTalk = true;

        // Transmission is only possible if NOT self-muted
        const shouldTransmit = !this.isSelfMuted && shouldTalk;

        if (this.isTalking !== shouldTransmit || this.lastWhisperState !== this.isWhispering) {
            this.isTalking = shouldTransmit;
            this.lastWhisperState = this.isWhispering;
            this.applyAudioState();
            
            // Only update UI and send signals if we are actually transmitting
            this.updateSpeakingUI(window.currentUserId, shouldTransmit, this.isWhispering);
            const msg = { type: 'voice_speaking', channel_id: this.currentChannelId, user_id: window.currentUserId, speaking: shouldTransmit, is_whisper: this.isWhispering };
            Object.keys(this.peers).forEach(pid => {
                if (this.isWhispering) {
                    if (this.whisperPeers.has(pid)) this.sendSignal(pid, msg);
                    else this.sendSignal(pid, { ...msg, speaking: false });
                } else this.sendSignal(pid, msg);
            });
        }
    }

    applyAudioState() {
        const streamToUse = this.processedStream || this.localStream;
        const shouldTransmit = !this.isSelfMuted && this.isTalking && (this.canSpeak || this.isWhispering);
        
        // Safety: ensure gain is 0 if muted to prevent any signal from reaching the analyzer or destination
        if (this.inputGainNode && this.audioContext) {
            const targetGain = this.isSelfMuted ? 0 : (this.settings.inputVolume || 1.0);
            this.inputGainNode.gain.setTargetAtTime(targetGain, this.audioContext.currentTime, 0.01);
        }

        if (streamToUse) {
            streamToUse.getAudioTracks().forEach(track => { track.enabled = shouldTransmit; });
            Object.entries(this.peers).forEach(([pid, pc]) => {
                const sender = pc.getSenders().find(s => s.track && s.track.kind === 'audio');
                if (sender) {
                    if (this.isWhispering) {
                        sender.track.enabled = shouldTransmit && this.whisperPeers.has(pid);
                    } else {
                        // FIX: Ensure track is only enabled if shouldTransmit is true
                        sender.track.enabled = shouldTransmit && !!this.participants[pid];
                    }
                }
            });
        }
        this.updateUserPanelButtons();
    }

    setMute(mute) { 
        this.isSelfMuted = mute; 
        this.updateMuteState(); 
        this.applyAudioState(); // Always update UI even if not "talking"
    }
    toggleMute() { if (this.canSpeak !== false) this.setMute(!this.isSelfMuted); }
    toggleDeafen() {
        this.isDeafened = !this.isDeafened;
        Object.values(this.remoteAudios).forEach(audio => {
            audio.muted = this.isDeafened;
            if (!this.isDeafened) audio.volume = this.settings.outputVolume || 1.0;
        });
        if (this.isDeafened && !this.isSelfMuted) this.setMute(true);
        else {
            this.applyAudioState();
        }
    }

    setOutputVolume(vol) {
        this.settings.outputVolume = parseFloat(vol);
        Object.values(this.remoteAudios).forEach(audio => { audio.volume = this.settings.outputVolume; });
    }
    setInputVolume(vol) { 
        this.settings.inputVolume = parseFloat(vol); 
        if (this.inputGainNode && this.audioContext && !this.isSelfMuted) {
            this.inputGainNode.gain.setTargetAtTime(this.settings.inputVolume, this.audioContext.currentTime, 0.01);
        }
    }

    async setInputDevice(deviceId) {
        this.settings.inputDevice = deviceId;
        if (this.currentChannelId && this.localStream) {
            const constraints = { audio: this.getAudioConstraints(), video: false };
            const newStream = await navigator.mediaDevices.getUserMedia(constraints);
            this.localStream.getTracks().forEach(t => t.stop());
            this.localStream = newStream;
            await this.setupVOX();
            this.applyAudioState();
        }
    }

    async setOutputDevice(deviceId) {
        this.settings.outputDevice = deviceId;
        Object.values(this.remoteAudios).forEach(audio => {
            if (typeof audio.setSinkId === 'function') audio.setSinkId(deviceId).catch(e => console.error(e));
        });
    }

    async updateAudioConstraints() {
        if (this.currentChannelId && this.localStream) {
            const constraints = { audio: this.getAudioConstraints(), video: false };
            try {
                console.log('Updating audio constraints:', constraints);
                const newStream = await navigator.mediaDevices.getUserMedia(constraints);
                this.localStream.getTracks().forEach(t => t.stop());
                this.localStream = newStream;
                await this.setupVOX();
                this.applyAudioState();
            } catch (e) { console.error(e); }
        }
    }

    updateUserPanelButtons() {
        const btnMute = document.getElementById('btn-panel-mute');
        const btnDeafen = document.getElementById('btn-panel-deafen');
        let displayMuted = this.isSelfMuted || this.canSpeak === false;
        if (btnMute) {
            btnMute.classList.toggle('active', displayMuted);
            btnMute.style.color = displayMuted ? '#f23f43' : 'var(--text-muted)';
            btnMute.innerHTML = displayMuted ? 
                '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="1" y1="1" x2="23" y2="23"></line><path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"></path><path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line></svg>' :
                '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="19" x2="16" y2="19"></line></svg>';
        }
        if (btnDeafen) {
            btnDeafen.classList.toggle('active', this.isDeafened);
            btnDeafen.style.color = this.isDeafened ? '#f23f43' : 'var(--text-muted)';
            btnDeafen.innerHTML = this.isDeafened ?
                '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="1" y1="1" x2="23" y2="23"></line><path d="M8.85 4.11A9 9 0 1 1 20 12"></path><path d="M11.64 6.64A5 5 0 1 1 15 10"></path></svg>' :
                '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18v-6a9 9 0 0 1 18 0v6"></path><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"></path></svg>';
        }
    }

    leave() {
        if (!this.currentChannelId) return;
        const cid = this.currentChannelId;
        const pid = this.myPeerId;
        sessionStorage.removeItem('activeVoiceChannel');
        sessionStorage.removeItem('activeVoicePeerId');
        if (this.pollInterval) clearInterval(this.pollInterval);
        fetch(`api_v1_voice.php?action=leave&room=${cid}&peer_id=${pid}`, { keepalive: true });
        if (this.localStream) this.localStream.getTracks().forEach(track => track.stop());
        if (this.processedStream) this.processedStream.getTracks().forEach(track => track.stop());
        if (this.analysisStream) this.analysisStream.getTracks().forEach(track => track.stop());
        if (this.scriptProcessor) { try { this.scriptProcessor.disconnect(); this.scriptProcessor.onaudioprocess = null; } catch(e) {} }
        if (this.rnnoiseNode) { try { this.rnnoiseNode.disconnect(); } catch(e) {} }
        if (this.inputGainNode) { try { this.inputGainNode.disconnect(); } catch(e) {} }
        if (this.microphone) { try { this.microphone.disconnect(); } catch(e) {} }
        Object.values(this.peers).forEach(pc => pc.close());
        Object.values(this.remoteAudios).forEach(audio => { audio.pause(); audio.remove(); audio.srcObject = null; });
        this.peers = {}; this.remoteAudios = {}; this.participants = {}; this.currentChannelId = null; this.myPeerId = null; this.speakingUsers.clear();
        document.querySelectorAll('.voice-item').forEach(el => el.classList.remove('active'));
        this.updateVoiceUI();
    }

    updateVoiceUI() {
        VoiceChannel.refreshAllVoiceUsers();
        if (this.currentChannelId) {
            if (!document.querySelector('.voice-controls')) {
                const controls = document.createElement('div');
                controls.className = 'voice-controls p-2 d-flex justify-content-between align-items-center border-top bg-dark';
                controls.style.backgroundColor = '#232428';
                controls.innerHTML = `
                    <div class="d-flex align-items-center">
                        <div class="voice-status-icon text-success me-2" style="font-size: 8px;">●</div>
                        <div class="small fw-bold" style="font-size: 11px; color: #248046;">Voice (${this.settings.mode.toUpperCase()})</div>
                    </div>
                    <div>
                        <button class="btn btn-sm text-muted" id="btn-voice-leave" title="Disconnect">
                            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-3.33-2.67m-2.67-3.34a19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91"></path><line x1="23" y1="1" x2="1" y2="23"></line></svg>
                        </button>
                    </div>`;
                const sidebar = document.querySelector('.channels-sidebar');
                if (sidebar) sidebar.appendChild(controls);
                const btnLeave = document.getElementById('btn-voice-leave');
                if (btnLeave) btnLeave.onclick = () => this.leave();
            }
        } else {
            const controls = document.querySelector('.voice-controls');
            if (controls) controls.remove();
        }
    }

    updateSpeakingUI(userId, isSpeaking, isWhisper = false) {
        userId = String(userId);
        if (isSpeaking) this.speakingUsers.add(userId); else this.speakingUsers.delete(userId);
        const userEls = document.querySelectorAll(`.voice-user[data-user-id="${userId}"]`);
        userEls.forEach(el => {
            const avatar = el.querySelector('.message-avatar');
            if (avatar) avatar.style.boxShadow = isSpeaking ? (isWhisper ? '0 0 0 2px #00a8fc' : '0 0 0 2px #23a559') : 'none';
            if (isWhisper && isSpeaking && userId !== String(window.currentUserId)) {
                if (!el.querySelector('.whisper-label')) {
                    const label = document.createElement('span');
                    label.className = 'whisper-label badge bg-info ms-1';
                    label.style.fontSize = '8px';
                    label.innerText = 'WHISPER';
                    el.querySelector('span.text-truncate').after(label);
                }
            } else { const label = el.querySelector('.whisper-label'); if (label) label.remove(); }
        });
    }

    static async refreshAllVoiceUsers() {
        try {
            const resp = await fetch('api_v1_voice.php?action=list_all');
            const data = await resp.json();
            if (data.success) {
                document.querySelectorAll('.voice-users-list').forEach(el => el.innerHTML = '');
                document.querySelectorAll('.voice-item').forEach(el => el.classList.remove('connected'));
                Object.keys(data.channels).forEach(channelId => {
                    const voiceItem = document.querySelector(`.voice-item[data-channel-id="${channelId}"]`);
                    if (voiceItem) {
                        if (window.voiceHandler && window.voiceHandler.currentChannelId == channelId) voiceItem.classList.add('connected');
                        const container = voiceItem.closest('.channel-item-container');
                        if (container) {
                            const listEl = container.querySelector('.voice-users-list');
                            if (listEl) {
                                data.channels[channelId].forEach(p => {
                                    const pid = String(p.user_id);
                                    const isSpeaking = window.voiceHandler && window.voiceHandler.speakingUsers.has(pid);
                                    VoiceChannel.renderUserToUI(listEl, p.user_id, p.display_name || p.username, p.avatar_url, isSpeaking, p.is_muted, p.is_deafened);
                                });
                            }
                        }
                    }
                });
            }
        } catch (e) { console.error('Failed refresh voice users:', e); }
    }

    static renderUserToUI(container, userId, username, avatarUrl, isSpeaking = false, isMuted = false, isDeafened = false) {
        const userEl = document.createElement('div');
        userEl.className = 'voice-user small text-muted d-flex align-items-center mb-1';
        userEl.dataset.userId = userId;
        userEl.style.paddingLeft = '8px';
        const avatarStyle = avatarUrl ? `background-image: url('${avatarUrl}'); background-size: cover;` : "background-color: #555;";
        const boxShadow = isSpeaking ? 'box-shadow: 0 0 0 2px #23a559;' : '';
        let icons = '';
        if (isDeafened) icons += '<i class="fa-solid fa-volume-xmark ms-auto text-danger" style="font-size: 10px;"></i>';
        else if (isMuted) icons += '<i class="fa-solid fa-microphone-slash ms-auto text-danger" style="font-size: 10px;"></i>';
        userEl.innerHTML = `
            <div class="message-avatar me-2" style="width: 16px; height: 16px; border-radius: 50%; transition: box-shadow 0.2s; ${avatarStyle} ${boxShadow}"></div>
            <span class="text-truncate" style="font-size: 13px; max-width: 100px;">${username}</span>
            ${icons}`;
        container.appendChild(userEl);
    }
}