import useAxios from "axios-hooks";
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
import sound from "./new-notification-7-210334.mp3";
import { cookieGrabber, useCookiePolling } from "../utils/cookiePolling";

/**
 * Represents the details of the last message in a chat room overview.
 * @typedef {object} LastMessage
 * @property {string} id - The unique identifier of the message.
 * @property {string} text - The text content of the message.
 * @property {object} media - Media attachments associated with the message (structure might vary based on actual usage).
 * @property {string} created_at - ISO 8601 timestamp string of when the message was created.
 */

/**
 * Represents the profile picture details associated with a chat room overview.
 * @typedef {object} ProfilePicture
 * @property {string} url - The URL of the profile picture.
 * @property {string} mimetype - The MIME type of the picture (e.g., 'image/png').
 */

/**
 * Represents an individual chat room within a course grouping (overview).
 * @typedef {object} ChatRoom
 * @property {string} id - The unique identifier for the chat room (UUID string).
 * @property {string} account_id - The ID of the associated account (likely user or instructor).
 * @property {string} profile_name - The display name for the chat room or the other participant.
 * @property {string} account_type - The type of the associated account (e.g., 'INSTRUCTOR', 'STUDENT').
 * @property {boolean} is_private - Indicates if the chat room is private (e.g., a direct message).
 * @property {boolean} is_online - Indicates the online status associated with the room/participant.
 * @property {boolean} pin_status - Indicates if this specific chat room is pinned by the current user.
 * @property {string} created_at - ISO 8601 timestamp string of when the room was created.
 * @property {number} unread_count - The number of unread messages in this specific room for the current user.
 * @property {LastMessage | null} last_message - The last message object sent in the room, or null if no messages exist.
 * @property {ProfilePicture | null} profile_picture - The profile picture object associated with the room/participant, or null if none exists.
 */

/**
 * Represents a grouping of chat rooms associated with a specific course.
 * Corresponds to an element in the 'results' array from the API for room lists.
 * @typedef {object} CourseChatGrouping
 * @property {number} id - The unique identifier for the course grouping (likely the course ID).
 * @property {string} name - The name of the course.
 * @property {boolean} pin_status - Indicates if the overall course grouping is pinned by the current user.
 * @property {number} total_unread_count - The aggregated total number of unread messages across all rooms listed in this grouping for the current user.
 * @property {ChatRoom[]} rooms - An array of individual chat rooms associated with this course.
 */

/**
 * Information about the sender of a message within chat history.
 * @typedef {object} SenderInfo
 * @property {string} id - The unique identifier of the sender.
 * @property {string} first_name - The first name of the sender.
 */

/**
 * Detailed information about a media attachment within chat history.
 * @typedef {object} MediaDetails
 * @property {string} id - Unique identifier for the media file.
 * @property {string} url - URL to access the media file stream.
 * @property {string} name - The original filename of the media.
 * @property {number} size - The size of the media file in bytes.
 * @property {string} duration - The duration of the media (e.g., "HH:MM:SS" format).
 * @property {string} mimetype - The MIME type of the media file (e.g., 'audio/x-wav').
 * @property {string | null} last_used - ISO 8601 timestamp indicating the last usage, or null.
 * @property {string} created_at - ISO 8601 timestamp string of when the media was uploaded/created.
 * @property {string} updated_at - ISO 8601 timestamp string of the last update to the media record.
 * @property {number} course_count - Count related to course usage.
 * @property {string | null} thumbnail_url - URL for a thumbnail image, or null.
 * @property {string} convertion_status - Status of any media conversion process.
 */

/**
 * Represents a single chat message within chat history.
 * @typedef {object} ChatMessage
 * @property {string} id - The unique identifier for the message (UUID string).
 * @property {string | null} text - The text content of the message. Null if only media.
 * @property {MediaDetails | object | null} media - Media details, an empty object, or null.
 * @property {SenderInfo} sender - Information about the message sender.
 * @property {boolean} is_read - Read status flag.
 * @property {boolean} is_delivered - Delivery status flag.
 * @property {string} created_at - ISO 8601 timestamp string when the message was created.
 * @property {boolean} user_message - True if sent by the current user, false otherwise.
 * @property {string} date - Specific date (YYYY-MM-DD format) message was sent.
 */

/**
 * Represents a grouping of messages sent on a specific date within chat history.
 * @typedef {object} MessagesByDate
 * @property {string} date - Display string for the date group (e.g., "today", "Apr 04").
 * @property {ChatMessage[]} messages - Array of message objects for this date group.
 */

/**
 * Represents the chat history structure for a single room, used as values in the state.chats object.
 * @typedef {object} RoomChatHistory
 * @property {MessagesByDate[]} results - An array containing messages grouped by date for the specific room.
 */

/**
 * Represents the expected structure of the API response data payload
 * containing messages grouped by date.
 * @typedef {object} MessageApiResponseData
 * @property {number} count - Total number of items available across all pages (if pagination applies to items, not groups).
 * @property {string | null} next - URL for the next page of results (date groups), or null.
 * @property {string | null} previous - URL for the previous page of results (date groups), or null.
 * @property {MessagesByDate[]} results - Array containing messages grouped by date.
 */

/**
 * Represents the profile picture details within the sender object of a sent message response.
 * Note: This differs slightly from the ProfilePicture type used in room overviews.
 * @typedef {object} SenderProfilePicture
 * @property {string} id - Unique identifier for the profile picture file.
 * @property {string} url - URL to access the profile picture.
 * @property {string} name - The original filename of the picture.
 * @property {number} size - The size of the picture file in bytes.
 * @property {string} duration - Duration associated with the media (likely "00:00:00" for images).
 * @property {string} mimetype - The MIME type of the picture file (e.g., 'image/png').
 * @property {string | null} last_used - ISO 8601 timestamp indicating the last usage, or null.
 * @property {string} created_at - ISO 8601 timestamp string of when the picture was uploaded/created.
 * @property {string} updated_at - ISO 8601 timestamp string of the last update to the picture record.
 * @property {number} course_count - Count related to course usage.
 * @property {string | null} thumbnail_url - URL for a thumbnail image, or null.
 * @property {string} convertion_status - Status of any media conversion process (e.g., 'DEFAULT').
 */

/**
 * Represents the media attachment details within a sent message response.
 * Note: This differs slightly from the MediaDetails type used in chat history.
 * @typedef {object} SentMessageMedia
 * @property {string} id - Unique identifier for the media file.
 * @property {string} name - The original filename of the media.
 * @property {string} url - Relative URL path to access the media file stream.
 * @property {number} size - The size of the media file (unit might vary, e.g., KB or bytes).
 * @property {string | null} thumbnail_url - URL for a thumbnail image, or null.
 * @property {string} mimetype - The MIME type of the media file (e.g., 'video/mp4').
 * @property {string} created_at - ISO 8601 timestamp string of when the media was uploaded/created.
 * @property {string} updated_at - ISO 8601 timestamp string of the last update to the media record.
 */

/**
 * Represents the sender details within a sent message response.
 * @typedef {object} SentMessageSender
 * @property {string} id - The unique identifier of the sender.
 * @property {string} name - The name of the sender (might be same as full_name).
 * @property {string} full_name - The full name of the sender.
 * @property {object} cover_photo - Cover photo details (currently an empty object in the example).
 * @property {SenderProfilePicture} profile_picture - Detailed profile picture information for the sender.
 * @property {string | null} bio - The sender's biography text, or null.
 */

