/ src/components/VideoCall.js

import React, { useState, useEffect, useRef, useCallback } from 'react';
import { Mic, MicOff, Camera, CameraOff, PhoneOff } from 'lucide-react';
import webSocketManager from '../services/WebSocketManager'; // Adjust the import path
import { useDispatch, useSelector } from 'react-redux';
import { endCall } from '../slices/notificationSlice'; // Adjust import path

const VideoCall = ({
  selectedUser,
  currentUser,
  roomName,
  callType, // 'audio' or 'video'
  isInitiator,
  handleEndCall, // Callback to inform parent components
}) => {
  const dispatch = useDispatch();

  /**
   * Logging Function
   * Accepts a message, a logging level, and additional arguments.
   */
  const log = useCallback((message, level = 'info', ...args) => {
    const validLevels = ['log', 'info', 'warn', 'error', 'debug'];
    const logLevel = validLevels.includes(level) ? level : 'log';
    
    if (process.env.NODE_ENV !== 'production' || level === 'error') {
      if (args.length > 0) {
        console[logLevel]([VideoCall] ${message}, ...args);
      } else {
        console[logLevel]([VideoCall] ${message});
      }
    }
  }, []);

  /**
   * Helper Function to Normalize selectedUser
   * Handles both nested (selectedUser.user) and flat (selectedUser).
   */
  const getUserInfo = () => {
    if (selectedUser) {
      // Handle Nested Structure
      if (selectedUser.user && selectedUser.user.id) {
        const { id, first_name, last_name, fullName } = selectedUser.user;
        return {
          id,
          fullName:
            ${first_name || ''} ${last_name || ''}.trim() ||
            fullName ||
            'Unknown User',
        };
      }
      // Handle Flat Structure
      else if (selectedUser.id) {
        const { id, first_name, last_name, fullName } = selectedUser;
        return {
          id,
          fullName:
            ${first_name || ''} ${last_name || ''}.trim() ||
            fullName ||
            'Unknown User',
        };
      }
    }

    console.error("VideoCall: selectedUser is missing 'id' and 'user' properties.");
    return { id: null, fullName: 'Unknown User' };
  };

  const { id: selectedUserId, fullName: selectedUserFullName } = getUserInfo();

  // Logging for debugging
  log('VideoCall: selectedUser:', 'info', selectedUser);
  log('VideoCall: selectedUserId:', 'info', selectedUserId);
  log('VideoCall: fullName:', 'info', selectedUserFullName);

  const [localStream, setLocalStream] = useState(null);
  const [remoteStream, setRemoteStream] = useState(null);
  const [isMuted, setIsMuted] = useState(false);
  const [isCameraOff, setIsCameraOff] = useState(callType === 'audio');

  const localVideoRef = useRef(null);
  const remoteVideoRef = useRef(null);
  const peerConnectionRef = useRef(null);

  // Track null ICE candidates to avoid multiple sends
  const hasSentNullCandidate = useRef(false);

  // Negotiation flag to prevent overlapping negotiations
  const isNegotiating = useRef(false);

  useEffect(() => {
    if (!selectedUserId) {
      log('Cannot initiate call without selectedUserId.', 'error');
      handleEndCallInternal();
      return;
    }

    log(Initializing peer connection for room: ${roomName}, initiator: ${isInitiator}, 'info');

    initializePeerConnection();
    initializeMedia();

    const messageHandler = (data) => {
      log(Received signaling message: ${JSON.stringify(data)}, 'info');
      if (data.roomName !== roomName) {
        log(Message roomName (${data.roomName}) does not match current room (${roomName}). Ignoring., 'info');
        return;
      }

      // Normalize the message type
      let messageType = data.type;
      if (messageType.endsWith('_event')) {
        messageType = messageType.replace('_event', '');
      } else if (messageType === 'ice_candidate') {
        messageType = 'ice-candidate';
      }

      switch (messageType) {
        case 'offer':
          handleOfferMessage(data.offer);
          break;
        case 'answer':
          handleAnswerMessage(data.answer);
          break;
        case 'ice-candidate':
          handleICECandidateMessage(data.candidate);
          break;
        case 'end-call':
          log('Call ended by peer.', 'info');
          handleEndCallInternal();
          break;
        default:
          log(Unknown message type: ${data.type}, 'warn');
          break;
      }
    };

    webSocketManager.onMessage(messageHandler);
    log('WebSocket message handler attached.', 'info');

    return () => {
      webSocketManager.offMessage(messageHandler);
      log('WebSocket message handler detached.', 'info');
      cleanupCall();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [roomName, log, isInitiator, selectedUserId]);

  const initializePeerConnection = () => {
    const configuration = {
      iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },
        // Consider adding TURN servers for better connectivity
      ],
    };
    const peerConnection = new RTCPeerConnection(configuration);
    peerConnectionRef.current = peerConnection;

    log('Created RTCPeerConnection with configuration:', 'info', configuration);

    // Add transceivers for audio and video to maintain consistent m-line order
    peerConnectionRef.current.addTransceiver('audio', { direction: 'sendrecv' });
    peerConnectionRef.current.addTransceiver('video', { direction: 'sendrecv' });

    log('Added audio and video transceivers.', 'info');

    peerConnection.onicecandidate = (event) => {
      if (event.candidate) {
        // Send the ICE candidate to the server
        log(New ICE candidate found: ${JSON.stringify(event.candidate)}, 'info');
        webSocketManager.sendSignalingMessage(selectedUserId, 'ice-candidate', {
          candidate: event.candidate.toJSON(),
        });
        log('Sent ICE candidate.', 'info');
      } else {
        // ICE gathering completed
        if (!hasSentNullCandidate.current) {
          log('ICE gathering completed. Sending null candidate.', 'info');
          webSocketManager.sendSignalingMessage(selectedUserId, 'ice-candidate', {
            candidate: null,
          });
          hasSentNullCandidate.current = true;
          log('Sent ICE gathering complete.', 'info');
        } else {
          log('ICE gathering complete already sent. Skipping.', 'info');
        }
      }
    };

    peerConnection.ontrack = (event) => {
      log('Received remote track.', 'info');
      if (!remoteStream) {
        log('Setting remote stream.', 'info');
        setRemoteStream(event.streams[0]);
        if (remoteVideoRef.current) {
          remoteVideoRef.current.srcObject = event.streams[0];
        }
      } else {
        log('Remote stream already set.', 'info');
      }
    };

    peerConnection.oniceconnectionstatechange = () => {
      const state = peerConnectionRef.current.iceConnectionState;
      log(ICE connection state changed to: ${state}, 'info');
      if (state === 'disconnected' || state === 'failed' || state === 'closed') {
        log(ICE connection state is ${state}. Ending call., 'info');
        handleEndCallInternal();
      }
    };

    peerConnection.onconnectionstatechange = () => {
      const state = peerConnectionRef.current.connectionState;
      log(Connection state changed to: ${state}, 'info');
      if (state === 'connected') {
        log('Peers connected successfully.', 'info');
      } else if (state === 'disconnected' || state === 'failed' || state === 'closed') {
        log(Connection state is ${state}. Ending call., 'info');
        handleEndCallInternal();
      }
    };

    peerConnection.onnegotiationneeded = async () => {
      log('Negotiation needed.', 'info');
      if (isNegotiating.current) {
        log('Already negotiating. Skipping.', 'info');
        return;
      }
      isNegotiating.current = true;

      try {
        if (isInitiator) {
          await createOffer();
        }
      } catch (error) {
        log(Error during negotiation: ${error.message}, 'error');
      } finally {
        isNegotiating.current = false;
      }
    };

    peerConnection.onicegatheringstatechange = () => {
      const gatheringState = peerConnectionRef.current.iceGatheringState;
      log(ICE gathering state changed to: ${gatheringState}, 'info');
    };
  };

  const initializeMedia = async () => {
    try {
      log('Requesting user media.', 'info');
      // Always request both audio and video
      const stream = await navigator.mediaDevices.getUserMedia({
        audio: true,
        video: true,
      });
      log('User media obtained successfully.', 'info');
      setLocalStream(stream);

      // If callType === 'audio', then disable video tracks initially
      if (callType === 'audio') {
        log('Call type is audio. Disabling video tracks.', 'info');
        stream.getVideoTracks().forEach((track) => (track.enabled = false));
        setIsCameraOff(true);
      }

      if (localVideoRef.current) {
        localVideoRef.current.srcObject = stream;
        log('Local video element set with user media stream.', 'info');
      }

      // Replace the tracks on transceivers instead of adding tracks directly
      const audioSender = peerConnectionRef.current.getSenders().find(
        (s) => s.track && s.track.kind === 'audio'
      );
      if (audioSender && stream.getAudioTracks().length > 0) {
        audioSender.replaceTrack(stream.getAudioTracks()[0]);
        log('Replaced audio track on sender.', 'info');
      }

      const videoSender = peerConnectionRef.current.getSenders().find(
        (s) => s.track && s.track.kind === 'video'
      );
      if (videoSender && stream.getVideoTracks().length > 0) {
        videoSender.replaceTrack(stream.getVideoTracks()[0]);
        log('Replaced video track on sender.', 'info');
      }

      if (isInitiator) {
        log('User is initiator. Creating offer.', 'info');
        await createOffer();
      }
    } catch (error) {
      log(Error accessing media devices: ${error.message}, 'error');
      alert(Error accessing camera/microphone: ${error.message});
      handleEndCallInternal();
    }
  };

  const createOffer = async () => {
    if (peerConnectionRef.current.signalingState !== 'stable') {
      log(Cannot create offer in signaling state: ${peerConnectionRef.current.signalingState}, 'warn');
      return;
    }

    try {
      log('Creating SDP offer.', 'info');
      const offer = await peerConnectionRef.current.createOffer();
      log('SDP offer created:', 'info', offer);
      await peerConnectionRef.current.setLocalDescription(offer);
      log('Set local description with offer.', 'info');
      webSocketManager.sendSignalingMessage(selectedUserId, 'offer', {
        offer: peerConnectionRef.current.localDescription,
      });
      log('Sent SDP offer to signaling server.', 'info');
    } catch (error) {
      log(Error creating offer: ${error.message}, 'error');
    }
  };

  const handleOfferMessage = async (offer) => {
    try {
      log('Handling received SDP offer.', 'info');
      if (peerConnectionRef.current.signalingState !== 'stable') {
        log(Signaling state not stable (${peerConnectionRef.current.signalingState}), rolling back., 'warn');
        await peerConnectionRef.current.setLocalDescription({ type: 'rollback' });
      }
      await peerConnectionRef.current.setRemoteDescription(new RTCSessionDescription(offer));
      log('Set remote description with received offer.', 'info');
      const answer = await peerConnectionRef.current.createAnswer();
      log('SDP answer created:', 'info', answer);
      await peerConnectionRef.current.setLocalDescription(answer);
      log('Set local description with answer.', 'info');
      webSocketManager.sendSignalingMessage(selectedUserId, 'answer', {
        answer: peerConnectionRef.current.localDescription,
      });
      log('Sent SDP answer to signaling server.', 'info');
    } catch (error) {
      log(Error handling offer: ${error.message}, 'error');
      alert(Failed to handle offer: ${error.message});
      handleEndCallInternal();
    }
  };

  const handleAnswerMessage = async (answer) => {
    try {
      log('Handling received SDP answer.', 'info');
      await peerConnectionRef.current.setRemoteDescription(new RTCSessionDescription(answer));
      log('Set remote description with received answer.', 'info');
    } catch (error) {
      log(Error setting remote description: ${error.message}, 'error');
      alert(Failed to set remote description: ${error.message});
      handleEndCallInternal();
    }
  };

  const handleICECandidateMessage = async (candidate) => {
    log(Handling received ICE candidate: ${JSON.stringify(candidate)}, 'info');
    try {
      if (candidate) {
        await peerConnectionRef.current.addIceCandidate(new RTCIceCandidate(candidate));
        log('Added ICE candidate to peer connection.', 'info');
      } else {
        // ICE gathering complete
        log('Received null ICE candidate. ICE gathering completed.', 'info');
      }
    } catch (error) {
      log(Error adding ICE candidate: ${error.message}, 'error');
      alert(Failed to add ICE candidate: ${error.message});
      handleEndCallInternal();
    }
  };

  const toggleMute = async () => {
    if (localStream) {
      const audioTracks = localStream.getAudioTracks();
      if (audioTracks.length > 0) {
        const currentState = audioTracks[0].enabled;
        audioTracks.forEach((track) => (track.enabled = !currentState));
        setIsMuted(!currentState);
        log(Audio ${!currentState ? 'muted' : 'unmuted'}., 'info');

        // Replace the track on the sender
        const audioSender = peerConnectionRef.current.getSenders().find(
          (s) => s.track && s.track.kind === 'audio'
        );
        if (audioSender) {
          if (!currentState) {
            // Muted, replace track with null
            await audioSender.replaceTrack(null);
            log('Audio track replaced with null (muted).', 'info');
          } else {
            // Unmuted, replace track with actual track
            await audioSender.replaceTrack(audioTracks[0]);
            log('Audio track replaced with actual track (unmuted).', 'info');
          }
        }
      }
    }
  };

  const toggleCamera = async () => {
    if (localStream) {
      const videoTracks = localStream.getVideoTracks();
      if (videoTracks.length > 0) {
        const currentState = videoTracks[0].enabled;
        videoTracks.forEach((track) => (track.enabled = !currentState));
        setIsCameraOff(!currentState);
        log(Camera ${!currentState ? 'turned off' : 'turned on'}., 'info');

        // Replace the track on the sender
        const videoSender = peerConnectionRef.current.getSenders().find(
          (s) => s.track && s.track.kind === 'video'
        );
        if (videoSender) {
          if (!currentState) {
            // Camera turned off, replace track with null
            await videoSender.replaceTrack(null);
            log('Video track replaced with null (camera off).', 'info');
          } else {
            // Camera turned on, replace track with actual track
            await videoSender.replaceTrack(videoTracks[0]);
            log('Video track replaced with actual track (camera on).', 'info');
          }
        }
      }
    }
  };

  const cleanupCall = () => {
    log('Cleaning up call resources.', 'info');
    if (localStream) {
      log('Stopping local media tracks.', 'info');
      localStream.getTracks().forEach((track) => track.stop());
    }
    if (remoteStream) {
      log('Stopping remote media tracks.', 'info');
      remoteStream.getTracks().forEach((track) => track.stop());
    }
    if (peerConnectionRef.current) {
      log('Closing peer connection.', 'info');
      peerConnectionRef.current.close();
    }
  };

  /**
   * Internal handler to end the call.
   */
  const handleEndCallInternal = () => {
    log('handleEndCallInternal: Ending call.', 'info');
    cleanupCall();
    // Notify the server to end the call
    webSocketManager.endCall(selectedUserId, roomName);
    log('Sent end-call message to signaling server.', 'info');
    // Dispatch Redux action to update state
    dispatch(endCall({ roomName }));
    log('Dispatched endCall action to Redux.', 'info');
    // Reset null-candidate and negotiation flags
    hasSentNullCandidate.current = false;
    isNegotiating.current = false;

    // Notify parent components (e.g., GlobalCallNotification)
    if (handleEndCall) {
      log('Notifying parent component to handle end call.', 'info');
      handleEndCall();
    }
  };

  return (
    <div className="video-call-container">
      {/* Remote Video */}
      <div className="remote-video-wrapper">
        {remoteStream ? (
          <video
            ref={remoteVideoRef}
            autoPlay
            playsInline
            className="remote-video"
            onPlay={() => log('Remote video started playing.', 'info')}
            onError={(e) => log(Error on remote video element: ${e.message}, 'error')}
          />
        ) : (
          <div className="remote-placeholder">
            <p>Waiting for remote video...</p>
          </div>
        )}
      </div>

      {/* Local Video / "audio only" indicator */}
      <div className={local-video-wrapper ${isCameraOff ? 'audio-call' : ''}}>
        {localStream && (
          <>
            <video
              ref={localVideoRef}
              autoPlay
              playsInline
              muted
              className="local-video"
              style={{ display: isCameraOff ? 'none' : 'block' }}
              onPlay={() => log('Local video started playing.', 'info')}
              onError={(e) => log(Error on local video element: ${e.message}, 'error')}
            />
            {isCameraOff && (
              <div className="audio-indicator">
                <Mic size={32} />
                {log('Displaying audio-only indicator.', 'info')}
              </div>
            )}
          </>
        )}
      </div>

      <div className="controls">
        <button onClick={toggleMute} className="control-button" aria-label="Toggle Mute">
          {isMuted ? <MicOff size={24} /> : <Mic size={24} />}
        </button>
        <button onClick={toggleCamera} className="control-button" aria-label="Toggle Camera">
          {isCameraOff ? <CameraOff size={24} /> : <Camera size={24} />}
        </button>
        <button onClick={handleEndCallInternal} className="control-button end-call" aria-label="End Call">
          <PhoneOff size={24} />
        </button>
      </div>

      {/* Inline CSS for demonstration; ideally, use external CSS files */}
      <style jsx>{
        .video-call-container {
          position: fixed;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          background-color: rgba(0, 0, 0, 0.8);
          display: flex;
          flex-direction: column;
          align-items: center;
          justify-content: center;
          z-index: 1000;
        }
        .remote-video-wrapper {
          position: absolute;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
        }
        .remote-video {
          width: 100%;
          height: 100%;
          object-fit: cover;
        }
        .remote-placeholder {
          width: 100%;
          height: 100%;
          background-color: #333;
          display: flex;
          align-items: center;
          justify-content: center;
          color: #fff;
        }
        .local-video-wrapper {
          position: absolute;
          bottom: 80px;
          right: 20px;
          width: 200px;
          height: 150px;
          border: 2px solid #fff;
          border-radius: 8px;
          overflow: hidden;
          background-color: #000;
          display: flex;
          align-items: center;
          justify-content: center;
        }
        .audio-call {
          width: 150px;
          height: 150px;
          border-radius: 50%;
          background-color: #333;
          display: flex;
          align-items: center;
          justify-content: center;
        }
        .audio-indicator {
          color: #fff;
        }
        .local-video {
          width: 100%;
          height: 100%;
          object-fit: cover;
        }
        .controls {
          position: absolute;
          bottom: 20px;
          display: flex;
          justify-content: center;
          width: 100%;
        }
        .control-button {
          margin: 0 10px;
          padding: 10px;
          background-color: rgba(255, 255, 255, 0.2);
          color: white;
          border: none;
          border-radius: 50%;
          cursor: pointer;
          display: flex;
          align-items: center;
          justify-content: center;
          transition: background-color 0.3s;
        }
        .control-button:hover {
          background-color: rgba(255, 255, 255, 0.3);
        }
        .end-call {
          background-color: rgba(255, 0, 0, 0.6);
        }
        .end-call:hover {
          background-color: rgba(255, 0, 0, 0.8);
        }
      }</style>
    </div>
  );
};

