import {unstable_batchedUpdates} from 'react-native';
import debounce from 'lodash.debounce';
import {create} from 'zustand';
import {
  LOCAL_INITIAL_VERSION,
  MESSAGES_BATCH_SIZE,
  PRIORITY_CONVERSATION_COUNT,
} from '@modules/chats/constants';
import {
  getLatestMessagesForGroup,
  getAllConversations,
} from '@modules/chats/db';
import {
  ProcessAttachmentParamItem,
  fileHandler,
} from '@modules/chats/instances/FileHandler';
import {
  Asset,
  Conversation,
  ConversationLatest,
  DeliverStatus,
  HuDataMember,
  Message,
  MessageStatus,
} from '@modules/chats/interfaces';
import {PendingAssets} from '@modules/chats/instances/PendingAssets';
import {MessagesOrchestrator} from '@modules/chats/instances/MessagesOrchestrator';
import {MessageRangesHandler} from '@modules/chats/instances/MessageRangesHandler';
import {sortMessages, sortMessagesByTsHelper} from '@modules/chats/utils';
import {AssetsUpdaterService} from '@modules/chats/instances/updaters';
import {isChatUserDeactivated} from '@modules/chats/utils/user';
import {persistedUserId} from '@services/localStorage';
import {isAndroid, isIos, sentryLogs} from '@shared/utils';
import {BlockingLatch} from '@shared/utils/promises/BlockingLatch';
import {maxUlid} from '@shared/utils/ulid';

import {addReactionsFromStores} from './reactions';
import {visibleChatStore} from './visibleChat';

const FIRST_PAGE_LOAD_TIMEOUT_MS = 15_000;
const MAX_LOAD_FIRST_PAGE_RETRIES = 1;

let scrollingTimerId: Nullable<NodeJS.Timeout> = null;

const clearScrollToMessageTimer = () => {
  if (scrollingTimerId) {
    clearTimeout(scrollingTimerId);
  }
};

// Use a global variable to avoid re-renders when we reset it
export let isScrollingToMessage = false;

export type OnScrollShouldStart = () => void;
export type OnScrollEnd = () => void;

export interface GroupData {
  groupInfo: Conversation;
  messagesWindow: Message[];
  pendingMessages: Message[];
  isActiveMembership?: boolean;
  isScrolling?: boolean;
  lastMessageTs: string;
  /**
   * Flag to indicate if conversations has only 1 message and need to get first page asynchronously
   * This is used to show loader before render LegendList and side effects
   */
  shouldFetchInitialMessages: boolean;
  hasFirstPageLoadError?: boolean;
}

// TODO if there will be issues with performance then allow messages in between and just properly merge in set functions
export interface GroupsStore {
  targetMessageId: string | null;
  setTargetMessageId: (ts: string | null) => void;
  numberOfPendingAsyncActions: number;
  /**
   * Conversations from database have been populated in the store.
   */
  isConversationsLoaded: boolean;
  setIsConversationsLoaded: (value: boolean) => void;
  /**
   * If true, all updates to the groups object will be blocked.
   * This starts as true and is set to false when the initial sync is done.
   */
  isUpdatesBlocked: boolean;
  /**
   * Enable updates to this store. After calling this, there is a flush of pending updates.
   */
  enableUpdates: () => void;
  /**
   * Sync Events is running, this is used to show spinner in chat list
   */
  isFetchingEvents: boolean;
  /**
   * Set the fetching events state.
   */
  setIsFetchingEvents: (value: boolean) => void;
  /**
   * All conversations are being fetched from server (from RecentsHandler)
   * This is used to show a loader in Chat list while is fetching.
   */
  isFetchingAllConversations: boolean;
  /**
   * Set the fetching all conversations state.
   */
  setIsFetchingAllConversations: (value: boolean) => void;
  /**
   * Save recent conversations (top 20 with 100 messages each)
   * @param data Recent conversations data from server
   */
  saveRecentConversations: (
    data: {
      group_conversation: Conversation;
      latest_messages: Message[];
    }[],
  ) => void;
  /**
   * Save all conversations with latest message as message window (if there is no message window)
   * @param conversations Conversations array to save
   */
  saveAllConversations: (
    conversations: Conversation[],
    latestMessages?: Message[],
  ) => void;
  /**
   * If true, some error happened while starting chats, like:
   * - Failed to fetch recents
   * - DB connection error
   * - Sync events error
   * This is used to show a error message in the chat list
   */
  isError: boolean;
  /**
   * Set the error state.
   */
  setIsError: (value: boolean) => void;
  isFetchingAbove: {[key: string]: boolean};
  isFetchingBelow: {[key: string]: boolean};
  groups: {[key: string]: GroupData};
  scrollToBottomCallbacks: {[key: string]: () => void};
  incPendingAsyncActions: () => void;
  decPendingAsyncActions: () => void;
  initGroups: () => Promise<void>;
  updateGroup: (groupInfo: Conversation) => void;
  updateGroups: (groupInfo: Conversation[]) => void;
  deleteGroup: (groupId: string) => void;
  updateMessage: (message: Message) => void;
  updateMessagesBatch: (message: Message[]) => void;
  startScrollToMessage: ({
    groupId,
    ts,
    onScrollShouldStart,
  }: {
    groupId: string;
    ts: string;
    onScrollShouldStart: OnScrollShouldStart;
  }) => Promise<OnScrollEnd>;
  setScrollToBottom: (groupId: string, scrollToBottom: boolean) => void;
  registerScrollToBottom: (groupId: string, callback: () => void) => void;
  unregisterScrollToBottom: (groupId: string) => void;
  setIsScrolling: (isScrolling: boolean) => void;
  resetConversationWithLatestMessages: (groupId: string) => Promise<void>;
  loadAboveMessages: (groupId: string) => Promise<void> | undefined;
  loadBelowMessages: (groupId: string) => Promise<void> | undefined;
  loadFirstPageMessages: (
    groupId: string,
    retryCount?: number,
  ) => Promise<void> | undefined;
  maintainPendingMessages: (
    groupData: GroupData,
    action: 'above' | 'below',
  ) => GroupData;
  clear: () => void;
  saveUserUpdatesBatch: (users: HuDataMember[]) => void;
  updateSentMessage: (params: Message) => void;
  clearScrollToMessage: () => void;
  updateDeliverStatus: (params: {
    fromMessageTs: string;
    toMessageTs: string;
    channelId: string;
    deliverStatus: DeliverStatus;
  }) => void;
  updateAssets: (params: {
    assets: Asset[];
    messageId: string;
    channelId: string;
  }) => void;
  /**
   * Set the messages as latest for the given channel
   * This is used to ensure keep consistency between chat list and chat messages window.
   * @param messages Messages to set as latest
   */
  setMessagesAsLatest: (messages: Message[]) => void;
  /**
   * Load below messages if needed, only if the last message in the window is not the latest message
   * @param channel Channel id
   * @param latestId Latest message id from server (if not provided, the latest message from the stored conversation will be used)
   */
  loadBelowMessagesIfNeeded: (channel: string, latestId?: string) => void;
  /**
   * Update the message height in the store
   * @param channel
   * @param client_msg_id
   * @param height
   */
  updateMessageHeight: (
    channel: string,
    client_msg_id: string,
    height: number,
  ) => void;
}

