import { type InfiniteData } from 'react-query';

import { type AxiosResponse } from 'axios';
import { ulid } from 'ulid';

import { queryClient } from 'src/config/react-query';
import {
  getConversationsInfo,
  SERVICE_BASE_URL,
} from 'src/pages/dashboard/Conversations/services';
import {
  type Channel,
  type ConversationEvents,
  type ConversationMemberResponse,
  type ConversationMessage,
  type ConversationsInfoResponse,
  type ConversationsMessagesResponse,
  type ConversationsResponse,
  type ConversationsUnreadsResponse,
  HighlightedSpanType,
  type Membership,
  type MessageReaction,
  MessageTypeSocket,
  type ParamsReaction,
  type ReadStatus,
} from 'src/pages/dashboard/Conversations/types';
import {
  compareIds,
  createObjectLatest,
  findInsertIndexForMessage,
  flatMapPages,
  isNewerMessage,
  requireUpdate,
} from 'src/pages/dashboard/Conversations/utils';
import { isUnixTimestampAlmostEqualToNow } from 'src/utils/date';

type ConversationsListData = InfiniteData<AxiosResponse<ConversationsResponse>>;
type MessagesListData = InfiniteData<
  AxiosResponse<ConversationsMessagesResponse>
>;
type MembersListData = InfiniteData<AxiosResponse<ConversationMemberResponse>>;

type TypedQueryKey<T = unknown> = (string | number | boolean)[] & {
  __type?: T;
};

type UnreadMessages = {
  channelId: string;
  markedUnreadTs?: string;
};

type SetUnreadMessagesParams = {
  channelId: string;
  count?: number;
  replace?: boolean;
};

type UpdateGroupData = {
  id: string;
  newName?: string;
  newPictureId?: string;
  newDescription?: string;
  version?: number;
};

export const conversationKeys = {
  all: (): TypedQueryKey => ['conversations'],
  list: (name = ''): TypedQueryKey<ConversationsListData> => [
    ...conversationKeys.all(),
    'list',
    name,
  ],
  users: (filter = ''): TypedQueryKey => [
    ...conversationKeys.all(),
    filter,
    'users',
  ],
  messages: {
    all: (id: string): TypedQueryKey<MessagesListData> => [
      ...conversationKeys.all(),
      'messages',
      id,
    ],
    search: (id: string, query: string): TypedQueryKey => [
      ...conversationKeys.messages.all(id),
      'search',
      query,
    ],
    pinned: (id: string): TypedQueryKey<ConversationMessage[]> => [
      ...conversationKeys.messages.all(id),
      'pinned',
    ],
    file: (
      id: string,
      url: string,
      imageClass = '',
    ): TypedQueryKey<ConversationMessage[]> => [
      ...conversationKeys.messages.all(id),
      'file',
      url,
      imageClass,
    ],
  },
  members: {
    all: (): TypedQueryKey => [...conversationKeys.all(), 'members'],
    list: (id: string, filter = ''): TypedQueryKey<MembersListData> => [
      ...conversationKeys.all(),
      filter,
      id,
    ],
    readMessage: (channelId: string, messageTs: string): TypedQueryKey => [
      ...conversationKeys.all(),
      channelId,
      messageTs,
      'memberReadMessage',
    ],
  },
  unreadChannels: (): TypedQueryKey<
    InfiniteData<AxiosResponse<ConversationsUnreadsResponse>>
  > => [...conversationKeys.all(), 'unreadChannels'],
  info: (
    id: string,
  ): TypedQueryKey<AxiosResponse<ConversationsInfoResponse>> => [
    ...conversationKeys.all(),
    id,
    'info',
  ],
  file: (url: string, imageClass = ''): TypedQueryKey => [
    ...conversationKeys.all(),
    url,
    'file',
    imageClass,
  ],
  poster: (url: string): TypedQueryKey => [
    ...conversationKeys.all(),
    url,
    'poster',
  ],
  reactions: (
    channel: string,
    ts: string,
  ): TypedQueryKey<MessageReaction[]> => [
    ...conversationKeys.all(),
    channel,
    ts,
    'reactions',
  ],
  events: (): TypedQueryKey<ConversationEvents> => [
    ...conversationKeys.all(),
    'events',
  ],
};

/** Typed wrapper around queryClient.getQueryData */
export const getQueryData = <T>(queryKey: TypedQueryKey<T>): T | undefined =>
  queryClient.getQueryData<T>(queryKey);

