import React, { createContext, useContext, useState, useEffect, useRef, useCallback } from 'react';
import { Room, RoomEvent, VideoPresets, ConnectionQuality, DisconnectReason } from 'livekit-client';
import { LiveKitRoom, RoomAudioRenderer } from '@livekit/components-react';
import { useQuery, useConvex } from 'convex/react';
import { api } from '../../../../convex/_generated/api';
import { findTrackPubs } from '../utils/streamUtils.jsx';
import { VoiceRecorder } from '../utils/voiceRecorder';
import { usePlatform } from '../platform';
import '@livekit/components-styles';

import joinSound from '../assets/sounds/join_call.mp3';
import leaveSound from '../assets/sounds/leave_call.mp3';
import muteSound from '../assets/sounds/mute.mp3';
import unmuteSound from '../assets/sounds/unmute.mp3';
import deafenSound from '../assets/sounds/deafen.mp3';
import undeafenSound from '../assets/sounds/undeafen.mp3';
import viewerJoinSound from '../assets/sounds/screenshare_viewer_join.mp3';
import viewerLeaveSound from '../assets/sounds/screenshare_viewer_leave.mp3';
import screenshareStartSound from '../assets/sounds/screenshare_start.mp3';
import screenshareStopSound from '../assets/sounds/screenshare_stop.mp3';
import cameraOnSound from '../assets/sounds/camera_on.mp3';
import cameraOffSound from '../assets/sounds/camera_off.mp3';

const soundMap = {
    join: joinSound,
    leave: leaveSound,
    mute: muteSound,
    unmute: unmuteSound,
    deafen: deafenSound,
    undeafen: undeafenSound,
    viewer_join: viewerJoinSound,
    viewer_leave: viewerLeaveSound,
    screenshare_start: screenshareStartSound,
    screenshare_stop: screenshareStopSound,
    camera_on: cameraOnSound,
    camera_off: cameraOffSound,
};

const VoiceContext = createContext();

export const useVoice = () => useContext(VoiceContext);

let _suppressAppSounds = false;

function playSound(type) {
    if (_suppressAppSounds) return;
    const src = soundMap[type];
    if (!src) return;
    const audio = new Audio(src);
    audio.volume = 0.5;
    audio.play().catch(e => console.error("Sound play failed", e));
}

function playSoundUrl(url) {
    if (_suppressAppSounds) return;
    const audio = new Audio(url);
    audio.volume = 0.5;
    audio.play().catch(e => console.error("Sound play failed", e));
}