const DEBOUNCE_TIME = 250;
const initialState = {
  numberOfPendingAsyncActions: 0,
  isConversationsLoaded: false,
  isUpdatesBlocked: true,
  isFetchingEvents: false,
  isError: false,
  isFetchingAbove: {},
  isFetchingBelow: {},
  groups: {},
  scrollToBottomCallbacks: {},
} as GroupsStore;

// TODO read groups synchronously from db
// TODO check if expo executes write/read operations synchronously (looks like it does on android as it uses singleThreaded DispatchContext)

export const useGroups = create<GroupsStore>((set, get) => ({
  ...initialState,
  updateGroups: (groupInfos: Conversation[]) => {
    set(state => {
      const newState = groupInfos.reduce<{[key: string]: GroupData}>(
        (acc, groupInfo) => {
          if (!groupInfo.id || typeof groupInfo.id !== 'string') {
            sentryLogs.error({
              message: 'Conversation id not defined',
              module: 'CHATS',
              component: 'updateGroups',
              data: {
                conversation: groupInfo,
              },
            });
            return acc;
          }

          const groupId = groupInfo.id;
          const updatedGroup = getUpdatedGroupHelper({
            currentValue: state.groups?.[groupId],
            newValue: groupInfo,
          });
          acc[groupId] = updatedGroup;

          return acc;
        },
        {},
      );

      return {
        groups: {
          ...state.groups,
          ...newState,
        },
      };
    });
  },
  updateGroup: (groupInfo: Conversation) => {
    if (!groupInfo.id || typeof groupInfo.id !== 'string') {
      sentryLogs.error({
        message: 'Conversation id not defined',
        module: 'CHATS',
        component: 'updateGroup',
        data: {
          conversation: groupInfo,
        },
      });
      return;
    }
    set(state => {
      const groupId = groupInfo.id;

      const updatedGroup = getUpdatedGroupHelper({
        currentValue: state.groups[groupId],
        newValue: groupInfo,
      });

      return {
        groups: {
          ...state.groups,
          [groupId]: updatedGroup,
        },
      };
    });
  },
  deleteGroup: (groupId: string) =>
    set(state => {
      const newGroups = {...state.groups};
      delete newGroups[groupId];
      return {groups: newGroups};
    }),
  updateMessage: (param: Message) =>
    set(state => {
      const message = PendingAssets.setMessage(param);
      const groupId = message.channel;
      const group = state.groups[groupId];

      if (
        !group ||
        (group.messagesWindow?.length === 0 &&
          message.status !== MessageStatus.Sending &&
          state.isUpdatesBlocked)
      ) {
        return state;
      }

      let newGroupInfoWrapper: {groupInfo: Conversation} | object = {};
      if (
        group.groupInfo?.latest?.ts === undefined ||
        group.groupInfo?.latest?.ts < message.ts ||
        group.groupInfo?.latest?.client_msg_id === message.client_msg_id
      ) {
        newGroupInfoWrapper = {
          groupInfo: {
            ...group.groupInfo,
            latest: message,
          },
        };
      }

      if (
        message.version === LOCAL_INITIAL_VERSION &&
        (message.status === MessageStatus.Error ||
          message.status === MessageStatus.Sending)
      ) {
        const pendingMessages = state.groups[groupId].pendingMessages || [];
        const newPending =
          pendingMessages.length === 0
            ? [message]
            : pendingMessages.reduce((acc, msg, index) => {
                if (msg.client_msg_id === message.client_msg_id) {
                  acc.push(message);
                  return acc;
                }
                const isLast = index === pendingMessages.length - 1;
                if (isLast) {
                  acc.push(msg, message);
                  return acc;
                }
                acc.push(msg);
                return acc;
              }, [] as Message[]);

        return {
          groups: {
            ...state.groups,
            [groupId]: {
              ...state.groups[groupId],
              pendingMessages: newPending,
              ...newGroupInfoWrapper,
            },
          },
        };
      }

      const pendingMessages = (
        state.groups[groupId].pendingMessages || []
      ).filter(msg => msg.client_msg_id !== message.client_msg_id);

      const messagesWindow = state.groups[groupId].messagesWindow || [];
      const currentRangeLeftTs = messagesWindow.at(0)?.ts;
      const currentRangeRightTs = messagesWindow.at(-1)?.ts;

      // Check if message already exists in window (update case)
      const existsInWindow = messagesWindow.some(
        msg =>
          msg.client_msg_id === message.client_msg_id || msg.ts === message.ts,
      );

      // We already added the message so we just gonna update it
      if (existsInWindow) {
        return {
          groups: {
            ...state.groups,
            [groupId]: {
              ...state.groups[groupId],
              messagesWindow: messagesWindow.map(msg =>
                msg.client_msg_id === message.client_msg_id ||
                msg.ts === message.ts
                  ? message
                  : msg,
              ),
              pendingMessages,
              ...newGroupInfoWrapper,
            },
          },
        };
      }

      // There is no messages in the window yet so just add this new one
      if (!currentRangeLeftTs && !currentRangeRightTs) {
        return {
          groups: {
            ...state.groups,
            [groupId]: {
              ...state.groups[groupId],
              messagesWindow: [message],
              pendingMessages,
              shouldFetchInitialMessages: false,
              ...newGroupInfoWrapper,
            },
          },
        };
      }

      // Message is newer than current range - append to end
      if (currentRangeRightTs && message.ts > currentRangeRightTs) {
        return {
          groups: {
            ...state.groups,
            [groupId]: {
              ...state.groups[groupId],
              messagesWindow: [...messagesWindow, message],
              pendingMessages,
              lastMessageTs:
                message.ts > state.groups[groupId].lastMessageTs
                  ? message.ts
                  : state.groups[groupId].lastMessageTs,
              ...newGroupInfoWrapper,
            },
          },
        };
      }

      // Message is older than current range - prepend to start
      if (currentRangeLeftTs && message.ts < currentRangeLeftTs) {
        return {
          groups: {
            ...state.groups,
            [groupId]: {
              ...state.groups[groupId],
              messagesWindow: [message, ...messagesWindow],
              pendingMessages,
              ...newGroupInfoWrapper,
            },
          },
        };
      }

      // Message is within the current range but not part of the window - insert in sorted position
      if (
        currentRangeRightTs &&
        currentRangeLeftTs &&
        message.ts < currentRangeRightTs &&
        message.ts > currentRangeLeftTs
      ) {
        const newMessagesWindow = [...messagesWindow];
        const insertIndex = newMessagesWindow.findIndex(
          msg => msg.ts > message.ts,
        );

        // this won't happen probably but find index can return -1 so we better check it
        if (insertIndex === -1) {
          newMessagesWindow.push(message);
        } else {
          // Insert new message before the first message with a later timestamp
          newMessagesWindow.splice(insertIndex, 0, message);
        }

        return {
          groups: {
            ...state.groups,
            [groupId]: {
              ...state.groups[groupId],
              messagesWindow: newMessagesWindow,
              pendingMessages,
              ...newGroupInfoWrapper,
            },
          },
        };
      }

      return state;
    }),
  updateMessagesBatch: (messages: Message[]) =>
    set(state => {
      if (messages.length === 0) {
        return state;
      }

      const nextGroups = {...state.groups};

      for (const message of messages) {
        const groupId = message.channel;
        const group = nextGroups[groupId];

        if (
          !group ||
          (group.messagesWindow?.length === 0 &&
            message.status !== MessageStatus.Sending &&
            state.isUpdatesBlocked)
        ) {
          continue;
        }

        let newGroupInfoWrapper: {groupInfo: Conversation} | object = {};
        if (
          group.groupInfo?.latest?.ts === undefined ||
          group.groupInfo?.latest?.ts < message.ts ||
          group.groupInfo?.latest?.client_msg_id === message.client_msg_id
        ) {
          newGroupInfoWrapper = {
            groupInfo: {
              ...group.groupInfo,
              latest: message,
            },
          };
        }

        if (
          message.version === LOCAL_INITIAL_VERSION &&
          (message.status === MessageStatus.Error ||
            message.status === MessageStatus.Sending)
        ) {
          const pendingMessages = group.pendingMessages || [];
          const newPending =
            pendingMessages.length === 0
              ? [message]
              : pendingMessages.reduce((acc, msg, index) => {
                  if (msg.client_msg_id === message.client_msg_id) {
                    acc.push(message);
                    return acc;
                  }
                  const isLast = index === pendingMessages.length - 1;
                  if (isLast) {
                    acc.push(msg, message);
                    return acc;
                  }
                  acc.push(msg);
                  return acc;
                }, [] as Message[]);
          nextGroups[groupId] = {
            ...group,
            pendingMessages: newPending,
            ...newGroupInfoWrapper,
          };
          continue;
        }

        const pendingMessages = (group.pendingMessages || []).filter(
          msg => msg.client_msg_id !== message.client_msg_id,
        );

        const groupMessagesWindow = group.messagesWindow || [];
        const currentRangeLeftTs = groupMessagesWindow.at(0)?.ts;
        const currentRangeRightTs = groupMessagesWindow.at(-1)?.ts;

        // Check if message already exists in window (update case)
        const existsInWindow = groupMessagesWindow.some(
          msg =>
            msg.client_msg_id === message.client_msg_id ||
            msg.ts === message.ts,
        );

        if (existsInWindow) {
          nextGroups[groupId] = {
            ...group,
            messagesWindow: groupMessagesWindow.map(msg =>
              msg.client_msg_id === message.client_msg_id ||
              msg.ts === message.ts
                ? message
                : msg,
            ),
            pendingMessages,
            ...newGroupInfoWrapper,
          };
          continue;
        }

        // No messages in window yet
        if (!currentRangeLeftTs && !currentRangeRightTs) {
          nextGroups[groupId] = {
            ...group,
            messagesWindow: [message],
            pendingMessages,
            ...newGroupInfoWrapper,
            shouldFetchInitialMessages: false,
          };
          continue;
        }

        // Message is newer than current range - append to end
        if (currentRangeRightTs && message.ts > currentRangeRightTs) {
          nextGroups[groupId] = {
            ...group,
            messagesWindow: [...groupMessagesWindow, message],
            pendingMessages,
            lastMessageTs:
              message.ts > group.lastMessageTs
                ? message.ts
                : group.lastMessageTs,
            ...newGroupInfoWrapper,
          };
          continue;
        }

        // Message is older than current range - prepend to start
        if (currentRangeLeftTs && message.ts < currentRangeLeftTs) {
          nextGroups[groupId] = {
            ...group,
            messagesWindow: [message, ...groupMessagesWindow],
            pendingMessages,
            ...newGroupInfoWrapper,
          };
          continue;
        }

        // Message is within the current range but not part of the window - insert in sorted position
        if (
          currentRangeRightTs &&
          currentRangeLeftTs &&
          message.ts < currentRangeRightTs &&
          message.ts > currentRangeLeftTs
        ) {
          const newMessagesWindow = [...groupMessagesWindow];
          const insertIndex = newMessagesWindow.findIndex(
            msg => msg.ts > message.ts,
          );

          // this won't happen probably but find index can return -1 so we better check it
          if (insertIndex === -1) {
            newMessagesWindow.push(message);
          } else {
            // Insert new message before the first message with a later timestamp
            newMessagesWindow.splice(insertIndex, 0, message);
          }

          nextGroups[groupId] = {
            ...group,
            messagesWindow: newMessagesWindow,
            pendingMessages,
            ...newGroupInfoWrapper,
          };
          continue;
        }

        // fallthrough: no change
      }

      return {
        groups: nextGroups,
      };
    }),
  resetConversationWithLatestMessages: async (groupId: string) => {
    let messages: Message[] = [];
    let pendingMessages = [] as Message[];

    get().incPendingAsyncActions();
    messages = await getLatestMessagesForGroup(groupId);
    addReactionsFromStores(messages);

    const [pendings, others] = messages.reduce(
      (acc, msg) => {
        if (msg.version === LOCAL_INITIAL_VERSION) {
          acc[0].push(PendingAssets.setMessage(msg));
        } else {
          acc[1].push(msg);
        }
        return acc;
      },
      [[], []] as [Message[], Message[]],
    );

    pendingMessages = pendings;
    messages = others;
    get().decPendingAsyncActions();

    get().scrollToBottomCallbacks[groupId]?.();

    const hasLastestMessageInWindow = () => {
      const group = get().groups[groupId];
      const latest = group?.groupInfo?.latest;
      if (latest) {
        const allMessages = [...group.messagesWindow, ...group.pendingMessages];
        return (
          allMessages.findIndex(
            m => m.client_msg_id === latest.client_msg_id,
          ) !== -1
        );
      }
      return false;
    };

    /*
    Skip force update if we are already in the conversation we are updating
    if the messagesWindow doesn't include the latest message - it renders prev page (after scrollToMessage)
    in this case we should reset the data
     */
    if (
      visibleChatStore.getState().id === groupId &&
      hasLastestMessageInWindow()
    ) {
      return;
    }

    set(state => {
      if (!state.groups[groupId]) {
        return {};
      }

      messages = messages.sort(sortMessagesByTsHelper);
      return {
        groups: {
          ...state.groups,
          [groupId]: {
            ...state.groups[groupId],
            messagesWindow: messages,
            pendingMessages,
            lastMessageTs: messages.at(-1)?.ts || '',
          },
        },
      };
    });
  },
  setIsFetchingEvents: value =>
    set(state =>
      state.isFetchingEvents === value ? state : {isFetchingEvents: value},
    ),
  setIsConversationsLoaded: value =>
    set(state =>
      state.isConversationsLoaded === value
        ? state
        : {isConversationsLoaded: value},
    ),
  setIsFetchingAllConversations: value =>
    set(state =>
      state.isFetchingAllConversations === value
        ? state
        : {isFetchingAllConversations: value},
    ),
  saveRecentConversations: data => {
    // Convert data to groups object
    // Note: MessageRangesHandler is updated in Recents.ts after calling this
    const groups = data.reduce((acc, item) => {
      acc[item.group_conversation.id] = {
        groupInfo: item.group_conversation,
        messagesWindow: item.latest_messages,
        pendingMessages: [],
        lastMessageTs: item.latest_messages.at(-1)?.ts || '',
        shouldFetchInitialMessages: false,
      };

      return acc;
    }, {} as {[key: string]: GroupData});

    set(state => {
      return {
        groups: {
          ...state.groups,
          ...groups,
        },
      };
    });
  },
  saveAllConversations: (conversations, latestMessages = []) => {
    set(state => {
      if (conversations.length === 0) {
        return state;
      }

      const latestMessagesByConversationId = new Map(
        latestMessages.map(message => [message.channel, message]),
      );
      const groups: {[key: string]: GroupData} = {};
      for (const conversation of conversations) {
        const currentValue = state.groups[conversation.id];
        const groupData = getUpdatedGroupHelper({
          currentValue,
          newValue: conversation,
        });
        const hasMessagesWindow = currentValue?.messagesWindow?.length;
        const measuredLatestMessage = latestMessagesByConversationId.get(
          conversation.id,
        );
        const messagesWindow = hasMessagesWindow
          ? currentValue.messagesWindow
          : measuredLatestMessage
          ? [measuredLatestMessage]
          : [];
        groups[conversation.id] = {
          ...groupData,
          messagesWindow,
          lastMessageTs: maxUlid(
            groupData.lastMessageTs,
            messagesWindow.at(-1)?.ts || conversation.latest?.ts,
          ),
          shouldFetchInitialMessages: messagesWindow.length <= 1,
        };
      }
      return {
        groups,
      };
    });
  },
  loadFirstPageMessages: async (groupId: string, retryCount = 0) => {
    const {groups} = get();
    if (!groups[groupId]?.shouldFetchInitialMessages) {
      return;
    }

    if (groups[groupId]?.hasFirstPageLoadError) {
      set(state => ({
        groups: {
          ...state.groups,
          [groupId]: {
            ...state.groups[groupId],
            hasFirstPageLoadError: false,
          },
        },
      }));
    }

    const {incPendingAsyncActions, decPendingAsyncActions} = get();
    const firstMessage =
      groups[groupId]?.messagesWindow[0] || groups[groupId]?.pendingMessages[0];

    if (!firstMessage) {
      return;
    }

    try {
      incPendingAsyncActions();

      const {messages, anchor} = await MessagesOrchestrator.loadMessagesAbove(
        groupId,
        firstMessage.ts,
        {timeout: FIRST_PAGE_LOAD_TIMEOUT_MS},
      );

      if (messages.length > 0 || anchor) {
        const hydrationTargets = anchor ? [...messages, anchor] : messages;
        addReactionsFromStores(hydrationTargets);
        AssetsUpdaterService.processAttachments(hydrationTargets);

        const preAwaitGroup = groups[groupId];
        const currentGroup = get().groups[groupId];

        const seen = new Set<string>();
        const allMessages = [
          ...messages,
          ...(anchor ? [anchor] : []),
          ...(preAwaitGroup?.messagesWindow || []),
          ...(preAwaitGroup?.pendingMessages || []),
          ...(currentGroup?.messagesWindow || []),
          ...(currentGroup?.pendingMessages || []),
        ].filter(msg => {
          const key = msg.client_msg_id;
          if (seen.has(key)) {
            return false;
          }
          seen.add(key);
          return true;
        });

        const [pendingMessages, regularMessages] = allMessages.reduce(
          (acc, msg) => {
            if (
              msg.version === LOCAL_INITIAL_VERSION &&
              (msg.status === MessageStatus.Error ||
                msg.status === MessageStatus.Sending)
            ) {
              acc[0].push(PendingAssets.setMessage(msg));
            } else {
              acc[1].push(msg);
            }
            return acc;
          },
          [[], []] as [Message[], Message[]],
        );

        const sortedMessages = regularMessages.sort(sortMessagesByTsHelper);

        set(state => {
          const currentGroupInfo = state.groups[groupId].groupInfo;
          const currentLatest = currentGroupInfo?.latest;
          // Replace latest only if it's the same message (now fuller) or
          // strictly newer. Avoids stomping a fresher one set by a socket
          // event that arrived while the fetch was in-flight.
          const shouldReplaceLatest =
            !!anchor &&
            (!currentLatest ||
              currentLatest.client_msg_id === anchor.client_msg_id ||
              currentLatest.ts < anchor.ts);

          return {
            groups: {
              ...state.groups,
              [groupId]: {
                ...state.groups[groupId],
                groupInfo:
                  shouldReplaceLatest && currentGroupInfo
                    ? {...currentGroupInfo, latest: anchor}
                    : currentGroupInfo,
                messagesWindow: sortedMessages,
                pendingMessages,
                lastMessageTs: maxUlid(
                  state.groups[groupId].lastMessageTs,
                  sortedMessages.at(-1)?.ts || firstMessage.ts,
                ),
                shouldFetchInitialMessages: false,
                hasFirstPageLoadError: false,
              },
            },
          };
        });
      } else {
        set(state => ({
          groups: {
            ...state.groups,
            [groupId]: {
              ...state.groups[groupId],
              shouldFetchInitialMessages: false,
              hasFirstPageLoadError: false,
            },
          },
        }));
      }
    } catch (error) {
      sentryLogs.error({
        message: 'Error loading first page messages',
        module: 'CHATS',
        component: 'useGroups',
        error,
      });
      if (retryCount < MAX_LOAD_FIRST_PAGE_RETRIES) {
        get().loadFirstPageMessages(groupId, retryCount + 1);
      } else {
        set(state => ({
          groups: {
            ...state.groups,
            [groupId]: {
              ...state.groups[groupId],
              hasFirstPageLoadError: true,
            },
          },
        }));
      }
    } finally {
      decPendingAsyncActions();
    }
  },
  loadAboveMessages: debounce(
    async (groupId: string) => {
      const groupData = get().groups[groupId];
      if (!groupData) {
        return;
      }

      const firstMessage = groupData.messagesWindow[0];
      if (!firstMessage) {
        return;
      }

      if (get().isFetchingAbove[groupId]) {
        return;
      }

      set(state => ({
        isFetchingAbove: {
          ...state.isFetchingAbove,
          [groupId]: true,
        },
      }));

      try {
        get().incPendingAsyncActions();

        const {messages} = await MessagesOrchestrator.loadMessagesAbove(
          groupId,
          firstMessage.ts,
        );

        if (messages.length > 0) {
          const [pendingMessages, regularMessages] = messages.reduce(
            (acc, msg) => {
              if (msg.version === LOCAL_INITIAL_VERSION) {
                acc[0].push(PendingAssets.setMessage(msg));
              } else {
                acc[1].push(msg);
              }
              return acc;
            },
            [[], []] as [Message[], Message[]],
          );

          addReactionsFromStores(messages);
          AssetsUpdaterService.processAttachments(messages);

          set(state => {
            if (!state.groups[groupId]) {
              return {};
            }

            const sortedMessages = regularMessages.sort(sortMessagesByTsHelper);

            // Use the original anchor (firstMessage.ts) that we captured before the async call
            // This prevents race conditions where the window changes during the fetch
            const anchorTs = firstMessage.ts;
            const messagesAbove = sortedMessages.filter(
              msg => msg.ts < anchorTs,
            );

            return {
              groups: {
                ...state.groups,
                [groupId]: get().maintainPendingMessages(
                  {
                    ...state.groups[groupId],
                    messagesWindow: [
                      ...messagesAbove,
                      ...state.groups[groupId].messagesWindow,
                    ],
                    pendingMessages: [
                      ...pendingMessages,
                      ...state.groups[groupId].pendingMessages,
                    ],
                  },
                  'above',
                ),
              },
            };
          });
        }
      } catch (error) {
        sentryLogs.error({
          message: 'Error loading above messages',
          module: 'CHATS',
          component: 'useGroups.loadAboveMessages',
          error,
        });
      } finally {
        get().decPendingAsyncActions();
        set(state => ({
          isFetchingAbove: {
            ...state.isFetchingAbove,
            [groupId]: false,
          },
        }));
      }
    },
    DEBOUNCE_TIME,
    {leading: true, trailing: false},
  ),
  loadBelowMessages: debounce(
    async (groupId: string) => {
      const groupData = get().groups[groupId];
      if (!groupData) {
        return;
      }

      const lastMessage = groupData.messagesWindow.at(-1);
      if (!lastMessage) {
        return;
      }

      if (get().isFetchingBelow[groupId]) {
        return;
      }

      set(state => ({
        isFetchingBelow: {
          ...state.isFetchingBelow,
          [groupId]: true,
        },
      }));

      try {
        get().incPendingAsyncActions();

        const {messages} = await MessagesOrchestrator.loadMessagesBelow(
          groupId,
          lastMessage.ts,
        );

        if (messages.length > 0) {
          const [pendingMessages, regularMessages] = messages.reduce(
            (acc, msg) => {
              if (msg.version === LOCAL_INITIAL_VERSION) {
                acc[0].push(PendingAssets.setMessage(msg));
              } else {
                acc[1].push(msg);
              }
              return acc;
            },
            [[], []] as [Message[], Message[]],
          );

          addReactionsFromStores(messages);
          AssetsUpdaterService.processAttachments(messages);

          set(state => {
            if (!state.groups[groupId]) {
              return {};
            }

            const sortedMessages = regularMessages.sort(sortMessagesByTsHelper);

            // Use the original anchor (lastMessage.ts) that we captured before the async call
            // This prevents race conditions where the window changes during the fetch
            const anchorTs = lastMessage.ts;
            const messagesBelow = sortedMessages.filter(
              msg => msg.ts > anchorTs,
            );

            return {
              groups: {
                ...state.groups,
                [groupId]: get().maintainPendingMessages(
                  {
                    ...state.groups[groupId],
                    messagesWindow: [
                      ...state.groups[groupId].messagesWindow,
                      ...messagesBelow,
                    ],
                    pendingMessages: [
                      ...pendingMessages,
                      ...state.groups[groupId].pendingMessages,
                    ],
                  },
                  'below',
                ),
              },
            };
          });
        }
      } catch (error) {
        sentryLogs.error({
          message: 'Error loading below messages',
          module: 'CHATS',
          component: 'useGroups.loadBelowMessages',
          error,
        });
      } finally {
        get().decPendingAsyncActions();
        set(state => ({
          isFetchingBelow: {
            ...state.isFetchingBelow,
            [groupId]: false,
          },
        }));
      }
    },
    DEBOUNCE_TIME,
    {leading: true, trailing: false},
  ),
  initGroups: async () => {
    try {
      const startTime = Date.now();

      sentryLogs.debug({
        message: 'Starting to populate groups from DB',
        module: 'CHATS',
        component: 'useGroups.initGroups',
      });

      const conversations = await getAllConversations();

      sentryLogs.info({
        message: 'Conversations found in DB',
        module: 'CHATS',
        component: 'useGroups.initGroups',
        data: {
          totalConversations: conversations.length,
          totalMs: Date.now() - startTime,
        },
      });

      const updatedState: {[id: string]: GroupData} = {};

      const processAttachmentsData: ProcessAttachmentParamItem[] = [];
      const accumulatedMessages: Message[] = [];
      for (let i = 0; i < conversations.length; i++) {
        const conversation = conversations[i];
        let messages = conversation.messages || [];
        accumulatedMessages.push(...messages);

        const {pendingMessages, filteredMessages, attachments} =
          messages.reduce(
            (acc, msg) => {
              if (
                msg.version === LOCAL_INITIAL_VERSION &&
                (msg.status === MessageStatus.Error ||
                  msg.status === MessageStatus.Sending)
              ) {
                acc.pendingMessages.push(PendingAssets.setMessage(msg));
              } else {
                acc.filteredMessages.push(msg);
              }
              if (msg.attachments?.length) {
                acc.attachments.push({
                  attachments: msg.attachments,
                  messageId: msg.client_msg_id,
                  messageMetadata: {
                    hidden: msg.hidden,
                    channel: conversation.id,
                  },
                });
              }
              return acc;
            },
            {pendingMessages: [], filteredMessages: [], attachments: []} as {
              pendingMessages: Message[];
              filteredMessages: Message[];
              attachments: ProcessAttachmentParamItem[];
            },
          );
        processAttachmentsData.push(...attachments);
        messages = filteredMessages.sort(sortMessagesByTsHelper);

        const isPriorityConversation = i < PRIORITY_CONVERSATION_COUNT;
        // Determines if we need to fetch initial messages via the orchestrator (resolves from DB or server).
        // Priority conversations (top 20) load up to CONVERSATION_MESSAGES_LIMIT msgs from DB,
        // so we only fetch if the window is empty or has fewer than MESSAGES_BATCH_SIZE
        // and the range indicates there are older messages (not at conversation start).
        // Non-priority conversations load only 1 msg from DB, so they always need a fetch.
        const shouldFetchInitialMessages = isPriorityConversation
          ? messages.length === 0 ||
            (messages.length < MESSAGES_BATCH_SIZE &&
              !MessageRangesHandler.isAtConversationStart(
                conversation.id,
                messages[0].ts,
              ))
          : true;

        updatedState[conversation.id] = {
          groupInfo: {
            ...conversation,
            latest: conversation.latest
              ? PendingAssets.setMessage(conversation.latest)
              : null,
          },
          messagesWindow: messages,
          pendingMessages,
          /*
          Important!
          We need to setup lastMessageTs, to avoid calling the setLatestMessages when we send a msg on a first app opening
          without this - we override pendingMessages with empty array
          */
          lastMessageTs: messages.at(-1)?.ts || '',
          shouldFetchInitialMessages,
        };

        // Initialize message range for this conversation
        if (messages.length > 0) {
          MessageRangesHandler.addMessagesAndUpdateRanges(
            conversation.id,
            messages,
            {isEnd: true, isStart: false},
          );
        }
      }
      if (accumulatedMessages.length) {
        addReactionsFromStores(accumulatedMessages);
      }
      if (processAttachmentsData.length) {
        // Register attachments and let FileHandler eagerly prefetch audios
        // (the only kind of attachment auto-downloaded at this stage —
        // images / video posters are served by ExpoImage, video files
        // download on user tap, documents follow the user's auto-download
        // setting).
        fileHandler.processAttachmentsBatch(processAttachmentsData);
      }

      set(state => ({
        groups: {
          ...state.groups,
          ...updatedState,
        },
        isConversationsLoaded: true,
      }));
    } catch (error) {
      sentryLogs.error({
        message: 'Error populating groups from DB',
        module: 'CHATS',
        component: 'useGroups.initGroups',
        error,
        data: {
          currentGroupsCount: Object.keys(get().groups).length,
        },
      });

      // Set error state to show error message in chat list
      set({isError: true});
    }
  },
  incPendingAsyncActions: () =>
    set(state => {
      return {
        numberOfPendingAsyncActions: state.numberOfPendingAsyncActions + 1,
      };
    }),
  decPendingAsyncActions: () =>
    set(state => {
      return {
        numberOfPendingAsyncActions: state.numberOfPendingAsyncActions - 1,
      };
    }),
  enableUpdates: () => {
    set({isUpdatesBlocked: false});
  },
  maintainPendingMessages: (groupData: GroupData) => {
    let pendingMessages = [
      ...(groupData.pendingMessages || []),
      ...(groupData.messagesWindow || []).filter(
        msg => msg.status === MessageStatus.Sending,
      ),
    ];
    const messagesWindow = (groupData.messagesWindow || []).filter(
      msg => msg.status !== MessageStatus.Sending,
    );

    const pendingUq: {[key: string]: Message} = {};
    for (const message of pendingMessages) {
      if (pendingUq[message.client_msg_id]) {
        if (pendingUq[message.client_msg_id].version < message.version) {
          pendingUq[message.client_msg_id] = message;
        }
        continue;
      }
      pendingUq[message.client_msg_id] = message;
    }

    pendingMessages = Object.values(pendingUq);

    pendingMessages = pendingMessages.filter(
      msg =>
        !messagesWindow.find(
          message => message.client_msg_id === msg.client_msg_id,
        ),
    );

    for (const message of messagesWindow) {
      pendingMessages = pendingMessages.filter(
        msg => msg.client_msg_id !== message.client_msg_id,
      );
    }

    return {
      ...groupData,
      pendingMessages,
      messagesWindow,
    };
  },
  clear: () => {
    set(initialState);
  },

  saveUserUpdatesBatch: (users: HuDataMember[]) => {
    set(state => {
      const newConversations: {[key: string]: GroupData} = {};

      // Create a map for faster user lookup
      const userMap = new Map<number, HuDataMember>();
      users.forEach(user => {
        userMap.set(user.id, user);
      });

      // Get a new array of conversations with updated user info
      Object.values(state.groups).forEach(conversation => {
        newConversations[conversation.groupInfo.id] = {
          ...conversation,
        };

        const otherUserId = conversation.groupInfo.other_user?.id;
        if (otherUserId && userMap.has(otherUserId)) {
          newConversations[conversation.groupInfo.id].groupInfo.other_user =
            userMap.get(otherUserId)!;
        }
      });

      return {
        groups: newConversations,
      };
    });
  },
  setTargetMessageId: (ts: string | null) => {
    set(state =>
      state.targetMessageId === ts ? state : {targetMessageId: ts},
    );
  },
  startScrollToMessage: async ({groupId, ts, onScrollShouldStart}) => {
    const groupData = get().groups[groupId];
    if (!groupData) {
      return () => {};
    }

    // Check if message is already in window
    const messageInWindow = groupData.messagesWindow.find(msg => msg.ts === ts);

    if (!messageInWindow) {
      // We need to fetch neighborhood using orchestrator
      try {
        get().incPendingAsyncActions();

        const {messages} = await MessagesOrchestrator.loadMessagesAroundTs(
          groupId,
          ts,
        );

        if (messages.length > 0) {
          addReactionsFromStores(messages);
          AssetsUpdaterService.processAttachments(messages);

          // Sorted new messages window
          const sortedMessages = messages.sort(sortMessagesByTsHelper);

          const messagesTs = new Set(sortedMessages.map(msg => msg.ts));

          //(iOS) Merged current window + new window, needed just for scrolling, then old window will be removed
          const newMessagesWindow = isIos
            ? [
                // Remove messages that have the same ts as the new ones,
                ...groupData.messagesWindow.filter(
                  msg => !messagesTs.has(msg.ts),
                ),
                ...sortedMessages,
              ].sort(sortMessagesByTsHelper)
            : sortedMessages;

          const targetExists = messagesTs.has(ts);

          if (targetExists) {
            isScrollingToMessage = true;

            set(state => ({
              groups: {
                ...state.groups,
                [groupId]: {
                  ...state.groups[groupId],
                  messagesWindow: newMessagesWindow,
                },
              },
            }));

            get().decPendingAsyncActions();
            onScrollShouldStart();

            clearScrollToMessageTimer();

            return () => {
              clearScrollToMessageTimer();

              if (isAndroid) {
                isScrollingToMessage = false;
                return;
              }

              // Needed for iOS only
              scrollingTimerId = setTimeout(() => {
                clearScrollToMessageTimer();

                set(state => ({
                  groups: {
                    ...state.groups,
                    [groupId]: {
                      ...state.groups[groupId],
                      messagesWindow: sortedMessages,
                    },
                  },
                }));

                isScrollingToMessage = false;
              }, 500);
            };
          }
        }

        get().decPendingAsyncActions();
      } catch (e) {
        sentryLogs.error({
          message:
            'Error in startScrollToMessage while fetching messages around ts',
          module: 'CHATS',
          component: 'useGroups',
          error: e,
          data: {groupId, ts},
        });
        get().decPendingAsyncActions();

        isScrollingToMessage = false;

        return () => {};
      }
    }

    isScrollingToMessage = true;

    onScrollShouldStart();
    return () => {
      isScrollingToMessage = false;
    };
  },
  clearScrollToMessage: () => {
    isScrollingToMessage = false;

    set(state =>
      state.targetMessageId === null ? state : {targetMessageId: null},
    );
  },
  setScrollToBottom: (groupId: string, scrollToBottom: boolean) => {
    set(state => ({
      groups: {
        ...state.groups,
        [groupId]: {
          ...state.groups[groupId],
          scrollToBottom,
        },
      },
    }));
  },
  registerScrollToBottom: (groupId: string, callback: () => void) => {
    set(state => ({
      scrollToBottomCallbacks: {
        ...state.scrollToBottomCallbacks,
        [groupId]: callback,
      },
    }));
  },
  unregisterScrollToBottom: (groupId: string) => {
    set(state => {
      const newCallbacks = {...state.scrollToBottomCallbacks};
      delete newCallbacks[groupId];
      return {scrollToBottomCallbacks: newCallbacks};
    });
  },
  setIsScrolling: (isScrolling: boolean) => {
    isScrollingToMessage = isScrolling;
  },
  updateSentMessage: (message: Message) => {
    set(state => {
      const group = state.groups[message.channel];
      if (!group) {
        return state;
      }
      return {
        groups: {
          ...state.groups,
          [message.channel]: {
            ...group,
            groupInfo: {
              ...group.groupInfo,
              latest:
                group.groupInfo.latest?.client_msg_id === message.client_msg_id
                  ? message
                  : group.groupInfo.latest,
            },
            pendingMessages: (group.pendingMessages || []).map(msg =>
              msg.client_msg_id === message.client_msg_id ? message : msg,
            ),
          },
        },
      };
    });
  },
  updateDeliverStatus: ({
    fromMessageTs,
    toMessageTs,
    channelId,
    deliverStatus,
  }: {
    fromMessageTs: string;
    toMessageTs: string;
    channelId: string;
    deliverStatus: DeliverStatus;
  }) => {
    set(state => {
      const group = state.groups[channelId];
      let latest = group?.groupInfo?.latest;
      if (!group) {
        return state;
      }
      if (
        (latest?.ts || '') >= fromMessageTs &&
        (latest?.ts || '') <= toMessageTs &&
        latest?.user === persistedUserId?.toString()
      ) {
        const newDeliverStatus = {
          read_at: deliverStatus.read_at || latest?.deliver_status?.read_at,
          received_at:
            deliverStatus.received_at || latest?.deliver_status?.received_at,
        };
        latest = {
          ...latest,
          deliver_status: newDeliverStatus,
        } as ConversationLatest;
      }
      return {
        groups: {
          ...state.groups,
          [channelId]: {
            ...group,
            messagesWindow:
              group.messagesWindow?.map(msg => {
                if (
                  (msg.ts || '') >= fromMessageTs &&
                  (msg.ts || '') <= toMessageTs &&
                  msg.user === persistedUserId?.toString()
                ) {
                  const newDeliverStatus = {
                    read_at:
                      deliverStatus.read_at || msg.deliver_status?.read_at,
                    received_at:
                      deliverStatus.received_at ||
                      msg.deliver_status?.received_at,
                  };
                  return {
                    ...msg,
                    deliver_status: newDeliverStatus,
                  };
                }
                return msg;
              }) || [],
            groupInfo: {
              ...group.groupInfo,
              latest: latest,
            },
          },
        },
      };
    });
  },
  updateAssets: ({assets, messageId, channelId}) => {
    set(state => {
      const group = state.groups[channelId];
      if (!group) {
        return state;
      }

      // Verificar si el mensaje está en la messagesWindow
      const msgIndex = group.messagesWindow?.findIndex(
        msg => msg.client_msg_id === messageId,
      );
      if (msgIndex === -1) {
        return state;
      }

      const messageInWindow = [...(group.messagesWindow || [])] as Message[];

      messageInWindow[msgIndex] = {
        ...messageInWindow[msgIndex],
        attachments: assets,
      };

      return {
        ...state,
        groups: {
          ...state.groups,
          [channelId]: {
            ...group,
            messagesWindow: messageInWindow,
          },
        },
      };
    });
  },
  setMessagesAsLatest: (messages: Message[]) => {
    set(state => {
      const newGroups = {...state.groups};
      for (const message of messages) {
        const group = newGroups[message.channel];
        if (!group?.groupInfo) {
          continue;
        }
        let latest = group.groupInfo.latest;

        // Update latest if it is not set or if the new message is newer
        if (!group.groupInfo.latest || group.groupInfo.latest.ts < message.ts) {
          latest = message;
        }

        newGroups[message.channel] = {
          ...group,
          groupInfo: {...group.groupInfo, latest},
        };
      }
      return {
        groups: newGroups,
      };
    });
  },
  loadBelowMessagesIfNeeded: (channel: string, latestId?: string) => {
    const {groups, loadBelowMessages} = get();
    const conversation = groups[channel];

    if (!conversation) {
      return;
    }

    const messages = sortMessages(
      conversation.messagesWindow,
      conversation.pendingMessages,
    );
    const lastMessage = messages.at(-1);
    const latestMessageId =
      latestId || conversation?.groupInfo.latest?.client_msg_id;

    if (
      !!lastMessage &&
      !!latestMessageId &&
      lastMessage.client_msg_id !== latestMessageId
    ) {
      sentryLogs.info({
        message: 'Missed new messages, loading below messages',
        module: 'CHATS',
        component: 'LoadBelowMessagesIfNeeded',
        data: {
          channel,
          latestMessageFromConversation: latestMessageId,
          latestMessageFromMessages: lastMessage?.client_msg_id || '',
        },
      });
      loadBelowMessages(channel);
    }
  },
  setIsError: (value: boolean) => {
    set(state => (state.isError === value ? state : {isError: value}));
  },
  updateMessageHeight: (
    channelId: string,
    client_msg_id: string,
    height: number,
  ) => {
    set(state => {
      const group = state.groups[channelId];
      if (!group) {
        return state;
      }

      const updatedMessageWindow = group.messagesWindow?.map(msg => {
        if (msg.client_msg_id === client_msg_id) {
          return {
            ...msg,
            height,
          };
        }
        return msg;
      });

      return {
        groups: {
          ...state.groups,
          [channelId]: {
            ...group,
            messagesWindow: updatedMessageWindow,
          },
        },
      };
    });
  },
}));