export default VideoCall;// src/services/WebSocketManager.js

import store from "../store"; // Ensure the path is correct
import axios from "axios";
import {
  receiveMessage,
  receiveCallNotification,
  receiveSignalingMessage, // **Import the new action**
  receiveError,
  prependMessages,
  updateUserStatus,
  updateMessageStatus,
  endCall,
  receiveCallAccepted, // Ensure this is imported
} from "../slices/notificationSlice";
import { generateRoomName } from "../utils"; // Ensure generateRoomName is correctly implemented

class WebSocketManager {
  constructor() {
    // Implement Singleton Pattern
    if (WebSocketManager.instance) {
      console.log(
        "WebSocketManager: Constructor called but instance already exists. Returning existing instance."
      );
      return WebSocketManager.instance;
    }

    console.log("WebSocketManager: Creating a new instance...");

    // Initialize properties
    this.socket = null;
    this.isConnected = false;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 5;
    this.reconnectDelay = 3000; // 3 seconds
    this.messageQueue = [];
    this.token = null; // Store token for reconnection
    this.beforeMap = {}; // To track the 'before' timestamp per room
    this.hasMoreMap = {}; // To track if more messages are available per room
    this.messageHandlers = []; // Array of registered message handlers
    this.desiredRooms = new Set(); // Initialize without any rooms

    WebSocketManager.instance = this;
  }

