import { useState, useEffect, useRef, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Phone, PhoneOff, Video, VideoOff, Mic, MicOff, Monitor, MonitorOff, Maximize, Minimize, SwitchCamera, Minimize2, Maximize2, Volume2, ShieldCheck, ShieldOff, ChevronUp } from 'lucide-react';
import { getSocket } from '../lib/socket';
import { api } from '../lib/api';
import { useLang } from '../lib/i18n';
import { playCallRingtone, stopCallRingtone, playUnavailableSound } from '../lib/sounds';

type CallState = 'idle' | 'calling' | 'incoming' | 'connected' | 'ended';

interface CallModalProps {
  isOpen: boolean;
  onClose: () => void;
  targetUser: { id: string; displayName?: string; username: string; avatar?: string | null } | null;
  callType: 'voice' | 'video';
  incoming?: {
    from: string;
    offer: RTCSessionDescriptionInit;
    callType: 'voice' | 'video';
    callerInfo?: { displayName?: string; username?: string; avatar?: string | null } | null;
  } | null;
}

// ICE servers cache (fetched from server)
let cachedIceConfig: RTCConfiguration | null = null;
let iceCacheFetchedAt = 0;
const ICE_CACHE_TTL = 3600_000; // 1 hour

const FALLBACK_ICE: RTCConfiguration = {
  iceServers: [
    { urls: 'stun:stun.yandex.ru:3478' },
    { urls: 'stun:stun.sipnet.net:3478' },
    { urls: 'stun:stun.l.google.com:19302' },
  ],
};

async function getIceServers(): Promise<RTCConfiguration> {
  if (cachedIceConfig && Date.now() - iceCacheFetchedAt < ICE_CACHE_TTL) {
    return cachedIceConfig;
  }
  try {
    const data = await api.getIceServers();
    console.log('[WebRTC] Received ICE config:', data);
    if (data.iceServers && data.iceServers.length > 0) {
      cachedIceConfig = { iceServers: data.iceServers };
      iceCacheFetchedAt = Date.now();
      return cachedIceConfig;
    }
  } catch (e) {
    console.warn('Failed to fetch ICE servers, using fallback STUN:', e);
  }
  return FALLBACK_ICE;
}

// Try to get video+audio, falling back to audio-only
// If preferDeviceId is given, try that camera first
async function getMediaWithCameraFallback(
  wantVideo: boolean,
  preferDeviceId?: string
): Promise<{ stream: MediaStream; hasVideo: boolean }> {
  if (!wantVideo) {
    const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } });
    return { stream, hasVideo: false };
  }

  // 1) If we have a preferred camera, try it first
  if (preferDeviceId) {
    try {
      console.log('[getMedia] Trying preferred camera:', preferDeviceId.slice(0, 12));
      const stream = await navigator.mediaDevices.getUserMedia({
        audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },
        video: { deviceId: { exact: preferDeviceId } },
      });
      console.log('[getMedia] Preferred camera success');
      return { stream, hasVideo: true };
    } catch (e) {
      console.warn('[getMedia] Preferred camera failed:', e);
    }
  }

  // 2) Try default audio+video
  try {
    console.log('[getMedia] Requesting audio+video (default camera)...');
    const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true }, video: true });
    console.log('[getMedia] Success —', stream.getVideoTracks().map(t => `${t.label}:${t.readyState}`));
    return { stream, hasVideo: true };
  } catch (e) {
    console.warn('[getMedia] audio+video failed:', e);
  }

  // 3) Final fallback: audio only
  console.warn('[getMedia] Camera unavailable, falling back to audio only');
  const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } });
  return { stream, hasVideo: false };
}

// Find a video sender on the peer connection, even if sender.track is null (recvonly transceiver)
function findVideoSender(pc: RTCPeerConnection): RTCRtpSender | undefined {
  // First try sender with an active video track
  const withTrack = pc.getSenders().find(s => s.track?.kind === 'video');
  if (withTrack) return withTrack;
  // Fall back to sender from a video transceiver (may have null track)
  const videoTransceiver = pc.getTransceivers().find(
    t => t.receiver?.track?.kind === 'video'
  );
  return videoTransceiver?.sender;
}

