import React, { useState, useEffect, useRef, useCallback } from 'react';
import './MediaControl.css';
import VolumeOff from '@mui/icons-material/VolumeOff';
import VolumeUp from '@mui/icons-material/VolumeUp';
import MicOff from '@mui/icons-material/MicOff';
import Mic from '@mui/icons-material/Mic';
import VideocamOff from '@mui/icons-material/VideocamOff';
import Videocam from '@mui/icons-material/Videocam';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import { ReadyState } from 'react-use-websocket';
import { Session, GlobalContext } from './GlobalContext';
import { useContext } from 'react';
import WebRTCStatus from './WebRTCStatus';
import Moveable from 'react-moveable';
import { flushSync } from 'react-dom';
import { SxProps, Theme } from '@mui/material';

const debug = true;
// When true, do not send host candidates to the signaling server. Keeps TURN relays preferred.
const FILTER_HOST_CANDIDATES = false; // Temporarily disabled to test direct connections

/* ---------- Synthetic Tracks Helpers ---------- */

// Helper to hash a string to a color
function nameToColor(name: string): string {
  // Simple hash function (djb2)
  let hash = 5381;
  for (let i = 0; i < name.length; i++) {
    hash = (hash << 5) + hash + name.charCodeAt(i);
  }
  // Generate HSL color from hash
  const hue = Math.abs(hash) % 360;
  const sat = 60 + (Math.abs(hash) % 30); // 60-89%
  const light = 45 + (Math.abs(hash) % 30); // 45-74%
  return `hsl(${hue},${sat}%,${light}%)`;
}

// Accepts an optional name for color selection
const createAnimatedVideoTrack = ({
  width = 320,
  height = 240,
  name,
}: { width?: number; height?: number; name?: string } = {}): MediaStreamTrack => {
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;

  const ctx = canvas.getContext('2d');
  if (!ctx) throw new Error('Could not get canvas context');

  // Pick color based on name, fallback to default
  const ballColor = name ? nameToColor(name) : '#00ff88';

  const ball = {
    x: width / 2,
    y: height / 2,
    radius: Math.min(width, height) * 0.06,
    dx: 3,
    dy: 2,
    color: ballColor,
  };

  const stream = canvas.captureStream(15);
  const track = stream.getVideoTracks()[0];

  let animationId: number;

  function drawFrame() {
    if (!ctx) return;
    ctx.fillStyle = '#000000';
    ctx.fillRect(0, 0, width, height);

    ball.x += ball.dx;
    ball.y += ball.dy;
    if (ball.x + ball.radius >= width || ball.x - ball.radius <= 0) ball.dx = -ball.dx;
    if (ball.y + ball.radius >= height || ball.y - ball.radius <= 0) ball.dy = -ball.dy;
    ball.x = Math.max(ball.radius, Math.min(width - ball.radius, ball.x));
    ball.y = Math.max(ball.radius, Math.min(height - ball.radius, ball.y));

    ctx.beginPath();
    ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
    ctx.fillStyle = ball.color;
    ctx.fill();

    ctx.fillStyle = '#ffffff';
    ctx.font = '12px Arial';
    ctx.fillText(`Frame: ${Date.now() % 10000}`, 10, 20);
  }

  function animate() {
    drawFrame();
    animationId = requestAnimationFrame(animate);
  }
  animate();

  // Store cleanup function on track
  (track as any).stopAnimation = () => {
    if (animationId) cancelAnimationFrame(animationId);
  };

  track.enabled = true;
  return track;
};

const createSilentAudioTrack = (): MediaStreamTrack => {
  const audioContext = new AudioContext();
  const oscillator = audioContext.createOscillator();
  const gainNode = audioContext.createGain();
  const destination = audioContext.createMediaStreamDestination();
  gainNode.gain.value = 0;
  oscillator.connect(gainNode);
  gainNode.connect(destination);
  oscillator.start();
  const track = destination.stream.getAudioTracks()[0];

  // Store cleanup function
  (track as any).stopOscillator = () => {
    oscillator.stop();
    audioContext.close();
  };

  track.enabled = true;
  return track;
};

/* ---------- Peer Types ---------- */

interface Peer {
  session_id: string;
  peer_name: string;
  attributes: Record<string, any>;
  muted: boolean;
  video_on: boolean;
  local: boolean;
  dead: boolean;
  connection?: RTCPeerConnection;
  connectionState?: string;
  isNegotiating?: boolean;
}
export type { Peer };

interface AddPeerConfig {
  peer_id: string;
  peer_name: string;
  has_media?: boolean; // Whether this peer provides audio/video streams
  should_create_offer?: boolean;
}

interface SessionDescriptionData {
  peer_id: string;
  peer_name: string;
  session_description: RTCSessionDescriptionInit;
}

interface IceCandidateData {
  peer_id: string;
  peer_name: string;
  candidate: RTCIceCandidateInit;
}

interface RemovePeerData {
  peer_name: string;
  peer_id: string;
}

/* ---------- Video Element with Fade-in ---------- */

interface VideoProps extends React.VideoHTMLAttributes<HTMLVideoElement> {
  srcObject: MediaStream;
  local?: boolean;
}

const Video: React.FC<VideoProps> = ({ srcObject, local, ...props }) => {
  const refVideo = useRef<HTMLVideoElement>(null);
  const clickHandlerRef = useRef<(() => void) | null>(null);
  const hasUserInteractedRef = useRef<boolean>(false);

  useEffect(() => {
    if (!refVideo.current || !srcObject) return;

    const ref = refVideo.current;
    console.log('media-agent - <Video/> - Setting video srcObject:', srcObject);

    // Remove any existing click handler
    if (clickHandlerRef.current) {
      ref.removeEventListener('click', clickHandlerRef.current);
      clickHandlerRef.current = null;
      ref.style.cursor = 'default';
    }

    // Check if srcObject actually changed
    const oldSrcObject = ref.srcObject;
    ref.srcObject = srcObject;

    // Always mute initially to satisfy autoplay policy
    ref.muted = true;

    const playVideo = async () => {
      try {
        // Force restart playback even if it was already playing
        if (!ref.paused) {
          ref.pause();
          await new Promise(resolve => setTimeout(resolve, 50));
        }

        await ref.play();
        ref.classList.add('fade-in');
        console.log(
          `media-agent - <Video/> - Video started playing for ${local ? 'local' : 'remote'} video`
        );

        // For remote videos, only unmute if user has already interacted
        if (!local && hasUserInteractedRef.current && !props.muted) {
          try {
            ref.muted = false;
          } catch (e) {
            console.log('media-agent - <Video/> - Unmute requires user interaction');
          }
        }

        // If not local and not yet interacted, add click handler for unmuting
        if (!local && !hasUserInteractedRef.current) {
          const handleClick = async () => {
            hasUserInteractedRef.current = true;
            try {
              ref.muted = false;
              ref.removeEventListener('click', handleClick);
              clickHandlerRef.current = null;
              ref.style.cursor = 'default';
              console.log(`media-agent - <Video/> - Unmuted after user interaction`);
            } catch (err) {
              console.error(`media-agent - <Video/> - Failed to unmute:`, err);
            }
          };

          clickHandlerRef.current = handleClick;
          ref.addEventListener('click', handleClick);
          ref.style.cursor = 'pointer';

          // Show visual hint that click is needed for audio
          console.log(`media-agent - <Video/> - Click video to enable audio`);
        }
      } catch (error) {
        console.error(
          `media-agent - <Video/> - Error playing video for ${local ? 'local' : 'remote'} video`,
          error
        );

        // If autoplay fails completely, add handler to start playback
        const handleClick = async () => {
          hasUserInteractedRef.current = true;
          try {
            await ref.play();
            ref.muted = false;
            ref.removeEventListener('click', handleClick);
            clickHandlerRef.current = null;
            ref.style.cursor = 'default';
            console.log(`media-agent - <Video/> - Video started after user click`);
          } catch (err) {
            console.error(`media-agent - <Video/> - Failed to play after click:`, err);
          }
        };

        clickHandlerRef.current = handleClick;
        ref.addEventListener('click', handleClick);
        ref.style.cursor = 'pointer';
      }
    };

    // Always try to play when srcObject changes
    const timeoutId = setTimeout(playVideo, 100);

    return () => {
      clearTimeout(timeoutId);
      if (ref && oldSrcObject !== srcObject) {
        ref.pause();
        if (clickHandlerRef.current) {
          ref.removeEventListener('click', clickHandlerRef.current);
          clickHandlerRef.current = null;
        }
        ref.classList.remove('fade-in');
        ref.style.cursor = 'default';
      }
    };
  }, [srcObject, local, props.muted]);

  // Monitor document interaction separately
  useEffect(() => {
    const handleInteraction = () => {
      hasUserInteractedRef.current = true;
      // Try to unmute any playing remote videos
      if (
        refVideo.current &&
        !local &&
        !refVideo.current.paused &&
        refVideo.current.muted &&
        !props.muted
      ) {
        try {
          refVideo.current.muted = false;
          console.log('media-agent - <Video/> - Unmuted after document interaction');
        } catch (e) {
          // Still might fail if interaction wasn't sufficient
        }
      }
    };

    document.addEventListener('click', handleInteraction, { once: true });
    document.addEventListener('keydown', handleInteraction, { once: true });

    return () => {
      document.removeEventListener('click', handleInteraction);
      document.removeEventListener('keydown', handleInteraction);
    };
  }, [local, props.muted]);

  return <video ref={refVideo} autoPlay playsInline muted {...props} />;
};