/** Read → transform → write cache entry. Returns false if key has no data. */
const updateQueryCache = <T>(
  queryKey: TypedQueryKey<T>,
  updater: (data: T) => T,
): boolean => {
  const current = queryClient.getQueryData<T>(queryKey);
  if (current === undefined) return false;
  queryClient.setQueryData(queryKey, updater(current));
  return true;
};

/** Find a channel within paginated conversation pages */
const findChannelInPages = (
  pages: AxiosResponse<ConversationsResponse>[],
  channelId: string,
) => {
  const pageIndex = pages.findIndex(page =>
    page.data.channels?.some((channel: Channel) => channel.id === channelId),
  );

  if (pageIndex === -1) return null;

  const channelIndex = pages[pageIndex].data.channels?.findIndex(
    (channel: Channel) => channel.id === channelId,
  );

  if (channelIndex == null || channelIndex === -1) return null;

  return {
    pageIndex,
    channelIndex,
    channel: pages[pageIndex].data.channels[channelIndex],
  };
};

const updatePageChannels = (
  page: AxiosResponse<ConversationsResponse>,
  updateFn: (channels: Channel[]) => Channel[],
) => ({
  ...page,
  data: { ...page.data, channels: updateFn(page.data.channels) },
});

const mapMemberships = (
  data: MembersListData,
  updateFn: (memberships: Membership[]) => Membership[],
  targetPage?: number,
): MembersListData => ({
  ...data,
  pages: data.pages.map((page, index) =>
    targetPage !== undefined && index !== targetPage
      ? page
      : {
          ...page,
          data: { ...page.data, memberships: updateFn(page.data.memberships) },
        },
  ),
});

const updateReadStatus = (
  existingStatus: ReadStatus | undefined,
  shouldApply: boolean,
  readAt?: number,
  receivedAt?: number,
): ReadStatus => ({
  ...existingStatus,
  ...(readAt &&
    shouldApply && {
      read_at: Math.max(existingStatus?.read_at ?? 0, readAt),
      received_at: Math.max(existingStatus?.received_at ?? 0, readAt),
    }),
  ...(receivedAt &&
    shouldApply && {
      received_at: Math.max(existingStatus?.received_at ?? 0, receivedAt),
    }),
});

const updateMessageReadStatusCache = (
  msg: ConversationMessage,
  toMessageTs: string,
  readAt?: number,
  receivedAt?: number,
) => {
  const isNewer = isNewerMessage({
    newTs: msg.ts,
    currentTs: toMessageTs,
  });
  return {
    ...msg,
    hu_data: {
      ...msg.hu_data,
      read_status: updateReadStatus(
        msg.hu_data.read_status,
        !isNewer,
        readAt,
        receivedAt,
      ),
    },
  };
};

const updateListDelete =
  (message: ConversationMessage) =>
  (conv: Channel): Channel => {
    const isNewer = isNewerMessage({
      newTs: message.ts,
      currentTs: conv.latest.ts,
    });
    if (!isNewer) return conv;
    return {
      ...conv,
      latest: {
        ...conv.latest,
        subtype: MessageTypeSocket.DELETED,
        blocks: undefined,
        type: MessageTypeSocket.MESSAGE,
        text: '',
      },
    };
  };

const updateListEdited =
  (message: ConversationMessage) =>
  (conv: Channel): Channel => {
    const isNewer = isNewerMessage({
      newTs: message.ts,
      currentTs: conv.latest.ts,
    });

    if (!isNewer) return conv;
    return {
      ...conv,
      latest: {
        ...conv.latest,
        edited: message.edited,
        ts: message.edited?.ts || message.hu_data.message_ts,
        text: message.text || '',
        attachments: message.attachments,
      },
    };
  };

const hasOlderVersion = (
  cached: MessageReaction[],
  incoming: MessageReaction[],
): boolean =>
  incoming.some(r => {
    const match = cached.find(c => c.name === r.name);
    return match && r.version < match.version;
  });

const getConversationListData = (name = '') =>
  getQueryData(conversationKeys.list(name));

/** Returns all active list cache key suffixes (e.g. "", "pro", "proj") */
const getAllConversationListKeys = (): string[] => {
  const queries = queryClient.getQueriesData<ConversationsListData>({
    queryKey: ['conversations', 'list'],
  });

  const filteredQueries = queries
    .filter(([, data]) => data !== undefined)
    .map(([queryKey]) => (queryKey[2] as string) ?? '');

  return [...new Set(filteredQueries)];
};