/**
 * Represents the structure of the API response after successfully sending a message.
 * @typedef {object} SentMessageResponse
 * @property {string} id - The unique identifier for the newly sent message (UUID string).
 * @property {boolean} user_message - Indicates if the message was sent by the current user (should be true in this context).
 * @property {SentMessageSender} sender - Detailed information about the message sender.
 * @property {string} room - The unique identifier of the chat room the message belongs to.
 * @property {string} text - The text content of the sent message.
 * @property {SentMessageMedia | null} media - Details of the attached media, or null if none.
 * @property {boolean} is_read - Read status flag (likely false initially).
 * @property {boolean} is_delivered - Delivery status flag (likely false initially, updated later).
 * @property {string} status - The status of the message (e.g., 'active').
 * @property {string} created_at - ISO 8601 timestamp string when the message was created on the server.
 */

/**
 * Represents the profile picture details within the sender object of a WebSocket message event.
 * Note: This structure might be simpler than SenderProfilePicture.
 * @typedef {object} WebSocketSenderProfilePicture
 * @property {string} url - URL to access the profile picture.
 * @property {string} mimetype - The MIME type of the picture file (e.g., 'image/png').
 */

/**
 * Represents the sender details within a WebSocket message event data.
 * @typedef {object} WebSocketSender
 * @property {string} id - The unique identifier of the sender.
 * @property {string} name - The name of the sender.
 * @property {string} full_name - The full name of the sender.
 * @property {object} cover_photo - Cover photo details (currently an empty object in the example).
 * @property {WebSocketSenderProfilePicture} profile_picture - Profile picture information for the sender.
 * @property {string | null} bio - The sender's biography text, or null.
 */

/**
 * Represents the media attachment details within a WebSocket message event data.
 * Note: This is an empty object in the example, structure might vary.
 * @typedef {object | SentMessageMedia | null} WebSocketMedia - Can be an empty object, structured media details, or null.
 */

/**
 * Represents the data payload of a WebSocket message event (e.g., 'new.message.result').
 * @typedef {object} WebSocketMessageData
 * @property {string} id - The unique identifier for the message (UUID string).
 * @property {boolean} user_message - Indicates if the message was sent by the *currently authenticated* user.
 * @property {WebSocketSender} sender - Detailed information about the message sender.
 * @property {string} room - The unique identifier of the chat room the message belongs to.
 * @property {string | null} text - The text content of the message, or null if only media.
 * @property {WebSocketMedia} media - Details of the attached media (can be empty object or structured).
 * @property {boolean} is_read - Read status flag.
 * @property {boolean} is_delivered - Delivery status flag.
 * @property {string} status - The status of the message (e.g., 'active').
 * @property {string} created_at - ISO 8601 timestamp string when the message was created.
 */

/**
 * Represents the overall structure of a WebSocket message event.
 * @typedef {object} WebSocketMessageEvent
 * @property {string} event - The type of the event (e.g., 'new.message.result').
 * @property {WebSocketMessageData} data - The payload containing the message details.
 */

/**
 * Represents an optimistic message before server confirmation.
 * Minimal version without sender info stored directly.
 * @typedef {object} OptimisticMessage
 * @property {string} tempId - Client-generated unique temporary ID (e.g., UUID v4).
 * @property {'pending' | 'failed'} status - The current status of the optimistic message.
 * @property {string} courseId - The course ID associated with the message. Used for grouping pending.
 * @property {string} accountId - The target account ID associated with the message. Used for grouping pending.
 * @property {string | null} text - Message text.
 * @property {any | null} media - Media object/data sent by the user (structure might vary).
 * @property {string} created_at - Client-side ISO 8601 timestamp string when the message was initiated.
 * @property {boolean} user_message - Always true for optimistic messages.
 * @property {any | null} error - Stores error info if status is 'failed'.
 */

/**
 * Represents an item waiting in the message sending queue.
 * @typedef {object} QueueItem
 * @property {string} tempId - The temporary ID linking to the optimistic message.
 * @property {string} courseId - The course ID for the API call.
 * @property {string} accountId - The account ID for the API call.
 * @property {string | null} text - The message text content.
 * @property {any | null} media - The media data (e.g., File object, FormData details). Needs proper handling before API call.
 */