/* ---------- MediaAgent (signaling + peer setup) ---------- */

type MediaAgentProps = {
  session: Session;
  peers: Record<string, Peer>;
  setPeers: React.Dispatch<React.SetStateAction<Record<string, Peer>>>;
};

type JoinStatus = { status: 'Not joined' | 'Joining' | 'Joined' | 'Error'; message?: string };

const MediaAgent = (props: MediaAgentProps) => {
  const { peers, setPeers, session } = props;
  const [joinStatus, setJoinStatus] = useState<JoinStatus>({ status: 'Not joined' });
  const [media, setMedia] = useState<MediaStream | null>(null);
  const [pendingPeers, setPendingPeers] = useState<AddPeerConfig[]>([]);

  // Extract has_media value to avoid unnecessary effect re-runs when session object changes
  const localUserHasMedia = session?.has_media !== false; // Default to true for backward compatibility

  // Refs for singleton resources
  const mediaStreamRef = useRef<MediaStream | null>(null);
  const connectionsRef = useRef<Map<string, RTCPeerConnection>>(new Map());
  const mountedRef = useRef<boolean>(true);
  // Keep track of last processed message to avoid duplicates
  const lastProcessedMessageRef = useRef<string | null>(null);
  const isNegotiatingRef = useRef<Map<string, boolean>>(new Map());
  const makingOfferRef = useRef<Map<string, boolean>>(new Map());
  const initiatedOfferRef = useRef<Set<string>>(new Set());
  const pendingIceCandidatesRef = useRef<Map<string, RTCIceCandidateInit[]>>(new Map());

  // Update peer states when connection state changes
  const updatePeerConnectionState = useCallback(
    (peerId: string, connectionState: string, isNegotiating: boolean = false) => {
      setPeers(prevPeers => {
        const updatedPeers = { ...prevPeers };
        if (updatedPeers[peerId]) {
          updatedPeers[peerId] = {
            ...updatedPeers[peerId],
            connectionState,
            isNegotiating,
          };
        }
        return updatedPeers;
      });
    },
    [setPeers]
  );

  // Use the global websocket provided by RoomView to avoid duplicate sockets
  const { sendJsonMessage, lastJsonMessage, readyState } = useContext(GlobalContext);

  useEffect(() => {
    for (let peer in peers) {
      console.log(`media-agent - useEffect(peers): Peer ${peers[peer].peer_name}:`, peers[peer]);
    }
  }, [peers]);

  const addPeer = useCallback(
    async (config: AddPeerConfig) => {
      console.log(`media-agent - addPeer:${config.peer_name} - Callback triggered`, config);
      // Check if connection already exists
      const existingConnection = connectionsRef.current.get(config.peer_id);
      if (existingConnection) {
        const state = existingConnection.connectionState;
        if (state === 'new' || state === 'connected' || state === 'connecting') {
          console.log(
            `media-agent - addPeer:${config.peer_name} Connection already exists for ${config.peer_name} in state: ${state}`
          );
          return;
        } else {
          // Clean up failed/closed connection
          console.log(
            `media-agent - addPeer:${config.peer_name} Cleaning up stale connection for ${config.peer_name} in state: ${state}`
          );
          existingConnection.close();
          connectionsRef.current.delete(config.peer_id);
        }
      }

      // Queue peer if we need local media but don't have it yet
      // Only queue if we're expected to provide media (local user has media)
      const localUserHasMedia = session?.has_media !== false; // Default to true for backward compatibility

      // Only need to wait for media if we (local user) are supposed to provide it
      if (!media && localUserHasMedia) {
        console.log(`media-agent - addPeer:${config.peer_name} - No local media yet, queuing peer`);
        setPendingPeers(prev => {
          // Avoid duplicate queuing
          if (!prev.some(p => p.peer_id === config.peer_id)) {
            return [...prev, config];
          }
          return prev;
        });
        return;
      }

      const peer_id = config.peer_id;

      // Check if peer already exists and is alive
      if (peer_id in peers && !peers[peer_id].dead) {
        console.log(
          `media-agent - addPeer:${config.peer_name} - ${config.peer_name} already in peers and alive`
        );
        return;
      }

      // Create or revive peer
      const updatedPeers = { ...peers };
      const peer: Peer = {
        session_id: peer_id,
        peer_name: config.peer_name,
        attributes: {},
        muted: updatedPeers[peer_id]?.muted || false,
        video_on: updatedPeers[peer_id]?.video_on !== false,
        local: false,
        dead: false,
      };

      updatedPeers[peer_id] = peer;
      console.log(
        `media-agent - addPeer:${peer.peer_name} - ${
          peer_id in peers ? 'reviving' : 'starting new'
        } peer ${peer.peer_name}`
      );

      // Create RTCPeerConnection
      const connection = new RTCPeerConnection({
        iceTransportPolicy: 'all', // Allow both direct and relay connections
        iceServers: [
          { urls: 'stun:ketrenos.com:3478' },
          {
            urls: 'turns:ketrenos.com:5349',
            username: 'ketra',
            credential: 'ketran',
          },
          // DO NOT add Google's public STUN server as fallback; if ketrenos.com STUN/TURN fails,
          // this is an infrastructure failure that must be resolved (not silently bypassed).
          // { urls: "stun:stun.l.google.com:19302" },
        ],
      });

      peer.connection = connection;
      connectionsRef.current.set(peer_id, connection);

      if (config.should_create_offer) {
        initiatedOfferRef.current.add(peer_id);
        // Pre-set the makingOffer flag to prevent negotiationneeded from firing
        makingOfferRef.current.set(peer_id, true);
      }

      // Set up event handlers
      connection.addEventListener('negotiationneeded', async event => {
        console.log(
          `media-agent - addPeer:${peer.peer_name} negotiationneeded - ${connection.signalingState}`,
          event
        );

        // Check if we're already making an offer (set by should_create_offer)
        const makingOffer = makingOfferRef.current.get(peer_id) || false;
        if (makingOffer) {
          console.log(
            `media-agent - addPeer:${peer.peer_name} Already making offer, skipping negotiationneeded...`
          );
          return;
        }

        // Check if we're already negotiating
        const isNegotiating = isNegotiatingRef.current.get(peer_id) || false;
        if (isNegotiating) {
          console.log(`media-agent - addPeer:${peer.peer_name} Already negotiating, skipping...`);
          return;
        }

        // Only proceed if we're in a stable state
        if (connection.signalingState !== 'stable') {
          console.log(
            `media-agent - addPeer:${peer.peer_name} Skipping negotiation, signalingState: ${connection.signalingState}`
          );
          return;
        }

        console.log(
          `media-agent - addPeer:${peer.peer_name} Handling negotiationneeded for ${peer.peer_name}`
        );

        // Mark as negotiating
        isNegotiatingRef.current.set(peer_id, true);
        updatePeerConnectionState(peer_id, connection.connectionState, true);

        try {
          makingOfferRef.current.set(peer_id, true);
          isNegotiatingRef.current.set(peer_id, true);

          // Set transceiver direction to avoid m-line ordering issues
          const transceivers = connection.getTransceivers();
          transceivers.forEach(transceiver => {
            if (transceiver.sender.track) {
              transceiver.direction = 'sendrecv';
            }
          });

          const local_description = await connection.createOffer();

          // Check if connection is still in the right state
          if (connection.signalingState !== 'stable') {
            console.log(
              `media-agent - addPeer:${peer.peer_name} State changed during offer creation, aborting`
            );
            return;
          }

          await connection.setLocalDescription(local_description);

          sendJsonMessage({
            type: 'relaySessionDescription',
            data: { peer_id: peer_id, session_description: local_description },
          });
          console.log(`media-agent - addPeer:${peer.peer_name} Offer sent`, local_description);
        } catch (error) {
          console.error(
            `media-agent - addPeer:${peer.peer_name} negotiationneeded handling failed:`,
            error
          );
        } finally {
          makingOfferRef.current.set(peer_id, false);
          // Keep isNegotiating true until we get an answer
        }
      });

      connection.addEventListener('connectionstatechange', event => {
        console.log(
          `media-agent - addPeer:${peer.peer_name} connectionstatechange - `,
          connection.connectionState,
          event
        );

        // Update peer connection state
        updatePeerConnectionState(peer_id, connection.connectionState);

        if (connection.connectionState === 'failed') {
          console.error(
            `media-agent - addPeer:${peer.peer_name} Connection failed for`,
            peer.peer_name
          );

          // Immediate cleanup of failed connection
          connectionsRef.current.delete(peer_id);
          makingOfferRef.current.delete(peer_id);
          isNegotiatingRef.current.delete(peer_id);
          initiatedOfferRef.current.delete(peer_id);

          // Clean up the peer from state
          setPeers(prevPeers => {
            const updated = { ...prevPeers };
            delete updated[peer_id];
            return updated;
          });

          // Close the connection
          try {
            connection.close();
          } catch (e) {
            console.warn(`media-agent - Error closing failed connection:`, e);
          }
        } else if (connection.connectionState === 'disconnected') {
          console.warn(
            `media-agent - addPeer:${peer.peer_name} Connection disconnected for`,
            peer.peer_name,
            '- may recover'
          );

          // Set a timeout for disconnected state recovery
          setTimeout(() => {
            if (
              connection.connectionState === 'disconnected' ||
              connection.connectionState === 'failed'
            ) {
              console.log(
                `media-agent - addPeer:${peer.peer_name} Connection did not recover, cleaning up`
              );
              connectionsRef.current.delete(peer_id);
            }
          }, 10000); // Give 10 seconds for recovery
        } else if (connection.connectionState === 'connected') {
          console.log(
            `media-agent - addPeer:${peer.peer_name} Connection established successfully`
          );
          // Clear any negotiation flags on successful connection
          isNegotiatingRef.current.set(peer_id, false);
          makingOfferRef.current.set(peer_id, false);
        }
      });

      connection.addEventListener('icecandidateerror', (event: Event) => {
        const evt = event as RTCPeerConnectionIceErrorEvent;
        // Try to extract candidate type from the hostCandidate string if present
        const hostCand = (evt as any).hostCandidate || null;
        const parseType = (candStr: string | null) => {
          if (!candStr) return 'unknown';
          const m = /\btyp\s+(host|srflx|relay|prflx)\b/.exec(candStr);
          return m ? m[1] : 'unknown';
        };
        const hostType = parseType(hostCand);
        console.error(
          `media-agent - addPeer:${peer.peer_name} ICE candidate error for ${peer.peer_name}:`,
          {
            evt,
            hostCandidate: hostCand,
            hostType,
            address: (evt as any).address,
            port: (evt as any).port,
            url: (evt as any).url,
          }
        );
      });

      connection.addEventListener('track', event => {
        console.log(
          `media-agent - addPeer:${peer.peer_name} Track event:`,
          event.track.kind,
          'muted:',
          event.track.muted,
          'enabled:',
          event.track.enabled
        );

        // Force unmute check after a delay
        setTimeout(() => {
          console.log(
            `media-agent - addPeer:${peer.peer_name} Track after delay:`,
            event.track.kind,
            'muted:',
            event.track.muted,
            'enabled:',
            event.track.enabled
          );
        }, 1000);
      });

      const _parseCandidateType = (candStr: string | null) => {
        if (!candStr) return 'eoc'; // end of candidates
        const m = /\btyp\s+(host|srflx|relay|prflx)\b/.exec(candStr);
        return m ? m[1] : 'unknown';
      };

      connection.onicecandidate = (event: RTCPeerConnectionIceEvent) => {
        if (!event.candidate) {
          console.log(
            `media-agent - addPeer:${peer.peer_name} ICE gathering complete: ${connection.connectionState}`
          );
          return;
        }

        const raw = event.candidate?.candidate || null;
        const candType = _parseCandidateType(raw);
        console.log(
          `media-agent - addPeer:${peer.peer_name} onicecandidate - type=${candType}`,
          event.candidate
        );

        // Optionally filter host candidates so we prefer TURN relays.
        if (FILTER_HOST_CANDIDATES && candType === 'host') {
          console.log(
            `media-agent - addPeer:${peer.peer_name} onicecandidate - skipping host candidate`
          );
          return;
        }

        // Send candidate to signaling server
        sendJsonMessage({
          type: 'relayICECandidate',
          data: {
            peer_id,
            candidate: event.candidate,
          },
        });
      };

      connection.ontrack = (event: RTCTrackEvent) => {
        console.log(`media-agent - addPeer:${config.peer_name} ontrack event received`, event);

        let stream: MediaStream;
        if (event.streams && event.streams[0]) {
          stream = event.streams[0];
        } else {
          stream = new MediaStream();
          stream.addTrack(event.track);
        }

        console.log(`media-agent - addPeer:${config.peer_name} ontrack - Stream:`, stream);
        console.log(`media-agent - addPeer:${config.peer_name} ontrack - Track:`, event.track);

        // Update peer with stream - use peer_id from closure, not peer object
        setPeers(prevPeers => {
          const updated = { ...prevPeers };
          if (updated[peer_id]) {
            // Don't check connection reference - just update if peer exists
            updated[peer_id].attributes = {
              ...updated[peer_id].attributes,
              srcObject: stream,
            };
            console.log(
              `media-agent - addPeer:${config.peer_name} ontrack - remote ${peer_id} track assigned, srcObject:`,
              stream
            );
          } else {
            console.error(
              `media-agent - addPeer:${config.peer_name} ontrack - peer ${peer_id} not found in state`
            );
          }
          return updated;
        });
      };

      connection.oniceconnectionstatechange = event => {
        console.log(
          `media-agent - addPeer:${peer.peer_name} iceconnectionstatechange - `,
          connection.iceConnectionState,
          event
        );

        if (connection.iceConnectionState === 'failed') {
          console.error(`media-agent - ICE connection failed for ${peer.peer_name}`);
          // Log ICE candidate gathering stats for debugging
          connection
            .getStats()
            .then(stats => {
              const candidateStats: any[] = [];
              stats.forEach(report => {
                if (report.type === 'local-candidate' || report.type === 'remote-candidate') {
                  candidateStats.push({
                    type: report.type,
                    candidateType: (report as any).candidateType,
                    protocol: (report as any).protocol,
                    address: (report as any).address,
                    port: (report as any).port,
                  });
                }
              });
              console.error(
                `media-agent - ICE candidates for failed connection to ${peer.peer_name}:`,
                candidateStats
              );
            })
            .catch(e => console.log('Could not get stats:', e));
        } else if (connection.iceConnectionState === 'disconnected') {
          console.warn(
            `media-agent - ICE connection disconnected for ${peer.peer_name}, may recover`
          );
        } else if (connection.iceConnectionState === 'connected') {
          console.log(
            `media-agent - ICE connection established successfully for ${peer.peer_name}`
          );
        } else if (connection.iceConnectionState === 'checking') {
          console.log(`media-agent - ICE connection checking for ${peer.peer_name}`);
        } else if (connection.iceConnectionState === 'completed') {
          console.log(`media-agent - ICE connection completed for ${peer.peer_name}`);
        }
      };

      // Add local tracks to the connection only if we have media and it's valid
      console.log(
        `media-agent - addPeer:${peer.peer_name} Adding local tracks to new peer connection (localHasMedia=${localUserHasMedia})`
      );
      if (media && localUserHasMedia) {
        media.getTracks().forEach(t => {
          // Check if we should enable/disable the track based on local mute state
          const localPeer = peers[session.id];
          if (localPeer) {
            if (t.kind === 'audio') {
              t.enabled = !localPeer.muted;
              console.log(
                `media-agent - addPeer:${peer.peer_name} Audio track ${t.id} enabled: ${t.enabled} (local muted: ${localPeer.muted})`
              );
            } else if (t.kind === 'video') {
              t.enabled = localPeer.video_on;
              console.log(
                `media-agent - addPeer:${peer.peer_name} Video track ${t.id} enabled: ${t.enabled} (local video_on: ${localPeer.video_on})`
              );
            }
          }

          console.log(`media-agent - addPeer:${peer.peer_name} Adding track:`, {
            kind: t.kind,
            enabled: t.enabled,
            muted: t.muted,
            readyState: t.readyState,
            label: t.label,
            id: t.id,
          });

          // Respect the local user's muted/video state. Do not force-enable or
          // mark pending consent for bots here. The local user's track.enabled
          // state (set above) will be used when adding tracks to the connection.

          // Add the existing track to the connection and rely on the track.enabled
          // value which was set above based on the local peer's muted/video state.
          connection.addTrack(t, media);
        });
      } else if (!localUserHasMedia) {
        console.log(
          `media-agent - addPeer:${peer.peer_name} - Local user has no media, skipping track addition`
        );
      } else {
        console.log(`media-agent - addPeer:${peer.peer_name} - No local media available yet`);
      }

      // Update peers state
      setPeers(updatedPeers);

      // Create offer if needed - with proper locking
      if (config.should_create_offer) {
        console.log(
          `media-agent - addPeer:${peer.peer_name} Creating RTC offer to ${peer.peer_name}`
        );

        try {
          isNegotiatingRef.current.set(peer_id, true);

          const local_description = await connection.createOffer();
          console.log(
            `media-agent - addPeer:${peer.peer_name} Local offer description created`,
            local_description
          );

          await connection.setLocalDescription(local_description);

          sendJsonMessage({
            type: 'relaySessionDescription',
            data: {
              peer_id,
              session_description: local_description,
            },
          });
          console.log(`media-agent - addPeer:${peer.peer_name} Offer sent to ${peer.peer_name}`);
        } catch (err) {
          console.error(
            `media-agent - addPeer:${peer.peer_name} Failed to create/send offer:`,
            err
          );
          isNegotiatingRef.current.set(peer_id, false);
          updatePeerConnectionState(peer_id, connection.connectionState, false);
        } finally {
          // Clear the makingOffer flag after we're done
          makingOfferRef.current.set(peer_id, false);
        }
      }
    },
    [
      peers,
      setPeers,
      media,
      sendJsonMessage,
      updatePeerConnectionState,
      session?.has_media,
      session?.id,
    ]
  );

  // Process queued peers when media becomes available
  useEffect(() => {
    if (media && pendingPeers.length > 0) {
      console.log(`media-agent - Processing ${pendingPeers.length} queued peers`);
      const peersToAdd = [...pendingPeers];
      setPendingPeers([]);
      peersToAdd.forEach(config => addPeer(config));
    }
  }, [media, pendingPeers, addPeer]);

  const sessionDescription = useCallback(
    async (props: SessionDescriptionData) => {
      const { peer_id, peer_name, session_description } = props;
      const peer = peers[peer_id];

      console.log(`media-agent - sessionDescription:${peer_name} - Callback triggered.`, props);

      if (!peer || !peer.connection) {
        console.error(`media-agent - sessionDescription:${peer_name} - No peer for ${peer_id}`);
        return;
      }

      const pc = peer.connection;
      const desc = new RTCSessionDescription(session_description);

      // Check for glare condition (both peers creating offers simultaneously)
      const makingOffer = makingOfferRef.current.get(peer_id) || false;
      const offerCollision =
        desc.type === 'offer' && (makingOffer || pc.signalingState !== 'stable');

      // Use polite peer pattern - we're impolite if we initiated the connection
      const weInitiated = initiatedOfferRef.current.has(peer_id);
      const ignoreOffer = weInitiated && offerCollision;

      if (ignoreOffer) {
        console.log(
          `media-agent - sessionDescription:${peer_name} - Ignoring offer due to collision`
        );
        return;
      }

      console.log(`media-agent - sessionDescription:${peer_name} - `, {
        peer_id,
        type: desc.type,
        signalingState: pc.signalingState,
      });

      try {
        await pc.setRemoteDescription(desc);
        isNegotiatingRef.current.set(peer_id, false); // Negotiation complete
        updatePeerConnectionState(peer_id, pc.connectionState, false);
        console.log(`media-agent - sessionDescription:${peer_name} - Remote description set`);

        // Process any queued ICE candidates
        const pendingCandidates = pendingIceCandidatesRef.current.get(peer_id);
        if (pendingCandidates && pendingCandidates.length > 0) {
          console.log(
            `media-agent - sessionDescription:${peer_name} - Processing ${pendingCandidates.length} queued ICE candidates`
          );

          for (const candidate of pendingCandidates) {
            try {
              if (!candidate.candidate) {
                // End-of-candidates signal
                await pc.addIceCandidate(undefined);
                console.log(
                  `media-agent - sessionDescription:${peer_name} - Queued end-of-candidates added`
                );
              } else {
                // Coerce and sanitize the incoming candidate before handing to the browser
                let candStr: string | null = candidate.candidate ?? null;
                if (typeof candStr === 'string') {
                  candStr = candStr.trim();
                  // Strip leading 'a=' if present (sometimes sent from SDP parsing)
                  if (candStr.startsWith('a=candidate:')) {
                    candStr = candStr.replace(/^a=/, '');
                  }
                  // Ensure the string starts with the expected keyword
                  if (!candStr.startsWith('candidate:')) {
                    candStr = `candidate:${candStr}`;
                  }
                }

                const candidateInit: RTCIceCandidateInit = {
                  candidate: candStr ?? '',
                  sdpMid: candidate.sdpMid ?? undefined,
                  sdpMLineIndex:
                    typeof candidate.sdpMLineIndex === 'number'
                      ? candidate.sdpMLineIndex
                      : undefined,
                };

                try {
                  await pc.addIceCandidate(candidateInit);
                  console.log(
                    `media-agent - sessionDescription:${peer_name} - Queued ICE candidate added`
                  );
                } catch (err) {
                  console.error(
                    `media-agent - sessionDescription:${peer_name} - Failed to add queued ICE candidate:`,
                    {
                      candidateInit,
                      rawCandidate: candidate,
                      err,
                    }
                  );
                }
              }
            } catch (err) {
              console.error(
                `media-agent - sessionDescription:${peer_name} - Failed to add queued ICE candidate:`,
                err
              );
            }
          }

          // Clear the queue
          pendingIceCandidatesRef.current.delete(peer_id);
        }
      } catch (err) {
        console.error(
          `media-agent - sessionDescription:${peer_name} - Failed to set remote description:`,
          err
        );
        return;
      }

      // Create answer for offers
      if (session_description.type === 'offer') {
        console.log(
          `media-agent - sessionDescription:${peer_name} - Creating answer for received offer in state:`,
          pc.signalingState
        );
        try {
          const local_description = await pc.createAnswer();
          await pc.setLocalDescription(local_description);
          sendJsonMessage({
            type: 'relaySessionDescription',
            data: { peer_id, session_description: local_description },
          });
          console.log(`media-agent - sessionDescription:${peer_name} - Answer sent`);
        } catch (err) {
          console.error(
            `media-agent - sessionDescription:${peer_name} - Failed to create/send answer:`,
            err
          );
        }
      }
    },
    [peers, sendJsonMessage, updatePeerConnectionState]
  );

  const removePeer = useCallback(
    (props: RemovePeerData) => {
      const { peer_id, peer_name } = props;
      const peer = peers[peer_id];
      if (!peer) {
        console.error(`media-agent - removePeer:${peer_name} - No peer for ${peer_id}`);
        return;
      }

      console.log(`media-agent - removePeer:${peer_name} - Removing peer.`);

      // Close and remove connection
      const connection = connectionsRef.current.get(peer_id);
      if (connection) {
        console.log(`media-agent - removePeer:${peer_name} - Closing connection.`);
        connection.close();
        connectionsRef.current.delete(peer_id);
      }

      // Clean up negotiation state
      isNegotiatingRef.current.delete(peer_id);
      makingOfferRef.current.delete(peer_id);
      initiatedOfferRef.current.delete(peer_id);
      pendingIceCandidatesRef.current.delete(peer_id);

      // Update peers state
      const updatedPeers = { ...peers };
      if (updatedPeers[peer_id]) {
        updatedPeers[peer_id].dead = true;
        updatedPeers[peer_id].connection = undefined;
      }

      setPeers(updatedPeers);
    },
    [peers, setPeers]
  );

  const iceCandidate = useCallback(
    async (props: IceCandidateData) => {
      const { peer_id, peer_name, candidate } = props;
      const peer = peers[peer_id];
      const parse = (candStr: string | null) => {
        if (!candStr) return 'eoc';
        const m = /\btyp\s+(host|srflx|relay|prflx)\b/.exec(candStr);
        return m ? m[1] : 'unknown';
      };
      console.log(`media-agent - iceCandidate:${peer_name} - `, {
        peer_id,
        candidate,
        peer,
        candidateType: parse(candidate?.candidate || null),
      });

      if (!peer?.connection) {
        console.error(
          `media-agent - iceCandidate:${peer_name} - No peer connection for ${peer_id}`
        );
        return;
      }

      // Check if remote description is set
      if (!peer.connection.remoteDescription) {
        console.log(
          `media-agent - iceCandidate:${peer_name} - Remote description not set yet, queuing ICE candidate`
        );

        // Queue the candidate
        const pending = pendingIceCandidatesRef.current.get(peer_id) || [];
        pending.push(candidate);
        pendingIceCandidatesRef.current.set(peer_id, pending);
        return;
      }

      // Add the ICE candidate
      if (!candidate.candidate) {
        // End-of-candidates signal
        peer.connection
          .addIceCandidate(undefined)
          .then(() =>
            console.log(
              `media-agent - iceCandidate::${peer_name} - End-of-candidates added for ${peer.peer_name}`
            )
          )
          .catch(err =>
            console.error(
              `media-agent - iceCandidate::${peer_name} - Failed to add end-of-candidates:`,
              err
            )
          );
      } else {
        // Sanitize and coerce incoming candidate
        let candStr: string | null = candidate.candidate ?? null;
        if (typeof candStr === 'string') {
          candStr = candStr.trim();
          if (candStr.startsWith('a=candidate:')) {
            candStr = candStr.replace(/^a=/, '');
          }
          if (!candStr.startsWith('candidate:')) {
            candStr = `candidate:${candStr}`;
          }
        }

        const candidateInit: RTCIceCandidateInit = {
          candidate: candStr ?? '',
          sdpMid: candidate.sdpMid ?? undefined,
          sdpMLineIndex:
            typeof candidate.sdpMLineIndex === 'number' ? candidate.sdpMLineIndex : undefined,
        };

        peer.connection
          .addIceCandidate(candidateInit)
          .then(() =>
            console.log(
              `media-agent - iceCandidate::${peer_name} - ICE candidate added for ${peer.peer_name}`
            )
          )
          .catch(err =>
            console.error(
              `media-agent - iceCandidate::${peer_name} - Failed to add ICE candidate:`,
              {
                candidateInit,
                rawCandidate: candidate,
                err,
              }
            )
          );
      }
    },
    [peers]
  );

  const handleWebSocketMessage = useCallback(
    (data: any) => {
      switch (data.type) {
        case 'join_status':
          setJoinStatus({ status: data.status, message: data.message });
          break;
        case 'addPeer':
          addPeer(data.data);
          break;
        case 'removePeer':
          removePeer(data.data);
          break;
        case 'iceCandidate':
          iceCandidate(data.data);
          break;
        case 'sessionDescription':
          sessionDescription(data.data);
          break;
        default:
          break;
      }
    },
    [addPeer, removePeer, iceCandidate, sessionDescription]
  );

  // Effect runs when new message arrives
  useEffect(() => {
    if (!lastJsonMessage || !session) return;

    const msgKey = JSON.stringify(lastJsonMessage);
    if (lastProcessedMessageRef.current === msgKey) {
      // already handled this one
      return;
    }
    lastProcessedMessageRef.current = msgKey;

    handleWebSocketMessage(lastJsonMessage);
  }, [lastJsonMessage, handleWebSocketMessage, session]);

  // Join lobby when media is ready
  useEffect(() => {
    // Only attempt to join once we have local media, an open socket, and a known session name.
    // Joining with a null/empty name can cause the signaling server to treat the peer as anonymous
    // which results in other peers not receiving expected addPeer/track messages.
    if (
      media &&
      joinStatus.status === 'Not joined' &&
      readyState === ReadyState.OPEN &&
      session &&
      session.name
    ) {
      console.log(`media-agent - Initiating media join for ${session.name}`);
      setJoinStatus({ status: 'Joining' });
      sendJsonMessage({
        type: 'join',
        data: {
          has_media: session.has_media !== false, // Default to true for backward compatibility
        },
      });
    }
  }, [media, joinStatus.status, sendJsonMessage, readyState, session.name, session.has_media]);

  // Update local peer in peers list
  useEffect(() => {
    if (!session || !media) return;

    setPeers(prevPeers => {
      const updated = { ...prevPeers };

      // Remove old local peer if session ID changed
      Object.keys(updated).forEach(key => {
        if (updated[key].local && key !== session.id) {
          delete updated[key];
        }
      });

      // Add or update current local peer
      if (!updated[session.id]) {
        // Disable tracks based on initial muted state before assigning to peer
        if (media) {
          media.getAudioTracks().forEach(track => {
            track.enabled = false; // Start muted
            console.log(`media-agent - Local audio track ${track.id} disabled (initial state)`);
          });
          media.getVideoTracks().forEach(track => {
            track.enabled = false; // Start with video off
            console.log(`media-agent - Local video track ${track.id} disabled (initial state)`);
          });
        }

        updated[session.id] = {
          peer_name: session.name || 'Unknown',
          session_id: session.id,
          local: true,
          muted: true,
          video_on: false,
          attributes: {
            local: true,
            srcObject: media,
          },
          dead: false,
        };
        console.log(`media-agent - Added local peer to peers list with tracks disabled`);
      }

      return updated;
    });
  }, [session, media, setPeers]);

  // Setup local media
  const setup_local_media = useCallback(async (): Promise<MediaStream> => {
    console.log(`media-agent - Requesting access to local audio/video`);
    const attempt = { get_audio: true, get_video: true };
    let media = null;

    // Try to get user media with fallback
    while (attempt.get_audio || attempt.get_video) {
      try {
        const constraints: any = {};
        if (attempt.get_audio) constraints.audio = true;
        if (attempt.get_video) constraints.video = true;

        console.log(
          `media-agent - Attempting getUserMedia: audio=${attempt.get_audio}, video=${attempt.get_video}`
        );
        media = await navigator.mediaDevices.getUserMedia(constraints);
        break;
      } catch (error) {
        if (attempt.get_video && attempt.get_audio) {
          console.log(`media-agent - Disabling video, trying audio only`);
          attempt.get_video = false;
        } else if (attempt.get_audio && !attempt.get_video) {
          console.log(`media-agent - Disabling audio, trying video only`);
          attempt.get_video = true;
          attempt.get_audio = false;
        } else {
          console.log(`media-agent - No media available`);
          attempt.get_video = false;
          attempt.get_audio = false;
        }
      }
    }

    // Build tracks array
    const tracks: MediaStreamTrack[] = [];
    let hasRealAudio = false;
    let hasRealVideo = false;

    if (media) {
      const audioTracks = media.getAudioTracks();
      const videoTracks = media.getVideoTracks();

      if (audioTracks.length > 0) {
        const audioTrack = audioTracks[0];
        tracks.push(audioTrack);
        hasRealAudio = true;
        console.log('media-agent - Using real audio:', {
          enabled: audioTrack.enabled,
          muted: audioTrack.muted,
          readyState: audioTrack.readyState,
          label: audioTrack.label,
          id: audioTrack.id,
        });
      }

      if (videoTracks.length > 0) {
        videoTracks[0].applyConstraints({
          width: { min: 160, max: 320 },
          height: { min: 120, max: 240 },
        });
        tracks.push(videoTracks[0]);
        hasRealVideo = true;
        console.log('media-agent - Using real video');
      }
    }

    // Add synthetic tracks if needed
    if (!hasRealAudio) {
      tracks.push(createSilentAudioTrack());
      console.log('media-agent - Using synthetic audio');
    }

    if (!hasRealVideo) {
      tracks.push(createAnimatedVideoTrack({ name: session.name || '' }));
      console.log('media-agent - Using synthetic video');
    }

    const finalMedia = new MediaStream(tracks);
    console.log(`media-agent - Media setup complete:`, {
      totalTracks: finalMedia.getTracks().length,
      audioTracks: finalMedia.getAudioTracks().length,
      videoTracks: finalMedia.getVideoTracks().length,
      hasRealAudio,
      hasRealVideo,
    });
    return finalMedia;
  }, [session.name]);

  // Initialize media once
  useEffect(() => {
    mountedRef.current = true;

    if (mediaStreamRef.current || readyState !== ReadyState.OPEN) return;

    // Capture the connections at effect setup time
    const connectionsToCleanup = connectionsRef.current;

    if (localUserHasMedia) {
      console.log(`media-agent - Setting up local media`);
      setup_local_media().then(mediaStream => {
        if (!mountedRef.current) {
          // Component unmounted, clean up
          mediaStream.getTracks().forEach(track => {
            track.stop();
            if ((track as any).stopAnimation) (track as any).stopAnimation();
            if ((track as any).stopOscillator) (track as any).stopOscillator();
          });
          return;
        }

        mediaStreamRef.current = mediaStream;
        setMedia(mediaStream);
      });
    } else {
      console.log(`media-agent - Local user has no media, creating empty stream`);
      // Create an empty media stream for users without media
      const emptyStream = new MediaStream();
      mediaStreamRef.current = emptyStream;
      setMedia(emptyStream);
    }

    return () => {
      mountedRef.current = false;

      // Clean up media stream
      if (mediaStreamRef.current) {
        mediaStreamRef.current.getTracks().forEach(track => {
          track.stop();
          if ((track as any).stopAnimation) (track as any).stopAnimation();
          if ((track as any).stopOscillator) (track as any).stopOscillator();
        });
        mediaStreamRef.current = null;
      }

      // Clean up connections using the captured ref value
      connectionsToCleanup.forEach(connection => connection.close());
      connectionsToCleanup.clear();
    };
  }, [readyState, setup_local_media, localUserHasMedia]);

  return null;
};