  /**
   * Upload files via HTTP and return their URLs.
   * @param {File[]} files - Array of File objects to upload.
   * @returns {Promise<string[]>} - Array of uploaded file URLs.
   */
  async uploadFiles(files) {
    console.log("WebSocketManager: uploadFiles called with", files);
    const uploadUrl = "/api/upload/"; // Adjust to your upload endpoint
    const formData = new FormData();
    files.forEach((file) => formData.append("files", file));

    try {
      const response = await axios.post(uploadUrl, formData, {
        headers: {
          "Content-Type": "multipart/form-data",
          Authorization: Bearer ${this.token},
        },
      });
      console.log(
        "WebSocketManager: uploadFiles success, server responded with:",
        response.data
      );
      return response.data.fileUrls; // Expecting { fileUrls: [url1, url2, ...] }
    } catch (error) {
      console.error("WebSocketManager: File upload failed:", error);
      throw error;
    }
  }

  /**
   * Fetch all chat rooms the user is part of by generating them based on appointments.
   * @returns {Promise<Array>} - Array of unique room objects with roomName property.
   */
  async fetchUserChatRooms() {
    console.log("WebSocketManager: fetchUserChatRooms() => Function started.");

    try {
      console.log("WebSocketManager: Retrieving application state from store.");
      const state = store.getState();

      // Determine if the user is a patient or a doctor
      let userType = null;
      let currentUserId = null;

      if (state.user?.userData?.patient?.user?.id) {
        userType = "patient";
        currentUserId = state.user.userData.patient.user.id;
        console.log("WebSocketManager: Determined user type as 'patient'.");
      } else if (state.doctor?.doctorData?.doctor?.user?.id) {
        userType = "doctor";
        currentUserId = state.doctor.doctorData.doctor.user.id;
        console.log("WebSocketManager: Determined user type as 'doctor'.");
      } else {
        console.log(
          "WebSocketManager: User type not found in store. Checking localStorage."
        );
        // Attempt to get from localStorage
        const storedUserData = JSON.parse(localStorage.getItem("userData"));
        const storedDoctorData = JSON.parse(localStorage.getItem("doctorData"));

        if (storedUserData?.patient?.user?.id) {
          userType = "patient";
          currentUserId = storedUserData.patient.user.id;
          console.log(
            "WebSocketManager: Determined user type as 'patient' from localStorage."
          );
        } else if (storedDoctorData?.doctor?.user?.id) {
          userType = "doctor";
          currentUserId = storedDoctorData.doctor.user.id;
          console.log(
            "WebSocketManager: Determined user type as 'doctor' from localStorage."
          );
        } else {
          console.error(
            "WebSocketManager: Unable to determine user type from state or localStorage."
          );
          throw new Error("Unable to determine user type.");
        }
      }

      // Ensure token is available
      if (!this.token) {
        console.error(
          "WebSocketManager: fetchUserChatRooms => Authentication token is not available."
        );
        throw new Error("Authentication token is not available.");
      }
      console.log(
        "WebSocketManager: fetchUserChatRooms => Authentication token is available."
      );

      let appointments = [];
      let endpoint = "";

      // Fetch appointments based on user type
      if (userType === "patient") {
        endpoint = "/api/get_patient_appointments/";
        console.log(
          WebSocketManager: fetchUserChatRooms => Fetching patient appointments from '${endpoint}'.
        );
        // Fetch patient appointments
        const response = await axios.get(endpoint, {
          headers: {
            Authorization: Bearer ${this.token},
          },
        });

        console.log(
          "WebSocketManager: fetchUserChatRooms => Patient appointments fetched. API Response:",
          response.data
        );

        if (response.data && Array.isArray(response.data)) {
          appointments = response.data;
          console.log(
            WebSocketManager: Number of patient appointments retrieved: ${appointments.length}.
          );
        } else {
          console.warn(
            "WebSocketManager: No appointments data found for patient."
          );
        }
      } else if (userType === "doctor") {
        endpoint = "/api/doctor/appointments/";
        console.log(
          WebSocketManager: fetchUserChatRooms => Fetching doctor appointments from '${endpoint}'.
        );
        // Fetch doctor appointments
        const response = await axios.get(endpoint, {
          headers: {
            Authorization: Bearer ${this.token},
          },
        });

        console.log(
          "WebSocketManager: fetchUserChatRooms => Doctor appointments fetched. API Response:",
          response.data
        );

        if (response.data && Array.isArray(response.data)) {
          appointments = response.data;
          console.log(
            WebSocketManager: Number of doctor appointments retrieved: ${appointments.length}.
          );
        } else {
          console.warn(
            "WebSocketManager: No appointments data found for doctor."
          );
        }
      }

      // Check if any appointments were fetched
      if (appointments.length === 0) {
        console.warn(
          "WebSocketManager: No appointments available to generate chat rooms."
        );
        return [];
      }

      // Extract unique room names using generateRoomName
      const roomNameSet = new Set();
      console.log(
        "WebSocketManager: Processing appointments to generate room names."
      );

      appointments.forEach((appointment, index) => {
        console.log(
          WebSocketManager: Processing appointment ${index + 1}/${
            appointments.length
          }: ID ${appointment.id}.
        );

        const doctorId = appointment.doctor?.user?.id;
        const patientId = appointment.patient?.user?.id;

        if (doctorId && patientId) {
          const roomName = generateRoomName(doctorId, patientId);
          console.log(
            WebSocketManager: Generated room name '${roomName}' for appointment ID ${appointment.id}.
          );
          roomNameSet.add(roomName);
        } else {
          console.warn(
            WebSocketManager: Missing doctor or patient ID in appointment ID ${appointment.id}. Skipping.
          );
        }
      });

      console.log(
        "WebSocketManager: Unique room names extracted:",
        Array.from(roomNameSet)
      );

      // Create room objects
      const rooms = Array.from(roomNameSet).map((roomName) => ({ roomName }));
      console.log("WebSocketManager: Generated Chat Rooms:", rooms);

      console.log(
        "WebSocketManager: fetchUserChatRooms => Completed successfully."
      );
      return rooms;
    } catch (error) {
      console.error(
        "WebSocketManager: fetchUserChatRooms => Error occurred while fetching user chat rooms:",
        error
      );
      // Dispatch a Redux action to notify the user if needed
      store.dispatch(
        receiveError({
          message: "Failed to fetch chat rooms based on appointments.",
          roomName: null,
        })
      );
      return [];
    }
  }