const useMessageKit = () => {
  const selectedAccountId = useCookiePolling("defaultAccountID");
  const defaultAccountType = useCookiePolling("defaultAccountType");
  const selectedAccount = {
    id: selectedAccountId,
    type: defaultAccountType,
  };
  const token = useCookiePolling("access");
  const buildSocketUrl = useCallback(() => {
    const baseUrl =
      window.location.hostname.includes("staging") ||
      window.location.hostname.includes("localhost")
        ? "https://dev-117782726-api.learngual.com"
        : "https://api.learngual.com";

    return (
      baseUrl.replace("http", "ws") +
      `/notify/v1/ws/connect/?authorization=${token}&_account=${selectedAccount.id}`
    );
  }, []);

  const affiliateAccount = useCookiePolling("affiliateAccount", 1000);
  // console.log(affiliateAccount, affiliatesActive, "affiliate");
  // get account type
  const [, request] = useAxios(
    {
      method: "GET",
      params: {
        _account: selectedAccount && selectedAccount.id,
      },
    },
    {
      manual: true,
    },
  );

  //get account type

  // initialize message store

  /**
   * React state hook managing application chat data.
   *
   * The `state` object holds chat rooms grouped by courses (`roomsByCourses`)
   * and the detailed message history for individual chat rooms (`chats`).
   * The `setState` function is used to update this state.
   *
   * @type {[
   * { roomsByCourses: CourseChatGrouping[], chats: Record<string, RoomChatHistory> },
   * React.Dispatch<React.SetStateAction<{ roomsByCourses: CourseChatGrouping[], chats: Record<string, RoomChatHistory> }>>
   * ]} stateTuple - A tuple containing the state object and the dispatcher function. Note the type for `chats`.
   * @property {CourseChatGrouping[]} stateTuple[0].roomsByCourses - Chat rooms grouped by their associated course. Initialized as empty array.
   * @property {Record<string, RoomChatHistory>} stateTuple[0].chats - An object mapping Room IDs (string keys) to their respective chat history (`RoomChatHistory` objects).
   * Example structure: { "roomId123": { results: [...] }, "roomId456": { results: [...] } }
   */

  // initialize message store
  const [state, setState] = useState({
    roomsByCourses: [],
    chats: {}, // Maps roomId -> RoomChatHistory (MessagesByDate[])
    loadingRoomIds: new Set(),
    isLoadingRooms: false,
    hasLoadedRooms: false,
    /**
     * Stores optimistic messages sent by the user but not yet confirmed.
     * Key: `pending_${courseId}_${accountId}`
     * Value: OptimisticMessage[]
     * @type {Record<string, OptimisticMessage[]>}
     */
    optimisticMessages: {},
    /**
     * Queue of messages waiting to be sent via API.
     * @type {QueueItem[]}
     */
    messageQueue: [],
    /**
     * Flag indicating if the queue processor is currently sending a message.
     * @type {boolean}
     */
    isProcessingQueue: false,
  });

  // Ref for synchronous queue processing lock
  const isCurrentlyProcessingRef = useRef(false);
  const processingMessageIdsRef = useRef(new Set());

  // State for rooms that should be automatically marked as read
  const [autoReadRoomIds, setAutoReadRoomIds] = useState(() => new Set());

  // Ref to access the latest autoReadRoomIds within the async WebSocket handler
  const autoReadRoomIdsRef = useRef(autoReadRoomIds);
  useEffect(() => {
    autoReadRoomIdsRef.current = autoReadRoomIds;
  }, [autoReadRoomIds]);

  useEffect(() => {
    const queuedTempIds = new Set(
      state.messageQueue.map((message) => message.tempId),
    );

    processingMessageIdsRef.current.forEach((tempId) => {
      if (!queuedTempIds.has(tempId)) {
        processingMessageIdsRef.current.delete(tempId);
      }
    });
  }, [state.messageQueue]);

  // Memoize the notification sound object
  const notificationSound = useMemo(() => {
    if (typeof window !== "undefined" && typeof Audio !== "undefined") {
      try {
        return new Audio(sound);
      } catch (e) {
        console.error("Error creating notification Audio object:", e);
        return null;
      }
    }
    return null;
  }, []); // Empty dependency array ensures it's created only once

  // get room grouped by course list
  const getMessageRoomsByCourses = async (
    nextPage,
    once = false,
    limit = 100,
  ) => {
    if (!selectedAccount) return;
    const isFirstPage = !nextPage;

    if (isFirstPage) {
      setState((prevState) => ({
        ...prevState,
        isLoadingRooms: true,
        hasLoadedRooms: once ? prevState.hasLoadedRooms : false,
      }));
    }

    try {
      // selected account exists
      let response;
      if (String(selectedAccount.type).toLowerCase() === "enterprise") {
        response = await request({
          url: nextPage || "/notify/v1/enterprise/rooms/",
          params: {
            limit,
            _account: selectedAccount.id,
          },
        });
      }

      if (selectedAccount.type.toLowerCase() === "instructor") {
        response = await request({
          url:
            nextPage ||
            `/notify/v1/instructor${
              affiliateAccount ? "/".concat(affiliateAccount) : ""
            }/rooms/`,
          params: {
            limit,
            _account: selectedAccount.id,
          },
        });
      } // incomplete
      if (String(selectedAccount.type).toLowerCase() === "personal") {
        response = await request({
          url: nextPage || "/notify/v1/rooms/",
          params: {
            limit,
            _account: selectedAccount.id,
          },
        });
      } // incomplete
      if (!response?.data) {
        setState((prevState) => ({
          ...prevState,
          isLoadingRooms: false,
          hasLoadedRooms: true,
        }));
        return;
      }
      //found data

      const { results: newCourseGroupings, next } = response.data;
      setState((prevState) => {
        const combined = [...prevState.roomsByCourses, ...newCourseGroupings];
        // Use a Map to filter out duplicates based on the course grouping 'id'
        const uniqueMap = new Map(combined.map((item) => [item.id, item]));
        const uniqueCourseGroupings = Array.from(uniqueMap.values());

        return {
          ...prevState,
          roomsByCourses: uniqueCourseGroupings,
        };
      });
      if (next && !once) {
        // Ensure pagination finishes before the sidebar exposes the final list.
        return await getMessageRoomsByCourses(next);
      }
      setState((prevState) => ({
        ...prevState,
        isLoadingRooms: false,
        hasLoadedRooms: true,
      }));
      return response.data;
    } catch (error) {
      setState((prevState) => ({
        ...prevState,
        isLoadingRooms: false,
        hasLoadedRooms: true,
      }));
      throw error;
    }
  };

  // get individual chats
  const getIndividualChats = async (nextPage, roomId, limit = 1000) => {
    if (!selectedAccount) return;

    // Add roomId to loading state
    setState((prevState) => ({
      ...prevState,
      loadingRoomIds: new Set([...prevState.loadingRoomIds, roomId]),
    }));

    try {
      let response;
      if (String(selectedAccount.type).toLowerCase() === "enterprise") {
        response = await request({
          url: nextPage || "/notify/v1/enterprise/chats/",
          params: {
            _account: selectedAccount.id,
            room_id: roomId,
            limit,
          },
        });
      }

      if (String(selectedAccount.type).toLowerCase() === "instructor") {
        response = await request({
          url:
            nextPage ||
            `/notify/v1/instructor${
              affiliateAccount ? "/".concat(affiliateAccount) : ""
            }/chats/`,
          params: {
            _account: selectedAccount.id,
            room_id: roomId,
            limit,
          },
        });
      }

      if (String(selectedAccount.type).toLowerCase() === "personal") {
        response = await request({
          url: nextPage || `/notify/v1/chats/`,
          params: {
            _account: selectedAccount.id,
            room_id: roomId,
            limit,
          },
        });
      }
      if (!response?.data) return;

      /** @type {MessageApiResponseData} */
      const responseData = response.data;
      const { results: newDateGroups } = responseData;

      // --- Find the latest message ID from the current fetch ---
      let latestMessageIdFromFetch = null;
      if (newDateGroups && newDateGroups.length > 0) {
        const allNewMessages = newDateGroups.flatMap((group) => group.messages);
        if (allNewMessages.length > 0) {
          allNewMessages.sort(
            (a, b) => new Date(b.created_at) - new Date(a.created_at),
          );
          latestMessageIdFromFetch = allNewMessages[0];
        }
      }
      // --- End Find latest message ID ---

      setState((prevState) => {
        // --- Update chats ONLY ---
        // Retrieve existing date groups directly from the array mapped to roomId
        // Count updates are now handled by readRoomMessages
        const existingDateGroups = prevState.chats[roomId] || [];
        const allDateGroups = [...existingDateGroups, ...newDateGroups];

        // Group messages by date and de-duplicate messages within each date using their ID (existing logic)
        const messagesGroupedByDate = {};
        allDateGroups.forEach((dateGroup) => {
          if (!messagesGroupedByDate[dateGroup.date]) {
            // Use a Map for efficient de-duplication by message ID
            messagesGroupedByDate[dateGroup.date] = {
              date: dateGroup.date, // Store the date string
              messagesMap: new Map(),
            };
          }
          dateGroup.messages.forEach((message) => {
            messagesGroupedByDate[dateGroup.date].messagesMap.set(
              message.id,
              message,
            );
          });
        });

        // Reconstruct the final array of date groups with unique messages
        const finalDateGroups = Object.values(messagesGroupedByDate).map(
          (group) => ({
            date: group.date,
            // Convert Map values back to an array and sort messages by creation time
            messages: Array.from(group.messagesMap.values()).sort(
              (a, b) => new Date(a.created_at) - new Date(b.created_at),
            ),
          }),
        );

        // Optional: Sort the date groups themselves if needed (e.g., chronologically)
        // finalDateGroups.sort((a, b) => /* comparison logic based on date */);

        // --- Return combined state update (only updating chats here) ---
        return {
          ...prevState,
          // roomsByCourses is NOT updated here anymore
          chats: {
            ...prevState.chats,
            // Update the structure: Map roomId directly to the array of finalDateGroups
            [roomId]: finalDateGroups,
          },
        };
      });

      // Call readRoomMessages only on the first page fetch, if a latest message ID was found and is its not read
      if (
        !nextPage &&
        latestMessageIdFromFetch?.id && // Optional chaining for safety
        !latestMessageIdFromFetch?.is_read // Optional chaining for safety
      ) {
        // Pass both message ID and room ID to readRoomMessages
        await readRoomMessages({
          latestMessageId: latestMessageIdFromFetch.id,
          roomId: roomId,
        });
      }

      // Only remove from loading state if this is the last page or there was an error
      if (!responseData.next) {
        setState((prevState) => {
          const updatedLoadingRoomIds = new Set(prevState.loadingRoomIds);
          updatedLoadingRoomIds.delete(roomId);
          return {
            ...prevState,
            loadingRoomIds: updatedLoadingRoomIds,
          };
        });
      }

      // Pass roomId correctly in the recursive call
      if (responseData.next) getIndividualChats(responseData.next, roomId);
      return responseData;
    } catch (error) {
      // Remove from loading state in case of error
      setState((prevState) => {
        const updatedLoadingRoomIds = new Set(prevState.loadingRoomIds);
        updatedLoadingRoomIds.delete(roomId);
        return {
          ...prevState,
          loadingRoomIds: updatedLoadingRoomIds,
        };
      });
      throw error; // Re-throw to allow error handling upstream
    }
  };

  // read messages (using the latest message ID passed) and update counts
  const readRoomMessages = async ({ latestMessageId, roomId }) => {
    // 1. Make the API call to mark as read
    try {
      if (selectedAccount.type.toLowerCase() === "enterprise") {
        await request({
          url: `/notify/v1/enterprise/chats/${latestMessageId}/read_message/`,
        });
      }
      if (String(selectedAccount.type).toLowerCase() === "instructor") {
        await request({
          url: `/notify/v1/instructor${
            affiliateAccount ? "/".concat(affiliateAccount) : ""
          }/chats/${latestMessageId}/read_message/`,
        });
      }
      if (String(selectedAccount.type).toLowerCase() === "personal") {
        await request({
          url: `/notify/v1/chats/${latestMessageId}/read_message/`,
        });
      }
      // 2. If API call is successful, update the local state counts
      setState((prevState) => {
        let unreadCountToSubtract = 0;

        const updatedRoomsByCourses = prevState.roomsByCourses.map(
          (courseGroup) => {
            const roomIndex = courseGroup.rooms.findIndex(
              (room) => room.id === roomId,
            );

            if (roomIndex !== -1) {
              const targetRoom = courseGroup.rooms[roomIndex];
              // Store the count *before* resetting, only if it's positive
              unreadCountToSubtract = Math.max(0, targetRoom.unread_count);

              if (unreadCountToSubtract > 0) {
                // Only update if there were unread messages
                const updatedRooms = [
                  ...courseGroup.rooms.slice(0, roomIndex),
                  { ...targetRoom, unread_count: 0 }, // Reset count
                  ...courseGroup.rooms.slice(roomIndex + 1),
                ];

                return {
                  ...courseGroup,
                  total_unread_count: Math.max(
                    0, // Ensure total doesn't go below zero
                    courseGroup.total_unread_count - unreadCountToSubtract,
                  ),
                  rooms: updatedRooms,
                };
              }
            }
            // If room not found or no unread messages, return group unchanged
            return courseGroup;
          },
        );

        // Only return updated state if changes were actually made
        if (unreadCountToSubtract > 0) {
          return {
            ...prevState,
            roomsByCourses: updatedRoomsByCourses,
          };
        } else {
          // No change needed, return previous state
          return prevState;
        }
      });
    } catch (error) {
      console.error("Failed to mark messages as read:", error);
      // Optionally handle the error (e.g., show a notification)
      // We don't update the state counts if the API call fails
    }
  };

  // --- Functions to manage auto-read rooms ---
  const addAutoReadRooms = useCallback((roomIds) => {
    const idsToAdd = Array.isArray(roomIds) ? roomIds : [roomIds];
    setAutoReadRoomIds((prevSet) => {
      const newSet = new Set(prevSet);
      idsToAdd.forEach((id) => id && newSet.add(id)); // Add only valid IDs
      return newSet;
    });
  }, []);

  const removeAutoReadRooms = useCallback((roomIds) => {
    const idsToRemove = Array.isArray(roomIds) ? roomIds : [roomIds];
    setAutoReadRoomIds((prevSet) => {
      const newSet = new Set(prevSet);
      idsToRemove.forEach((id) => id && newSet.delete(id)); // Remove only valid IDs
      return newSet;
    });
  }, []);
  // --- End functions to manage auto-read rooms ---

  // Helper to get YYYY-MM-DD - Useful for potential fallback or other date needs
  const getLocalDateString = (isoTimestamp) => {
    if (!isoTimestamp) return new Date().toISOString().split("T")[0];
    try {
      const date = new Date(isoTimestamp);
      if (isNaN(date.getTime())) throw new Error("Invalid date object");
      const year = date.getFullYear();
      const month = String(date.getMonth() + 1).padStart(2, "0");
      const day = String(date.getDate()).padStart(2, "0");
      return `${year}-${month}-${day}`;
    } catch (e) {
      console.error("Error parsing date:", isoTimestamp, e);
      const today = new Date();
      const year = today.getFullYear();
      const month = String(today.getMonth() + 1).padStart(2, "0");
      const day = String(today.getDate()).padStart(2, "0");
      return `${year}-${month}-${day}`;
    }
  };

  // *** UPDATED getDisplayDateLabel ***
  // Now accepts the full ISO timestamp string
  const getDisplayDateLabel = (createdAtISOString) => {
    if (!createdAtISOString) return "Unknown Date";

    try {
      const messageDate = new Date(createdAtISOString);
      // Ensure the date parsed correctly
      if (isNaN(messageDate.getTime())) {
        console.warn(
          "getDisplayDateLabel: Invalid date parsed from",
          createdAtISOString,
        );
        return createdAtISOString.split("T")[0] || "Invalid Date"; // Basic fallback
      }

      const now = new Date();

      // Get midnight today in the browser's local time
      const todayStart = new Date(
        now.getFullYear(),
        now.getMonth(),
        now.getDate(),
      );
      // Get midnight yesterday in the browser's local time
      const yesterdayStart = new Date(todayStart);
      yesterdayStart.setDate(todayStart.getDate() - 1);
      // Get midnight tomorrow (needed for the upper bound of "today")
      const tomorrowStart = new Date(todayStart);
      tomorrowStart.setDate(todayStart.getDate() + 1);

      // Compare the message's timestamp against local date boundaries
      if (messageDate >= todayStart && messageDate < tomorrowStart) {
        return "Today";
      } else if (messageDate >= yesterdayStart && messageDate < todayStart) {
        return "Yesterday";
      } else {
        // Format other dates using locale-specific representation
        let options = { month: "short", day: "numeric" };
        // Optionally add year if it's not the current year
        if (messageDate.getFullYear() !== now.getFullYear()) {
          options.year = "numeric";
        }
        // 'undefined' uses the browser's default locale
        return messageDate.toLocaleDateString(undefined, options);
      }
    } catch (e) {
      console.error(
        "Error formatting display date label for:",
        createdAtISOString,
        e,
      );
      return createdAtISOString.split("T")[0] || "Error Date"; // Basic fallback
    }
  };

  // *** NEW getSortableTimestampForGroup ***
  // Helper to get a reliable timestamp for sorting date groups
  const getSortableTimestampForGroup = (group) => {
    // Use the timestamp of the first message in the group for sorting
    // Assumes messages within the group are sorted ascending by created_at
    if (group?.messages?.length > 0 && group.messages[0]?.created_at) {
      try {
        const date = new Date(group.messages[0].created_at);
        if (!isNaN(date.getTime())) {
          // Return timestamp representing midnight of that day in local time for group sorting
          const groupDateStart = new Date(
            date.getFullYear(),
            date.getMonth(),
            date.getDate(),
          );
          return groupDateStart.getTime(); // Use positive timestamp
        }
      } catch {
        /* ignore errors, fallback below */
      }
    }
    // Fallback if no valid message/timestamp found
    return 0;
  };

  // This function now adds the message optimistically and places it in the queue.
  const sendMessage = useCallback(
    ({ text, media, courseId, accountId }) => {
      const clientTimestamp = new Date().toISOString();
      const tempId =
        typeof crypto !== "undefined" && crypto.randomUUID
          ? crypto.randomUUID()
          : `temp_${Date.now()}_${Math.random()}`;
      const pendingKey = `pending_${courseId}_${accountId}`;

      // --- Prepare Optimistic Message ---
      const optimisticMessage = {
        tempId,
        status: "pending", // Initially pending
        courseId,
        accountId,
        text,
        media, // Store original media reference for potential UI display needs
        created_at: clientTimestamp,
        user_message: true, // Always true
        error: null,
      };

      // --- Prepare Queue Item ---
      const queueItem = {
        tempId,
        courseId,
        accountId,
        text,
        media, // Pass the media data to the queue item for sending
      };

      // --- Add Optimistic Message AND Queue Item to State ---
      setState((prevState) => {
        // Add optimistic message for UI update
        const currentOptimistic =
          prevState.optimisticMessages[pendingKey] || [];
        const updatedOptimistic = {
          ...prevState.optimisticMessages,
          [pendingKey]: [...currentOptimistic, optimisticMessage],
        };

        // Add item to the sending queue
        const updatedQueue = [...prevState.messageQueue, queueItem];

        return {
          ...prevState,
          optimisticMessages: updatedOptimistic,
          messageQueue: updatedQueue,
        };
      });
      // NOTE: The actual API call is deferred to the queue processor effect.
    },
    [setState],
  ); // Dependency: setState

  // Effect to process the message queue sequentially using a Ref lock
  useEffect(() => {
    // **Synchronous Check + Claim:**
    // Use the ref for an immediate check. If already processing, bail out.
    // Also check if the queue has items.
    if (isCurrentlyProcessingRef.current || state.messageQueue.length === 0) {
      return;
    }

    // Get the next message details from the front of the queue.
    // Reading state here is fine, as we've already claimed the slot.
    const messageToSend = state.messageQueue[0];

    // Safety check: Ensure messageToSend exists (queue might have emptied)
    if (!messageToSend) {
      setState((prevState) => ({ ...prevState, isProcessingQueue: false }));
      console.warn(
        "Queue Processor: Attempted to process but queue was empty.",
      );
      return; // Nothing to process
    }

    const { tempId, courseId, accountId, text, media } = messageToSend;
    const pendingKey = `pending_${courseId}_${accountId}`;
    if (processingMessageIdsRef.current.has(tempId)) {
      return;
    }

    processingMessageIdsRef.current.add(tempId);

    // **Claim the processing slot synchronously**
    isCurrentlyProcessingRef.current = true;

    // Set the state flag as well (useful for potential UI indicators or triggering re-renders upon completion)
    // Note: This state update is still async, but the ref prevents the race condition.
    setState((prevState) => ({ ...prevState, isProcessingQueue: true }));

    // Define the async task for sending this specific message.
    const processMessage = async () => {
      try {
        // --- Make API Call ---
        let response;
        // TODO: Handle media preparation (e.g., check if 'media' is File, create FormData)
        const requestData = { text, media };
        const requestParams = {
          account_id: accountId,
          course_id: courseId,
          _account: selectedAccount.id,
        };

        if (selectedAccount.type.toLowerCase() === "enterprise") {
          response = await request({
            url: "/notify/v1/enterprise/chats/",
            params: requestParams,
            data: requestData,
            method: "Post",
          });
        } else if (
          String(selectedAccount.type).toLowerCase() === "instructor"
        ) {
          response = await request({
            url: `/notify/v1/instructor${
              affiliateAccount ? "/".concat(affiliateAccount) : ""
            }/chats/`,
            params: requestParams,
            data: requestData,
            method: "Post",
          });
        } else if (selectedAccount.type.toLowerCase() === "personal") {
          response = await request({
            url: `/notify/v1/chats/`,
            params: requestParams,
            data: requestData,
            method: "Post",
          });
        } else {
          throw new Error(
            "Queue Processor: Unsupported account type or configuration.",
          );
        }
        // --- End API Call ---

        if (!response?.data) {
          throw new Error(
            "Queue Processor: API request succeeded but returned no data.",
          );
        }

        // --- Handle SUCCESS ---
        /** @type {SentMessageResponse} */
        const sentMessageData = response.data;
        const confirmedRoomId = sentMessageData.room;
        const confirmedMessageId = sentMessageData.id;

        // Format confirmed message
        /** @type {ChatMessage} */
        const newChatMessage = {
          id: confirmedMessageId,
          text: sentMessageData.text,
          media: sentMessageData.media,
          sender: {
            id: sentMessageData.sender.id,
            first_name:
              sentMessageData.sender.full_name || sentMessageData.sender.name,
          },
          is_read: sentMessageData.is_read,
          is_delivered: sentMessageData.is_delivered,
          created_at: sentMessageData.created_at,
          user_message: sentMessageData.user_message,
          date: getLocalDateString(sentMessageData.created_at), // Use existing helper
        };

        // Update state atomically after success
        setState((prevState) => {
          // Safety check: Ensure the message we processed is still the first one
          if (
            prevState.messageQueue.length === 0 ||
            prevState.messageQueue[0].tempId !== tempId
          ) {
            console.warn(
              `Queue Processor: Mismatch processing successful message ${tempId}. Queue changed unexpectedly.`,
            );
            // Don't modify state if the queue front doesn't match
            return prevState;
          }

          // 1. Remove optimistic message
          let updatedOptimisticMessages = { ...prevState.optimisticMessages };
          if (updatedOptimisticMessages[pendingKey]) {
            const filteredList = updatedOptimisticMessages[pendingKey].filter(
              (msg) => msg.tempId !== tempId,
            );
            if (filteredList.length === 0)
              delete updatedOptimisticMessages[pendingKey];
            else updatedOptimisticMessages[pendingKey] = filteredList;
          }

          // 2. Add confirmed message to chats
          const updatedChats = { ...prevState.chats };
          const messageDateLabel = getDisplayDateLabel(
            newChatMessage.created_at,
          ); // Use corrected helper
          const currentChatHistory = updatedChats[confirmedRoomId] || [];
          let updatedChatHistory = [...currentChatHistory];
          let dateGroupIndex = updatedChatHistory.findIndex(
            (g) => g.date === messageDateLabel,
          );

          if (dateGroupIndex !== -1) {
            // Add to existing group if not duplicate
            if (
              !updatedChatHistory[dateGroupIndex].messages.some(
                (m) => m.id === newChatMessage.id,
              )
            ) {
              updatedChatHistory[dateGroupIndex].messages = [
                ...updatedChatHistory[dateGroupIndex].messages,
                newChatMessage,
              ].sort(
                (a, b) =>
                  new Date(a.created_at).getTime() -
                  new Date(b.created_at).getTime(),
              );
            }
          } else {
            // Create new group
            updatedChatHistory.push({
              date: messageDateLabel,
              messages: [newChatMessage],
            });
            updatedChatHistory.sort(
              (a, b) =>
                getSortableTimestampForGroup(b) -
                getSortableTimestampForGroup(a),
            ); // Use helper
          }
          updatedChats[confirmedRoomId] = updatedChatHistory;

          // 3. Update roomsByCourses last_message (if room metadata exists)
          let updatedRoomsByCourses = [...prevState.roomsByCourses];
          let roomExists = false;
          updatedRoomsByCourses = updatedRoomsByCourses.map((cg) => {
            if (String(cg.id) !== String(courseId)) return cg;
            const roomIdx = cg.rooms.findIndex((r) => r.id === confirmedRoomId);
            if (roomIdx !== -1) {
              roomExists = true;
              const updatedRoom = {
                ...cg.rooms[roomIdx],
                last_message: {
                  id: newChatMessage.id,
                  text: newChatMessage.text,
                  media: newChatMessage.media,
                  created_at: newChatMessage.created_at,
                },
              };
              const updatedRooms = [...cg.rooms];
              updatedRooms[roomIdx] = updatedRoom;
              return { ...cg, rooms: updatedRooms };
            }
            return cg;
          });

          // 4. Remove processed item from queue (immutable update)
          const updatedQueue = prevState.messageQueue.slice(1);

          // Trigger refetch if room metadata wasn't found (handled outside setState)
          if (!roomExists && typeof getMessageRoomsByCourses === "function") {
            // Check if function exists
            setTimeout(() => getMessageRoomsByCourses(null, true), 0);
          }

          // Return the combined state updates
          return {
            ...prevState,
            optimisticMessages: updatedOptimisticMessages,
            chats: updatedChats,
            roomsByCourses: updatedRoomsByCourses,
            messageQueue: updatedQueue,
            isProcessingQueue: true, // Keep true until finally block resets it below
          };
        });
      } catch (error) {
        // --- Handle FAILURE ---
        console.error(
          `Queue Processor: Failed to send message tempId ${tempId}:`,
          error,
        );
        setState((prevState) => {
          // Safety check for queue consistency
          if (
            prevState.messageQueue.length === 0 ||
            prevState.messageQueue[0].tempId !== tempId
          ) {
            console.warn(
              `Queue Processor: Mismatch processing failed message ${tempId}. Queue changed unexpectedly.`,
            );
            return prevState;
          }

          // 1. Update optimistic message status to 'failed'
          let updatedOptimisticMessages = { ...prevState.optimisticMessages };
          if (updatedOptimisticMessages[pendingKey]) {
            const messageIndex = updatedOptimisticMessages[
              pendingKey
            ].findIndex((msg) => msg.tempId === tempId);
            if (
              messageIndex !== -1 &&
              updatedOptimisticMessages[pendingKey][messageIndex].status ===
                "pending"
            ) {
              const failedMessage = {
                ...updatedOptimisticMessages[pendingKey][messageIndex],
                status: "failed",
                error: error?.message || "Unknown error",
              };
              const updatedPendingList = [
                ...updatedOptimisticMessages[pendingKey],
              ];
              updatedPendingList[messageIndex] = failedMessage;
              updatedOptimisticMessages[pendingKey] = updatedPendingList;
            } else {
              console.warn(
                `Queue Processor: Optimistic message tempId ${tempId} in ${pendingKey} not found or not pending for failure update.`,
              );
            }
          } else {
            console.warn(
              `Queue Processor: Pending key ${pendingKey} not found for failure update.`,
            );
          }

          // 2. Remove failed item from queue (immutable update)
          const updatedQueue = prevState.messageQueue.slice(1);
          // Return the combined state updates
          return {
            ...prevState,
            optimisticMessages: updatedOptimisticMessages,
            messageQueue: updatedQueue,
            isProcessingQueue: true, // Keep true until finally block resets it below
          };
        });
      } finally {
        // --- Finish Processing ---
        // **Crucial:** Release the synchronous lock first.
        isCurrentlyProcessingRef.current = false;
        // Then update the state flag. This state update will trigger a re-render.
        // If the queue still has items, the effect will run again, see the ref is false,
        // claim the lock, and start processing the next item.
        setState((prevState) => ({ ...prevState, isProcessingQueue: false }));
      }
    };

    // Execute the async sending logic for the current item.
    processMessage();

    // Dependencies:
    // - `state.isProcessingQueue`: Triggers check when processing finishes.
    // - `state.messageQueue`: Triggers check when queue content changes.
    // - `request`, `selectedAccount`, `affiliateAccount`, `getMessageRoomsByCourses`:
    //   Required by exhaustive-deps for use within the effect's scope.
  }, [
    state.isProcessingQueue,
    state.messageQueue,
    // request,
    selectedAccount,
    affiliateAccount,
  ]);

  /**
   * Retrieves combined confirmed and optimistic messages for a specific chat,
   * sorted chronologically and grouped by date.
   * Optimistic messages in the output will NOT have a 'sender' property.
   *
   * @param {string | null} roomId - The ID of the room (if known). Used ONLY for retrieving confirmed messages.
   * @param {string} courseId - The course ID associated with the chat. REQUIRED for finding optimistic messages.
   * @param {string} accountId - The account ID of the other participant. REQUIRED for finding optimistic messages.
   * @returns {MessagesByDate[]} An array of message groups. Confirmed messages will have a 'sender', optimistic messages will not.
   */
  const getMessagesForRoom = useCallback(
    (roomId, courseId, accountId) => {
      // --- 1. Determine Keys ---
      const chatKey = roomId; // Key for confirmed messages (state.chats)
      const pendingKey = `pending_${courseId}_${accountId}`; // Key for ALL optimistic messages for this pair

      // --- 2. Get Confirmed Messages (using roomId if provided) ---
      const confirmedDateGroups =
        chatKey && state.chats[chatKey] ? state.chats[chatKey] : [];
      // Confirmed messages already have the correct ChatMessage structure (including sender)
      const confirmedFlatMessages = confirmedDateGroups.flatMap(
        (group) => group.messages,
      );

      // --- 3. Get Optimistic Messages (ONLY from pendingKey) ---
      const uniqueOptimisticMessages = state.optimisticMessages[pendingKey]
        ? [...state.optimisticMessages[pendingKey]] // Get a copy
        : [];

      // --- 4. Adapt Optimistic Messages to a structure suitable for sorting/grouping ---
      //    (Does NOT add 'sender')
      const adaptedOptimisticMessages = uniqueOptimisticMessages.map(
        (optMsg) => ({
          id: optMsg.tempId,
          text: optMsg.text,
          media: optMsg.media, // The original media reference stored in optimistic state
          // NO SENDER FIELD added here
          is_read: false,
          is_delivered: false,
          created_at: optMsg.created_at, // Client timestamp
          user_message: optMsg.user_message, // Always true
          date: getLocalDateString(optMsg.created_at), // Use helper for basic date info
          // Add optimistic-specific fields for UI:
          status: optMsg.status, // 'pending' or 'failed'
          error: optMsg.error, // Error message if failed
          tempId: optMsg.tempId, // Keep tempId for keys or retry logic
        }),
      );

      // --- 5. Combine and Sort All Messages ---
      const combinedFlatMessages = [
        ...confirmedFlatMessages,
        ...adaptedOptimisticMessages,
      ];
      combinedFlatMessages.sort((a, b) => {
        const timeA = new Date(a.created_at).getTime();
        const timeB = new Date(b.created_at).getTime();
        // Handle potential NaN from invalid dates
        return (isNaN(timeA) ? 0 : timeA) - (isNaN(timeB) ? 0 : timeB);
      });

      // --- 6. Regroup Sorted Messages by Date ---
      /** @type {Record<string, { date: string; messages: any[] }>} */
      const groupedMessages = {};

      combinedFlatMessages.forEach((message) => {
        // Use the corrected helper with the full timestamp
        const displayLabel = getDisplayDateLabel(message.created_at);
        if (
          !displayLabel ||
          displayLabel === "Unknown Date" ||
          displayLabel === "Invalid Date" ||
          displayLabel === "Error Date"
        ) {
          console.warn("Skipping message due to invalid date label:", message);
          return;
        }

        if (!groupedMessages[displayLabel]) {
          groupedMessages[displayLabel] = {
            date: displayLabel, // The display label (e.g., "Today")
            messages: [],
          };
        }
        groupedMessages[displayLabel].messages.push(message);
      });

      // Convert grouped object back to array and sort the groups using timestamps
      const finalGroupedArray = Object.values(groupedMessages).sort(
        (a, b) =>
          getSortableTimestampForGroup(a) - getSortableTimestampForGroup(b),
      ); // Ascending date order so the latest date renders at the bottom.

      // Ensure messages *within* each final group are sorted (redundant but safe)
      finalGroupedArray.forEach((group) => {
        group.messages.sort(
          (a, b) =>
            new Date(a.created_at).getTime() - new Date(b.created_at).getTime(),
        );
      });

      return finalGroupedArray; // Array of { date: string, messages: any[] }
    },
    [state.chats, state.optimisticMessages],
  ); // Dependencies: only needs state parts used

  // Helper to get a sortable value from a display date label
  // const getSortableDateValue = (displayLabel, dateString) => {
  //   if (!dateString) return 0; // Handle undefined/null dateString
  //   if (displayLabel === "Today") return 2; // Highest priority
  //   if (displayLabel === "Yesterday") return 1; // Next highest
  //   // For specific dates, return the negative timestamp for descending sort
  //   try {
  //     const date = new Date(dateString + "T00:00:00Z"); // Use UTC base
  //     if (isNaN(date.getTime())) return 0; // Low priority if invalid
  //     // Older dates have smaller negative numbers (larger positive time), leading to descending sort
  //     return -date.getTime();
  //   } catch {
  //     return 0; // Fallback for errors
  //   }
  // };

  const pinRoom = async (roomId) => {
    // Optimistic update
    setState((prevState) => {
      return {
        ...prevState,
        roomsByCourses: prevState.roomsByCourses.map((courseGroup) => {
          const roomIndex = courseGroup.rooms.findIndex(
            (room) => room.id === roomId,
          );
          if (roomIndex !== -1) {
            const updatedRooms = [...courseGroup.rooms];
            const oldPinStatus = updatedRooms[roomIndex].pin_status;
            updatedRooms[roomIndex] = {
              ...updatedRooms[roomIndex],
              pin_status: !oldPinStatus,
            };

            const shouldCourseGroupBePinned = updatedRooms.some(
              (room) => room.pin_status,
            );

            return {
              ...courseGroup,
              pin_status: shouldCourseGroupBePinned,
              rooms: updatedRooms,
            };
          }
          return courseGroup;
        }),
      };
    });
    try {
      let response;
      if (selectedAccount.type.toLowerCase() === "enterprise") {
        response = await request({
          url: `/notify/v1/enterprise/rooms/${roomId}/change_pin_status/`,
          method: "patch",
        });
      }
      if (selectedAccount.type.toLowerCase() === "instructor") {
        response = await request({
          url: `/notify/v1/instructor${
            affiliateAccount ? "/".concat(affiliateAccount) : ""
          }/rooms/${roomId}/change_pin_status/`,
          method: "patch",
        });
      } // incomplete

      if (selectedAccount.type.toLowerCase() === "personal") {
        response = await request({
          url: `/notify/v1/rooms/${roomId}/change_pin_status/`,
          method: "patch",
        });
      }
    } catch (err) {
      // Revert optimistic update on failure (optional, depends on desired UX)
      // You might want to implement more specific error handling here
      getMessageRoomsByCourses(null, true);
    }

    // const isPinned = response.data.detail === "True";
    // The optimistic update should ideally handle the UI change,
    // so no further state update might be needed here based on the response.
    // However, you could add a check to ensure the optimistic update matches the server response.
  };

  const deleteRoom = async (roomId) => {
    // Optimistic update: Remove the room and potentially the group
    let previousState;
    setState((prevState) => {
      previousState = { ...prevState };
      let updatedRoomsByCourses = prevState.roomsByCourses
        .map((courseGroup) => {
          const updatedRooms = courseGroup.rooms.filter(
            (room) => room.id !== roomId,
          );

          if (
            updatedRooms.length === 0 &&
            courseGroup.rooms.some((room) => room.id === roomId)
          ) {
            // If the deleted room was the last one, remove the entire group
            return null;
          }

          return {
            ...courseGroup,
            rooms: updatedRooms,
            // Recalculate total unread count
            total_unread_count: updatedRooms.reduce(
              (sum, room) => sum + room.unread_count,
              0,
            ),
            pin_status: updatedRooms.some((room) => room.pin_status),
          };
        })
        .filter(Boolean); // Remove null entries (deleted groups)

      const updatedChats = { ...prevState.chats };
      delete updatedChats[roomId]; // Remove chat history

      return {
        ...prevState,
        roomsByCourses: updatedRoomsByCourses,
        chats: updatedChats,
      };
    });

    try {
      // let response;
      if (selectedAccount.type.toLowerCase() === "enterprise") {
        await request({
          url: `/notify/v1/enterprise/rooms/${roomId}/`,
          method: "delete",
        });
      }
      if (selectedAccount.type.toLowerCase() === "instructor") {
        await request({
          url: `/notify/v1/instructor${
            affiliateAccount ? "/".concat(affiliateAccount) : ""
          }/rooms/${roomId}/`,
          method: "delete",
        });
      } // incomplete
      if (selectedAccount.type.toLowerCase() === "personal") {
        await request({
          url: `/notify/v1/rooms/${roomId}/`,
          method: "delete",
        });
      }
    } catch (err) {
      console.error("Error deleting room:", err);
      // Revert the optimistic update on error
      setState(previousState);
    }
  };
  const reportChat = async ({
    reasons,
    accountId,
    messageIds,
    others, // optional
  }) => {
    let response;
    const data = {
      reasons,
      account_id: accountId,
      message_ids: messageIds,
      others,
    };
    if (selectedAccount.type.toLowerCase() === "enterprise") {
      console.log("Reporting chat in enterprise", data);
      response = await request({
        url: `/notify/v1/enterprise/chats/reports/`,
        method: "post",
        data,
      });
    }
    if (selectedAccount.type.toLowerCase() === "instructor") {
      response = await request({
        url: `/notify/v1/instructor${
          affiliateAccount ? "/".concat(affiliateAccount) : ""
        }/chats/reports/`,
        method: "post",
        data,
      });
    }
    if (selectedAccount.type.toLowerCase() === "personal") {
      response = await request({
        url: `/notify/v1/chats/reports/`,
        method: "post",
        data,
      });
    }
    return response;
  };
  // Initial data fetch effect
  useEffect(() => {
    (async () => {
      if (!selectedAccountId || !defaultAccountType || !token) return;
      await getMessageRoomsByCourses();
      // reportChat({
      //   reasons: ["Verbal harassment"],
      //   accountId: "24dbaeede1",
      //   messageIds: ["438745c081564ee9aeb76ed29052bd4b"],
      //   // others: "",
      // });
      // await deleteRoom("5be9b48281ac4c2885d3b719654ed59d");
      // await pinRoom("5be9b48281ac4c2885d3b719654ed59d");
      // Example: Fetch initial chat for a specific room if needed on load
      // await getIndividualChats(null, "c7e334452b974576aa8250aafb73b7d4");
      // await readRoomMessages({
      //   latestMessageId: "57e31128214a4269be8b9e3bb18495c4",
      //   roomId: "5be9b48281ac4c2885d3b719654ed59d",
      // });
      // await sendMessage({
      //   text: "automated message v2",
      //   courseId: "878",
      //   accountId: "24dbaeede1",
      // });
    })();
  }, [affiliateAccount, token, defaultAccountType, selectedAccountId]); // Rerun when selectedAccount changes
  // console.log(
  //   getMessagesForRoom("5be9b48281ac4c2885d3b719654ed59d", "878", "24dbaeede1"),
  //   "hold versions"
  // );
  // --- Handle Incoming WebSocket Messages ---
  useEffect(() => {
    const socket = new WebSocket(buildSocketUrl());
    socket.addEventListener("message", (event) => {
      const lastJsonMessage = JSON.parse(event.data);
      if (lastJsonMessage && lastJsonMessage.event === "new.message.result") {
        /** @type {WebSocketMessageData} */
        const webSocketData = lastJsonMessage.data;
        const roomId = webSocketData.room;

        // Helper to get YYYY-MM-DD from ISO string (duplicate from sendMessage, consider extracting)
        const getLocalDateString = (isoTimestamp) => {
          if (!isoTimestamp) return new Date().toISOString().split("T")[0];
          try {
            return new Date(isoTimestamp).toISOString().split("T")[0];
          } catch (e) {
            console.error("Error parsing date:", e);
            return new Date().toISOString().split("T")[0];
          }
        };

        // --- Check if room exists in current state ---
        let roomExists = false;
        for (const courseGroup of state.roomsByCourses) {
          if (courseGroup.rooms.some((room) => room.id === roomId)) {
            roomExists = true;
            break;
          }
        }

        // --- If room doesn't exist, refetch room list and skip state update for this message ---
        if (!roomExists) {
          console.log(
            `Room ${roomId} not found in state, refetching room list.`,
          );
          getMessageRoomsByCourses(null, true);
          setTimeout(() => {}, 1000);
        }

        // --- Format WebSocket data into ChatMessage ---
        /** @type {ChatMessage} */
        const newChatMessage = {
          id: webSocketData.id,
          text: webSocketData.text,
          // Map media - assuming WebSocketMedia is compatible enough
          media: webSocketData.media,
          sender: {
            id: webSocketData.sender.id,
            first_name:
              webSocketData.sender.full_name || webSocketData.sender.name,
          },
          is_read: webSocketData.is_read,
          is_delivered: webSocketData.is_delivered,
          created_at: webSocketData.created_at,
          user_message: webSocketData.user_message, // Crucial for unread count
          date: getLocalDateString(webSocketData.created_at),
        };

        // --- Update State & Play Sound ---
        setState((prevState) => {
          let messageWasAdded = false; // Flag to track if the message is actually added

          // --- Update chats state ---
          const currentChatHistory = prevState.chats[roomId] || [];
          let updatedChatHistory = [...currentChatHistory];
          const todayDateString = "today"; // Assume API uses "today" for current day

          const todayGroupIndex = updatedChatHistory.findIndex(
            (group) => group.date === todayDateString,
          );

          if (todayGroupIndex !== -1) {
            // "today" group exists
            const todayGroup = updatedChatHistory[todayGroupIndex];
            // Avoid adding duplicate messages if WS sends what we already added via sendMessage response
            const isDuplicate = todayGroup.messages.some(
              (msg) => msg.id === newChatMessage.id,
            );
            if (!isDuplicate) {
              const updatedTodayGroup = {
                ...todayGroup,
                messages: [...todayGroup.messages, newChatMessage], // Add message
              };
              updatedChatHistory.splice(todayGroupIndex, 1, updatedTodayGroup); // Replace group
              messageWasAdded = true; // Mark as added
            }
          } else {
            // "today" group doesn't exist, create it and prepend
            const newTodayGroup = {
              date: todayDateString,
              messages: [newChatMessage],
            };
            // Prepend to maintain reverse chronological order of groups
            updatedChatHistory = [newTodayGroup, ...updatedChatHistory];
            messageWasAdded = true; // Mark as added (since it's a new group)
          }

          // --- Determine conditions ---
          const isIncomingMessage = !newChatMessage.user_message;
          // Use ref here as setState callback might capture stale state otherwise
          const isAutoRead = autoReadRoomIdsRef.current.has(roomId);

          // --- Play sound only if it's a new incoming message and NOT auto-read ---
          if (
            messageWasAdded &&
            isIncomingMessage &&
            !isAutoRead &&
            notificationSound
          ) {
            notificationSound.currentTime = 0;
            notificationSound.play().catch((e) => {
              if (e.name !== "NotAllowedError") {
                console.error("Error playing notification sound:", e);
              }
            });
          }
          // --- End Play Sound ---

          const updatedChats = {
            ...prevState.chats,
            [roomId]: updatedChatHistory,
          };

          // --- Update roomsByCourses state ---
          const updatedRoomsByCourses = prevState.roomsByCourses.map(
            (courseGroup) => {
              const roomIndex = courseGroup.rooms.findIndex(
                (room) => room.id === roomId,
              );

              if (roomIndex !== -1) {
                const updatedRooms = [...courseGroup.rooms];
                const currentRoom = updatedRooms[roomIndex];

                // Increment count only if incoming and NOT auto-read
                const shouldIncrementCount = isIncomingMessage && !isAutoRead;
                const roomUnreadIncrement = shouldIncrementCount ? 1 : 0;

                updatedRooms[roomIndex] = {
                  ...currentRoom,
                  last_message: {
                    id: newChatMessage.id,
                    text: newChatMessage.text,
                    media: newChatMessage.media,
                    created_at: newChatMessage.created_at,
                  },
                  // Update unread count based on increment logic
                  unread_count: currentRoom.unread_count + roomUnreadIncrement,
                };
                return {
                  ...courseGroup,
                  rooms: updatedRooms,
                  // Update total unread count based on increment logic
                  total_unread_count:
                    courseGroup.total_unread_count + roomUnreadIncrement,
                };
              }
              return courseGroup; // Return unchanged if room not found in this group
            },
          );

          // --- Return combined state update ---
          return {
            ...prevState,
            chats: updatedChats,
            roomsByCourses: updatedRoomsByCourses,
          };
        });

        // --- Trigger auto-read if applicable ---
        // Check conditions *after* state update logic has run
        const isIncoming = !newChatMessage.user_message;
        const shouldAutoRead = autoReadRoomIdsRef.current.has(roomId); // Use ref again

        if (isIncoming && shouldAutoRead) {
          // Use a small delay to ensure state update completes before read call
          // although setState is async, this helps avoid potential race conditions
          // if readRoomMessages itself relies on the updated state immediately.
          setTimeout(() => {
            readRoomMessages({
              latestMessageId: newChatMessage.id,
              roomId: roomId,
            });
          }, 0);
        }
        // --- End Trigger auto-read ---
      }
    });

    return () => socket.close();
  }, []); // Add dependencies

  // send messages to account based on account type

  // Return the state and potentially the functions if they need to be called externally
  return {
    state,
    getIndividualChats,
    getMessageRoomsByCourses,
    sendMessage,
    readRoomMessages,
    addAutoReadRooms, // Expose new function
    removeAutoReadRooms, // Expose new function
    pinRoom,
    deleteRoom,
    reportChat,
    // Helper method to check if a specific room is loading
    isRoomLoading: (roomId) => state.loadingRoomIds.has(roomId),
    getMessagesForRoom,
  };
};

export default useMessageKit;
