import React, { useState, useEffect, useRef, useCallback } from "react";
import Moveable from "react-moveable";
import { flushSync } from "react-dom";
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 useWebSocket, { ReadyState } from "react-use-websocket";
import { Session } from "./GlobalContext";
import WebRTCStatus from "./WebRTCStatus";

const debug = true;
// When true, do not send host candidates to the signaling server. Keeps TURN relays preferred.
const FILTER_HOST_CANDIDATES = true;

/* ---------- 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;
  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 = {
  socketUrl: string;
  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, socketUrl, session } = props;
  const [joinStatus, setJoinStatus] = useState<JoinStatus>({ status: "Not joined" });
  const [media, setMedia] = useState<MediaStream | null>(null);
  const [pendingPeers, setPendingPeers] = useState<AddPeerConfig[]>([]);

  // 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]);

  const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket(socketUrl, {
    share: true,
    shouldReconnect: (closeEvent) => true, // Auto-reconnect on connection loss
    reconnectInterval: 5000,
    onError: (err) => {
      console.error(err);
    },
    onClose: (_event: CloseEvent) => {
      if (!session) return;

      console.log(`media-agent - ${session.name} Disconnected from signaling server`);

      // Clean up all peer connections
      connectionsRef.current.forEach((connection, peerId) => {
        connection.close();
      });
      connectionsRef.current.clear();

      // Mark all peers as dead
      const updatedPeers = { ...peers };
      Object.keys(updatedPeers).forEach((id) => {
        if (!updatedPeers[id].local) {
          updatedPeers[id].dead = true;
          updatedPeers[id].connection = undefined;
        }
      });

      if (debug) console.log(`media-agent - close`, updatedPeers);
      setPeers(updatedPeers);
    },
  });

  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 media not ready
      if (!media) {
        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: "relay",
        iceServers: [
          { urls: "stun:ketrenos.com:3478" },
          {
            urls: "turns:ketrenos.com:5349",
            username: "ketra",
            credential: "ketran",
          },
        ],
      });

      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") {
          setTimeout(() => {
            if (connection.connectionState === "failed") {
              console.log(`media-agent - addPeer:${peer.peer_name} Cleaning up failed connection for`, peer.peer_name);
              connectionsRef.current.delete(peer_id);
              // You might want to trigger a reconnection attempt here
            }
          }, 5000);
        }
      });

      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.log("media-agent - ICE connection failed for", peer.peer_name);
        }
      };

      // Add local tracks
      console.log(`media-agent - addPeer:${peer.peer_name} Adding local tracks to new peer connection`);
      media.getTracks().forEach((t) => {
        console.log(`media-agent - addPeer:${peer.peer_name} Adding track:`, t.kind, t.enabled);
        connection.addTrack(t, media);
      });

      // 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]
  );

  // 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 {
              await pc.addIceCandidate(new RTCIceCandidate(candidate));
              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:`, 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]
  );

  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
      peer.connection
        .addIceCandidate(new RTCIceCandidate(candidate))
        .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:`, err));
    },
    [peers]
  );

  const handleWebSocketMessage = useCallback(
    (data: any) => {
      console.log(`media-agent - WebSocket message received:`, data.type, data.data);

      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(() => {
    if (media && joinStatus.status === "Not joined" && readyState === ReadyState.OPEN) {
      console.log(`media-agent - Initiating media join for ${session.name}`);
      setJoinStatus({ status: "Joining" });
      sendJsonMessage({ type: "join" });
    }
  }, [media, joinStatus.status, sendJsonMessage, readyState, session.name]);

  // 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]) {
        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`);
      }

      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) {
        tracks.push(audioTracks[0]);
        hasRealAudio = true;
        console.log("media-agent - Using real audio");
      }

      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`);
    return finalMedia;
  }, [session.name]);

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

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

    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);
    });

    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
      connectionsRef.current.forEach((connection) => connection.close());
      connectionsRef.current.clear();
    };
  }, [readyState, setup_local_media]);

  return null;
};

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

interface MediaControlProps {
  isSelf: boolean;
  peer: Peer;
  className?: string;
}

const MediaControl: React.FC<MediaControlProps> = ({ isSelf, peer, className }) => {
  const [muted, setMuted] = useState<boolean>(peer?.muted || false);
  const [videoOn, setVideoOn] = useState<boolean>(peer?.video_on !== false);
  const [isValid, setIsValid] = useState<boolean>(false);

  useEffect(() => {
    if (!peer) return;
    setMuted(peer.muted);
    setVideoOn(peer.video_on);
  }, [peer]);

  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, peer.attributes?.srcObject, peer.connection]);

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

    const stream = peer.attributes.srcObject as MediaStream;
    stream.getAudioTracks().forEach((t) => {
      t.enabled = !muted;
    });
  }, [muted, peer]);

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

    const stream = peer.attributes.srcObject as MediaStream;
    stream.getVideoTracks().forEach((t) => {
      t.enabled = videoOn;
    });
  }, [videoOn, peer]);

  const toggleMute = (e: React.MouseEvent | React.TouchEvent) => {
    e.stopPropagation();
    if (peer) {
      peer.muted = !muted;
      setMuted(peer.muted);
    }
  };

  const toggleVideo = (e: React.MouseEvent | React.TouchEvent) => {
    e.stopPropagation();
    if (peer) {
      peer.video_on = !videoOn;
      setVideoOn(peer.video_on);
    }
  };

  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" }}>
      <div className={`MediaControlSpacer ${className}`} />
      <div className={`MediaControl ${className}`} data-peer={peer.session_id}>
        <div className="Controls">
          {isSelf ? (
            <div onTouchStart={toggleMute} onClick={toggleMute}>
              {muted ? <MicOff color={colorAudio} /> : <Mic color={colorAudio} />}
            </div>
          ) : (
            <div onTouchStart={toggleMute} onClick={toggleMute}>
              {muted ? <VolumeOff color={colorAudio} /> : <VolumeUp color={colorAudio} />}
            </div>
          )}
          <div onTouchStart={toggleVideo} onClick={toggleVideo}>
            {videoOn ? <Videocam color={colorVideo} /> : <VideocamOff color={colorVideo} />}
          </div>
        </div>
        {isValid ? (
          peer.attributes?.srcObject && (
            <Box sx={{ position: "relative" }}>
              <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} // Pass muted state
              />
              <WebRTCStatus isNegotiating={peer.isNegotiating || false} connectionState={peer.connectionState} />
            </Box>
          )
        ) : (
          <Box sx={{ position: "relative" }}>
            <div className="Video">Waiting for media…</div>
            <WebRTCStatus isNegotiating={peer.isNegotiating || false} connectionState={peer.connectionState} />
          </Box>
        )}
        {/* <Moveable
        flushSync={flushSync}
        pinchable
        draggable
        target={document.querySelector(`.MediaControl[data-peer="${peer.session_id}"]`) as HTMLElement}
        resizable
        keepRatio
        hideDefaultLines={false}
        edge
        onDragStart={(e) => e.set(frame.translate)}
        onDrag={(e) => {
          if (Array.isArray(e.beforeTranslate) && e.beforeTranslate.length === 2) {
            frame.translate = [e.beforeTranslate[0], e.beforeTranslate[1]];
          }
        }}
        onResizeStart={(e) => {
          e.setOrigin(["%", "%"]);
          e.dragStart && e.dragStart.set(frame.translate);
        }}
        onResize={(e) => {
          const { translate } = frame;
          e.target.style.width = `${e.width}px`;
          e.target.style.height = `${e.height}px`;
          e.target.style.transform = `translate(${translate[0]}px, ${translate[1]}px)`;
        }}
        onRender={(e) => {
          const { translate } = frame;
          e.target.style.transform = `translate(${translate[0]}px, ${translate[1]}px)`;
        }}
      /> */}
      </div>
    </Box>
  );
};

export { MediaControl, MediaAgent };