let pendingUpdates: Array<() => void> = [];
const isWaitingHolder = {value: true};

const flushPendingUpdates = () => {
  const copy = [...pendingUpdates];
  pendingUpdates = [];

  unstable_batchedUpdates(() => {
    copy.forEach(update => update());
  });
};

useGroups.subscribe(state => {
  isWaitingHolder.value =
    state.numberOfPendingAsyncActions > 0 || state.isUpdatesBlocked;
  if (isWaitingHolder.value === false) {
    flushPendingUpdates();
    return;
  }
});

export const updateGroup = (groupInfo: Conversation) => {
  if (isWaitingHolder.value) {
    useGroups.getState().updateGroup(groupInfo);
    pendingUpdates.push(() => useGroups.getState().updateGroup(groupInfo));
    return;
  }
  useGroups.getState().updateGroup(groupInfo);
};

export const deleteGroup = (groupId: string) => {
  if (isWaitingHolder.value) {
    pendingUpdates.push(() => deleteGroup(groupId));
    return;
  }
  useGroups.getState().deleteGroup(groupId);
};

export const updateMessage = (message: Message) => {
  if (isWaitingHolder.value) {
    useGroups.getState().updateMessage(message);
    pendingUpdates.push(() => useGroups.getState().updateMessage(message));
    return;
  }
  useGroups.getState().updateMessage(message);
};