const getConversationInfoData = (id: string) =>
  getQueryData(conversationKeys.info(id));

export const getConversationMembersData = (id: string) =>
  getQueryData(conversationKeys.members.list(id, ''));

export const getConversationInfo = (id: string): Channel | null =>
  getConversationInfoData(id)?.data.channel ?? null;

export const getConversationListMessages = (
  id: string,
): ConversationMessage[] => {
  const messageData = getQueryData(conversationKeys.messages.all(id));
  return messageData?.pages.flatMap(page => page.data.messages) ?? [];
};

export const getMessageInCache = (channelId: string, messageId: string) => {
  const messageData = getQueryData(conversationKeys.messages.all(channelId));
  if (!messageData) return;
  return messageData.pages
    .flatMap(page => page.data.messages)
    .find(msg => msg.hu_data.message_ts === messageId);
};

export const getChannelFromList = (channelId: string): Channel | undefined => {
  const pageListData = getConversationListData();
  if (!pageListData) return undefined;
  return findChannelInPages(pageListData.pages, channelId)?.channel;
};

export const getConversationEvents = (initialTs: string) =>
  getQueryData(conversationKeys.events()) || {
    currentTs: initialTs,
    events: [],
  };

/** For each cached list that contains the channel, run the updater on the full list data */
const forEachListWithChannel = (
  channelId: string,
  updater: (
    data: ConversationsListData,
    channelInfo: { pageIndex: number; channelIndex: number; channel: Channel },
  ) => ConversationsListData,
) => {
  const listKeys = getAllConversationListKeys();
  for (const listKey of listKeys) {
    updateQueryCache(conversationKeys.list(listKey), data => {
      const channelInfo = findChannelInPages(data.pages, channelId);
      if (!channelInfo) return data;
      return updater(data, channelInfo);
    });
  }
};

const updateConversationInfoChannel = (
  channelId: string,
  updateFn: (channel: Channel) => Channel,
) => {
  updateQueryCache(conversationKeys.info(channelId), info => ({
    ...info,
    data: { ...info.data, channel: updateFn(info.data.channel) },
  }));
};

/** Apply an updateFn to a channel across all cached conversation lists */
const updateChannelInAllLists = ({
  channelId,
  updateFn,
  messageTs,
  newVersion,
}: {
  channelId: string;
  updateFn: (channel: Channel) => Channel;
  messageTs?: string;
  newVersion?: number;
}) => {
  forEachListWithChannel(channelId, (data, channelInfo) => {
    const { channel } = channelInfo;
    const isNewer = isNewerMessage({
      newTs: messageTs,
      currentTs: channel.latest?.ts,
    });
    const shouldUpdate = requireUpdate(newVersion, channel.latest?.version);
    const isLatestMessage = channel.latest?.ts === messageTs;

    if (messageTs && !isLatestMessage && (!isNewer || !shouldUpdate)) {
      return data;
    }

    return {
      ...data,
      pages: data.pages.map((page, i) =>
        i !== channelInfo.pageIndex
          ? page
          : updatePageChannels(page, chs =>
              chs.map((ch, j) =>
                j !== channelInfo.channelIndex ? ch : updateFn(ch),
              ),
            ),
      ),
    };
  });
};

const movePinnedAtFirstInAllLists = (channelId: string) => {
  forEachListWithChannel(
    channelId,
    (data, { channel, channelIndex, pageIndex }) => ({
      ...data,
      pages: data.pages.map((page, i) => {
        const isFirst = i === 0;
        const isSource = i === pageIndex;
        if (!isFirst && !isSource) return page;
        return updatePageChannels(page, chs => {
          const removed = isSource
            ? chs.filter((_, j) => j !== channelIndex)
            : chs;
          return isFirst ? [channel, ...removed] : removed;
        });
      }),
    }),
  );
};