/* ---------- MediaControl (UI per peer) ---------- */

interface MediaControlProps {
  isSelf: boolean;
  peer: Peer;
  className?: string;
  sendJsonMessage?: (msg: any) => void;
  remoteAudioMuted?: boolean;
  remoteVideoOff?: boolean;
  sx?: SxProps<Theme>;
}

const MediaControl: React.FC<MediaControlProps> = (props: MediaControlProps) => {
  const { isSelf, peer, className, sendJsonMessage, remoteAudioMuted, remoteVideoOff, sx } = props;

  const [muted, setMuted] = useState<boolean>(peer?.muted || false);
  const [videoOn, setVideoOn] = useState<boolean>(peer?.video_on !== false);
  const [isValid, setIsValid] = useState<boolean>(false);
  const [frame, setFrame] = useState<{
    translate: [number, number];
    width?: number;
    height?: number;
  }>({ translate: [0, 0] });
  // Remember last released moveable position/size so we can restore to it
  const lastSavedRef = useRef<{
    translate: [number, number];
    width?: number;
    height?: number;
  } | null>(null);
  // Whether the target is currently snapped to the spacer (true) or in a free position (false)
  const [isAttached, setIsAttached] = useState<boolean>(true);
  const containerRef = useRef<HTMLDivElement>(null);
  const targetRef = useRef<HTMLDivElement>(null);
  const spacerRef = useRef<HTMLDivElement>(null);
  const controlsRef = useRef<HTMLDivElement>(null);
  const indicatorsRef = useRef<HTMLDivElement>(null);
  const audioRef = useRef<HTMLAudioElement | null>(null);
  const moveableRef = useRef<any>(null);
  const [isDragging, setIsDragging] = useState<boolean>(false);
  // Controls expansion state for hover/tap compact mode
  const [controlsExpanded, setControlsExpanded] = useState<boolean>(false);
  const touchCollapseTimeoutRef = useRef<number | null>(null);
  // Get sendJsonMessage from props
  // Reset drag state on pointerup/touchend/mouseup anywhere in the document
  useEffect(() => {
    const resetDrag = () => setIsDragging(false);
    document.addEventListener('pointerup', resetDrag);
    document.addEventListener('touchend', resetDrag);
    document.addEventListener('mouseup', resetDrag);
    return () => {
      document.removeEventListener('pointerup', resetDrag);
      document.removeEventListener('touchend', resetDrag);
      document.removeEventListener('mouseup', resetDrag);
    };
  }, []);

  // Double-click toggles between spacer-attached and last saved free position
  const handleDoubleClick = useCallback(
    (e: React.MouseEvent) => {
      e.stopPropagation();
      // Ignore double-clicks on control buttons
      const targetEl = e.target as HTMLElement | null;
      if (targetEl && (targetEl.closest('button') || targetEl.closest('.MuiIconButton-root')))
        return;

      if (!targetRef.current || !spacerRef.current) return;

      // If currently attached to spacer -> restore to last saved moveable position
      if (isAttached) {
        const last = lastSavedRef.current;
        if (last) {
          targetRef.current.style.transform = `translate(${last.translate[0]}px, ${last.translate[1]}px)`;
          if (typeof last.width === 'number') targetRef.current.style.width = `${last.width}px`;
          if (typeof last.height === 'number') targetRef.current.style.height = `${last.height}px`;
          setFrame(last);
          setIsAttached(false);
        }
        return;
      }

      // If not attached -> move back to spacer (origin)
      const spacerRect = spacerRef.current.getBoundingClientRect();
      targetRef.current.style.transform = 'translate(0px, 0px)';
      targetRef.current.style.width = `${spacerRect.width}px`;
      targetRef.current.style.height = `${spacerRect.height}px`;
      setFrame({ translate: [0, 0], width: spacerRect.width, height: spacerRect.height });
      setIsAttached(true);
    },
    [isAttached]
  );

  useEffect(() => {
    console.log(
      `media-agent - MediaControl mounted for peer ${peer?.peer_name}, local=${
        peer?.local
      }, hasSrcObject=${!!peer?.attributes?.srcObject}`
    );
    if (!peer) return;
    console.log(`media-agent - MediaControl peer changed for ${peer.peer_name}, updating state`);
    setMuted(peer.muted);
    setVideoOn(peer.video_on);
  }, [peer]);

  // Initialize size to match spacer
  useEffect(() => {
    if (spacerRef.current && targetRef.current && !frame.width) {
      const spacerRect = spacerRef.current.getBoundingClientRect();
      targetRef.current.style.width = `${spacerRect.width}px`;
      targetRef.current.style.height = `${spacerRect.height}px`;
    }
  }, [frame.width]);

  useEffect(() => {
    if (!peer || peer.dead || !peer.attributes?.srcObject) {
      console.log(
        `media-agent - isValid effect:${peer?.peer_name} - No valid media`,
        peer?.dead,
        peer?.attributes?.srcObject
      );
      setIsValid(false);
      return;
    }

    const stream = peer.attributes.srcObject as MediaStream;

    // For remote peers, check if we have a connected peer connection
    // For local peers, just check if we have live tracks
    if (!peer.local && peer.connection) {
      const isConnected =
        peer.connection.connectionState === 'connected' ||
        peer.connection.connectionState === 'connecting';
      console.log(
        `media-agent - isValid effect:${peer.peer_name} - Remote peer connection state: ${peer.connection.connectionState}`
      );
      setIsValid(isConnected);
    } else {
      const hasLiveTracks = stream.getTracks().some(track => track.readyState === 'live');
      console.log(
        `media-agent - isValid effect:${peer.peer_name} - Local peer has live tracks: ${hasLiveTracks}`
      );
      setIsValid(hasLiveTracks);
    }

    // Listen for connection state changes for remote peers
    if (!peer.local && peer.connection) {
      const handleConnectionChange = () => {
        const isConnected =
          peer.connection?.connectionState === 'connected' ||
          peer.connection?.connectionState === 'connecting';
        console.log(
          `media-agent - isValid effect:${peer.peer_name} - Connection state changed: ${peer.connection?.connectionState}`
        );
        setIsValid(isConnected);
      };

      console.log(
        `media-agent - isValid effect:${peer.peer_name} - Adding connection state listener`
      );
      peer.connection.addEventListener('connectionstatechange', handleConnectionChange);

      return () => {
        peer.connection?.removeEventListener('connectionstatechange', handleConnectionChange);
      };
    }

    // Listen for track state changes
    const handleTrackStateChange = () => {
      if (!peer.local && peer.connection) {
        const isConnected =
          peer.connection.connectionState === 'connected' ||
          peer.connection.connectionState === 'connecting';
        console.log(
          `media-agent - isValid effect:${peer.peer_name} - Track state changed, connection state: ${peer.connection.connectionState}`
        );
        setIsValid(isConnected);
      } else {
        const currentlyLive = stream.getTracks().some(track => track.readyState === 'live');
        console.log(
          `media-agent - isValid effect:${peer.peer_name} - Track state changed, live tracks: ${currentlyLive}`
        );
        setIsValid(currentlyLive);
      }
    };

    stream.getTracks().forEach(track => {
      track.addEventListener('unmute', handleTrackStateChange);
      track.addEventListener('ended', handleTrackStateChange);
    });

    return () => {
      stream.getTracks().forEach(track => {
        track.removeEventListener('unmute', handleTrackStateChange);
        track.removeEventListener('ended', handleTrackStateChange);
      });
    };
  }, [peer]);

  useEffect(() => {
    if (!peer || peer.dead || !peer.attributes?.srcObject) {
      console.log(
        `media-agent - Audio track control: skipping for ${peer?.peer_name} (dead=${
          peer?.dead
        }, hasSrcObject=${!!peer?.attributes?.srcObject})`
      );
      return;
    }

    console.log(
      `media-agent - Audio track control useEffect running for ${peer.peer_name}, muted=${muted}`
    );
    const stream = peer.attributes.srcObject as MediaStream;
    stream.getAudioTracks().forEach(t => {
      const shouldEnable = !muted;
      console.log(
        `media-agent - Setting audio track ${t.id} enabled: ${shouldEnable} (was ${t.enabled})`
      );
      t.enabled = shouldEnable;
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [muted, peer?.attributes?.srcObject, peer?.dead, peer]);

  // Attach remote stream to a hidden <audio> element so audio can be played/unmuted
  useEffect(() => {
    if (!audioRef.current) return;
    const audioEl = audioRef.current;
    if (!peer || peer.dead || !peer.attributes?.srcObject) {
      // Clear srcObject when no media
      try {
        audioEl.pause();
      } catch (e) {
        /* ignore */
      }
      (audioEl as any).srcObject = null;
      return;
    }

    const stream = peer.attributes.srcObject as MediaStream;
    (audioEl as any).srcObject = stream;
    // Ensure audio element muted state matches our UI muted flag (local consumption mute)
    audioEl.muted = !!(peer.local || muted);

    // Try to play - if browser blocks autoplay, this will be allowed when user interacts
    audioEl
      .play()
      .then(() => console.log(`media-agent - audio element playing for ${peer.peer_name}`))
      .catch(err => console.log(`media-agent - audio play blocked for ${peer.peer_name}:`, err));

    return () => {
      try {
        audioEl.pause();
      } catch (e) {
        /* ignore */
      }
      (audioEl as any).srcObject = null;
    };
    // We intentionally depend on muted so the audio element updates when user toggles mute
  }, [peer?.attributes?.srcObject, peer?.dead, peer?.local, muted]);

  useEffect(() => {
    if (!peer || peer.dead || !peer.attributes?.srcObject) return;

    console.log(
      `media-agent - Video track control useEffect running for ${peer.peer_name}, videoOn=${videoOn}`
    );
    const stream = peer.attributes.srcObject as MediaStream;
    stream.getVideoTracks().forEach(t => {
      const shouldEnable = videoOn;
      console.log(
        `media-agent - Setting video track ${t.id} enabled: ${shouldEnable} (was ${t.enabled})`
      );
      t.enabled = shouldEnable;
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [videoOn, peer?.attributes?.srcObject, peer?.dead, peer]);

  const toggleMute = useCallback(
    (e: React.MouseEvent | React.TouchEvent) => {
      e.stopPropagation();
      if (!peer) return;
      const newMutedState = !muted;
      setMuted(newMutedState);
      peer.muted = newMutedState;
      console.log(`media-agent - toggleMute: ${peer.peer_name} muted=${newMutedState}`);
      // Only broadcast if this is the local user (isSelf)
      if (isSelf && sendJsonMessage) {
        sendJsonMessage({
          type: 'peer_state_update',
          data: {
            peer_id: peer.session_id,
            muted: newMutedState,
            video_on: videoOn,
          },
        });
      }

      // If we have an attached audio element (remote or local copy), update it and try to play when unmuting.
      try {
        if (audioRef.current) {
          audioRef.current.muted = !!(peer.local || newMutedState);
          if (!audioRef.current.muted) {
            audioRef.current.play().catch(err => console.log('media-agent - play error:', err));
          } else {
            // When muting, pause to stop consuming resources
            audioRef.current.pause();
          }
        }
      } catch (err) {
        console.warn('media-agent - toggleMute audioRef handling failed:', err);
      }
    },
    [peer, muted, videoOn, sendJsonMessage, isSelf]
  );

  const toggleVideo = useCallback(
    (e: React.MouseEvent | React.TouchEvent) => {
      e.stopPropagation();
      if (!peer) return;
      const newVideoState = !videoOn;
      setVideoOn(newVideoState);
      peer.video_on = newVideoState;
      console.log(`media-agent - toggleVideo: ${peer.peer_name} video_on=${newVideoState}`);
      // Only broadcast if this is the local user (isSelf)
      if (isSelf && sendJsonMessage) {
        sendJsonMessage({
          type: 'peer_state_update',
          data: {
            peer_id: peer.session_id,
            muted: muted,
            video_on: newVideoState,
          },
        });
      }
    },
    [peer, videoOn, muted, sendJsonMessage, isSelf]
  );

  // Snap-back functionality
  const checkSnapBack = (x: number, y: number) => {
    if (!spacerRef.current) return false;

    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const spacerRect = spacerRef.current.getBoundingClientRect();
    const threshold = 50; // pixels from original position to trigger snap

    // Check if close to original position
    const closeToOrigin = Math.abs(x) < threshold && Math.abs(y) < threshold;

    return closeToOrigin;
  };

  if (!peer) return null;

  const colorAudio = isValid ? 'primary' : 'disabled';
  const colorVideo = isValid ? 'primary' : 'disabled';

  return (
    <Box
      sx={{
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        // minWidth: '200px',
        // minHeight: '100px',
        ...sx,
      }}
    >
      <div
        ref={containerRef}
        className="MediaControlContainer"
        style={{
          position: 'relative',
          width: 'max-content',
          height: 'max-content',
        }}
      >
        {/* Drop target spacer */}
        <div
          ref={spacerRef}
          className={`MediaControlSpacer ${className}`}
          style={{
            opacity: isDragging ? 1 : 0.3,
            transition: 'opacity 0.2s',
          }}
          onDoubleClick={handleDoubleClick}
        >
          {isDragging && (
            <Box
              sx={{
                position: 'absolute',
                top: '50%',
                left: '50%',
                transform: 'translate(-50%, -50%)',
                fontSize: '0.7em',
                color: '#888',
                pointerEvents: 'none',
                userSelect: 'none',
              }}
            >
              Drop here
            </Box>
          )}
        </div>

        {/* Moveable element - positioned absolute relative to container */}
        <div
          ref={targetRef}
          className={`MediaControl ${className} ${controlsExpanded ? 'Expanded' : 'Small'}`}
          data-peer={peer.session_id}
          onDoubleClick={handleDoubleClick}
          style={{
            position: 'absolute',
            top: '0px',
            left: '0px',
            width: frame.width ? `${frame.width}px` : undefined,
            height: frame.height ? `${frame.height}px` : undefined,
            transform: `translate(${frame.translate[0]}px, ${frame.translate[1]}px)`,
          }}
          onMouseEnter={() => {
            // Expand controls for mouse
            setControlsExpanded(true);
          }}
          onMouseLeave={e => {
            // Collapse when leaving with mouse, but keep expanded if the pointer
            // moved into the interactive controls or indicators (which are rendered
            // outside the target box to avoid disappearing when target is small).
            const related = (e as React.MouseEvent).relatedTarget as Node | null;
            try {
              if (related && controlsRef.current && controlsRef.current.contains(related)) {
                // Pointer moved into the controls; do not collapse
                return;
              }
              if (related && indicatorsRef.current && indicatorsRef.current.contains(related)) {
                // Pointer moved into the indicators; keep expanded
                return;
              }
            } catch (err) {
              // In some browsers relatedTarget may be null or inaccessible; fall back to collapsing
            }
            setControlsExpanded(false);
          }}
          onTouchStart={e => {
            // Expand on touch; stop propagation so Moveable doesn't interpret as drag start
            setControlsExpanded(true);
            // Prevent immediate drag when the user intends to tap the controls
            e.stopPropagation();
            // Start a collapse timeout for touch devices
            if (touchCollapseTimeoutRef.current) clearTimeout(touchCollapseTimeoutRef.current);
            touchCollapseTimeoutRef.current = window.setTimeout(
              () => setControlsExpanded(false),
              4000
            );
          }}
          onClick={e => {
            // Keep controls expanded while user interacts inside
            setControlsExpanded(true);
            if (touchCollapseTimeoutRef.current) clearTimeout(touchCollapseTimeoutRef.current);
          }}
        >
          {/* Visual indicators: placed inside a clipped container that matches the
              Moveable target size so indicators scale with and are clipped by the target. */}
          <Box
            className="Indicators"
            sx={{ display: 'flex', flexDirection: 'row', color: 'grey', pointerEvents: 'none' }}
            ref={indicatorsRef}
          >
            {isSelf ? (
              <>
                {muted ? <MicOff sx={{ height: '100%' }} /> : <Mic />}
                {videoOn ? <Videocam /> : <VideocamOff />}
              </>
            ) : (
              <>
                {muted ? <VolumeOff /> : <VolumeUp />}
                {videoOn ? <Videocam /> : <VideocamOff />}
              </>
            )}
            {!isSelf && (
              <>
                {remoteAudioMuted && <MicOff color="warning" />}
                {remoteVideoOff && <VideocamOff color="warning" />}
              </>
            )}
          </Box>

          {/* Interactive controls: rendered inside target but referenced separately */}
          <Box
            className="Controls"
            ref={controlsRef}
            sx={{ display: 'flex', flexDirection: 'row', justifyItems: 'center' }}
          >
            <IconButton onClick={toggleMute}>
              {isSelf ? (
                muted ? (
                  <MicOff color={colorAudio} />
                ) : (
                  <Mic color={colorAudio} />
                )
              ) : muted ? (
                <VolumeOff color={colorAudio} />
              ) : (
                <VolumeUp color={colorAudio} />
              )}
            </IconButton>
            <IconButton onClick={toggleVideo}>
              {videoOn ? <Videocam color={colorVideo} /> : <VideocamOff color={colorVideo} />}
            </IconButton>
          </Box>
          {isValid ? (
            peer.attributes?.srcObject && (
              <>
                <Video
                  key={`video-${peer.session_id}-${peer.attributes.srcObject.id}`}
                  className="Video"
                  data-id={peer.peer_name}
                  autoPlay
                  srcObject={peer.attributes.srcObject}
                  local={peer.local}
                  muted={peer.local || muted}
                  onDoubleClick={handleDoubleClick}
                />
                {/* Hidden audio element to ensure audio playback/unmute works reliably */}
                <audio
                  ref={el => {
                    audioRef.current = el;
                  }}
                  style={{ display: 'none' }}
                  // Important: playsInline not standard on audio but keep attributes minimal
                />
                <WebRTCStatus
                  isNegotiating={peer.isNegotiating || false}
                  connectionState={peer.connectionState}
                />
                <WebRTCStatus
                  isNegotiating={peer.isNegotiating || false}
                  connectionState={peer.connectionState}
                />
              </>
            )
          ) : (
            <>
              <div className="Video">Waiting for media…</div>
              <WebRTCStatus
                isNegotiating={peer.isNegotiating || false}
                connectionState={peer.connectionState}
              />
            </>
          )}
        </div>

        <Moveable
          ref={moveableRef}
          flushSync={flushSync}
          container={containerRef.current}
          pinchable={true}
          draggable={true}
          target={targetRef.current}
          resizable={true}
          keepRatio={true}
          hideDefaultLines={false}
          snappable={true}
          snapThreshold={5}
          origin={false}
          edge
          onDragStart={(e: any) => {
            const controls = containerRef.current?.querySelector('.Controls');
            const target = e.inputEvent?.target as HTMLElement;
            if (
              controls &&
              target &&
              (target.closest('button') || target.closest('.MuiIconButton-root'))
            ) {
              if (typeof e.stopDrag === 'function') {
                e.stopDrag();
              }
              return;
            }
            setIsDragging(true);
          }}
          onDrag={e => {
            if (targetRef.current) {
              targetRef.current.style.transform = e.transform;
            }
            const matrix = new DOMMatrix(e.transform);
            const shouldSnap = checkSnapBack(matrix.m41, matrix.m42);
            if (shouldSnap && spacerRef.current) {
              spacerRef.current.style.borderColor = '#0088ff';
            } else if (spacerRef.current) {
              spacerRef.current.style.borderColor = '#666';
            }
          }}
          onDragEnd={e => {
            setIsDragging(false);
            if (targetRef.current) {
              const computedStyle = getComputedStyle(targetRef.current);
              const transform = computedStyle.transform;
              if (transform && transform !== 'none') {
                const matrix = new DOMMatrix(transform);
                const shouldSnap = checkSnapBack(matrix.m41, matrix.m42);
                if (shouldSnap) {
                  targetRef.current.style.transform = 'translate(0px, 0px)';
                  // Snap back to spacer origin
                  setFrame(prev => ({ translate: [0, 0], width: prev.width, height: prev.height }));
                  if (spacerRef.current) {
                    const spacerRect = spacerRef.current.getBoundingClientRect();
                    targetRef.current.style.width = `${spacerRect.width}px`;
                    targetRef.current.style.height = `${spacerRect.height}px`;
                    setFrame({
                      translate: [0, 0],
                      width: spacerRect.width,
                      height: spacerRect.height,
                    });
                  }
                  // Remember that we're attached to spacer
                  setIsAttached(true);
                } else {
                  setFrame({
                    translate: [matrix.m41, matrix.m42],
                    width: frame.width,
                    height: frame.height,
                  });
                  // Save last free position
                  lastSavedRef.current = {
                    translate: [matrix.m41, matrix.m42],
                    width: frame.width,
                    height: frame.height,
                  };
                  setIsAttached(false);
                }
              } else {
                setFrame({ translate: [0, 0], width: frame.width, height: frame.height });
              }
            }
            if (spacerRef.current) {
              spacerRef.current.style.borderColor = '#666';
            }
          }}
          onResizeStart={e => {
            e.setOrigin(['%', '%']);
            setIsDragging(true);
          }}
          onResize={e => {
            e.target.style.width = `${e.width}px`;
            e.target.style.height = `${e.height}px`;
            setFrame({
              ...frame,
              width: e.width,
              height: e.height,
            });
          }}
          onResizeEnd={() => {
            setIsDragging(false);
            // Save last size when user finishes resizing; preserve translate
            if (targetRef.current) {
              const computedStyle = getComputedStyle(targetRef.current);
              const transform = computedStyle.transform;
              let tx = 0,
                ty = 0;
              if (transform && transform !== 'none') {
                const matrix = new DOMMatrix(transform);
                tx = matrix.m41;
                ty = matrix.m42;
              }
              lastSavedRef.current = {
                translate: [tx, ty],
                width: frame.width,
                height: frame.height,
              };
              // If we resized while attached to spacer, consider that we are free
              if (tx !== 0 || ty !== 0) setIsAttached(false);
              else setIsAttached(true);
            }
          }}
        />
      </div>
    </Box>
  );
};

export { MediaControl, MediaAgent };