export const saveUserUpdatesBatch = (users: HuDataMember[]) => {
  useGroups.getState().saveUserUpdatesBatch(users);
};

export const updateSentMessage = (message: Message) => {
  useGroups.getState().updateSentMessage(message);
};

export const clearScrollToMessage = (
  groupId: string,
  shouldResetMessageWindow?: boolean,
) => {
  useGroups.getState().clearScrollToMessage();
  shouldResetMessageWindow && resetMessageWindow(groupId);
};

export const resetMessageWindow = async (groupId: string) => {
  // Wait for all messages sent to this group to be processed
  await BlockingLatch.wait(groupId);
  await useGroups.getState().resetConversationWithLatestMessages(groupId);
};

/**
 * Checks if the current messagesWindow represents the latest page of messages.
 * Returns false when the user navigated to a distant/old message window (e.g. via scrollToMessage).
 */
export const isInLatestMessageWindow = (groupId: string): boolean => {
  const group = useGroups.getState().groups[groupId];
  if (!group) {
    return true;
  }

  const lastWindowTs = group.messagesWindow.at(-1)?.ts;
  if (!lastWindowTs || !group.lastMessageTs) {
    return true;
  }

  return lastWindowTs === group.lastMessageTs;
};

export const setScrollToBottom = (groupId: string, scrollToBottom: boolean) => {
  useGroups.getState().setScrollToBottom(groupId, scrollToBottom);
};