const insertChannelAfterPinnedChannels = ({
  list,
  listKey,
  channelInfo,
}: {
  list: ConversationsListData;
  listKey: string;
  channelInfo: {
    pageIndex?: number;
    channelIndex?: number;
    channel: Channel;
  };
}) => {
  const { pageIndex, channelIndex, channel } = channelInfo;
  const matchesSearch =
    listKey === '' ||
    channel.name?.toLowerCase().includes(listKey.toLowerCase());

  if (!matchesSearch) {
    return;
  }

  const firstPageChannels = list.pages[0]?.data.channels ?? [];

  if (channel?.properties?.pinned_at) {
    const updatedPages = list.pages.map((page, i) =>
      i !== 0
        ? page
        : updatePageChannels(page, chs =>
            chs.map(ch => (ch.id === channel.id ? channel : ch)),
          ),
    );
    queryClient.setQueryData(conversationKeys.list(listKey), {
      ...list,
      pages: updatedPages,
    });
    return;
  }

  const removedFromSource = list.pages.map((page, i) => {
    if (
      pageIndex === undefined ||
      channelIndex === undefined ||
      i !== pageIndex
    )
      return page;
    return updatePageChannels(page, chs =>
      chs.filter((_, j) => j !== channelIndex),
    );
  });

  const insertIndex = firstPageChannels.filter(
    (ch, j) =>
      ch.properties?.pinned_at && !(j === channelIndex && pageIndex === 0),
  ).length;

  const updatedPages = removedFromSource.map((page, i) =>
    i !== 0
      ? page
      : updatePageChannels(page, chs => [
          ...chs.slice(0, insertIndex),
          channel,
          ...chs.slice(insertIndex),
        ]),
  );

  queryClient.setQueryData(conversationKeys.list(listKey), {
    ...list,
    pages: updatedPages,
  });
};

const updateLastPage = (
  channelId: string,
  updateFn: (messages: ConversationMessage[]) => ConversationMessage[],
) => {
  updateQueryCache(conversationKeys.messages.all(channelId), data => {
    const lastIndex = data.pages.length - 1;
    return {
      ...data,
      pages: data.pages.map((page, i) =>
        i !== lastIndex
          ? page
          : {
              ...page,
              data: { ...page.data, messages: updateFn(page.data.messages) },
            },
      ),
    };
  });
};

/** Apply an updater to messages across all pages, optionally filtered by findMessage */
const updateMessageInCache = (
  channelId: string,
  updateMessage: (msg: ConversationMessage) => ConversationMessage,
  findMessage?: (msg: ConversationMessage) => boolean,
) => {
  updateQueryCache(conversationKeys.messages.all(channelId), data => ({
    ...data,
    pages: data.pages.map(page => {
      if (findMessage && !page.data.messages.some(findMessage)) return page;
      return {
        ...page,
        data: {
          ...page.data,
          messages: page.data.messages.map(msg =>
            !findMessage || findMessage(msg) ? updateMessage(msg) : msg,
          ),
        },
      };
    }),
  }));
};

const updateUnreadInfoCount = (
  channelId: string,
  count: number | ((prev: number) => number),
  markUnreadAt?: number,
) => {
  updateConversationInfoChannel(channelId, channel => {
    const newCount =
      typeof count === 'function' ? count(channel.unread_count_display) : count;

    return {
      ...channel,
      unread_count_display: newCount,
      hu_data: {
        ...channel.hu_data,
        mentioned: newCount > 0 ? channel.hu_data.mentioned : false,
      },
      properties: {
        ...channel.properties,
        marked_unread_at: markUnreadAt,
      },
    };
  });
};

const updateNumMembers = (channelId: string, delta: number) => {
  updateConversationInfoChannel(channelId, channel => ({
    ...channel,
    num_members: Math.max(0, (channel.num_members ?? 0) + delta),
  }));
};

const syncReactionsCaches = (
  channel: string,
  ts: string,
  reactions: MessageReaction[],
) => {
  updateMessageInCache(
    channel,
    msg => ({ ...msg, reactions }),
    msg => msg.hu_data.message_ts === ts,
  );

  updateQueryCache(conversationKeys.reactions(channel, ts), () => reactions);
};

const fetchConversation = async (channelId: string, callback: () => void) => {
  try {
    const { data } = await getConversationsInfo({
      params: { channel: channelId },
      headers: {
        'X-Request-Id': ulid(),
        'X-Call-Site': 'updateChannelProperties',
      },
    });
    return data.channel;
  } catch {
    callback();
  }
};