  /**
   * Establish WebSocket connection using the provided token.
   * @param {string} token - Authentication token.
   */
  async connect(token) {
    console.log("WebSocketManager: connect called with token:", token);

    if (
      this.socket &&
      (this.socket.readyState === WebSocket.OPEN ||
        this.socket.readyState === WebSocket.CONNECTING)
    ) {
      console.warn(
        "WebSocketManager: connect => WebSocket is already connected or connecting."
      );
      return;
    }

    if (!token) {
      console.error(
        "WebSocketManager: connect => Token is required to establish WebSocket connection."
      );
      return;
    }

    this.token = token; // Store token for reconnection

    const wsScheme = window.location.protocol === "https:" ? "wss" : "ws";
    const socketUrl = ${wsScheme}://${
      window.location.host
    }/ws/chat/?token=${encodeURIComponent(token)};

    console.log(
      "WebSocketManager: connect => Attempting to open socket:",
      socketUrl
    );

    this.socket = new WebSocket(socketUrl);

    this.socket.onopen = async () => {
      console.log("WebSocketManager: socket.onopen event fired.");
      await this.handleSocketOpen();
    };

    this.socket.onmessage = (event) => {
      console.log(
        "WebSocketManager: socket.onmessage => Raw event data:",
        event.data
      );
      try {
        const data = JSON.parse(event.data);
        console.log("WebSocketManager: Received WebSocket message:", data);

        // Handle specific message types
        switch (data.type) {
          case "chat_message":
          case "chat_message_event":
            this.handleChatMessage(data);
            break;
          case "previous_messages":
            this.handlePreviousMessages(data);
            break;
          case "call":
          case "call_message_event": // Handle normalized type
            this.handleCallNotification(data);
            break;
          case "offer":
          case "offer_event": // Handle normalized type
            this.handleSignalingMessage(data);
            break;
          case "answer":
          case "answer_event": // Handle normalized type
            this.handleSignalingMessage(data);
            break;
          case "ice-candidate":
          case "ice_candidate_event": // Handle normalized type
            this.handleSignalingMessage(data);
            break;
          case "call-accepted":
          case "call_accepted_event": // Handle normalized type
            this.handleCallAccepted(data); // Corrected handling
            break;
          case "user_status":
            this.handleUserStatus(data);
            break;
          case "message_status":
            this.handleMessageStatus(data);
            break;
          case "error":
            this.handleError(data);
            break;
          case "end-call":
          case "end_call_message_event": // Handle normalized type
            this.handleEndCall(data);
            break;
          default:
            console.warn(
              "WebSocketManager: Unhandled message type:",
              data.type
            );
        }

        // Notify all registered handlers
        this.messageHandlers.forEach((handler) => handler(data));
      } catch (error) {
        console.error(
          "WebSocketManager: Error parsing WebSocket message:",
          error
        );
      }
    };