export const registerScrollToBottom = (
  groupId: string,
  callback: () => void,
) => {
  useGroups.getState().registerScrollToBottom(groupId, callback);
};

export const unregisterScrollToBottom = (groupId: string) => {
  useGroups.getState().unregisterScrollToBottom(groupId);
};

export const resetConversationMentioned = (channelId: string) => {
  const conversation = useGroups.getState().groups[channelId]?.groupInfo;
  if (!conversation || !conversation.mentioned) {
    return;
  }

  updateGroup({
    ...conversation,
    mentioned: false,
  });
};

function getUpdatedGroupHelper({
  currentValue,
  newValue,
}: {
  currentValue?: GroupData;
  newValue: Conversation;
}): GroupData {
  if (!currentValue) {
    return {
      groupInfo: newValue,
      messagesWindow: [],
      pendingMessages: [],
      lastMessageTs: '',
      shouldFetchInitialMessages: false,
    };
  }

  if (!currentValue.groupInfo) {
    return {
      ...currentValue,
      groupInfo: newValue,
    };
  }

  const shouldUseNewValue = newValue.version >= currentValue.groupInfo.version;
  const groupInfo = shouldUseNewValue ? newValue : currentValue.groupInfo;

  let latest = currentValue.groupInfo.latest;

  if (
    shouldUseNewValue &&
    (latest?.ts === undefined ||
      (newValue.latest?.ts && newValue.latest.ts > latest.ts))
  ) {
    latest = newValue.latest;
  }

  return {
    ...currentValue,
    groupInfo: {
      ...groupInfo,
      latest: latest,
    },
  };
}