export default function CallModal({ isOpen, onClose, targetUser, callType: initialCallType, incoming }: CallModalProps) {
  const { t } = useLang();
  const [callState, setCallState] = useState<CallState>('idle');
  const [callType, setCallType] = useState<'voice' | 'video'>(initialCallType);
  const [isMuted, setIsMuted] = useState(false);
  const [isVideoOff, setIsVideoOff] = useState(false);
  const [isScreenSharing, setIsScreenSharing] = useState(false);
  const [hasRemoteVideo, setHasRemoteVideo] = useState(false);
  const [isFullscreen, setIsFullscreen] = useState(false);
  const [isMinimized, setIsMinimized] = useState(false);
  const [duration, setDuration] = useState(0);
  const [cameras, setCameras] = useState<MediaDeviceInfo[]>([]);
  const [showCameraMenu, setShowCameraMenu] = useState(false);
  const [activeCameraId, setActiveCameraId] = useState<string>('');
  const [remoteVolume, setRemoteVolume] = useState(1);
  const [showVolumeSlider, setShowVolumeSlider] = useState(false);
  const [noiseSuppression, setNoiseSuppression] = useState(false);
  const [microphones, setMicrophones] = useState<MediaDeviceInfo[]>([]);
  const [activeMicId, setActiveMicId] = useState<string>('');
  const [showMicMenu, setShowMicMenu] = useState(false);

  const peerRef = useRef<RTCPeerConnection | null>(null);
  const localStreamRef = useRef<MediaStream | null>(null);
  const screenStreamRef = useRef<MediaStream | null>(null);
  const localVideoRef = useRef<HTMLVideoElement>(null);
  const remoteVideoRef = useRef<HTMLVideoElement>(null);
  const remoteAudioRef = useRef<HTMLAudioElement>(null);
  const timerRef = useRef<ReturnType<typeof setInterval>>(undefined);
  const callTimeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
  const closeTimeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
  const targetUserIdRef = useRef<string>('');
  const iceCandidateBufferRef = useRef<RTCIceCandidateInit[]>([]);
  const callEndedRef = useRef(false);
  const videoContainerRef = useRef<HTMLDivElement>(null);
  const remoteStreamRef = useRef<MediaStream | null>(null);
  // Track whether video was active before screen share to decide restoration behavior
  const hadVideoBeforeScreenShareRef = useRef(false);
  // Noise gate refs
  const noiseGateCtxRef = useRef<AudioContext | null>(null);
  const noiseGateGainRef = useRef<GainNode | null>(null);
  const noiseGateRafRef = useRef<number>(0);
  const noiseGateTrackRef = useRef<MediaStreamTrack | null>(null);

  const cleanup = useCallback(() => {
    if (timerRef.current) clearInterval(timerRef.current);
    if (callTimeoutRef.current) clearTimeout(callTimeoutRef.current);
    if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);
    stopCallRingtone();
    if (localStreamRef.current) {
      localStreamRef.current.getTracks().forEach(track => track.stop());
      localStreamRef.current = null;
    }
    if (screenStreamRef.current) {
      screenStreamRef.current.getTracks().forEach(track => track.stop());
      screenStreamRef.current = null;
    }
    if (peerRef.current) {
      peerRef.current.onconnectionstatechange = null;
      peerRef.current.onicecandidate = null;
      peerRef.current.ontrack = null;
      peerRef.current.close();
      peerRef.current = null;
    }
    iceCandidateBufferRef.current = [];
    remoteStreamRef.current = null;
    // Clean up noise gate
    if (noiseGateRafRef.current) cancelAnimationFrame(noiseGateRafRef.current);
    if (noiseGateTrackRef.current) { noiseGateTrackRef.current.stop(); noiseGateTrackRef.current = null; }
    if (noiseGateCtxRef.current) { noiseGateCtxRef.current.close().catch(() => {}); noiseGateCtxRef.current = null; }
    noiseGateGainRef.current = null;
    setDuration(0);
    setIsMuted(false);
    setIsVideoOff(false);
    setIsScreenSharing(false);
    setHasRemoteVideo(false);
    setIsFullscreen(false);
    setIsMinimized(false);
    setShowCameraMenu(false);
    setShowVolumeSlider(false);
    setShowMicMenu(false);
    setNoiseSuppression(false);
  }, []);

  // Schedule a tracked close timeout
  const scheduleClose = useCallback((delay = 1500) => {
    if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);
    closeTimeoutRef.current = setTimeout(() => {
      closeTimeoutRef.current = undefined;
      onClose();
    }, delay);
  }, [onClose]);

  // End call (safe to call multiple times)
  const endCallSafe = useCallback(() => {
    if (callEndedRef.current) return;
    callEndedRef.current = true;
    const socket = getSocket();
    socket?.emit('call_end', { targetUserId: targetUserIdRef.current });
    stopCallRingtone();
    setCallState('ended');
    cleanup();
    scheduleClose();
  }, [cleanup, scheduleClose]);

  // Setup common peer connection event handlers
  const setupPeerHandlers = useCallback((pc: RTCPeerConnection) => {
    pc.onicecandidate = (e) => {
      if (e.candidate) {
        const socket = getSocket();
        socket?.emit('ice_candidate', {
          targetUserId: targetUserIdRef.current,
          candidate: e.candidate,
        });
      }
    };

    pc.ontrack = (event) => {
      const track = event.track;
      const streams = event.streams;
      console.log(`[WebRTC] Received track: ${track.kind} state: ${track.readyState} streams: ${streams.length}`);

      // Ensure we have a stable remote stream object
      if (!remoteStreamRef.current) {
        remoteStreamRef.current = streams[0] || new MediaStream([track]);
      } else if (!remoteStreamRef.current.getTracks().includes(track)) {
        remoteStreamRef.current.addTrack(track);
      }

      const stream = remoteStreamRef.current;

      // Helper to update video UI state
      const checkVideo = () => {
        const stream = remoteStreamRef.current;
        if (!stream) {
          setHasRemoteVideo(false);
          return;
        }
        const videoTracks = stream.getVideoTracks();
        // Be more lenient: if track is 'live', trust it even if it reports 'muted' initially.
        // Some browsers report muted while the buffer is filling.
        const hasVideo = videoTracks.length > 0 && videoTracks.some(
          t => t.readyState === 'live' && t.enabled
        );
        
        console.log('[checkVideo] remote videoTracks:', videoTracks.map(t =>
          `${t.label} state=${t.readyState} enabled=${t.enabled} muted=${t.muted}`
        ), '→ hasRemoteVideo:', hasVideo);
        
        setHasRemoteVideo(hasVideo);
        if (hasVideo && callType !== 'video') {
          setCallType('video');
        }
      };

      // Force binding to the SINGLE stable video element
      if (remoteVideoRef.current) {
        if (remoteVideoRef.current.srcObject !== stream) {
            console.log('[WebRTC] Binding stream to video element');
            remoteVideoRef.current.srcObject = stream;
            // Force load to ensure browser picks up streams properly
            remoteVideoRef.current.load();
        }
        // Try to play. If fails, it's usually autoplay policy
        remoteVideoRef.current.play().catch(() => {
          console.warn('[WebRTC] Autoplay blocked, waiting for interaction');
          const retryPlay = () => {
             if (!remoteVideoRef.current) return;
             remoteVideoRef.current.play().then(() => {
               console.log('[WebRTC] Video started after retry');
             }).catch(() => {
                // If blocked, ensure it's muted and try again
                if (remoteVideoRef.current) remoteVideoRef.current.muted = true;
                setTimeout(retryPlay, 2000);
             });
          };
          retryPlay();
        });
      }

      // Handle remote media via the remoteVideoRef
      if (remoteVideoRef.current) {
        if (remoteVideoRef.current.srcObject !== stream) {
            console.log('[WebRTC] Binding stream to video element');
            remoteVideoRef.current.srcObject = stream;
            remoteVideoRef.current.load();
        }
        // Important: Unmute for remote audio to work
        remoteVideoRef.current.muted = false;
        remoteVideoRef.current.play().catch(e => {
          console.warn('[WebRTC] Play failed, waiting for user interaction:', e);
        });
      }

      track.onunmute = checkVideo;
      track.onmute = checkVideo;
      track.onended = checkVideo;

      checkVideo();
      setTimeout(checkVideo, 500);
      setTimeout(checkVideo, 2000);
    };

    pc.onconnectionstatechange = () => {
      console.log('[WebRTC] Connection state:', pc.connectionState);
      if (callEndedRef.current) return;
      if (pc.connectionState === 'disconnected' || pc.connectionState === 'failed') {
        endCallSafe();
      }
    };

    pc.oniceconnectionstatechange = () => {
      console.log('[WebRTC] ICE Connection state:', pc.iceConnectionState);
    };
  }, [endCallSafe]); // REMOVED callType dependency to prevent handler reset during call

  // Start outgoing call
  const startCall = useCallback(async () => {
    if (!targetUser) return;
    targetUserIdRef.current = targetUser.id;
    callEndedRef.current = false;
    setCallState('calling');

    try {
      // Security context check (critical for KeenDNS)
      if (!window.isSecureContext) {
        alert('Критическая ошибка: Совершение аудио/видео звонков требует безопасного соединения (HTTPS).\n\nЗайдите в настройки KeenDNS и убедитесь, что включено HTTPS (SSL), а в браузере используйте https:// вместо http://');
        setCallState('ended');
        return;
      }

      // Get media (video with camera enumeration fallback)
      console.log('[startCall] Getting media, wantVideo:', callType === 'video');
      const { stream, hasVideo } = await getMediaWithCameraFallback(callType === 'video');
      console.log('[startCall] Got media — hasVideo:', hasVideo,
        'audioTracks:', stream.getAudioTracks().length,
        'videoTracks:', stream.getVideoTracks().length);
      let effectiveCallType = callType;
      if (callType === 'video' && !hasVideo) {
        effectiveCallType = 'voice';
        setCallType('voice');
      }

      if (callEndedRef.current) {
        stream.getTracks().forEach(t => t.stop());
        return;
      }

      localStreamRef.current = stream;

      // Track active camera & mic
      const videoTrackSettings = stream.getVideoTracks()[0]?.getSettings();
      if (videoTrackSettings?.deviceId) setActiveCameraId(videoTrackSettings.deviceId);
      const audioTrackSettings = stream.getAudioTracks()[0]?.getSettings();
      if (audioTrackSettings?.deviceId) setActiveMicId(audioTrackSettings.deviceId);
      refreshCameras();
      refreshMicrophones();

      const iceConfig = await getIceServers();
      const pc = new RTCPeerConnection(iceConfig);
      peerRef.current = pc;

      // Add all tracks from the stream
      stream.getTracks().forEach(track => pc.addTrack(track, stream));

      // Always add a video transceiver (even for voice calls)
      // This ensures we have a video m-line for future camera/screen share,
      // making renegotiation much more stable across browsers.
      const hasVideoTransceiver = pc.getTransceivers().some(t => t.receiver?.track?.kind === 'video');
      if (!hasVideoTransceiver) {
        pc.addTransceiver('video', { direction: 'recvonly' });
      }

      setupPeerHandlers(pc);

      const offer = await pc.createOffer();
      await pc.setLocalDescription(offer);

      const socket = getSocket();
      socket?.emit('call_offer', {
        targetUserId: targetUser.id,
        offer: pc.localDescription,
        callType: effectiveCallType,
      });

      // 15 second no-answer timeout
      callTimeoutRef.current = setTimeout(async () => {
        if (callEndedRef.current) return;
        callEndedRef.current = true;
        stopCallRingtone();
        const s = getSocket();
        s?.emit('call_end', { targetUserId: targetUserIdRef.current });
        cleanup();
        setCallState('ended');
        await playUnavailableSound();
        onClose();
      }, 15000);
    } catch (err: any) {
      console.error('Error starting call:', err);
      if (err?.name === 'NotAllowedError' || err?.name === 'NotFoundError') {
        alert(callType === 'video'
          ? 'Разрешите доступ к камере и микрофону в настройках браузера для совершения звонков'
          : 'Разрешите доступ к микрофону в настройках браузера для совершения звонков');
      }
      setCallState('ended');
      cleanup();
    }
  }, [targetUser, callType, cleanup, onClose, setupPeerHandlers]);

  // Accept incoming call
  const acceptCall = useCallback(async () => {
    if (!incoming) return;
    targetUserIdRef.current = incoming.from;
    callEndedRef.current = false;
    stopCallRingtone();

    try {
      console.log('[acceptCall] Getting media, wantVideo:', incoming.callType === 'video');
      const { stream, hasVideo } = await getMediaWithCameraFallback(incoming.callType === 'video');
      console.log('[acceptCall] Got media — hasVideo:', hasVideo,
        'audioTracks:', stream.getAudioTracks().length,
        'videoTracks:', stream.getVideoTracks().length,
        'videoEnabled:', stream.getVideoTracks().map(t => t.enabled));
      let effectiveCallType = incoming.callType;
      if (incoming.callType === 'video' && !hasVideo) {
        effectiveCallType = 'voice';
      }

      if (callEndedRef.current) {
        stream.getTracks().forEach(t => t.stop());
        return;
      }

      localStreamRef.current = stream;

      // Track active camera & mic
      const videoTrackSettings = stream.getVideoTracks()[0]?.getSettings();
      if (videoTrackSettings?.deviceId) setActiveCameraId(videoTrackSettings.deviceId);
      const audioTrackSettings = stream.getAudioTracks()[0]?.getSettings();
      if (audioTrackSettings?.deviceId) setActiveMicId(audioTrackSettings.deviceId);
      refreshCameras();
      refreshMicrophones();

      const iceConfig = await getIceServers();
      const pc = new RTCPeerConnection(iceConfig);
      peerRef.current = pc;

      // Setup handlers first so ontrack is ready when setRemoteDescription fires
      setupPeerHandlers(pc);

      // Standard answerer pattern: set remote description FIRST
      // This creates transceivers from the offer's m-lines
      await pc.setRemoteDescription(new RTCSessionDescription(incoming.offer));

      // Now add local tracks — they reuse the transceivers created from the offer,
      // changing direction from recvonly to sendrecv
      stream.getTracks().forEach(track => {
        pc.addTrack(track, stream);
      });

      // Always ensure we have a video transceiver.
      // After setRemoteDescription, one usually exists from the offer if it was a video call or if the caller added one.
      const hasVideoTransceiver = pc.getTransceivers().some(
        t => t.receiver?.track?.kind === 'video'
      );
      if (!hasVideoTransceiver) {
        pc.addTransceiver('video', { direction: 'recvonly' });
      }

      console.log('[acceptCall] Transceivers after setup:',
        pc.getTransceivers().map(t => ({
          mid: t.mid,
          direction: t.direction,
          senderTrack: t.sender?.track?.kind ?? null,
          receiverTrack: t.receiver?.track?.kind ?? null,
        })));

      if (callEndedRef.current) {
        pc.onconnectionstatechange = null;
        pc.close();
        peerRef.current = null;
        stream.getTracks().forEach(t => t.stop());
        localStreamRef.current = null;
        return;
      }

      // Flush buffered ICE candidates
      for (const candidate of iceCandidateBufferRef.current) {
        pc.addIceCandidate(new RTCIceCandidate(candidate)).catch(console.error);
      }
      iceCandidateBufferRef.current = [];

      const answer = await pc.createAnswer();
      await pc.setLocalDescription(answer);

      console.log('[acceptCall] Answer created, transceivers:',
        pc.getTransceivers().map(t => ({
          mid: t.mid,
          direction: t.direction,
          currentDirection: t.currentDirection,
          senderTrack: t.sender?.track?.kind ?? null,
        })));

      if (callEndedRef.current) {
        pc.onconnectionstatechange = null;
        pc.close();
        peerRef.current = null;
        stream.getTracks().forEach(t => t.stop());
        localStreamRef.current = null;
        return;
      }

      const socket = getSocket();
      socket?.emit('call_answer', {
        targetUserId: incoming.from,
        answer: pc.localDescription,
      });

      setCallType(effectiveCallType);
      setCallState('connected');
      console.log('[acceptCall] Call connected, effectiveCallType:', effectiveCallType);
      timerRef.current = setInterval(() => setDuration((d) => d + 1), 1000);
    } catch (err: any) {
      console.error('Error accepting call:', err);
      if (err?.name === 'NotAllowedError' || err?.name === 'NotFoundError') {
        alert('Разрешите доступ к микрофону в настройках браузера для совершения звонков');
      }
      if (!callEndedRef.current) {
        setCallState('ended');
        cleanup();
      }
    }
  }, [incoming, cleanup, setupPeerHandlers]);

  // Decline incoming call
  const declineCall = useCallback(() => {
    if (incoming) {
      const socket = getSocket();
      socket?.emit('call_decline', { targetUserId: incoming.from });
    }
    callEndedRef.current = true;
    stopCallRingtone();
    setCallState('ended');
    cleanup();
    scheduleClose();
  }, [incoming, cleanup, scheduleClose]);

  // Toggle mic
  const toggleMic = () => {
    if (localStreamRef.current) {
      localStreamRef.current.getAudioTracks().forEach((t) => {
        t.enabled = !t.enabled;
      });
      setIsMuted(!isMuted);
    }
  };

  // Volume control for remote audio
  const handleVolumeChange = useCallback((vol: number) => {
    const v = Math.max(0, Math.min(1, vol));
    setRemoteVolume(v);
    if (remoteAudioRef.current) remoteAudioRef.current.volume = v;
  }, []);

  // Noise gate with look-ahead: analyse undelayed signal, gate delayed signal
  // This prevents clipping word beginnings and rejects short transients (clicks)
  const applyNoiseGate = useCallback(async () => {
    const pc = peerRef.current;
    if (!pc || !localStreamRef.current) return;
    const rawTrack = localStreamRef.current.getAudioTracks()[0];
    if (!rawTrack) return;

    try {
      const ctx = new AudioContext();
      const source = ctx.createMediaStreamSource(new MediaStream([rawTrack]));

      // Look-ahead delay: signal is delayed so gate can open before audio arrives
      const delayNode = ctx.createDelay(0.2);
      delayNode.delayTime.value = 0.05; // 50ms look-ahead

      // Analysis path: bandpass on voice fundamentals (undelayed, sees audio early)
      const analysisHP = ctx.createBiquadFilter();
      analysisHP.type = 'highpass'; analysisHP.frequency.value = 100; analysisHP.Q.value = 0.7;
      const analysisLP = ctx.createBiquadFilter();
      analysisLP.type = 'lowpass'; analysisLP.frequency.value = 4000; analysisLP.Q.value = 0.7;

      const analyser = ctx.createAnalyser();
      analyser.fftSize = 2048;
      analyser.smoothingTimeConstant = 0.3;

      const gainNode = ctx.createGain();
      gainNode.gain.value = 0;
      const dest = ctx.createMediaStreamDestination();

      // Signal path: source → delay → gain → output (full quality, just gated)
      source.connect(delayNode);
      delayNode.connect(gainNode);
      gainNode.connect(dest);

      // Analysis path (no delay): source → bandpass → analyser
      source.connect(analysisHP);
      analysisHP.connect(analysisLP);
      analysisLP.connect(analyser);

      const dataArray = new Float32Array(analyser.fftSize);
      const OPEN_THRESHOLD = -38;  // dB in voice band to trigger pending
      const CLOSE_THRESHOLD = -48; // dB hysteresis / transient rejection
      const CONFIRM_MS = 22;       // ms energy must persist to confirm voice (rejects clicks)
      const HOLD_TIME = 150;       // ms keep gate open after speech stops
      const ATTACK = 0.002;        // fast open (gate already decided ahead of time)
      const RELEASE = 0.02;        // smooth close

      // State machine: 0=closed, 1=pending (waiting to confirm voice), 2=open
      let state = 0;
      let pendingSince = 0;
      let holdUntil = 0;

      const check = () => {
        analyser.getFloatTimeDomainData(dataArray);
        let sum = 0;
        for (let i = 0; i < dataArray.length; i++) sum += dataArray[i] * dataArray[i];
        const rms = Math.sqrt(sum / dataArray.length);
        const db = rms > 0 ? 20 * Math.log10(rms) : -100;
        const now = performance.now();

        if (state === 0) { // closed
          if (db > OPEN_THRESHOLD) {
            state = 1; // pending
            pendingSince = now;
          }
        } else if (state === 1) { // pending — wait to confirm sustained energy (voice) vs transient (click)
          if (db <= CLOSE_THRESHOLD) {
            state = 0; // energy dropped fast → was a click, stay closed
          } else if (now - pendingSince >= CONFIRM_MS) {
            state = 2; // sustained → voice confirmed, open gate
            holdUntil = now + HOLD_TIME;
            gainNode.gain.setTargetAtTime(1, ctx.currentTime, ATTACK);
          }
        } else { // open
          if (db > CLOSE_THRESHOLD) {
            holdUntil = now + HOLD_TIME;
          }
          if (now >= holdUntil) {
            state = 0;
            gainNode.gain.setTargetAtTime(0, ctx.currentTime, RELEASE);
          }
        }
        noiseGateRafRef.current = requestAnimationFrame(check);
      };
      check();

      const gatedTrack = dest.stream.getAudioTracks()[0];
      gatedTrack.enabled = !isMuted;

      // Replace in PeerConnection
      const sender = pc.getSenders().find(s => s.track?.kind === 'audio');
      if (sender) await sender.replaceTrack(gatedTrack);

      noiseGateCtxRef.current = ctx;
      noiseGateGainRef.current = gainNode;
      noiseGateTrackRef.current = gatedTrack;
      setNoiseSuppression(true);
    } catch (err) {
      console.error('Failed to apply noise gate:', err);
    }
  }, [isMuted]);

  const removeNoiseGate = useCallback(async () => {
    const pc = peerRef.current;
    // Restore raw mic track directly to PeerConnection
    if (pc && localStreamRef.current) {
      const rawTrack = localStreamRef.current.getAudioTracks()[0];
      if (rawTrack) {
        const sender = pc.getSenders().find(s => s.track?.kind === 'audio');
        if (sender) await sender.replaceTrack(rawTrack);
      }
    }
    if (noiseGateRafRef.current) { cancelAnimationFrame(noiseGateRafRef.current); noiseGateRafRef.current = 0; }
    if (noiseGateTrackRef.current) { noiseGateTrackRef.current.stop(); noiseGateTrackRef.current = null; }
    if (noiseGateCtxRef.current) { noiseGateCtxRef.current.close().catch(() => {}); noiseGateCtxRef.current = null; }
    noiseGateGainRef.current = null;
    setNoiseSuppression(false);
  }, []);

  const toggleNoiseSuppression = useCallback(async () => {
    if (noiseSuppression) {
      await removeNoiseGate();
    } else {
      await applyNoiseGate();
    }
  }, [noiseSuppression, applyNoiseGate, removeNoiseGate]);

  // Enumerate microphones
  const refreshMicrophones = useCallback(async () => {
    try {
      const devices = await navigator.mediaDevices.enumerateDevices();
      const audioInputs = devices.filter(d => d.kind === 'audioinput');
      setMicrophones(audioInputs);
      return audioInputs;
    } catch { return []; }
  }, []);

  // Switch microphone
  const switchMicrophone = useCallback(async (deviceId: string) => {
    const pc = peerRef.current;
    if (!pc) return;
    setShowMicMenu(false);
    try {
      const newStream = await navigator.mediaDevices.getUserMedia({
        audio: { deviceId: { exact: deviceId }, echoCancellation: true, noiseSuppression: true, autoGainControl: true },
      });
      const newTrack = newStream.getAudioTracks()[0];
      if (!newTrack) return;
      newTrack.enabled = !isMuted;

      // Remove noise gate if active (will re-apply after switch if needed)
      const wasGated = noiseSuppression;
      if (wasGated) await removeNoiseGate();

      // Replace in local stream
      if (localStreamRef.current) {
        localStreamRef.current.getAudioTracks().forEach(t => {
          localStreamRef.current!.removeTrack(t);
          t.stop();
        });
        localStreamRef.current.addTrack(newTrack);
      }
      // Replace in PeerConnection
      const sender = pc.getSenders().find(s => s.track?.kind === 'audio');
      if (sender) await sender.replaceTrack(newTrack);
      setActiveMicId(deviceId);

      // Re-apply noise gate if it was active
      if (wasGated) await applyNoiseGate();
    } catch (err) {
      console.error('Switch mic failed:', err);
    }
  }, [isMuted, noiseSuppression, removeNoiseGate, applyNoiseGate]);

  // Enumerate cameras
  const refreshCameras = useCallback(async () => {
    try {
      const devices = await navigator.mediaDevices.enumerateDevices();
      const videoInputs = devices.filter(d => d.kind === 'videoinput');
      setCameras(videoInputs);
      return videoInputs;
    } catch (e) {
      console.warn('Could not enumerate cameras:', e);
      return [];
    }
  }, []);

  // Open camera menu (await enumeration, then show)
  const openCameraMenu = useCallback(async () => {
    if (showCameraMenu) {
      setShowCameraMenu(false);
      return;
    }
    const cams = await refreshCameras();
    if (cams.length > 0) {
      setShowCameraMenu(true);
    }
  }, [showCameraMenu, refreshCameras]);

  // Switch to a specific camera by deviceId
  const switchCamera = useCallback(async (deviceId: string) => {
    const pc = peerRef.current;
    if (!pc) return;
    setShowCameraMenu(false);
    const prevCameraId = activeCameraId;

    try {
      // Strategy: try to acquire new camera FIRST without stopping the old one.
      // If that fails (some devices can't have 2 cameras open), stop old then retry.
      let newStream: MediaStream | null = null;

      try {
        newStream = await navigator.mediaDevices.getUserMedia({
          video: { deviceId: { exact: deviceId } },
        });
      } catch (e1) {
        console.warn('Could not open new camera while old is active, releasing old first:', e1);
        // Stop old camera tracks, then retry
        const currentSender = findVideoSender(pc);
        if (currentSender?.track) currentSender.track.stop();
        if (localStreamRef.current) {
          localStreamRef.current.getVideoTracks().forEach(t => {
            localStreamRef.current!.removeTrack(t);
            t.stop();
          });
        }
        newStream = await navigator.mediaDevices.getUserMedia({
          video: { deviceId: { exact: deviceId } },
        });
      }

      const newTrack = newStream.getVideoTracks()[0];
      if (!newTrack) {
        console.error('No video track from selected camera');
        return;
      }

      // Now stop old tracks (if they weren't stopped already in the fallback path above)
      const currentSender = findVideoSender(pc);
      if (localStreamRef.current) {
        localStreamRef.current.getVideoTracks().forEach(t => {
          if (t !== newTrack) {
            localStreamRef.current!.removeTrack(t);
            t.stop();
          }
        });
      }
      if (currentSender?.track && currentSender.track !== newTrack) {
        currentSender.track.stop();
      }

      // Replace track on sender or add new one
      if (currentSender) {
        await currentSender.replaceTrack(newTrack);
        // If direction was recvonly, change to sendrecv
        const transceiver = pc.getTransceivers().find(t => t.sender === currentSender);
        if (transceiver && (transceiver.direction === 'recvonly' || transceiver.direction === 'inactive')) {
          transceiver.direction = 'sendrecv';
          const offer = await pc.createOffer();
          await pc.setLocalDescription(offer);
          const socket = getSocket();
          socket?.emit('renegotiate', {
            targetUserId: targetUserIdRef.current,
            offer: pc.localDescription,
          });
        }
      } else {
        pc.addTrack(newTrack, localStreamRef.current || newStream);
        const offer = await pc.createOffer();
        await pc.setLocalDescription(offer);
        const socket = getSocket();
        socket?.emit('renegotiate', {
          targetUserId: targetUserIdRef.current,
          offer: pc.localDescription,
        });
      }

      // Update local stream
      if (localStreamRef.current) {
        localStreamRef.current.addTrack(newTrack);
      } else {
        localStreamRef.current = new MediaStream([newTrack]);
      }

      setActiveCameraId(deviceId);
      setIsVideoOff(false);
      setCallType('video');
    } catch (err) {
      console.error('Switch camera failed:', err);
      // Try to restore previous camera so video isn't permanently lost
      try {
        let restoreStream: MediaStream | undefined;
        // Try the exact previous camera first
        if (prevCameraId) {
          try {
            restoreStream = await navigator.mediaDevices.getUserMedia({
              video: { deviceId: { exact: prevCameraId } },
            });
          } catch { /* previous camera also unavailable */ }
        }
        // Fall back to any camera
        if (!restoreStream) {
          try {
            restoreStream = await navigator.mediaDevices.getUserMedia({ video: true });
          } catch { /* no camera at all */ }
        }
        if (restoreStream) {
          const restoreTrack = restoreStream.getVideoTracks()[0];
          if (restoreTrack) {
            const sender = findVideoSender(pc);
            if (sender) {
              await sender.replaceTrack(restoreTrack);
              // Ensure direction allows sending + renegotiate
              const transceiver = pc.getTransceivers().find(t => t.sender === sender);
              if (transceiver && (transceiver.direction === 'recvonly' || transceiver.direction === 'inactive')) {
                transceiver.direction = 'sendrecv';
                const offer = await pc.createOffer();
                await pc.setLocalDescription(offer);
                const socket = getSocket();
                socket?.emit('renegotiate', {
                  targetUserId: targetUserIdRef.current,
                  offer: pc.localDescription,
                });
              }
            }
            if (localStreamRef.current) {
              localStreamRef.current.addTrack(restoreTrack);
            } else {
              localStreamRef.current = restoreStream;
            }
            setIsVideoOff(false);
          }
        }
      } catch { /* nothing we can do */ }
    }
  }, [activeCameraId]);

  // Toggle video: enable/disable video track, or get camera if none
  const toggleVideo = useCallback(async () => {
    const pc = peerRef.current;
    if (!pc) return;

    if (!isVideoOff) {
      // Turn off: disable video track on sender and revert to voice
      const sender = findVideoSender(pc);
      if (sender?.track) {
        sender.track.enabled = false;
      }
      setIsVideoOff(true);
      setCallType('voice');
      const socket = getSocket();
      socket?.emit('call_type_changed', { targetUserId: targetUserIdRef.current, callType: 'voice' });
    } else {
      // Turn on: re-enable existing track or get new camera
      const sender = findVideoSender(pc);
      if (sender?.track) {
        sender.track.enabled = true;
        setIsVideoOff(false);
      } else {
        try {
          const { stream: camStream, hasVideo } = await getMediaWithCameraFallback(true);
          if (!hasVideo) {
            console.warn('No camera available');
            return;
          }
          const videoTrack = camStream.getVideoTracks()[0];

          // Find a video sender (may have null track from transceiver)
          const existingSender = findVideoSender(pc);
          if (existingSender) {
            await existingSender.replaceTrack(videoTrack);
            // If direction was recvonly, change to sendrecv and renegotiate
            const transceiver = pc.getTransceivers().find(t => t.sender === existingSender);
            if (transceiver && (transceiver.direction === 'recvonly' || transceiver.direction === 'inactive')) {
              transceiver.direction = 'sendrecv';
              const offer = await pc.createOffer();
              await pc.setLocalDescription(offer);
              const socket = getSocket();
              socket?.emit('renegotiate', {
                targetUserId: targetUserIdRef.current,
                offer: pc.localDescription,
              });
            }
          } else {
            pc.addTrack(videoTrack, localStreamRef.current || camStream);
            // Renegotiate since new track added
            const offer = await pc.createOffer();
            await pc.setLocalDescription(offer);
            const socket = getSocket();
            socket?.emit('renegotiate', {
              targetUserId: targetUserIdRef.current,
              offer: pc.localDescription,
            });
          }

          if (localStreamRef.current) {
            localStreamRef.current.addTrack(videoTrack);
          }

          setIsVideoOff(false);
          setCallType('video');
          const socket = getSocket();
          socket?.emit('call_type_changed', { targetUserId: targetUserIdRef.current, callType: 'video' });
        } catch (err) {
          console.error('Could not start camera:', err);
        }
      }
    }
  }, [isVideoOff]);

  // Screen sharing
  const toggleScreenShare = useCallback(async () => {
    const pc = peerRef.current;
    if (!pc) return;

    if (isScreenSharing) {
      // Stop screen share
      if (screenStreamRef.current) {
        screenStreamRef.current.getTracks().forEach(t => t.stop());
        screenStreamRef.current = null;
      }

      const hadVideo = hadVideoBeforeScreenShareRef.current;

      if (hadVideo) {
        // Restore camera — request video only (no audio) to avoid interfering with existing mic
        let cameraRestored = false;
        try {
          const constraints: MediaStreamConstraints = activeCameraId
            ? { video: { deviceId: { exact: activeCameraId } } }
            : { video: true };
          const camStream = await navigator.mediaDevices.getUserMedia(constraints);
          const cameraTrack = camStream.getVideoTracks()[0];
          if (cameraTrack) {
            const sender = findVideoSender(pc);
            if (sender) {
              await sender.replaceTrack(cameraTrack);
            }
            if (localStreamRef.current) {
              localStreamRef.current.getVideoTracks().forEach(t => {
                localStreamRef.current!.removeTrack(t);
                t.stop();
              });
              localStreamRef.current.addTrack(cameraTrack);
            }
            cameraRestored = true;
          }
        } catch (err) {
          console.error('Error restoring camera:', err);
        }
        if (!cameraRestored) {
          // Null out sender and go recvonly
          const sender = findVideoSender(pc);
          if (sender) {
            await sender.replaceTrack(null);
            const transceiver = pc.getTransceivers().find(t => t.sender === sender);
            if (transceiver && transceiver.direction !== 'recvonly') {
              transceiver.direction = 'recvonly';
              try {
                const offer = await pc.createOffer();
                await pc.setLocalDescription(offer);
                const socket = getSocket();
                socket?.emit('renegotiate', {
                  targetUserId: targetUserIdRef.current,
                  offer: pc.localDescription,
                });
              } catch (e) { console.error('Renegotiation after screen share stop failed:', e); }
            }
          }
          if (localStreamRef.current) {
            localStreamRef.current.getVideoTracks().forEach(t => {
              localStreamRef.current!.removeTrack(t);
              t.stop();
            });
          }
          setIsVideoOff(true);
        }
      } else {
        // Was voice call — don't restore camera, just null out the video sender
        const sender = findVideoSender(pc);
        if (sender) {
          await sender.replaceTrack(null);
          const transceiver = pc.getTransceivers().find(t => t.sender === sender);
          if (transceiver && transceiver.direction !== 'recvonly') {
            transceiver.direction = 'recvonly';
            try {
              const offer = await pc.createOffer();
              await pc.setLocalDescription(offer);
              const socket = getSocket();
              socket?.emit('renegotiate', {
                targetUserId: targetUserIdRef.current,
                offer: pc.localDescription,
              });
            } catch (e) { console.error('Renegotiation after screen share stop failed:', e); }
          }
        }
        if (localStreamRef.current) {
          localStreamRef.current.getVideoTracks().forEach(t => {
            localStreamRef.current!.removeTrack(t);
            t.stop();
          });
        }
        setCallType('voice');
        setIsVideoOff(false);
        const socket = getSocket();
        socket?.emit('call_type_changed', { targetUserId: targetUserIdRef.current, callType: 'voice' });
      }

      setIsScreenSharing(false);
    } else {
      // Start screen share — remember current video state
      hadVideoBeforeScreenShareRef.current = callType === 'video' && !isVideoOff;
      try {
        const screenStream = await navigator.mediaDevices.getDisplayMedia({
          video: { width: { ideal: 1920 }, height: { ideal: 1080 }, frameRate: { ideal: 30 } },
          audio: false,
        });
        screenStreamRef.current = screenStream;
        const screenTrack = screenStream.getVideoTracks()[0];

        // Find video sender and replace track
        const sender = findVideoSender(pc);
        if (sender) {
          await sender.replaceTrack(screenTrack);
          const transceiver = pc.getTransceivers().find(t => t.sender === sender);
          if (transceiver && (transceiver.direction === 'recvonly' || transceiver.direction === 'inactive')) {
            transceiver.direction = 'sendrecv';
            const offer = await pc.createOffer();
            await pc.setLocalDescription(offer);
            const socket = getSocket();
            socket?.emit('renegotiate', {
              targetUserId: targetUserIdRef.current,
              offer: pc.localDescription,
            });
          }
        } else {
          // No video sender — add track associated with localStream to keep audio on same stream
          pc.addTrack(screenTrack, localStreamRef.current || screenStream);
          const offer = await pc.createOffer();
          await pc.setLocalDescription(offer);
          const socket = getSocket();
          socket?.emit('renegotiate', {
            targetUserId: targetUserIdRef.current,
            offer: pc.localDescription,
          });
        }

        screenTrack.onended = async () => {
          setIsScreenSharing(false);
          if (screenStreamRef.current) {
            screenStreamRef.current.getTracks().forEach(t => t.stop());
            screenStreamRef.current = null;
          }

          const hadVideo = hadVideoBeforeScreenShareRef.current;
          const snd = peerRef.current ? findVideoSender(peerRef.current) : undefined;

          if (hadVideo) {
            // Restore camera — video only, no audio
            let cameraRestored = false;
            try {
              const constraints: MediaStreamConstraints = activeCameraId
                ? { video: { deviceId: { exact: activeCameraId } } }
                : { video: true };
              const camStream = await navigator.mediaDevices.getUserMedia(constraints);
              const camTrack = camStream.getVideoTracks()[0];
              if (camTrack) {
                if (snd) await snd.replaceTrack(camTrack);
                if (localStreamRef.current) {
                  localStreamRef.current.getVideoTracks().forEach(t => {
                    localStreamRef.current!.removeTrack(t);
                    t.stop();
                  });
                  localStreamRef.current.addTrack(camTrack);
                }
                cameraRestored = true;
              }
            } catch (e) {
              console.error('Error restoring camera:', e);
            }
            if (!cameraRestored && snd) {
              await snd.replaceTrack(null);
              const currentPc = peerRef.current;
              if (currentPc) {
                const transceiver = currentPc.getTransceivers().find(t => t.sender === snd);
                if (transceiver && transceiver.direction !== 'recvonly') {
                  transceiver.direction = 'recvonly';
                  try {
                    const offer = await currentPc.createOffer();
                    await currentPc.setLocalDescription(offer);
                    const socket = getSocket();
                    socket?.emit('renegotiate', {
                      targetUserId: targetUserIdRef.current,
                      offer: currentPc.localDescription,
                    });
                  } catch (e) { console.error('Renegotiation after screen share onended failed:', e); }
                }
              }
              if (localStreamRef.current) {
                localStreamRef.current.getVideoTracks().forEach(t => {
                  localStreamRef.current!.removeTrack(t);
                  t.stop();
                });
              }
            }
          } else {
            // Was voice call — just null out video sender
            if (snd) {
              await snd.replaceTrack(null);
              const currentPc = peerRef.current;
              if (currentPc) {
                const transceiver = currentPc.getTransceivers().find(t => t.sender === snd);
                if (transceiver && transceiver.direction !== 'recvonly') {
                  transceiver.direction = 'recvonly';
                  try {
                    const offer = await currentPc.createOffer();
                    await currentPc.setLocalDescription(offer);
                    const socket = getSocket();
                    socket?.emit('renegotiate', {
                      targetUserId: targetUserIdRef.current,
                      offer: currentPc.localDescription,
                    });
                  } catch (e) { console.error('Renegotiation after screen share onended failed:', e); }
                }
              }
            }
            if (localStreamRef.current) {
              localStreamRef.current.getVideoTracks().forEach(t => {
                localStreamRef.current!.removeTrack(t);
                t.stop();
              });
            }
            setCallType('voice');
            const socket = getSocket();
            socket?.emit('call_type_changed', { targetUserId: targetUserIdRef.current, callType: 'voice' });
          }
        };

        setIsScreenSharing(true);
        setCallType('video');
        const socket = getSocket();
        socket?.emit('call_type_changed', { targetUserId: targetUserIdRef.current, callType: 'video' });
      } catch (err) {
        console.error('Error starting screen share:', err);
      }
    }
  }, [isScreenSharing, activeCameraId, callType, isVideoOff]);

  // Fullscreen toggle
  const toggleFullscreen = useCallback(async () => {
    if (!videoContainerRef.current) return;
    try {
      if (!document.fullscreenElement) {
        await videoContainerRef.current.requestFullscreen();
        setIsFullscreen(true);
      } else {
        await document.exitFullscreen();
        setIsFullscreen(false);
      }
    } catch (e) {
      console.error('Fullscreen error:', e);
    }
  }, []);

  useEffect(() => {
    const onFsChange = () => setIsFullscreen(!!document.fullscreenElement);
    document.addEventListener('fullscreenchange', onFsChange);
    return () => document.removeEventListener('fullscreenchange', onFsChange);
  }, []);

  // Socket event listeners
  useEffect(() => {
    const socket = getSocket();
    if (!socket) return;

    const onCallAnswered = async (data: { from: string; answer: RTCSessionDescriptionInit }) => {
      if (!peerRef.current || data.from !== targetUserIdRef.current) return;
      if (callTimeoutRef.current) clearTimeout(callTimeoutRef.current);
      stopCallRingtone();
      console.log('[onCallAnswered] Setting remote description (answer)');
      await peerRef.current.setRemoteDescription(new RTCSessionDescription(data.answer));
      console.log('[onCallAnswered] Transceivers after answer:',
        peerRef.current.getTransceivers().map(t => ({
          mid: t.mid,
          direction: t.direction,
          currentDirection: t.currentDirection,
          senderTrack: t.sender?.track?.kind ?? null,
          receiverTrack: t.receiver?.track?.kind ?? null,
        })));
      for (const candidate of iceCandidateBufferRef.current) {
        peerRef.current?.addIceCandidate(new RTCIceCandidate(candidate)).catch(console.error);
      }
      iceCandidateBufferRef.current = [];
      setCallState('connected');
      timerRef.current = setInterval(() => setDuration((d) => d + 1), 1000);
    };

    const onIceCandidate = (data: { from: string; candidate: RTCIceCandidateInit }) => {
      if (peerRef.current && peerRef.current.remoteDescription) {
        peerRef.current.addIceCandidate(new RTCIceCandidate(data.candidate)).catch(console.error);
      } else {
        iceCandidateBufferRef.current.push(data.candidate);
      }
    };

    const onCallEnded = (data: { from: string }) => {
      if (callEndedRef.current) return;
      if (data.from !== targetUserIdRef.current) return;
      callEndedRef.current = true;
      stopCallRingtone();
      setCallState('ended');
      cleanup();
      scheduleClose();
    };

    const onCallDeclined = (data: { from: string }) => {
      if (callEndedRef.current) return;
      if (data.from !== targetUserIdRef.current) return;
      callEndedRef.current = true;
      stopCallRingtone();
      setCallState('ended');
      cleanup();
      scheduleClose();
    };

    const onCallUnavailable = () => {
      if (callEndedRef.current) return;
      callEndedRef.current = true;
      stopCallRingtone();
      setCallState('ended');
      cleanup();
      scheduleClose();
    };

    // Renegotiation (e.g. when remote adds screen share to audio-only call)
    const onRenegotiate = async (data: { from: string; offer: RTCSessionDescriptionInit }) => {
      if (!peerRef.current || data.from !== targetUserIdRef.current) return;
      try {
        await peerRef.current.setRemoteDescription(new RTCSessionDescription(data.offer));
        const answer = await peerRef.current.createAnswer();
        await peerRef.current.setLocalDescription(answer);
        socket.emit('renegotiate_answer', {
          targetUserId: targetUserIdRef.current,
          answer: peerRef.current.localDescription,
        });
      } catch (err) {
        console.error('Renegotiation error:', err);
      }
    };

    const onRenegotiateAnswer = async (data: { from: string; answer: RTCSessionDescriptionInit }) => {
      if (!peerRef.current || data.from !== targetUserIdRef.current) return;
      try {
        await peerRef.current.setRemoteDescription(new RTCSessionDescription(data.answer));
      } catch (err) {
        console.error('Renegotiation answer error:', err);
      }
    };

    const onCallTypeChanged = (data: { from: string; callType: 'voice' | 'video' }) => {
      if (data.from !== targetUserIdRef.current) return;
      console.log('[onCallTypeChanged] New call type:', data.callType);
      setCallType(data.callType);
      
      // If remote switched to video, nudge the video element
      if (data.callType === 'video') {
        setTimeout(() => {
          if (remoteVideoRef.current && remoteStreamRef.current) {
            remoteVideoRef.current.srcObject = remoteStreamRef.current;
            remoteVideoRef.current.play().catch(() => {});
          }
        }, 500);
      }
    };

    socket.on('call_answered', onCallAnswered);
    socket.on('ice_candidate', onIceCandidate);
    socket.on('call_ended', onCallEnded);
    socket.on('call_declined', onCallDeclined);
    socket.on('call_unavailable', onCallUnavailable);
    socket.on('renegotiate', onRenegotiate);
    socket.on('renegotiate_answer', onRenegotiateAnswer);
    socket.on('call_type_changed', onCallTypeChanged);

    return () => {
      socket.off('call_answered', onCallAnswered);
      socket.off('ice_candidate', onIceCandidate);
      socket.off('call_ended', onCallEnded);
      socket.off('call_declined', onCallDeclined);
      socket.off('call_unavailable', onCallUnavailable);
      socket.off('renegotiate', onRenegotiate);
      socket.off('renegotiate_answer', onRenegotiateAnswer);
      socket.off('call_type_changed', onCallTypeChanged);
    };
  }, [cleanup, scheduleClose]);

  // Start call on mount (outgoing)
  useEffect(() => {
    if (isOpen && !incoming && targetUser && callState === 'idle') {
      startCall();
    }
    if (isOpen && incoming && callState === 'idle') {
      targetUserIdRef.current = incoming.from;
      setCallState('incoming');
      setCallType(incoming.callType);
      playCallRingtone();
    }
  }, [isOpen, incoming, targetUser, callState, startCall]);

  // Sync local video ref with stream (only when srcObject actually changes)
  useEffect(() => {
    if (!localVideoRef.current) return;
    const desired = isScreenSharing && screenStreamRef.current
      ? screenStreamRef.current
      : localStreamRef.current;
    if (desired && localVideoRef.current.srcObject !== desired) {
      localVideoRef.current.srcObject = desired;
    }
  });

  // Sync remote video ref with remote stream (only when srcObject actually changes)
  useEffect(() => {
    if (!remoteVideoRef.current || !remoteStreamRef.current) return;
    if (remoteVideoRef.current.srcObject !== remoteStreamRef.current) {
      console.log('[useEffect] Syncing remote video srcObject');
      remoteVideoRef.current.srcObject = remoteStreamRef.current;
      remoteVideoRef.current.play().catch(() => { });
    }
  }, [hasRemoteVideo, callType]);

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      stopCallRingtone();
      if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);
      cleanup();
    };
  }, [cleanup]);

  const formatDuration = (sec: number) => {
    const m = Math.floor(sec / 60);
    const s = sec % 60;
    return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
  };

  const displayName = incoming
    ? incoming.callerInfo?.displayName || incoming.callerInfo?.username || '...'
    : targetUser?.displayName || targetUser?.username || '...';

  const displayAvatar = incoming
    ? incoming.callerInfo?.avatar
    : targetUser?.avatar;

  const initials = displayName
    .split(' ')
    .map((w: string) => w[0])
    .join('')
    .slice(0, 2)
    .toUpperCase();

  if (!isOpen) return null;

  const showVideoArea = callState === 'connected' && (hasRemoteVideo || (callType === 'video' && (!isVideoOff || isScreenSharing)));
  const hasLocalVideo = !!(
    localStreamRef.current?.getVideoTracks().some(t => t.enabled) || isScreenSharing
  );

  return (
    <AnimatePresence>
      {/* Remote audio is handled by the video element below */}

      {/* === MINIMIZED VIEW === */}
      {isMinimized && callState === 'connected' ? (
        <motion.div
          key="call-minimized"
          initial={{ opacity: 0, y: 50, scale: 0.8 }}
          animate={{ opacity: 1, y: 0, scale: 1 }}
          exit={{ opacity: 0, y: 50, scale: 0.8 }}
          className="fixed bottom-6 right-6 z-[100] flex items-center gap-3 px-4 py-3 rounded-2xl glass-strong shadow-2xl shadow-black/50 border border-white/10 cursor-pointer select-none"
          onClick={() => setIsMinimized(false)}
        >
          {/* Avatar */}
          <div className="relative">
            <div className="absolute inset-0 rounded-full bg-vortex-500/30 animate-call-wave" />
            {displayAvatar ? (
              <img src={displayAvatar} alt="" className="relative w-10 h-10 rounded-full object-cover" />
            ) : (
              <div className="relative w-10 h-10 rounded-full bg-gradient-to-br from-vortex-500 to-purple-600 flex items-center justify-center text-white font-bold text-sm">
                {initials}
              </div>
            )}
          </div>
          <div className="min-w-0">
            <p className="text-sm text-white font-medium truncate max-w-[120px]">{displayName}</p>
            <p className="text-xs text-zinc-400 font-mono">{formatDuration(duration)}</p>
          </div>
          {/* Quick controls */}
          <div className="flex items-center gap-2 ml-2" onClick={(e) => e.stopPropagation()}>
            <button
              onClick={toggleMic}
              className={`w-8 h-8 rounded-full flex items-center justify-center transition-colors ${isMuted ? 'bg-red-500/20 text-red-400' : 'bg-white/10 text-white hover:bg-white/20'}`}
            >
              {isMuted ? <MicOff size={14} /> : <Mic size={14} />}
            </button>
            <button
              onClick={endCallSafe}
              className="w-8 h-8 rounded-full bg-red-500 hover:bg-red-600 flex items-center justify-center text-white transition-colors"
            >
              <PhoneOff size={14} />
            </button>
          </div>
          {/* No hidden elements here - they are managed in the main content area */}
        </motion.div>
      ) : (

      /* === FULL VIEW === */
      <motion.div
        key="call-overlay"
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        exit={{ opacity: 0 }}
        role="dialog"
        aria-modal="true"
        aria-label="Call"
        className="fixed inset-0 z-[100] flex items-center justify-center bg-surface/90 backdrop-blur-xl overflow-hidden"
        onClick={() => { setShowCameraMenu(false); setShowVolumeSlider(false); setShowMicMenu(false); }}
      >
        {/* Ambient background glow for call modal */}
        <div className="absolute inset-0 pointer-events-none opacity-40">
          <div className="absolute top-[10%] left-[20%] w-[50vh] h-[50vh] bg-vortex-500/30 rounded-full blur-[120px] animate-float" />
          <div className="absolute bottom-[10%] right-[20%] w-[50vh] h-[50vh] bg-emerald-500/20 rounded-full blur-[120px] animate-float-delayed" />
        </div>

        <motion.div
          initial={{ scale: 0.9, opacity: 0, y: 20 }}
          animate={{ scale: 1, opacity: 1, y: 0 }}
          exit={{ scale: 0.9, opacity: 0, y: 20 }}
          transition={{ type: 'spring', damping: 25, stiffness: 300 }}
          className={`relative w-full mx-4 rounded-[2.5rem] glass-strong shadow-2xl shadow-black/50 overflow-hidden border border-white/5 ${showVideoArea ? 'max-w-5xl' : 'max-w-md'
            }`}
        >
          {/* Mic selector popup */}
          {showMicMenu && microphones.length > 0 && (
            <>
              <div className="fixed inset-0 z-[200]" onClick={() => setShowMicMenu(false)} />
              <div className="fixed z-[201] left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[320px] rounded-xl bg-zinc-800/95 backdrop-blur-md border border-zinc-600 shadow-2xl py-2">
                <div className="px-3 py-1.5 text-xs text-zinc-400 uppercase tracking-wider border-b border-zinc-700 mb-1">{t('selectMicrophone')}</div>
                {microphones.map((mic, i) => (
                  <button
                    key={mic.deviceId}
                    onClick={() => switchMicrophone(mic.deviceId)}
                    className={`w-full text-left px-4 py-2.5 text-sm transition-colors ${activeMicId === mic.deviceId
                        ? 'text-vortex-400 bg-vortex-500/20 font-medium'
                        : 'text-zinc-200 hover:bg-zinc-700'
                      }`}
                  >
                    {mic.label || `${t('microphone')} ${i + 1}`}
                  </button>
                ))}
              </div>
            </>
          )}
          {/* Camera selector popup - rendered outside overflow-hidden container */}
          {showCameraMenu && cameras.length > 0 && (
            <>
              {/* Backdrop to close on click outside */}
              <div className="fixed inset-0 z-[200]" onClick={() => setShowCameraMenu(false)} />
              <div className="fixed z-[201] left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[300px] rounded-xl bg-zinc-800/95 backdrop-blur-md border border-zinc-600 shadow-2xl py-2">
                <div className="px-3 py-1.5 text-xs text-zinc-400 uppercase tracking-wider border-b border-zinc-700 mb-1">{t('switchCamera')}</div>
                {cameras.map((cam, i) => (
                  <button
                    key={cam.deviceId}
                    onClick={() => switchCamera(cam.deviceId)}
                    className={`w-full text-left px-4 py-2.5 text-sm transition-colors ${activeCameraId === cam.deviceId
                        ? 'text-vortex-400 bg-vortex-500/20 font-medium'
                        : 'text-zinc-200 hover:bg-zinc-700'
                      }`}
                  >
                    {cam.label || `Camera ${i + 1}`}
                  </button>
                ))}
              </div>
            </>
          )}
          {/* Volume slider popup */}
          {showVolumeSlider && (
            <>
              <div className="fixed inset-0 z-[200]" onClick={() => setShowVolumeSlider(false)} />
              <div className="fixed z-[201] left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[250px] rounded-xl bg-zinc-800/95 backdrop-blur-md border border-zinc-600 shadow-2xl p-4" onClick={(e) => e.stopPropagation()}>
                <div className="text-xs text-zinc-400 uppercase tracking-wider mb-3">{t('volume')}</div>
                <div className="flex items-center gap-3">
                  <Volume2 size={16} className="text-zinc-400 shrink-0" />
                  <input
                    type="range"
                    min="0"
                    max="1"
                    step="0.05"
                    value={remoteVolume}
                    onChange={(e) => handleVolumeChange(parseFloat(e.target.value))}
                    className="w-full h-1.5 rounded-full appearance-none bg-zinc-600 accent-vortex-500 cursor-pointer"
                  />
                  <span className="text-xs text-zinc-300 w-8 text-right">{Math.round(remoteVolume * 100)}%</span>
                </div>
              </div>
            </>
          )}
          {/* === Main Content Area === */}
          <div className="flex-1 flex flex-col relative overflow-hidden">
            {/* 
                CRITICAL: Single stable remote video element.
                We keep it always mounted so WebRTC srcObject is never lost 
                during transitions between voice and video modes.
            */}
            <video
              ref={remoteVideoRef}
              autoPlay
              playsInline
              className={`
                bg-black transition-all duration-500
                ${showVideoArea 
                  ? 'w-full h-full object-contain' 
                  : 'w-0 h-0 opacity-0 absolute pointer-events-none'
                }
              `}
              onContextMenu={(e) => { e.preventDefault(); setShowVolumeSlider(true); }}
            />

            {showVideoArea ? (
              <div 
                ref={videoContainerRef}
                className="absolute inset-0 flex items-center justify-center bg-black"
              >
                {/* Placeholder shown when video track is not yet arrived or muted */}
                {!hasRemoteVideo && (
                  <div className="absolute inset-0 flex flex-col items-center justify-center bg-zinc-900/50 backdrop-blur-sm z-10">
                    <VideoOff size={48} className="text-zinc-500 mb-2" />
                    <span className="text-sm text-zinc-500">{displayName}</span>
                  </div>
                )}

                {/* Local video PIP (bottom-right) */}
                {hasLocalVideo && (
                  <div className="absolute bottom-3 right-3 w-48 rounded-xl overflow-hidden border-2 border-white/20 shadow-lg bg-black z-20"
                    style={{ aspectRatio: '16 / 9' }}
                  >
                    <video
                      ref={localVideoRef}
                      autoPlay
                      playsInline
                      muted
                      className="w-full h-full object-contain bg-black"
                    />
                  </div>
                )}

                {/* Duration badge */}
                <div className="absolute top-4 left-1/2 -translate-x-1/2 px-4 py-1 rounded-full bg-black/50 backdrop-blur-sm z-20">
                  <span className="text-sm text-white font-mono">{formatDuration(duration)}</span>
                </div>

                {/* Top-right buttons */}
                <div className="absolute top-4 right-4 flex items-center gap-2 z-20">
                  <button
                    onClick={() => setIsMinimized(true)}
                    className="w-8 h-8 rounded-full bg-black/50 backdrop-blur-sm flex items-center justify-center text-white/70 hover:text-white transition-colors"
                  >
                    <Minimize2 size={16} />
                  </button>
                  <button
                    onClick={toggleFullscreen}
                    className="w-8 h-8 rounded-full bg-black/50 backdrop-blur-sm flex items-center justify-center text-white/70 hover:text-white transition-colors"
                  >
                    {isFullscreen ? <Minimize size={16} /> : <Maximize size={16} />}
                  </button>
                </div>
              </div>
            ) : (
              /* === Voice / calling / incoming layout === */
              <div className="flex-1 px-8 py-12 flex flex-col items-center justify-center relative z-10">
                {/* Minimize button (top-right) */}
                {callState === 'connected' && (
                  <button
                    onClick={() => setIsMinimized(true)}
                    className="absolute top-4 right-4 w-8 h-8 rounded-full bg-white/10 flex items-center justify-center text-white/70 hover:text-white transition-colors"
                    title={t('minimize')}
                  >
                    <Minimize2 size={16} />
                  </button>
                )}
                
                <div className="relative mb-8 mt-4">
                  {(callState === 'calling' || callState === 'connected') && (
                    <>
                      <div className="absolute inset-0 rounded-full bg-vortex-500/30 animate-call-wave" />
                      <div className="absolute inset-0 rounded-full bg-vortex-500/20 animate-call-wave-delayed" />
                    </>
                  )}
                  {callState === 'incoming' && (
                    <>
                      <div className="absolute inset-0 rounded-full bg-emerald-500/30 animate-call-wave" />
                      <div className="absolute inset-0 rounded-full bg-emerald-500/20 animate-call-wave-delayed" />
                    </>
                  )}
                  <div className="relative z-10 p-1.5 rounded-full bg-gradient-to-br from-white/10 to-transparent backdrop-blur-md border border-white/10 shadow-2xl cursor-pointer">
                    {displayAvatar ? (
                      <img src={displayAvatar} alt="" className="w-32 h-32 rounded-full object-cover shadow-inner" />
                    ) : (
                      <div className="w-32 h-32 rounded-full bg-gradient-to-br from-vortex-500 to-purple-600 flex items-center justify-center text-white font-bold text-4xl shadow-inner">
                        {initials}
                      </div>
                    )}
                  </div>
                </div>

                <h3 className="text-2xl font-bold text-white mb-2 tracking-tight">{displayName}</h3>
                <p className="text-sm text-zinc-400 mb-8">
                  {callState === 'calling' && t('calling')}
                  {callState === 'incoming' && (callType === 'video' ? t('incomingVideoCall') : t('incomingCall'))}
                  {callState === 'connected' && formatDuration(duration)}
                  {callState === 'ended' && t('callEnded')}
                </p>

                {/* Local video preview (during calling with camera) */}
                {hasLocalVideo && callState === 'calling' && (
                  <div className="mb-6 rounded-2xl overflow-hidden bg-zinc-900 w-48 shadow-2xl border border-white/10">
                    <video
                      ref={localVideoRef}
                      autoPlay
                      playsInline
                      muted
                      className="w-full h-auto"
                    />
                  </div>
                )}
              </div>
            )}
          </div>

          {/* === Controls === */}
          <div className="px-8 pb-10 flex items-center justify-center gap-4 relative z-10 flex-wrap">
            {callState === 'incoming' && (
              <>
                <button
                  onClick={declineCall}
                  className="w-16 h-16 rounded-full bg-red-500 hover:bg-red-600 flex items-center justify-center text-white shadow-xl shadow-red-500/30 transition-all hover:scale-105"
                >
                  <PhoneOff size={24} />
                </button>
                <div className="w-8" />
                <button
                  onClick={acceptCall}
                  className="w-16 h-16 rounded-full bg-emerald-500 hover:bg-emerald-600 flex items-center justify-center text-white shadow-xl shadow-emerald-500/30 transition-all hover:scale-105 animate-pulse"
                >
                  {callType === 'video' ? <Video size={24} /> : <Phone size={24} className="animate-bounce" />}
                </button>
              </>
            )}

            {callState === 'connected' && (
              <>
                {/* Mic button with dropdown */}
                <div className="relative">
                  <button
                    onClick={toggleMic}
                    className={`w-11 h-11 rounded-full flex items-center justify-center transition-colors ${isMuted ? 'bg-red-500/20 text-red-400' : 'bg-white/10 text-white hover:bg-white/20'
                      }`}
                    title={isMuted ? t('unmute') : t('mute')}
                  >
                    {isMuted ? <MicOff size={18} /> : <Mic size={18} />}
                  </button>
                  <button
                    onClick={async (e) => { e.stopPropagation(); await refreshMicrophones(); setShowMicMenu(!showMicMenu); setShowCameraMenu(false); setShowVolumeSlider(false); }}
                    className="absolute -top-1 -right-1 w-5 h-5 rounded-full bg-zinc-700 hover:bg-zinc-600 flex items-center justify-center text-white/70 hover:text-white transition-colors border border-zinc-600"
                  >
                    <ChevronUp size={10} />
                  </button>
                </div>
                {/* Camera toggle */}
                <button
                  onClick={toggleVideo}
                  className={`w-11 h-11 rounded-full flex items-center justify-center transition-colors ${isVideoOff ? 'bg-red-500/20 text-red-400' : 'bg-white/10 text-white hover:bg-white/20'
                    }`}
                >
                  {isVideoOff ? <VideoOff size={18} /> : <Video size={18} />}
                </button>
                {/* Camera selector */}
                <button
                  onClick={openCameraMenu}
                  className="w-11 h-11 rounded-full flex items-center justify-center transition-colors bg-white/10 text-white hover:bg-white/20"
                  title={t('switchCamera')}
                >
                  <SwitchCamera size={18} />
                </button>
                <button
                  onClick={toggleScreenShare}
                  className={`w-11 h-11 rounded-full flex items-center justify-center transition-colors ${isScreenSharing ? 'bg-vortex-500/30 text-vortex-400' : 'bg-white/10 text-white hover:bg-white/20'
                    }`}
                  title={isScreenSharing ? t('stopScreenShare') : t('screenShare')}
                >
                  {isScreenSharing ? <MonitorOff size={18} /> : <Monitor size={18} />}
                </button>
                {/* Volume control */}
                <button
                  onClick={(e) => { e.stopPropagation(); setShowVolumeSlider(!showVolumeSlider); }}
                  className="w-11 h-11 rounded-full flex items-center justify-center transition-colors bg-white/10 text-white hover:bg-white/20"
                  title={t('volume')}
                >
                  <Volume2 size={18} />
                </button>
                {/* Noise suppression */}
                <button
                  onClick={toggleNoiseSuppression}
                  className={`w-11 h-11 rounded-full flex items-center justify-center transition-colors ${noiseSuppression ? 'bg-emerald-500/20 text-emerald-400' : 'bg-white/10 text-white hover:bg-white/20'
                    }`}
                  title={noiseSuppression ? t('noiseSuppressionOn') : t('noiseSuppressionOff')}
                >
                  {noiseSuppression ? <ShieldCheck size={18} /> : <ShieldOff size={18} />}
                </button>
                <button
                  onClick={endCallSafe}
                  className="w-14 h-14 rounded-full bg-red-500 hover:bg-red-600 flex items-center justify-center text-white shadow-xl shadow-red-500/30 transition-all hover:scale-105 ml-2"
                >
                  <PhoneOff size={22} />
                </button>
              </>
            )}

            {callState === 'calling' && (
              <button
                onClick={endCallSafe}
                className="w-16 h-16 rounded-full bg-red-500 hover:bg-red-600 flex items-center justify-center text-white shadow-xl shadow-red-500/30 transition-all hover:scale-105"
              >
                <PhoneOff size={24} />
              </button>
            )}

            {callState === 'ended' && (
              <p className="text-sm text-zinc-500">{t('callEnded')}</p>
            )}
          </div>
        </motion.div>
      </motion.div>
      )}
    </AnimatePresence>
  );
}