export const updateChannelProperties = ({
  pinnedAt,
  channelId,
  version,
  updateFn,
}: {
  pinnedAt?: number;
  channelId: string;
  version?: number;
  updateFn: (channel: Channel) => Channel;
}) => {
  const conversationsList = getConversationListData();
  const conversationInfo = getConversationInfoData(channelId);
  if (!conversationsList) return;

  const channelInfo = findChannelInPages(conversationsList.pages, channelId);
  if (channelInfo) {
    const shouldUpdate = requireUpdate(
      version,
      channelInfo.channel.properties?.version,
    );
    if (!shouldUpdate) return;
  }

  const didPinnedChange =
    channelInfo?.channel.properties.pinned_at !== pinnedAt;
  const isMissingFromRootList = !channelInfo;
  updateChannelInAllLists({ channelId, updateFn });

  if (didPinnedChange) {
    if (pinnedAt) {
      movePinnedAtFirstInAllLists(channelId);

      if (isMissingFromRootList) {
        queryClient.invalidateQueries(conversationKeys.list(), {
          exact: true,
          refetchInactive: true,
        });
      }
    } else {
      const listKeys = getAllConversationListKeys();
      for (const listKey of listKeys) {
        queryClient.invalidateQueries(conversationKeys.list(listKey));
      }
    }
  }

  if (!conversationInfo) return;
  conversationInfo.data.channel = updateFn(conversationInfo.data.channel);
  queryClient.setQueryData(conversationKeys.info(channelId), conversationInfo);
};

/** Reposition a conversation after a new message. Fetches channel data if not cached. */
export const changePositionOfConversation = async ({
  channelId,
  message,
  callback,
  userId,
}: {
  channelId: string;
  message: ConversationMessage;
  callback: () => void;
  userId?: number;
}) => {
  const listKeys = getAllConversationListKeys();
  let fetchedChannel: Channel | undefined;
  let fetchAttempted = false;

  for (const listKey of listKeys) {
    const conversationsList = getConversationListData(listKey);
    if (!conversationsList) continue;

    let channelInfo: {
      pageIndex?: number;
      channelIndex?: number;
      channel: Channel;
    } | null = findChannelInPages(conversationsList.pages, channelId);

    if (!channelInfo) {
      if (!fetchAttempted) {
        fetchAttempted = true;
        fetchedChannel = await fetchConversation(channelId, callback);
      }
      if (!fetchedChannel) continue;

      const matchesSearch =
        listKey === '' ||
        fetchedChannel.name?.toLowerCase().includes(listKey.toLowerCase());
      if (!matchesSearch) continue;

      const createdAt = fetchedChannel.hu_data.created_at;
      const isNewConversation =
        createdAt && isUnixTimestampAlmostEqualToNow(createdAt, 10);

      channelInfo = {
        channel: {
          ...fetchedChannel,
          unread_count_display: isNewConversation
            ? 0
            : Math.max(fetchedChannel.unread_count_display - 1, 0),
        },
      };
    } else {
      const isNewer = isNewerMessage({
        newTs: message.ts,
        currentTs: channelInfo.channel.latest.ts,
      });
      if (!isNewer) continue;
    }

    const isMentioned = message.hu_data.highlighted_spans?.some(
      span =>
        (span.type === HighlightedSpanType.CHANNEL_MENTION && !span.user_id) ||
        (span.type === HighlightedSpanType.USER_MENTION &&
          compareIds(span.user_id, userId)),
    );

    const isUserSender = compareIds(message.user, userId);

    const updatedChannel: Channel = {
      ...channelInfo.channel,
      hu_data: {
        ...channelInfo.channel.hu_data,
        ...(isMentioned && !isUserSender && { mentioned: true }),
      },
      latest: createObjectLatest(message),
    };

    insertChannelAfterPinnedChannels({
      list: conversationsList,
      listKey,
      channelInfo: {
        ...channelInfo,
        channel: updatedChannel,
      },
    });
  }
};

export const removeConversationFromAllLists = (channelId: string) => {
  forEachListWithChannel(channelId, (data, channelInfo) => ({
    ...data,
    pages: data.pages.map((page, i) =>
      i !== channelInfo.pageIndex
        ? page
        : updatePageChannels(page, chs =>
            chs.filter((_, j) => j !== channelInfo.channelIndex),
          ),
    ),
  }));
};

/** Remove a conversation from the lists cache and drop its info + members queries */
export const removeConversationFromCache = (channelId: string) => {
  removeConversationFromAllLists(channelId);
  queryClient.removeQueries(conversationKeys.members.list(channelId, ''));
  queryClient.removeQueries(conversationKeys.info(channelId));
};