export const VoiceProvider = ({ children }) => {
    const platform = usePlatform();
    const { idle, voiceService } = platform;
    const [activeChannelId, setActiveChannelId] = useState(null);
    const [activeChannelName, setActiveChannelName] = useState(null);
    const [connectionState, setConnectionState] = useState('disconnected');
    const [room, setRoom] = useState(null);
    const [token, setToken] = useState(null);
    const [activeSpeakers, setActiveSpeakers] = useState(new Set());
    const speakerRemovalTimers = useRef(new Map());
    const clearSpeakerTimers = useCallback(() => {
        for (const timer of speakerRemovalTimers.current.values()) {
            clearTimeout(timer);
        }
        speakerRemovalTimers.current.clear();
    }, []);
    const [isMuted, setIsMuted] = useState(false);
    const [isDeafened, setIsDeafened] = useState(false);
    const [isScreenSharing, setIsScreenSharingLocal] = useState(false);
    const [globalOutputVolume, setGlobalOutputVolume] = useState(() =>
        parseInt(localStorage.getItem('voiceOutputVolume') || '100')
    );
    const isMovingRef = useRef(false);
    const isDMCallRef = useRef(false);
    const lastSpokeRef = useRef(Date.now());
    const [isReceivingScreenShareAudio, setIsReceivingScreenShareAudio] = useState(false);
    const [isReconnecting, setIsReconnecting] = useState(false);
    const [connectionQualities, setConnectionQualities] = useState({});

    // Voice recording — crash-safe per-participant audio capture.
    // Only available on Electron (`platform.features.hasRecording`).
    // `recordingError` is surfaced once per failure so UI can toast
    // it; consumers should clear it after reading.
    const [isRecording, setIsRecording] = useState(false);
    const [recordingStartedAt, setRecordingStartedAt] = useState(null);
    const [recordingSessionId, setRecordingSessionId] = useState(null);
    const [recordingError, setRecordingError] = useState(null);
    const voiceRecorderRef = useRef(null);

    const convex = useConvex();

    // Stream watching state (lifted from VoiceStage so PiP can persist across navigation)
    const [watchingStreamOf, setWatchingStreamOfRaw] = useState(null);

    const setWatchingStreamOf = useCallback((identity) => {
        setWatchingStreamOfRaw(identity);
        // Sync to backend
        const userId = localStorage.getItem('userId');
        if (userId) {
            convex.mutation(api.voiceState.setWatchingStream, {
                userId,
                ...(identity ? { watchingStream: identity } : {}),
            }).catch(e => console.error('Failed to set watching stream:', e));
        }
        // Play join sound for the viewer starting to watch
        if (identity) {
            playSound('viewer_join');
        }
    }, [convex]);

    const clearWatchingStream = useCallback(() => {
        setWatchingStreamOfRaw(null);
        const userId = localStorage.getItem('userId');
        if (userId) {
            convex.mutation(api.voiceState.setWatchingStream, { userId }).catch(
                e => console.error('Failed to clear watching stream:', e)
            );
        }
    }, [convex]);

    // Personal mute state (persisted to localStorage)
    const [personallyMutedUsers, setPersonallyMutedUsers] = useState(() => {
        const saved = localStorage.getItem('personallyMutedUsers');
        return new Set(saved ? JSON.parse(saved) : []);
    });

    // Per-user volume state: userId → 0-200 (persisted to localStorage)
    const [userVolumes, setUserVolumes] = useState(() => {
        const saved = localStorage.getItem('userVolumes');
        return saved ? JSON.parse(saved) : {};
    });

    const setUserVolume = useCallback((userId, volume) => {
        setUserVolumes(prev => {
            const next = { ...prev, [userId]: volume };
            localStorage.setItem('userVolumes', JSON.stringify(next));
            return next;
        });
        // Apply volume to LiveKit participant (factoring in global output volume)
        const participant = room?.remoteParticipants?.get(userId);
        const globalVol = globalOutputVolume / 100;
        if (participant) participant.setVolume(Math.min(2, (volume / 100) * globalVol));
        // Sync personal mute state
        if (volume === 0) {
            setPersonallyMutedUsers(prev => {
                const next = new Set(prev);
                next.add(userId);
                localStorage.setItem('personallyMutedUsers', JSON.stringify([...next]));
                return next;
            });
        } else {
            setPersonallyMutedUsers(prev => {
                if (!prev.has(userId)) return prev;
                const next = new Set(prev);
                next.delete(userId);
                localStorage.setItem('personallyMutedUsers', JSON.stringify([...next]));
                return next;
            });
        }
    }, [room, globalOutputVolume]);

    const getUserVolume = useCallback((userId) => {
        return userVolumes[userId] ?? 100;
    }, [userVolumes]);

    const togglePersonalMute = (userId) => {
        const globalVol = globalOutputVolume / 100;
        setPersonallyMutedUsers(prev => {
            const next = new Set(prev);
            if (next.has(userId)) {
                next.delete(userId);
                // Restore to stored volume (default 100)
                const vol = userVolumes[userId] ?? 100;
                const restoreVol = vol === 0 ? 100 : vol;
                const participant = room?.remoteParticipants?.get(userId);
                if (participant) participant.setVolume(Math.min(2, (restoreVol / 100) * globalVol));
                // Update stored volume if it was 0
                if (vol === 0) {
                    setUserVolumes(p => {
                        const n = { ...p, [userId]: 100 };
                        localStorage.setItem('userVolumes', JSON.stringify(n));
                        return n;
                    });
                }
            } else {
                next.add(userId);
                const participant = room?.remoteParticipants?.get(userId);
                if (participant) participant.setVolume(0);
            }
            localStorage.setItem('personallyMutedUsers', JSON.stringify([...next]));
            return next;
        });
    };

    const isPersonallyMuted = (userId) => personallyMutedUsers.has(userId);

    const serverMute = async (targetUserId, isServerMuted) => {
        const actorUserId = localStorage.getItem('userId');
        if (!actorUserId) return;
        try {
            await convex.mutation(api.voiceState.serverMute, { actorUserId, targetUserId, isServerMuted });
        } catch (e) {
            console.error('Failed to server mute:', e);
        }
    };

    const disconnectUser = async (targetUserId) => {
        const actorUserId = localStorage.getItem('userId');
        if (!actorUserId) return;
        try {
            await convex.mutation(api.voiceState.disconnectUser, { actorUserId, targetUserId });
        } catch (e) {
            console.error('Failed to disconnect user:', e);
        }
    };

    const isServerMuted = (userId) => {
        for (const users of Object.values(voiceStates)) {
            const user = users.find(u => u.userId === userId);
            if (user) return !!user.isServerMuted;
        }
        return false;
    };

    const voiceStates = useQuery(api.voiceState.getAll) || {};
    const serverSettings = useQuery(api.serverSettings.get);

    // Subscribe to own join sound URL for self-join playback
    const myUserId = localStorage.getItem('userId');
    const myJoinSoundUrl = useQuery(
        api.auth.getMyJoinSoundUrl,
        myUserId ? { userId: myUserId } : "skip"
    );

    // Refs for detecting other-user joins via voiceStates changes
    const prevChannelUsersRef = useRef(new Set());
    const otherJoinInitRef = useRef(false);
    const isInAfkChannel = !!(activeChannelId && serverSettings?.afkChannelId === activeChannelId);

    async function updateVoiceState(fields) {
        const userId = localStorage.getItem('userId');
        if (!userId || !activeChannelId) return;
        try {
            await convex.mutation(api.voiceState.updateState, { userId, ...fields });
        } catch (e) {
            console.error('Failed to update voice state:', e);
        }
    }

    const connectToVoice = async (channelId, channelName, userId, isDMCall = false) => {
        if (activeChannelId === channelId) return;
        isDMCallRef.current = isDMCall;

        if (room) await room.disconnect();

        setActiveChannelId(channelId);
        setActiveChannelName(channelName);
        setConnectionState('connecting');
        window.__inVoiceCall = true;

        try {
            // Request microphone permission (triggers Android runtime prompt)
            try {
                const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
                stream.getTracks().forEach(t => t.stop());
            } catch (e) {
                console.error('Microphone permission denied:', e);
                setConnectionState('error');
                setActiveChannelId(null);
                return;
            }

            const { token: lkToken } = await convex.action(api.voice.getToken, {
                channelId,
                userId,
                username: localStorage.getItem('username') || 'Unknown'
            });

            if (!lkToken) throw new Error('Failed to get token');

            setToken(lkToken);

            // Voice Settings tab (settings/UserSettingsModal.tsx) writes
            // everything to a single `voiceSettings` JSON blob. Read it
            // here so user choices actually flow through to LiveKit.
            // Defaults: echoCancellation off (user preference — can be
            // re-enabled in settings), everything else on.
            let voiceSettings = {
                inputDeviceId: 'default',
                outputDeviceId: 'default',
                videoDeviceId: 'default',
                inputVolume: 100,
                noiseSuppression: true,
                echoCancellation: false,
                autoGainControl: true,
            };
            try {
                const raw = localStorage.getItem('voiceSettings');
                if (raw) {
                    const parsed = JSON.parse(raw);
                    if (parsed && typeof parsed === 'object') {
                        voiceSettings = { ...voiceSettings, ...parsed };
                    }
                }
            } catch {
                /* fall through to defaults */
            }
            const storedInputDevice = voiceSettings.inputDeviceId;
            const storedOutputDevice = voiceSettings.outputDeviceId;

            const isMobile = /Android|iPhone|iPad/i.test(navigator.userAgent);

            const newRoom = new Room({
              webAudioMix: true,
              adaptiveStream: true,
              dynacast: true,
              autoSubscribe: true,
              rtcConfig: {
                iceServers: [
                  { urls: 'stun:stun.l.google.com:19302' },
                  { urls: 'stun:stun1.l.google.com:19302' },
                ],
              },
              audioCaptureDefaults: {
                autoGainControl: voiceSettings.autoGainControl,
                echoCancellation: voiceSettings.echoCancellation,
                noiseSuppression: voiceSettings.noiseSuppression,
                channelCount: 1,
                sampleRate: 48000,
                ...(storedInputDevice && storedInputDevice !== 'default' ? { deviceId: { exact: storedInputDevice } } : {}),
              },
              videoCaptureDefaults: {
                resolution: VideoPresets.h720.resolution,
              },
              publishDefaults: {
                audioPreset: { maxBitrate: 96_000 },
                dtx: false,
                red: true,
                videoEncoding: VideoPresets.h720.encoding,
                videoCodec: isMobile ? 'vp8' : 'vp9',
                screenShareEncoding: {
                  maxBitrate: 10_000_000,
                  maxFramerate: 60,
                },
                screenShareSimulcastLayers: [
                  { maxBitrate: 2_000_000, maxFramerate: 15, width: 1280, height: 720 },
                ],
              },
            });
            await newRoom.connect(import.meta.env.VITE_LIVEKIT_URL, lkToken);

            if (storedOutputDevice && storedOutputDevice !== 'default') {
                await newRoom.switchActiveDevice('audiooutput', storedOutputDevice).catch(e =>
                    console.warn('Failed to switch output device on connect:', e)
                );
            }

            await newRoom.localParticipant.setMicrophoneEnabled(!isMuted && !isDeafened);

            setRoom(newRoom);
            setConnectionState('connected');
            // Start native foreground service for background voice on Android
            voiceService?.startService({ channelName, isMuted, isDeafened });
            // Play custom join sound if set, otherwise default
            if (myJoinSoundUrl) {
                playSoundUrl(myJoinSoundUrl);
            } else {
                playSound('join');
            }

            await convex.mutation(api.voiceState.join, {
                channelId,
                userId,
                username: localStorage.getItem('username') || 'Unknown',
                isMuted,
                isDeafened,
            });

            // Auto-mute when joining AFK channel
            if (serverSettings?.afkChannelId === channelId) {
                setIsMuted(true);
                await newRoom.localParticipant.setMicrophoneEnabled(false);
                await convex.mutation(api.voiceState.updateState, { userId, isMuted: true });
                voiceService?.updateNotification({ isMuted: true });
            }

            newRoom.on(RoomEvent.Disconnected, async (reason) => {
                console.warn('Voice Room Disconnected. Reason:', reason);
                // If we're being moved, skip leave mutation — we'll reconnect shortly
                if (isMovingRef.current) {
                    setRoom(null);
                    setToken(null);
                    clearSpeakerTimers();
                    setActiveSpeakers(new Set());
                    setConnectionQualities({});
                    return;
                }

                // Auto-reconnect on token expiry
                if (reason === DisconnectReason.TOKEN_EXPIRED) {
                    console.log('Token expired, auto-reconnecting...');
                    setRoom(null);
                    setToken(null);
                    clearSpeakerTimers();
                    setActiveSpeakers(new Set());
                    setConnectionQualities({});
                    try {
                        await connectToVoice(channelId, channelName, userId, isDMCallRef.current);
                    } catch (e) {
                        console.error('Auto-reconnect failed:', e);
                    }
                    return;
                }

                voiceService?.stopService();
                playSound('leave');
                setConnectionState('disconnected');
                setActiveChannelId(null);
                window.__inVoiceCall = false;
                setRoom(null);
                setToken(null);
                clearSpeakerTimers();
                setActiveSpeakers(new Set());
                setConnectionQualities({});

                try {
                    await convex.mutation(api.voiceState.leave, { userId });
                } catch (e) {
                    console.error('Failed to leave voice state:', e);
                }
            });

            newRoom.on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
                const currentSpeakerIds = new Set(speakers.map(p => p.identity));

                // Track when local user last spoke (for Android AFK detection)
                const localIdentity = newRoom.localParticipant?.identity;
                if (localIdentity && currentSpeakerIds.has(localIdentity)) {
                    lastSpokeRef.current = Date.now();
                }

                // Cancel pending removal timers for anyone who is speaking again
                for (const id of currentSpeakerIds) {
                    const timer = speakerRemovalTimers.current.get(id);
                    if (timer) {
                        clearTimeout(timer);
                        speakerRemovalTimers.current.delete(id);
                    }
                }

                setActiveSpeakers(prev => {
                    const next = new Set(prev);

                    // Add new speakers immediately
                    for (const id of currentSpeakerIds) {
                        next.add(id);
                    }

                    // Schedule delayed removal for speakers no longer in the event
                    for (const id of prev) {
                        if (!currentSpeakerIds.has(id) && !speakerRemovalTimers.current.has(id)) {
                            const timer = setTimeout(() => {
                                speakerRemovalTimers.current.delete(id);
                                setActiveSpeakers(s => {
                                    const updated = new Set(s);
                                    updated.delete(id);
                                    return updated;
                                });
                            }, 300);
                            speakerRemovalTimers.current.set(id, timer);
                        }
                    }

                    return next;
                });
            });

            newRoom.on(RoomEvent.Reconnecting, () => {
                console.warn('Voice room reconnecting...');
                setIsReconnecting(true);
                setConnectionState('reconnecting');
            });

            newRoom.on(RoomEvent.Reconnected, () => {
                console.log('Voice room reconnected');
                setIsReconnecting(false);
                setConnectionState('connected');
            });

            newRoom.on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
                setConnectionQualities(prev => ({
                    ...prev,
                    [participant.identity]: quality,
                }));
            });

        } catch (err) {
            console.error('Voice Connection Failed:', err);
            setConnectionState('error');
            setActiveChannelId(null);
            window.__inVoiceCall = false;
        }
    };

    // Heartbeat: send periodic heartbeat to prevent ghost voice states
    useEffect(() => {
        if (!activeChannelId) return;
        const userId = localStorage.getItem('userId');
        if (!userId) return;

        const sendHeartbeat = () => {
            convex.mutation(api.voiceState.heartbeat, { userId }).catch(e =>
                console.warn('Heartbeat failed:', e)
            );
        };

        // Send immediately, then every 30 seconds
        sendHeartbeat();
        const interval = setInterval(sendHeartbeat, 30_000);
        return () => clearInterval(interval);
    }, [activeChannelId, convex]);

    // Handle notification action buttons (Android foreground service)
    useEffect(() => {
        if (!voiceService) return;
        const listener = voiceService.addNotificationActionListener((event) => {
            switch (event.action) {
                case 'disconnect':
                    disconnectVoice();
                    break;
                case 'toggleMute':
                    toggleMute();
                    break;
                case 'toggleDeafen':
                    toggleDeafen();
                    break;
            }
        });
        return () => {
            if (listener && listener.remove) listener.remove();
            else if (listener && typeof listener.then === 'function') {
                listener.then(l => l?.remove?.());
            }
        };
    }, [voiceService, activeChannelId]);

    // Detect when another user moves us to a different voice channel
    useEffect(() => {
        const myUserId = localStorage.getItem('userId');
        if (!myUserId || !activeChannelId || isMovingRef.current) return;

        // Find which channel the server says we're in
        let serverChannelId = null;
        for (const [chId, users] of Object.entries(voiceStates)) {
            if (users.some(u => u.userId === myUserId)) {
                serverChannelId = chId;
                break;
            }
        }

        // If server says we're in a different channel, reconnect
        if (serverChannelId && serverChannelId !== activeChannelId) {
            isMovingRef.current = true;
            (async () => {
                try {
                    const channel = await convex.query(api.channels.get, { id: serverChannelId });
                    if (room) await room.disconnect();
                    await connectToVoice(serverChannelId, channel?.name || 'Voice', myUserId);
                } catch (e) {
                    console.error('Failed to reconnect after move:', e);
                } finally {
                    isMovingRef.current = false;
                }
            })();
        }
    }, [voiceStates, activeChannelId]);

    // Enforce server mute: force-disable mic when server muted, restore when lifted
    useEffect(() => {
        const myUserId = localStorage.getItem('userId');
        if (!myUserId || !room) return;
        if (isServerMuted(myUserId)) {
            room.localParticipant.setMicrophoneEnabled(false);
        } else if (!isMuted && !isDeafened) {
            room.localParticipant.setMicrophoneEnabled(true);
        }
    }, [voiceStates, room]);

    // Re-apply personal mutes/volumes when room or participants change
    useEffect(() => {
        if (!room) return;
        const globalVol = globalOutputVolume / 100;
        const applyVolumes = () => {
            for (const [identity, participant] of room.remoteParticipants) {
                if (personallyMutedUsers.has(identity)) {
                    participant.setVolume(0);
                } else {
                    const userVol = (userVolumes[identity] ?? 100) / 100;
                    participant.setVolume(Math.min(2, userVol * globalVol));
                }
            }
        };
        applyVolumes();
        room.on(RoomEvent.ParticipantConnected, applyVolumes);
        return () => room.off(RoomEvent.ParticipantConnected, applyVolumes);
    }, [room, personallyMutedUsers, userVolumes, globalOutputVolume]);

    // AFK idle polling: move user to AFK channel when idle exceeds timeout
    useEffect(() => {
        if (!activeChannelId || !serverSettings?.afkChannelId || isInAfkChannel) return;
        if (isDMCallRef.current) return; // Skip AFK for DM calls

        const isCapacitor = !!window.Capacitor?.isNativePlatform?.();

        // On desktop/web, require system idle API; on Capacitor, use lastSpokeRef
        if (!isCapacitor && !idle?.getSystemIdleTime) return;

        const afkTimeout = serverSettings.afkTimeout || 300;
        const interval = setInterval(async () => {
            try {
                let idleSeconds;
                if (isCapacitor) {
                    // On Android, idle = time since user last spoke in voice
                    idleSeconds = Math.floor((Date.now() - lastSpokeRef.current) / 1000);
                } else {
                    idleSeconds = await idle.getSystemIdleTime();
                }

                if (idleSeconds >= afkTimeout) {
                    const userId = localStorage.getItem('userId');
                    if (!userId) return;

                    // On Capacitor, also set user status to idle
                    if (isCapacitor) {
                        await convex.mutation(api.auth.updateStatus, { userId, status: 'idle' });
                    }

                    await convex.mutation(api.voiceState.afkMove, {
                        userId,
                        afkChannelId: serverSettings.afkChannelId,
                    });
                    // After server-side move, locally mute
                    setIsMuted(true);
                    if (room) room.localParticipant.setMicrophoneEnabled(false);
                    voiceService?.updateNotification({ isMuted: true });
                }
            } catch (e) {
                console.error('AFK check failed:', e);
            }
        }, 15000);

        return () => clearInterval(interval);
    }, [activeChannelId, serverSettings?.afkChannelId, serverSettings?.afkTimeout, isInAfkChannel]);

    // Detect other users joining the same voice channel and play their join sound
    useEffect(() => {
        if (!activeChannelId) {
            prevChannelUsersRef.current = new Set();
            otherJoinInitRef.current = false;
            return;
        }

        const selfId = localStorage.getItem('userId');
        const channelUsers = voiceStates[activeChannelId] || [];
        const currentUserIds = new Set(channelUsers.map(u => u.userId));

        // Guard: ignore transient empty states when we previously had users
        if (currentUserIds.size === 0 && prevChannelUsersRef.current.size > 0) {
            return;
        }

        // Skip the first render after joining to avoid playing sounds for users already in the channel
        if (!otherJoinInitRef.current) {
            otherJoinInitRef.current = true;
            prevChannelUsersRef.current = currentUserIds;
            return;
        }

        const prevIds = prevChannelUsersRef.current;

        // Detect new users (not self)
        for (const uid of currentUserIds) {
            if (uid !== selfId && !prevIds.has(uid)) {
                const userData = channelUsers.find(u => u.userId === uid);
                if (userData?.joinSoundUrl) {
                    playSoundUrl(userData.joinSoundUrl);
                } else {
                    playSound('join');
                }
                break; // one sound per update batch
            }
        }

        prevChannelUsersRef.current = currentUserIds;
    }, [voiceStates, activeChannelId]);

    // Manage screen share subscriptions — only subscribe when actively watching
    useEffect(() => {
        if (!room) return;

        const manageSubscriptions = () => {
            let receivingAudio = false;
            for (const p of room.remoteParticipants.values()) {
                const { screenSharePub, screenShareAudioPub } = findTrackPubs(p);

                const shouldSubscribe = watchingStreamOf === p.identity;

                if (screenSharePub && screenSharePub.isSubscribed !== shouldSubscribe) {
                    screenSharePub.setSubscribed(shouldSubscribe);
                }
                if (screenShareAudioPub && screenShareAudioPub.isSubscribed !== shouldSubscribe) {
                    screenShareAudioPub.setSubscribed(shouldSubscribe);
                }

                if (shouldSubscribe && screenShareAudioPub && screenShareAudioPub.isSubscribed) {
                    receivingAudio = true;
                }
            }
            _suppressAppSounds = receivingAudio;
            setIsReceivingScreenShareAudio(receivingAudio);
        };

        manageSubscriptions();

        const onTrackChange = () => manageSubscriptions();
        room.on(RoomEvent.TrackPublished, onTrackChange);
        room.on(RoomEvent.TrackSubscribed, onTrackChange);
        room.on(RoomEvent.TrackUnsubscribed, onTrackChange);

        return () => {
            room.off(RoomEvent.TrackPublished, onTrackChange);
            room.off(RoomEvent.TrackSubscribed, onTrackChange);
            room.off(RoomEvent.TrackUnsubscribed, onTrackChange);
            _suppressAppSounds = false;
            setIsReceivingScreenShareAudio(false);
        };
    }, [room, watchingStreamOf]);

    // Auto-exit if watched participant stops streaming or disconnects
    useEffect(() => {
        if (watchingStreamOf === null || !room) return;

        const checkWatched = () => {
            // Check if participant is still connected
            const participant = room.remoteParticipants.get(watchingStreamOf)
                || (room.localParticipant.identity === watchingStreamOf ? room.localParticipant : null);

            if (!participant) {
                clearWatchingStream();
                return;
            }

            // Check if they're still screen sharing
            const { screenSharePub } = findTrackPubs(participant);
            if (!screenSharePub) {
                clearWatchingStream();
            }
        };

        // Also listen for voiceStates changes — covered by the dependency array re-run
        room.on(RoomEvent.ParticipantDisconnected, checkWatched);
        room.on(RoomEvent.TrackUnpublished, checkWatched);

        return () => {
            room.off(RoomEvent.ParticipantDisconnected, checkWatched);
            room.off(RoomEvent.TrackUnpublished, checkWatched);
        };
    }, [room, watchingStreamOf]);

    // Reset watching state when room disconnects
    useEffect(() => {
        if (!room) {
            clearWatchingStream();
        }
    }, [room]);

    // Detect viewer join/leave for the stream we're watching and play sounds
    const prevViewersRef = useRef(new Set());
    const viewerDetectionInitRef = useRef(false);
    useEffect(() => {
        if (!watchingStreamOf) {
            prevViewersRef.current = new Set();
            viewerDetectionInitRef.current = false;
            return;
        }

        const myUserId = localStorage.getItem('userId');
        // Collect all users currently watching the same stream
        const currentViewers = new Set();
        for (const users of Object.values(voiceStates)) {
            for (const u of users) {
                if (u.watchingStream === watchingStreamOf && u.userId !== myUserId) {
                    currentViewers.add(u.userId);
                }
            }
        }

        // Skip first render to avoid spurious sounds on load
        if (!viewerDetectionInitRef.current) {
            viewerDetectionInitRef.current = true;
            prevViewersRef.current = currentViewers;
            return;
        }

        const prev = prevViewersRef.current;

        // New viewers joined
        for (const uid of currentViewers) {
            if (!prev.has(uid)) {
                playSound('viewer_join');
                break; // one sound per update batch
            }
        }

        // Viewers left (excluding self)
        for (const uid of prev) {
            if (!currentViewers.has(uid)) {
                playSound('viewer_leave');
                break; // one sound per update batch
            }
        }

        prevViewersRef.current = currentViewers;
    }, [voiceStates, watchingStreamOf]);

    // Detect screen-share publications starting / stopping across the
    // active voice channel (including the local user) and play a
    // "stream started" / "stream stopped" SFX on transitions. Reuses
    // the viewer_join / viewer_leave clips since they're already
    // loaded and sound right for the event.
    const prevSharersRef = useRef(new Set());
    const sharerDetectionInitRef = useRef(false);
    useEffect(() => {
        if (!activeChannelId) {
            prevSharersRef.current = new Set();
            sharerDetectionInitRef.current = false;
            return;
        }
        const channelUsers = voiceStates[activeChannelId] || [];
        const currentSharers = new Set();
        for (const u of channelUsers) {
            if (u.isScreenSharing) currentSharers.add(u.userId);
        }
        if (!sharerDetectionInitRef.current) {
            sharerDetectionInitRef.current = true;
            prevSharersRef.current = currentSharers;
            return;
        }
        const prev = prevSharersRef.current;
        // Stream started — someone in the channel is now sharing
        // who wasn't before.
        for (const uid of currentSharers) {
            if (!prev.has(uid)) {
                playSound('screenshare_start');
                break;
            }
        }
        // Stream stopped — someone who was sharing isn't anymore.
        for (const uid of prev) {
            if (!currentSharers.has(uid)) {
                playSound('screenshare_stop');
                break;
            }
        }
        prevSharersRef.current = currentSharers;
    }, [voiceStates, activeChannelId]);

    const disconnectVoice = () => {
        console.log('User manually disconnected voice');
        isDMCallRef.current = false;
        window.__inVoiceCall = false;
        voiceService?.stopService();
        if (room) room.disconnect();
    };

    const toggleMute = async () => {
        const myUserId = localStorage.getItem('userId');
        // Block unmute if server muted or in AFK channel
        if (isMuted && myUserId && isServerMuted(myUserId)) return;
        if (isMuted && isInAfkChannel) return;
        const nextState = !isMuted;
        setIsMuted(nextState);
        playSound(nextState ? 'mute' : 'unmute');
        voiceService?.updateNotification({ isMuted: nextState });
        if (room) {
            room.localParticipant.setMicrophoneEnabled(!nextState);
        }
        await updateVoiceState({ isMuted: nextState });
    };

    const toggleDeafen = async () => {
        const nextState = !isDeafened;
        setIsDeafened(nextState);
        playSound(nextState ? 'deafen' : 'undeafen');
        voiceService?.updateNotification({ isDeafened: nextState });
        if (room && !isMuted) {
            room.localParticipant.setMicrophoneEnabled(!nextState);
        }
        await updateVoiceState({ isDeafened: nextState });
    };

    // Actually flip the LiveKit screen-share publication on/off. The
    // earlier implementation only toggled local React state + the
    // voiceStates row, so the Share button lit up but no track ever
    // got published. `setScreenShareEnabled` is LiveKit's one-call
    // helper that prompts for `getDisplayMedia`, publishes the
    // resulting track, and tears it down on false.
    const setScreenSharing = async (active) => {
        if (!room) return;
        try {
            await room.localParticipant.setScreenShareEnabled(active, {
                audio: true,
            });
        } catch (e) {
            console.warn('Failed to toggle screen share:', e);
            // User cancelled the picker or permission was denied —
            // keep local state in sync with whatever actually happened
            // on the LiveKit side.
            const published = !!room.localParticipant.getTrackPublication?.(
                'screen_share',
            );
            setIsScreenSharingLocal(published);
            await updateVoiceState({ isScreenSharing: published });
            return;
        }
        setIsScreenSharingLocal(active);
        await updateVoiceState({ isScreenSharing: active });
    };

    // Camera toggle — publishes / unpublishes the local webcam track
    // via LiveKit's `setCameraEnabled` helper. Mirrors the mic and
    // screen-share code paths so the UI can show a single consistent
    // "active" state for each media type.
    const [isCameraOn, setIsCameraOn] = useState(false);
    const setCamera = async (active) => {
        if (!room) return;
        try {
            await room.localParticipant.setCameraEnabled(active);
            setIsCameraOn(active);
            playSound(active ? 'camera_on' : 'camera_off');
        } catch (e) {
            console.warn('Failed to toggle camera:', e);
            const published = !!room.localParticipant.isCameraEnabled;
            setIsCameraOn(published);
        }
    };
    const toggleCamera = () => setCamera(!isCameraOn);

    const switchDevice = useCallback(async (kind, deviceId) => {
        if (!room) return;
        try {
            await room.switchActiveDevice(kind, deviceId);
        } catch (e) {
            console.warn(`Failed to switch ${kind} device:`, e);
        }
    }, [room]);

    // ── Voice recording ─────────────────────────────────────────
    // Starts a `VoiceRecorder` bound to the current room. Returns
    // the session metadata on success so the UI can show a confirm
    // indicator. Gracefully no-ops (with a console warning) on
    // platforms that don't support recording.
    const startRecording = useCallback(async () => {
        if (!platform?.features?.hasRecording || !platform.recording) {
            console.warn('Recording is not available on this platform.');
            return null;
        }
        if (!room) {
            console.warn('Cannot start recording — not connected to a voice channel.');
            return null;
        }
        if (voiceRecorderRef.current) {
            return {
                sessionId: voiceRecorderRef.current.sessionId,
                startedAt: voiceRecorderRef.current.startedAt,
            };
        }
        try {
            // Resolve the user-preferred recording folder (settings
            // writes to localStorage under `voiceSettings`, same
            // bucket the mic/AGC toggles live in).
            let rootDir = null;
            try {
                const raw = localStorage.getItem('voiceSettings');
                if (raw) {
                    const parsed = JSON.parse(raw);
                    if (parsed && typeof parsed.recordingDir === 'string') {
                        rootDir = parsed.recordingDir;
                    }
                }
            } catch {}
            const recorder = new VoiceRecorder({
                platform,
                room,
                channelId: activeChannelId,
                channelName: activeChannelName,
                rootDir,
                onError: (err) => {
                    console.error('Voice recorder error:', err);
                    setRecordingError(err.message || 'Recording error');
                },
            });
            await recorder.start();
            voiceRecorderRef.current = recorder;
            setRecordingSessionId(recorder.sessionId);
            setRecordingStartedAt(recorder.startedAt);
            setIsRecording(true);
            setRecordingError(null);
            return { sessionId: recorder.sessionId, startedAt: recorder.startedAt };
        } catch (err) {
            console.error('Failed to start recording:', err);
            setRecordingError(err?.message || 'Failed to start recording');
            voiceRecorderRef.current = null;
            setIsRecording(false);
            setRecordingSessionId(null);
            setRecordingStartedAt(null);
            return null;
        }
    }, [platform, room, activeChannelId, activeChannelName]);

    const stopRecording = useCallback(async () => {
        const recorder = voiceRecorderRef.current;
        if (!recorder) return;
        voiceRecorderRef.current = null;
        try {
            await recorder.stop();
        } catch (err) {
            console.error('Failed to stop recording cleanly:', err);
            setRecordingError(err?.message || 'Failed to stop recording');
        } finally {
            setIsRecording(false);
            setRecordingSessionId(null);
            setRecordingStartedAt(null);
        }
    }, []);

    // Auto-stop the recorder if the user disconnects from the
    // voice channel — we don't want an orphaned VoiceRecorder
    // holding references to a destroyed LiveKit room.
    useEffect(() => {
        if (!room && voiceRecorderRef.current) {
            void stopRecording();
        }
    }, [room, stopRecording]);


    return (
        <VoiceContext.Provider value={{
            activeChannelId,
            activeChannelName,
            connectionState,
            connectToVoice,
            disconnectVoice,
            room,
            token,
            voiceStates,
            activeSpeakers,
            isMuted,
            isDeafened,
            toggleMute,
            toggleDeafen,
            isScreenSharing,
            setScreenSharing,
            isCameraOn,
            setCamera,
            toggleCamera,
            personallyMutedUsers,
            togglePersonalMute,
            isPersonallyMuted,
            userVolumes,
            setUserVolume,
            getUserVolume,
            serverMute,
            disconnectUser,
            isServerMuted,
            isInAfkChannel,
            serverSettings,
            watchingStreamOf,
            setWatchingStreamOf,
            switchDevice,
            globalOutputVolume,
            setGlobalOutputVolume,
            isReceivingScreenShareAudio,
            isReconnecting,
            connectionQualities,
            // Voice recording
            isRecording,
            recordingStartedAt,
            recordingSessionId,
            recordingError,
            startRecording,
            stopRecording,
            clearRecordingError: () => setRecordingError(null),
        }}>
            {children}
            {room && (
                <LiveKitRoom
                    room={room}
                    style={{ position: 'absolute', width: 0, height: 0, overflow: 'hidden' }}
                >
                     <RoomAudioRenderer muted={isDeafened} />
                </LiveKitRoom>
            )}
        </VoiceContext.Provider>
    );
};