export const updateDeliverStatus = (params: {
  fromMessageTs: string;
  toMessageTs: string;
  channelId: string;
  deliverStatus: DeliverStatus;
}) => {
  useGroups.getState().updateDeliverStatus(params);
};

export const updateAssets = (params: {
  assets: Asset[];
  messageId: string;
  channelId: string;
}) => {
  useGroups.getState().updateAssets(params);
};

export const checkIfNeedLoadInitialMessages = (channel: string): boolean => {
  const conversation = useGroups.getState().groups[channel];
  const totalMessages = conversation?.messagesWindow.length || 0;
  return totalMessages <= 1;
};

export const loadInitialMessagesIfNeeded = (channel: string) => {
  const {loadFirstPageMessages} = useGroups.getState();
  loadFirstPageMessages(channel);
};

export const saveRecentConversations = (
  data: {
    group_conversation: Conversation;
    latest_messages: Message[];
  }[],
) => {
  useGroups.getState().saveRecentConversations(data);
};

export const setMessagesAsLatest = (messages: Message[]) => {
  if (isWaitingHolder.value) {
    useGroups.getState().setMessagesAsLatest(messages);
    pendingUpdates.push(() =>
      useGroups.getState().setMessagesAsLatest(messages),
    );
    return;
  }
  useGroups.getState().setMessagesAsLatest(messages);
};

export const loadBelowMessagesIfNeeded = (
  channel: string,
  latestId?: string,
) => {
  useGroups.getState().loadBelowMessagesIfNeeded(channel, latestId);
};

export const updateMessageHeightInStore = (
  channel: string,
  client_msg_id: string,
  height: number,
) => {
  useGroups.getState().updateMessageHeight(channel, client_msg_id, height);
};

export const useIsChatUserDeactivated = (channel: string) => {
  return isChatUserDeactivated(
    useGroups.getState().groups[channel]?.groupInfo?.other_user?.status,
  );
};