/**
 * Sync a channel's presence in the `unreadChannels` cache.
 *
 * Idempotent: no-op when the cache already matches the desired state.
 *
 * @param channelId - The channel to track / untrack.
 * @param remove    - When true, untrack the channel; otherwise, track it.
 */
export const updateGlobalUnreadChannels = ({
  channelId,
  remove = false,
}: {
  channelId: string;
  remove?: boolean;
}) => {
  updateQueryCache(conversationKeys.unreadChannels(), data => {
    const isTracked = flatMapPages(
      data.pages,
      page => page.data.channels,
    ).includes(channelId);

    if (isTracked === !remove) return data;

    return {
      ...data,
      pages: data.pages.map((page, index) => {
        let nextChannels = page.data.channels;
        if (remove) {
          nextChannels = page.data.channels.filter(ch => ch !== channelId);
        } else if (index === 0) {
          nextChannels = [channelId, ...page.data.channels];
        }

        return {
          ...page,
          data: {
            ...page.data,
            ...(index === 0 && {
              unread_count: Math.max(
                0,
                page.data.unread_count + (remove ? -1 : 1),
              ),
            }),
            channels: nextChannels,
          },
        };
      }),
    };
  });
};

/**
 * Refetch every loaded page of the `unreadChannels` infinite query through
 * react-query's native code path, keeping the cache shape in sync with
 * `useInfiniteQuery` consumers.
 */
export const refetchUnreadChannels = () =>
  queryClient.refetchQueries(conversationKeys.unreadChannels());

export const addMessage = ({
  id,
  message,
  isSendMessage = false,
}: {
  id: string;
  message: ConversationMessage;
  isSendMessage?: boolean;
}) => {
  updateLastPage(id, messages => {
    const insertIndex = isSendMessage
      ? messages.length
      : findInsertIndexForMessage(messages, message);

    return [
      ...messages.slice(0, insertIndex),
      message,
      ...messages.slice(insertIndex),
    ];
  });
};

export const replaceMessage = ({
  channelId,
  messageInternId,
  newMessage,
}: {
  channelId: string;
  messageInternId: string;
  newMessage: ConversationMessage;
}) => {
  updateLastPage(channelId, messages =>
    messages.map(msg =>
      msg.client_msg_id === messageInternId ? newMessage : msg,
    ),
  );
};

export const updateLatestMessageInCache = (
  channelId: string,
  message: ConversationMessage,
) => {
  updateConversationInfoChannel(channelId, channel => ({
    ...channel,
    latest: createObjectLatest(message),
  }));
};

export const updateMessageReadStatus = (
  channelId: string,
  toMessageTs: string,
  readAt?: number,
  receivedAt?: number,
) => {
  updateMessageInCache(channelId, msg =>
    updateMessageReadStatusCache(msg, toMessageTs, readAt, receivedAt),
  );

  updateChannelInAllLists({
    channelId,
    updateFn: channel => {
      const isLatestCoveredByRead = !isNewerMessage({
        newTs: channel.latest.ts,
        currentTs: toMessageTs,
      });
      return {
        ...channel,
        latest: {
          ...channel.latest,
          read_status: updateReadStatus(
            channel.latest.read_status,
            isLatestCoveredByRead,
            readAt,
            receivedAt,
          ),
        },
      };
    },
  });
  updateGlobalUnreadChannels({ channelId, remove: true });
};

/** Handle message delete/edit events: updates both the list preview and message cache */
export const updateMessage = ({
  channelId,
  newMessage,
  forceUpdate,
}: {
  channelId: string;
  newMessage: ConversationMessage;
  forceUpdate?: boolean;
}) => {
  const {
    hu_data: { message_ts: messageTs, version: newVersion },
  } = newMessage;
  if (newMessage.type === MessageTypeSocket.DELETED) {
    updateChannelInAllLists({
      channelId,
      updateFn: updateListDelete(newMessage),
      messageTs,
      newVersion,
    });
  }
  if (newMessage.type === MessageTypeSocket.UPDATE) {
    updateChannelInAllLists({
      channelId,
      updateFn: updateListEdited(newMessage),
      messageTs,
      newVersion,
    });
  }

  const cachedMessage = getMessageInCache(channelId, messageTs);
  if (!cachedMessage) return;
  const {
    hu_data: { version: cachedVersion },
  } = cachedMessage;

  const shouldUpdate = forceUpdate || requireUpdate(newVersion, cachedVersion);

  if (!shouldUpdate) return;

  updateMessageInCache(
    channelId,
    () => newMessage,
    msg => msg.hu_data.message_ts === messageTs,
  );
};