    this.socket.onclose = (event) => {
      console.log("WebSocketManager: socket.onclose event fired:", event);
      this.socket = null;
      this.isConnected = false;
      if (this.reconnectAttempts < this.maxReconnectAttempts) {
        this.reconnectAttempts++;
        const delay = this.reconnectDelay * this.reconnectAttempts;
        console.log(
          WebSocketManager: Will attempt reconnect #${
            this.reconnectAttempts
          } in ${delay / 1000} seconds...
        );
        setTimeout(() => this.connect(this.token), delay); // Use stored token
      } else {
        console.error(
          "WebSocketManager: Max reconnect attempts reached. Please refresh the page."
        );
        // Dispatch a Redux action to notify the user
        store.dispatch(
          receiveError({
            message:
              "Unable to connect to the server. Please refresh the page.",
            roomName: null,
          })
        );
      }
    };

    this.socket.onerror = (error) => {
      console.error("WebSocketManager: socket.onerror =>", error.message);
      this.safeClose();
      // Dispatch a Redux action to notify the user
      store.dispatch(
        receiveError({
          message: "WebSocket encountered an error. Connection closed.",
          roomName: null,
        })
      );
    };
  }

  /**
   * Handle WebSocket connection open event.
   * Subscribe to all user rooms.
   */
  async handleSocketOpen() {
    console.log("WebSocketManager: handleSocketOpen => Connection is open!");
    this.reconnectAttempts = 0;
    this.isConnected = true;

    // Fetch all chat rooms the user is part of
    console.log(
      "WebSocketManager: handleSocketOpen => Attempting to fetchUserChatRooms..."
    );
    const rooms = await this.fetchUserChatRooms();
    console.log(
      "WebSocketManager: handleSocketOpen => Rooms to subscribe to:",
      rooms
    );

    rooms.forEach((room) => {
      this.subscribe(room.roomName);
    });

    // Subscribe to presence updates or other necessary channels
    this.subscribeToUserStatus();

    // Send any queued messages
    console.log(
      WebSocketManager: handleSocketOpen => Sending any queued messages. Queue length: ${this.messageQueue.length}
    );
    while (this.messageQueue.length > 0) {
      const message = this.messageQueue.shift();
      this.send(message);
    }
    console.log(
      "WebSocketManager: handleSocketOpen => Completed initialization."
    );
  }

  /**
   * Subscribe to a specific room.
   * @param {string} roomName - The name of the room to subscribe to.
   */
  subscribe(roomName) {
    console.log(
      WebSocketManager: subscribe => Called with roomName: ${roomName}
    );
    if (this.desiredRooms.has(roomName)) {
      console.warn("WebSocketManager: Already subscribed to room:", roomName);
      return;
    }

    this.desiredRooms.add(roomName);
    const subscribeMessage = {
      type: "subscribe",
      room: roomName,
    };
    this.send(subscribeMessage);
    console.log("WebSocketManager: Subscribed to room:", roomName);

    // Fetch initial messages upon subscribing
    this.fetchPreviousMessages(roomName, 10);
  }

  /**
   * Unsubscribe from a specific room.
   * @param {string} roomName - The name of the room to unsubscribe from.
   */
  unsubscribe(roomName) {
    console.log(
      WebSocketManager: unsubscribe => Called for roomName: ${roomName}
    );
    if (!this.desiredRooms.has(roomName)) {
      console.warn("WebSocketManager: Not subscribed to room:", roomName);
      return;
    }

    const unsubscribeMessage = {
      type: "unsubscribe",
      room: roomName,
    };
    this.send(unsubscribeMessage);
    console.log("WebSocketManager: Unsubscribed from room:", roomName);

    this.desiredRooms.delete(roomName);

    // Clean up related state
    delete this.beforeMap[roomName];
    delete this.hasMoreMap[roomName];
  }

  /**
   * Register a message handler.
   * @param {function} handler - Function to handle incoming messages.
   */
  onMessage(handler) {
    console.log("WebSocketManager: onMessage => Adding a new message handler.");
    if (typeof handler === "function") {
      this.messageHandlers.push(handler);
    } else {
      console.error("WebSocketManager: Handler must be a function");
    }
  }

  /**
   * Unregister a message handler.
   * @param {function} handler - Function to remove from handlers.
   */
  offMessage(handler) {
    console.log("WebSocketManager: offMessage => Removing a message handler.");
    this.messageHandlers = this.messageHandlers.filter((h) => h !== handler);
  }

  /**
   * Fetch previous messages with pagination.
   * @param {string} roomName - The name of the room.
   * @param {number} limit - Number of messages to fetch.
   */
  fetchPreviousMessages(roomName, limit = 10) {
    console.log(
      WebSocketManager: fetchPreviousMessages => Called for room: ${roomName}, limit: ${limit}
    );
    if (this.hasMoreMap[roomName] === false) {
      console.log(
        "WebSocketManager: fetchPreviousMessages => No more messages to fetch for room:",
        roomName
      );
      return;
    }

    const before = this.beforeMap[roomName] || null;
    const fetchMessagePayload = {
      type: "fetch_previous_messages",
      roomName,
      limit,
    };
    if (before) {
      fetchMessagePayload.before = before;
    }
    console.log(
      "WebSocketManager: fetchPreviousMessages => Sending payload:",
      fetchMessagePayload
    );
    this.send(fetchMessagePayload);
  }

  /**
   * Send a signaling message to the server.
   * @param {number} toUserId - The ID of the user to send the signal to.
   * @param {string} signalType - The type of signal (e.g., 'offer', 'answer', 'ice-candidate').
   * @param {object} payload - Additional data for the signal.
   */
  sendSignalingMessage(toUserId, signalType, payload = {}) {
    const currentUserId = this.getCurrentUserId();
    console.log(
      WebSocketManager: sendSignalingMessage => fromUserId=${currentUserId}, toUserId=${toUserId}, signalType=${signalType}, payload=,
      payload
    );
    if (!currentUserId) {
      console.error(
        "WebSocketManager: Cannot send signaling message without current user ID."
      );
      return;
    }

    const roomName = generateRoomName(currentUserId, toUserId);
    const message = {
      type: signalType,
      to: toUserId,
      roomName,
      ...payload,
    };
    this.send(message);
  }

  /**
   * Handle signaling messages like 'offer', 'answer', 'ice-candidate'.
   * @param {object} data - The signaling message data.
   */
  handleSignalingMessage(data) {
    const { type, from, fromFullName, roomName, ...rest } = data;
    console.log(
      "WebSocketManager: handleSignalingMessage => Received data:",
      data
    );

    if (!from || !fromFullName || !roomName) {
      console.error(
        WebSocketManager: handleSignalingMessage => Missing 'from', 'fromFullName', or 'roomName' in data.
      );
      return;
    }

    console.log(
      WebSocketManager: handleSignalingMessage => Received ${type} from ${fromFullName} (${from}) in room ${roomName},
      rest
    );

    // Dispatch appropriate Redux actions based on type
    switch (type) {
      case "offer":
      case "offer_event":
        // Handle offer
        store.dispatch(
          receiveSignalingMessage({
            signalType: "offer",
            fromId: from,
            roomName,
            offer: rest.offer, // Ensure 'offer' is present
          })
        );
        break;
      case "answer":
      case "answer_event":
        // Handle answer
        store.dispatch(
          receiveSignalingMessage({
            signalType: "answer",
            fromId: from,
            roomName,
            answer: rest.answer, // Ensure 'answer' is present
          })
        );
        break;
      case "ice-candidate":
      case "ice_candidate_event":
        // Handle ICE candidate
        store.dispatch(
          receiveSignalingMessage({
            signalType: "ice-candidate",
            fromId: from,
            roomName,
            candidate: rest.candidate, // Ensure 'candidate' is present
          })
        );
        break;
      default:
        console.warn(
          "WebSocketManager: handleSignalingMessage => Unknown signal type:",
          type
        );
    }
  }

  /**
   * Handle incoming chat messages.
   * Dispatches receiveMessage action to Redux store with isNew=true.
   * @param {object} data - The message data.
   */
  handleChatMessage(data) {
    console.log("WebSocketManager: handleChatMessage => Received data:", data);
    const { roomName, sender, senderFullName, message, files, timestamp, id } =
      data;

    if (!roomName || !sender || !message || !id) {
      console.error("WebSocketManager: Invalid chat_message payload:", data);
      return;
    }

    const formattedFiles = Array.isArray(files)
      ? files.map((file) =>
          typeof file === "string" ? { name: file, url: file } : file
        )
      : [];

    store.dispatch(
      receiveMessage({
        roomName,
        id,
        sender,
        senderFullName,
        content: message,
        files: formattedFiles,
        timestamp: timestamp || new Date().toISOString(),
        isCurrentUser: sender === this.getCurrentUserId(),
        isNew: true, // Mark as new message
      })
    );
  }

  /**
   * Handle incoming call notifications.
   * Dispatches receiveCallNotification action to Redux store.
   * @param {object} data - The call notification data.
   */
  handleCallNotification(data) {
    console.log(
      "WebSocketManager: handleCallNotification => Received data:",
      data
    );
    const { from, fromFullName, callType, roomName, patientId, doctorId } =
      data;

    if (!from || !fromFullName || !callType || !roomName) {
      console.error(
        "WebSocketManager: Invalid call notification payload:",
        data
      );
      return;
    }

    // Prepare the payload with optional patientId or doctorId
    const callNotificationPayload = {
      fromId: from,
      fromFullName,
      callType,
      roomName,
    };

    if (patientId) {
      callNotificationPayload.patientId = patientId;
      console.log(
        WebSocketManager: handleCallNotification => Added patientId: ${patientId}
      );
    }

    if (doctorId) {
      callNotificationPayload.doctorId = doctorId;
      console.log(
        WebSocketManager: handleCallNotification => Added doctorId: ${doctorId}
      );
    }

    store.dispatch(receiveCallNotification(callNotificationPayload));
  }

  /**
   * Handle incoming previous messages.
   * Dispatches prependMessages action to Redux store with isNew=false.
   * @param {object} data - The previous messages data.
   */
  handlePreviousMessages(data) {
    console.log(
      "WebSocketManager: handlePreviousMessages => Received data:",
      data
    );
    const { roomName, messages, hasMore } = data;

    if (!roomName || !Array.isArray(messages)) {
      console.warn(
        "WebSocketManager: Received previous_messages without a valid roomName or messages array."
      );
      return;
    }

    if (messages.length === 0) {
      console.log(
        "WebSocketManager: handlePreviousMessages => No previous messages received for room:",
        roomName
      );
      this.hasMoreMap[roomName] = false;
      return;
    }

    // Reverse messages to have oldest first
    const ascendingMessages = messages.reverse();

    const formattedMessages = ascendingMessages
      .map((msg) => {
        const {
          id,
          sender,
          senderFullName,
          message,
          files,
          timestamp,
          status,
        } = msg;

        if (!id || !sender || !message) {
          console.warn(
            "WebSocketManager: Invalid message format in previous_messages:",
            msg
          );
          return null;
        }

        const formattedFiles = Array.isArray(files)
          ? files.map((file) =>
              typeof file === "string" ? { name: file, url: file } : file
            )
          : [];

        return {
          id,
          roomName,
          sender,
          senderFullName,
          content: message,
          files: formattedFiles,
          timestamp: timestamp || new Date().toISOString(),
          status,
          isCurrentUser: sender === this.getCurrentUserId(),
          isNew: false,
        };
      })
      .filter((msg) => msg !== null);

    store.dispatch(
      prependMessages({
        roomName,
        messages: formattedMessages,
      })
    );

    // Set 'before' to the timestamp of the oldest message in the batch
    const earliestMessage = formattedMessages[0];
    if (earliestMessage) {
      this.beforeMap[roomName] = earliestMessage.timestamp;
    }

    // Update hasMoreMap based on hasMore flag from the server
    this.hasMoreMap[roomName] = hasMore;
    console.log(
      WebSocketManager: handlePreviousMessages => Updated hasMoreMap[${roomName}] = ${hasMore}
    );
  }

  /**
   * Handle incoming user status updates.
   * Dispatches updateUserStatus action to Redux store.
   * @param {object} data - The user status data.
   */
  handleUserStatus(data) {
    console.log("WebSocketManager: handleUserStatus => Received data:", data);
    const { userId, userFullName, status } = data;

    if (!userId || !userFullName || !status) {
      console.error("WebSocketManager: Invalid user_status payload:", data);
      return;
    }

    store.dispatch(
      updateUserStatus({
        userId,
        userFullName,
        status, // 'available', 'busy', or 'offline'
      })
    );
  }

  /**
   * Handle incoming 'end-call' messages.
   * Dispatches endCall action to Redux store.
   * @param {object} data - The 'end-call' message data.
   */
  handleEndCall(data) {
    console.log("WebSocketManager: handleEndCall => Received data:", data);
    const { from, fromFullName, roomName } = data;

    if (!from || !fromFullName || !roomName) {
      console.error(
        "WebSocketManager: handleEndCall => Missing 'from', 'fromFullName', or 'roomName'",
        data
      );
      return;
    }

    console.log(
      WebSocketManager: handleEndCall => Call ended by user ID ${from} (${fromFullName}) in room ${roomName}
    );

    store.dispatch(
      endCall({
        roomName,
        fromUserId: from,
        fromFullName,
      })
    );
  }

  /**
   * Handle incoming message status updates.
   * Dispatches updateMessageStatus action to Redux store.
   * @param {object} data - The message status data.
   */
  handleMessageStatus(data) {
    console.log(
      "WebSocketManager: handleMessageStatus => Received data:",
      data
    );
    const { message_id, status } = data;

    if (!message_id || !status) {
      console.error("WebSocketManager: Invalid message_status payload:", data);
      return;
    }

    store.dispatch(
      updateMessageStatus({
        message_id,
        status,
      })
    );
  }

  /**
   * Handle error messages from the server.
   * Dispatches receiveError action to Redux store.
   * @param {object} data - The error data.
   */
  handleError(data) {
    console.log("WebSocketManager: handleError => Received error data:", data);
    const { message, roomName } = data;

    if (!message) {
      console.error("WebSocketManager: Invalid error payload:", data);
      return;
    }

    store.dispatch(
      receiveError({
        message,
        roomName: roomName || null,
      })
    );
  }

  /**
   * Handle the 'call-accepted' message.
   * Dispatches receiveCallAccepted action to Redux store.
   * @param {object} data - The 'call-accepted' message data.
   */
  handleCallAccepted(data) {
    console.log("WebSocketManager: handleCallAccepted => Call accepted by peer:", data);
    const { from, fromFullName, roomName } = data; // Include fromFullName

    // Dispatch a new Redux action to notify the application
    store.dispatch(
      receiveCallAccepted({
        fromUserId: from,
        fromFullName, // Include fromFullName in the payload
        roomName,
      })
    );

    // Additionally, notify message handlers if necessary
    this.messageHandlers.forEach((handler) => handler(data));
  }

  /**
   * Helper method to get the current user's ID.
   * Retrieves from Redux store or localStorage.
   * @returns {number|null} - The current user's ID.
   */
  getCurrentUserId() {
    const state = store.getState();
    console.log(
      "WebSocketManager: getCurrentUserId => Checking store and localStorage."
    );

    // Check if the user is a patient
    if (state.user?.userData?.patient?.user?.id) {
      return state.user.userData.patient.user.id;
    }

    // Check if the user is a doctor
    if (state.doctor?.doctorData?.doctor?.user?.id) {
      return state.doctor.doctorData.doctor.user.id;
    }

    // Try to get from localStorage
    const storedUserData = JSON.parse(localStorage.getItem("userData"));
    const storedDoctorData = JSON.parse(localStorage.getItem("doctorData"));

    if (storedUserData?.patient?.user?.id) {
      return storedUserData.patient.user.id;
    }

    if (storedDoctorData?.doctor?.user?.id) {
      return storedDoctorData.doctor.user.id;
    }

    console.warn(
      "WebSocketManager: getCurrentUserId => Unable to determine current user ID."
    );
    return null;
  }

  /**
   * Subscribe to user status updates.
   */
  subscribeToUserStatus() {
    console.log("WebSocketManager: subscribeToUserStatus => Called.");
    const presenceRoom = "presence"; // You can keep this as a fixed room or update as needed
    if (!this.desiredRooms.has(presenceRoom)) {
      this.subscribe(presenceRoom);
      console.log(
        "WebSocketManager: subscribeToUserStatus => Subscribed to presence room."
      );
    }
  }

  /**
   * Send presence status update.
   * @param {string} status - 'available', 'busy', or 'offline'.
   */
  sendPresenceStatus(status) {
    console.log(
      "WebSocketManager: sendPresenceStatus => Called with status:",
      status
    );
    const presenceMessage = {
      type: "user_status",
      status, // 'available', 'busy', or 'offline'
    };
    this.send(presenceMessage);
  }

  /**
   * Send a read receipt for a specific message.
   * @param {number} toUserId - The ID of the user who sent the message.
   * @param {string} roomName - The name of the room.
   * @param {number} messageId - The ID of the message being read.
   */
  sendReadReceipt(toUserId, roomName, messageId) {
    console.log(
      WebSocketManager: sendReadReceipt => toUserId=${toUserId}, roomName=${roomName}, messageId=${messageId}
    );
    if (!toUserId || !roomName || !messageId) {
      console.error(
        "WebSocketManager: sendReadReceipt => Missing toUserId, roomName, or messageId."
      );
      return;
    }

    const readReceipt = {
      type: "read_receipt",
      to: toUserId,
      roomName,
      message_id: messageId,
    };
    this.send(readReceipt);
  }

  /**
   * End a call with a specific user.
   * @param {number} toUserId - The ID of the user to end the call with.
   * @param {string} roomName - The name of the room.
   */
  endCall(toUserId, roomName) {
    console.log(
      WebSocketManager: endCall => toUserId=${toUserId}, roomName=${roomName}
    );
    if (!toUserId || !roomName) {
      console.error(
        "WebSocketManager: endCall => Missing toUserId or roomName."
      );
      return;
    }

    const endCallPayload = {
      type: "end-call",
      to: toUserId,
      roomName,
    };

    this.send(endCallPayload);
    console.log(
      WebSocketManager: endCall => Sent end-call message to user ID ${toUserId} in room ${roomName}.
    );
  }

  /**
   * Notify the server that we have accepted the call from a specific user.
   * @param {number} toUserId - The user who initiated the call (caller).
   * @param {string} roomName - The room name for this call.
   */
  acceptCall(toUserId, roomName) {
    console.log(
      WebSocketManager: acceptCall => toUserId=${toUserId}, roomName=${roomName}
    );
    if (!toUserId || !roomName) {
      console.error(
        "WebSocketManager: acceptCall => Missing toUserId or roomName."
      );
      return;
    }

    const acceptCallPayload = {
      type: "call-accepted",
      to: toUserId,
      roomName,
    };
    this.send(acceptCallPayload);
    console.log(
      WebSocketManager: acceptCall => Sent 'call-accepted' to user ID ${toUserId} in room ${roomName}.
    );
  }

  /**
   * Send a message through the WebSocket connection.
   * If the connection isn't open, queue the message.
   * @param {object} data - The data to send.
   */
  send(data) {
    console.log("WebSocketManager: send => Attempting to send data:", data);
    if (this.socket && this.socket.readyState === WebSocket.OPEN) {
      try {
        this.socket.send(JSON.stringify(data));
        console.log("WebSocketManager: Message sent successfully.");
      } catch (error) {
        console.error("WebSocketManager: Error sending message:", error);
        this.messageQueue.push(data); // Re-queue if sending fails
      }
    } else {
      console.warn(
        "WebSocketManager: send => WebSocket is not open. Queueing message:",
        data
      );
      this.messageQueue.push(data);
    }
  }

  /**
   * Safely close the WebSocket connection.
   */
  safeClose() {
    console.log(
      "WebSocketManager: safeClose => Closing socket if not already closed."
    );
    if (this.socket && this.socket.readyState !== WebSocket.CLOSED) {
      this.socket.close();
      this.socket = null;
      this.isConnected = false;
    }
  }

  /**
   * Close the WebSocket connection and clear internal state.
   */
  close() {
    console.log(
      "WebSocketManager: close => Clearing WebSocket state and internal data."
    );
    this.safeClose();
    this.desiredRooms.clear();
    this.messageQueue = [];
    this.token = null;
    this.reconnectAttempts = 0;
    this.messageHandlers = [];
    this.beforeMap = {};
    this.hasMoreMap = {};
  }

  /**
   * Get the current WebSocket connection state.
   * @returns {number|null} - The WebSocket readyState or null if not connected.
   */
  getState() {
    console.log(
      "WebSocketManager: getState => returning socket.readyState or null."
    );
    return this.socket ? this.socket.readyState : null;
  }

  /**
   * Send a chat message to a specific user.
   * @param {number} toUserId - The ID of the user to send the message to.
   * @param {string} message - The message content.
   * @param {File[]} files - Optional array of files to send.
   * @returns {Promise<void>}
   */
  async sendMessage(toUserId, message, files = []) {
    console.log(
      "WebSocketManager: sendMessage => Called with toUserId:",
      toUserId,
      "message:",
      message,
      "files:",
      files
    );
    if (!toUserId || !message) {
      console.error(
        "WebSocketManager: sendMessage => Missing toUserId or message."
      );
      return;
    }

    // Upload files if any
    let fileUrls = [];
    if (files.length > 0) {
      try {
        fileUrls = await this.uploadFiles(files);
        console.log("WebSocketManager: sendMessage => fileUrls:", fileUrls);
      } catch (error) {
        console.error("WebSocketManager: Failed to upload files:", error);
        store.dispatch(
          receiveError({
            message: "Failed to upload files. Message not sent.",
            roomName: generateRoomName(this.getCurrentUserId(), toUserId),
          })
        );
        return;
      }
    }

    const roomName = generateRoomName(this.getCurrentUserId(), toUserId);
    console.log(
      WebSocketManager: sendMessage => Sending chat_message to room: ${roomName}
    );

    const messagePayload = {
      type: "chat_message",
      roomName,
      to: toUserId,
      message,
      files: fileUrls,
    };

    this.send(messagePayload);
  }

  /**
   * Initiate a call with a specific user.
   * @param {number} toUserId - The ID of the user to call.
   * @param {string} callType - 'audio' or 'video'.
   * @returns {Promise<void>}
   */
  async initiateCall(toUserId, callType = "audio") {
    console.log(
      WebSocketManager: initiateCall => toUserId=${toUserId}, callType=${callType}
    );
    if (!toUserId || !callType) {
      console.error(
        "WebSocketManager: initiateCall => Missing toUserId or callType."
      );
      return;
    }

    const roomName = generateRoomName(this.getCurrentUserId(), toUserId);
    console.log(
      WebSocketManager: initiateCall => Using roomName: ${roomName}
    );

    const callPayload = {
      type: "call",
      roomName,
      to: toUserId,
      call_type: callType,
    };

    this.send(callPayload);
  }

  /**
   * Generate a unique ID for messages.
   * @returns {string} - A unique identifier.
   */
  generateUniqueId() {
    const uniqueId = ${Date.now()}-${Math.random().toString(36).substr(2, 9)};
    console.log("WebSocketManager: generateUniqueId => Generated:", uniqueId);
    return uniqueId;
  }
}

// Instantiate and export a single WebSocketManager instance
const webSocketManager = new WebSocketManager();
export default webSocketManager;
hey offer and ice-candidate are handled properly here but the sdp answers are not sent properly