export const increaseUnreadMessagesInfo = ({ channelId }: UnreadMessages) =>
  updateUnreadInfoCount(channelId, prev => prev + 1);

export const resetUnreadMessagesInfo = ({ channelId }: UnreadMessages) =>
  updateUnreadInfoCount(channelId, 0);

export const updateUnreadCountDisplayListInfo = ({
  channelId,
  count = 1,
  replace = false,
}: SetUnreadMessagesParams) => {
  updateChannelInAllLists({
    channelId,
    updateFn: channel => ({
      ...channel,
      unread_count_display: replace
        ? count
        : channel.unread_count_display + count,
    }),
  });

  updateUnreadInfoCount(
    channelId,
    replace ? count : current => current + count,
  );
};

export const updateMarkedUnreadTs = ({
  channelId,
  markedUnreadTs,
}: UnreadMessages) => {
  updateConversationInfoChannel(channelId, channel => ({
    ...channel,
    local_marked_unread_ts: markedUnreadTs,
  }));
};

/** Reset unread count to 0 in all lists and subtract from global counter */
export const resetUnreadMessages = ({
  channelId,
  markedUnreadTs,
}: UnreadMessages) => {
  const currentData = getConversationListData();

  let unreadToRemove = 0;

  if (currentData) {
    const channelInfo = findChannelInPages(currentData.pages, channelId);
    if (channelInfo) {
      unreadToRemove = channelInfo.channel.unread_count_display;
      if (unreadToRemove === 0) {
        return;
      }
    }
  }

  if (markedUnreadTs) {
    updateMarkedUnreadTs({ channelId, markedUnreadTs });
  }

  updateChannelInAllLists({
    channelId,
    updateFn: channel => ({
      ...channel,
      unread_count_display: 0,
      hu_data: {
        ...channel.hu_data,
        mentioned: false,
      },
      properties: {
        ...channel.properties,
        marked_unread_at: undefined,
      },
    }),
  });

  updateGlobalUnreadChannels({ channelId, remove: true });
};

/** Handle member events (join/leave/admin) updating the members cache and member count */
export const updateMembers = ({
  channelId,
  type,
  membership,
  memberships,
  userId,
}: {
  channelId: string;
  type: string;
  membership?: Membership;
  memberships?: Membership[];
  userId?: number;
}) => {
  const cacheKey = conversationKeys.members.list(channelId, '');

  const patchMembership = (id: string, patch: Partial<Membership>) =>
    updateQueryCache(cacheKey, data =>
      mapMemberships(data, ms =>
        ms.map(m => (m.id === id ? { ...m, ...patch } : m)),
      ),
    );

  switch (type) {
    case MessageTypeSocket.JOINED: {
      if (!memberships?.length) return;
      const currentData = getConversationMembersData(channelId);
      if (!currentData?.pages.length) return;
      const activeIds = new Set(
        currentData.pages.flatMap(p =>
          p.data.memberships.filter(m => !m.deleted_at).map(m => m.id),
        ),
      );
      const newMembers = memberships.filter(m => !activeIds.has(m.id));
      if (!newMembers.length) return;

      const updated = updateQueryCache(cacheKey, data =>
        mapMemberships(data, current => [...current, ...newMembers], 0),
      );
      if (updated) updateNumMembers(channelId, newMembers.length);
      return;
    }
    case MessageTypeSocket.LEFT: {
      if (!membership) return;
      if (compareIds(userId, membership.user?.id)) {
        removeConversationFromCache(channelId);
        return;
      }
      const patched = patchMembership(membership.id, {
        deleted_at: membership.deleted_at ?? Date.now(),
      });
      if (patched) updateNumMembers(channelId, -1);
      return;
    }
    case MessageTypeSocket.ADMIN_ADD:
    case MessageTypeSocket.ADMIN_REVOKE: {
      if (!membership) return;
      patchMembership(membership.id, {
        is_admin: type === MessageTypeSocket.ADMIN_ADD,
      });
      return;
    }
  }
};

/** Update reactions from a socket event, skipping if cached version is newer */
export const updateMessageReactionsFromEvent = (
  message: ConversationMessage,
) => {
  const { channel, reactions: eventReactions } = message;
  const ts = message.hu_data.message_ts;
  if (!eventReactions) return;

  const cached = getMessageInCache(channel, ts);
  if (!cached || hasOlderVersion(cached.reactions || [], eventReactions)) {
    return;
  }

  syncReactionsCaches(channel, ts, eventReactions);
};

/** Optimistic add/remove of a reaction by the current user */
export const updateMessageReactions = (
  params: ParamsReaction & { userId: string },
) => {
  const { channel, ts, name, hasReacted: isRemove, userId } = params;
  const cached = getMessageInCache(channel, ts);
  if (!cached) return;

  const reactions = cached.reactions || [];
  const existing = reactions.find(r => r.name === name);
  const userReacted = existing?.users.includes(userId);

  if ((!existing || !userReacted) && isRemove) return;
  if (userReacted && !isRemove) return;

  let updated: MessageReaction[];
  if (!existing) {
    updated = [...reactions, { name, count: 1, users: [userId], version: 0 }];
  } else if (existing.count === 1 && isRemove) {
    updated = reactions.filter(r => r.name !== name);
  } else {
    updated = reactions.map(r =>
      r.name === name
        ? {
            ...r,
            count: r.count + (isRemove ? -1 : 1),
            users: isRemove
              ? r.users.filter(id => id !== userId)
              : [...r.users, userId],
          }
        : r,
    );
  }

  syncReactionsCaches(channel, ts, updated);
};

const updatePinnedCache = (
  channelId: string,
  updater: (messages: ConversationMessage[]) => ConversationMessage[],
) => updateQueryCache(conversationKeys.messages.pinned(channelId), updater);

export const updatePinnedMessagesCache = (
  channelId: string,
  messages: ConversationMessage[],
) => updatePinnedCache(channelId, () => messages);

export const removePinnedMessageCache = (
  channelId: string,
  messageTs: string,
) =>
  updatePinnedCache(channelId, messages =>
    messages.filter(m => m.hu_data.message_ts !== messageTs),
  );

export const addPinnedMessageCache = (
  channelId: string,
  message: ConversationMessage,
) =>
  updatePinnedCache(channelId, messages =>
    messages.some(m => m.hu_data.message_ts === message.hu_data.message_ts)
      ? messages
      : [...messages, message],
  );

export const updatePinnedMessageCache = (
  channelId: string,
  newMessage: ConversationMessage,
) => {
  const messageTs = newMessage.hu_data.message_ts;
  return updatePinnedCache(channelId, messages =>
    messages.some(m => m.hu_data.message_ts === messageTs)
      ? messages.map(m => (m.hu_data.message_ts === messageTs ? newMessage : m))
      : messages,
  );
};

/** Update group name, picture, or description in both info and list caches */
export const updateGroupData = ({
  id,
  newName,
  newPictureId,
  newDescription,
  version,
}: UpdateGroupData) => {
  const currentInfoData = getConversationInfoData(id);

  if (currentInfoData) {
    const shouldUpdate = requireUpdate(
      version,
      currentInfoData.data.channel.hu_data.version,
    );
    if (!shouldUpdate) return;
  }

  const baseURL = import.meta.env.VITE_BASE_URL;
  const newPictureUrl =
    newPictureId && baseURL
      ? `${baseURL}/${SERVICE_BASE_URL}/assets/${newPictureId}/conversation/${id}/picture`
      : undefined;

  const updateChannel = (channel: Channel): Channel => ({
    ...channel,
    ...(newName && { name: newName }),
    ...(newPictureUrl && {
      hu_data: {
        ...channel.hu_data,
        picture_url: newPictureUrl,
      },
    }),
    ...(newDescription !== undefined && {
      purpose: {
        ...channel.purpose,
        value: newDescription,
      },
    }),
  });

  updateConversationInfoChannel(id, updateChannel);
  updateChannelInAllLists({ channelId: id, updateFn: updateChannel });
};

export const updateLastRead = (channelId: string, lastRead: string) => {
  updateConversationInfoChannel(channelId, channel => ({
    ...channel,
    last_read: lastRead,
  }));
};

export const updateConversationTs = (currentTs: string) => {
  updateQueryCache(conversationKeys.events(), data => ({
    ...data,
    currentTs,
  }));
};
