import { useEffect, useMemo } from 'react';
import {
  type InfiniteData,
  type QueryClient,
  useInfiniteQuery,
  useMutation,
  useQuery,
} from 'react-query';
import { useNavigate } from 'react-router';

import { type IGif } from '@giphy/js-types';
import { captureException } from '@sentry/react';
import { AxiosError, type AxiosResponse } from 'axios';
import { ulid } from 'ulid';
import { useDebounce } from '@material-hu/hooks/useDebounce';
import { type FileAsset } from '@material-hu/types/attachments';

import { logEvent } from 'src/config/amplitude';
import api from 'src/config/api';
import { useAuth } from 'src/contexts/JWTContext';
import useGeneralError from 'src/hooks/useGeneralError';
import {
  type BunnyImageClass,
  CONVERSATION_TYPE,
  MAX_RETRY_ATTEMPTS,
  MIN_QUERY_LENGTH,
  PIN_DUPLICATED_ACTION_ERRORS,
  RETRY_DELAY_MS,
} from 'src/pages/dashboard/Conversations/constants';
import {
  addPin,
  addReaction,
  conversationEvents,
  createGroup,
  deleteMessage,
  editConversations,
  editMessage,
  forwardMessage,
  getConversationList,
  getConversationMembers,
  getConversationMessages,
  getConversationMessagesPeak,
  getConversationsInfo,
  getCorrectFileUrl,
  getMembersReadMessage,
  getPinnedMessages,
  getReactions,
  getUnreadChannels,
  getUsersList,
  grantPermissionsAdmin,
  inviteMembers,
  markMessage,
  openConversation,
  removePin,
  removeReaction,
  revokePermissionsAdmin,
  searchMessages,
  updateConversationProperties,
} from 'src/pages/dashboard/Conversations/services';
import {
  type ConversationMessage,
  type ConversationsResponse,
  type EditMessageResponse,
  type MessageReaction,
  type ParamsForward,
  type ParamsReaction,
  type PropertiesConversation,
  type SendImagesType,
  type UploadGifBody,
} from 'src/pages/dashboard/Conversations/types';
import {
  compareIds,
  convertUserToMember,
  deduplicateConversations,
  flatMapPages,
  getMembersFromMemberships,
  gifWidthAndHeightCalculator,
  isConversationsMaintenanceHttpError,
  isValidSearchQuery,
  isValidUlid,
  replaceSpacesWithPlus,
} from 'src/pages/dashboard/Conversations/utils';
import { getDimensions, uploadOneFile } from 'src/services/attachments';
import { getActiveVideoCall } from 'src/services/streaming';
import { EventName } from 'src/types/amplitude';
import { FileTypes, GIPHY } from 'src/types/attachments';
import { bytesToSize } from 'src/utils/bytes';
import { isUnixTimestampAlmostEqualToNow } from 'src/utils/date';
import { compressFile, getDownloadName } from 'src/utils/files';
import { useLokaliseTranslation } from 'src/utils/i18n';

import { videoCallKeys } from '../../videoCall/queries';
import {
  conversationKeys,
  getConversationEvents,
  getQueryData,
  updateMessage,
  updateMessageReactions,
  updatePinnedMessagesCache,
} from '../queries';
import { conversationsRoutes } from '../routes';
import { useConversationsMaintenance } from '../stores/maintenanceStore';
import { addMembers } from '../stores/membersStore';

export const useConversationList = (query = '', limit = 100) => {
  const showGeneralError = useGeneralError({
    showEnqueueSnackbar: false,
  });
  const { t } = useLokaliseTranslation('chat');
  const conversationsMaintenance = useConversationsMaintenance();

  const debouncedQuery = useDebounce(query);

  const conversationListData = useInfiniteQuery(
    conversationKeys.list(debouncedQuery),
    async ({ pageParam, signal }) => {
      const baseParams = {
        q: debouncedQuery,
        limit,
      };
      const dataPromise = getConversationList({
        ...baseParams,
        pinned: false,
        cursor: pageParam,
        signal,
      });

      const pinnedPromise =
        pageParam === undefined
          ? getConversationList({
              ...baseParams,
              pinned: true,
              cursor: null,
              signal,
            })
          : Promise.resolve(null);

      const [data, pinnedResponse] = await Promise.all([
        dataPromise,
        pinnedPromise,
      ]);

      const pinnedChannels = pinnedResponse
        ? [...pinnedResponse.data.channels].sort((a, b) => {
            const pinnedAtA = a.properties?.pinned_at || 0;
            const pinnedAtB = b.properties?.pinned_at || 0;
            return pinnedAtB - pinnedAtA;
          })
        : [];

      return {
        ...data,
        data: {
          ...data.data,
          channels: [...pinnedChannels, ...data.data.channels],
        },
      };
    },
    {
      getNextPageParam: lastPage =>
        lastPage?.data?.response_metadata?.next_cursor,
      onError: err => {
        if (isConversationsMaintenanceHttpError(err)) {
          conversationsMaintenance.setConversationsMaintenance(true);
          return;
        }

        showGeneralError(err, t('errors.get_conversations'));
      },
      onSuccess: () => {
        if (conversationsMaintenance.isConversationsMaintenance) {
          conversationsMaintenance.setConversationsMaintenance(false);
        }
      },
      refetchOnReconnect: false,
      refetchOnWindowFocus: false,
      retryDelay: RETRY_DELAY_MS,
      retry: MAX_RETRY_ATTEMPTS,
      staleTime: 1000 * 60 * 10,
    },
  );

  const conversations = flatMapPages(
    conversationListData.data?.pages,
    page => page?.data?.channels,
  );

  const deduplicatedConversations =
    conversations.length > 0 ? deduplicateConversations(conversations) : [];

  const conversationsDataResults = {
    conversations: deduplicatedConversations,
    isLoading: conversationListData?.isLoading,
    isSuccess: conversationListData?.isSuccess,
    fetchNextPage: conversationListData?.fetchNextPage,
    hasNextPage: conversationListData?.hasNextPage,
    isFetchingNextPage: conversationListData?.isFetchingNextPage,
  };

  return conversationsDataResults;
};

type ConversationsListCacheData = InfiniteData<
  AxiosResponse<ConversationsResponse>
>;

export const useGetPinnedCount = () => {
  const pinnedCountData = useQuery<ConversationsListCacheData, unknown, number>(
    conversationKeys.list(),
    () =>
      getQueryData(conversationKeys.list()) ||
      ({ pages: [], pageParams: [] } as ConversationsListCacheData),
    {
      enabled: false,
      select: data => {
        const conversations = flatMapPages(
          data?.pages,
          page => page?.data?.channels,
        );

        const deduplicatedConversations =
          conversations.length > 0
            ? deduplicateConversations(conversations)
            : [];

        return deduplicatedConversations.filter(
          conversation => !!conversation.properties?.pinned_at,
        ).length;
      },
    },
  );

  return pinnedCountData.data ?? 0;
};

export const useGetUserList = ({
  query = '',
  limit = 100,
  userId,
  enabled = true,
}: {
  query?: string;
  limit?: number;
  userId?: number;
  enabled?: boolean;
}) => {
  const showGeneralError = useGeneralError({
    showEnqueueSnackbar: false,
  });
  const { t } = useLokaliseTranslation('chat');
  const debouncedQuery = useDebounce(query);

  const usersListData = useInfiniteQuery(
    conversationKeys.users(debouncedQuery),
    ({ pageParam, signal }) =>
      getUsersList(debouncedQuery, limit, pageParam, signal),

    {
      getNextPageParam: lastPage =>
        lastPage?.data?.response_metadata?.next_cursor,
      onError: err => showGeneralError(err, t('errors.get_list_users')),
      retry: MAX_RETRY_ATTEMPTS,
      retryDelay: RETRY_DELAY_MS,
      staleTime: 1000 * 60 * 10,
      enabled,
    },
  );

  const members = flatMapPages(
    usersListData.data?.pages,
    page => page?.data?.members,
  ).filter(member => !compareIds(member.id, userId ?? ''));
  const usersListDataResults = {
    members,
    isLoading: usersListData?.isLoading,
    fetchNextPage: usersListData?.fetchNextPage,
    hasNextPage: usersListData?.hasNextPage,
    isFetchingNextPage: usersListData?.isFetchingNextPage,
  };

  return usersListDataResults;
};

export const useGetMessages = ({
  id = '',
  limit = 100,
  types = 'message',
  latestRead = '',
  enabled = true,
}: {
  id?: string;
  limit?: number;
  types?: string;
  latestRead?: string;
  enabled?: boolean;
}) => {
  const showGeneralError = useGeneralError({
    showEnqueueSnackbar: false,
  });
  const { t } = useLokaliseTranslation('chat');
  const conversationsMaintenance = useConversationsMaintenance();
  const messagesListData = useInfiniteQuery(
    conversationKeys.messages.all(id),
    async ({ pageParam, signal }) => {
      if (!pageParam) {
        return getConversationMessagesPeak({
          channel: id,
          limit: 100,
          types,
          pivot: latestRead,
          signal,
        });
      }

      return getConversationMessages({
        params: {
          channel: id,
          limit,
          types,
          cursor: pageParam.cursor,
          inclusive: false,
        },
        signal,
      });
    },
    {
      retry: MAX_RETRY_ATTEMPTS,
      retryDelay: RETRY_DELAY_MS,
      getNextPageParam: prevPage => {
        if (!prevPage) return undefined;
        if (prevPage.data?.response_metadata?.next_cursor) {
          return {
            cursor: prevPage.data.response_metadata.next_cursor,
          };
        }
        return undefined;
      },
      getPreviousPageParam: prevPage => {
        if (!prevPage) return undefined;
        if (prevPage.data?.response_metadata?.prev_cursor) {
          return {
            cursor: prevPage.data.response_metadata.prev_cursor,
          };
        }
        const isPrevPagePeak = prevPage.config.url?.includes('peak');
        if (!isPrevPagePeak && prevPage.data?.response_metadata?.next_cursor) {
          return {
            cursor: prevPage.data.response_metadata.next_cursor,
          };
        }
        return undefined;
      },
      onError: err => {
        if (isConversationsMaintenanceHttpError(err)) {
          conversationsMaintenance.setConversationsMaintenance(true);
          return;
        }
        showGeneralError(err, t('errors.get_list_messages'));
      },
      onSuccess: () => {
        if (conversationsMaintenance.isConversationsMaintenance) {
          conversationsMaintenance.setConversationsMaintenance(false);
        }
      },
      refetchOnReconnect: false,
      staleTime: 1000 * 60 * 10,
      enabled,
    },
  );

  useEffect(() => {
    if (messagesListData.hasNextPage && !messagesListData.isFetchingNextPage) {
      messagesListData.fetchNextPage();
    }
  }, [messagesListData.hasNextPage, messagesListData.isFetchingNextPage]);

  const messages = useMemo(
    () =>
      flatMapPages(messagesListData.data?.pages, page => page?.data?.messages),
    [messagesListData.data, messagesListData.dataUpdatedAt],
  );

  return {
    messages,
    isLoading: messagesListData.isLoading,
    isFetching: messagesListData.isFetching,
    fetchPreviousPage: messagesListData.fetchPreviousPage,
    hasPreviousPage: messagesListData.hasPreviousPage,
    isFetchingPreviousPage: messagesListData.isFetchingPreviousPage,
    refetch: messagesListData.refetch,
    isError: messagesListData.isError || false,
  };
};

export const useGetConversationMembers = ({
  id = '',
  query = '',
  limit = undefined,
  enabled = true,
}: {
  id: string | undefined;
  query?: string;
  limit?: number;
  enabled?: boolean;
}) => {
  const showGeneralError = useGeneralError({
    showEnqueueSnackbar: false,
  });

  const { t } = useLokaliseTranslation('chat');
  const conversationsMaintenance = useConversationsMaintenance();
  const debouncedQuery = useDebounce(query);

  const membersListData = useInfiniteQuery(
    conversationKeys.members.list(id, debouncedQuery),
    async ({ pageParam, signal }) =>
      getConversationMembers({
        channel: id,
        q: debouncedQuery,
        limit,
        cursor: pageParam,
        signal,
      }),

    {
      enabled: enabled && isValidUlid(id),
      getNextPageParam: lastPage =>
        lastPage?.data?.response_metadata?.next_cursor,
      onError: err => {
        if (isConversationsMaintenanceHttpError(err)) {
          conversationsMaintenance.setConversationsMaintenance(true);
          return;
        }
        showGeneralError(err, t('errors.get_list_members'));
      },
      onSuccess: data => {
        const firstPageResponse = data?.pages?.[0];
        const firstPage = firstPageResponse?.data;
        const firstPageMembers = firstPage?.memberships;

        if (conversationsMaintenance.isConversationsMaintenance) {
          conversationsMaintenance.setConversationsMaintenance(false);
        }

        if (!firstPageMembers?.length && debouncedQuery === '') {
          const error = new Error(
            'getConversationMembers: first page returned no members',
          );
          error.name = 'ConversationMembersEmpty';
          captureException(error, {
            extra: {
              channel: id,
              xRequestId: firstPageResponse?.config?.headers?.['X-Request-Id'],
              response: firstPageMembers,
            },
          });
        }
      },
      refetchOnReconnect: false,
      staleTime: 1000 * 60 * 10,
      retry: MAX_RETRY_ATTEMPTS,
      retryDelay: RETRY_DELAY_MS,
    },
  );

  useEffect(() => {
    if (
      membersListData.hasNextPage &&
      !membersListData.isFetchingNextPage &&
      enabled
    ) {
      membersListData.fetchNextPage();
    }
  }, [
    membersListData.hasNextPage,
    enabled,
    membersListData.isFetchingNextPage,
    membersListData.fetchNextPage,
  ]);

  const hasAllPages =
    !membersListData.hasNextPage && !membersListData.isFetchingNextPage;

  const members = useMemo(() => {
    const hasData =
      membersListData?.data?.pages?.length &&
      membersListData.data.pages.length > 0;

    const canProcessData = enabled
      ? hasAllPages && membersListData.isSuccess
      : hasData;

    if (!canProcessData || !membersListData?.data) {
      return [];
    }

    const allMembers = getMembersFromMemberships(
      flatMapPages(
        membersListData.data?.pages,
        page => page?.data?.memberships,
      ),
    );

    if (allMembers.length > 0 && enabled) {
      addMembers(allMembers);
    }

    return allMembers.filter(m => !m.deleted_at);
  }, [
    membersListData?.data,
    membersListData.dataUpdatedAt,
    enabled,
    hasAllPages,
  ]);

  const usersListDataResults = {
    members,
    isLoading: membersListData?.isLoading || !hasAllPages,
    isSuccess: membersListData?.isSuccess && hasAllPages,
    fetchNextPage: membersListData?.fetchNextPage,
    hasNextPage: membersListData?.hasNextPage,
    isFetchingNextPage: membersListData?.isFetchingNextPage,
    isFetching: membersListData.isFetching,
    refetch: membersListData.refetch,
    isError: membersListData?.isError || false,
  };

  return usersListDataResults;
};

type ParamsOpenConversation = {
  users: string[];
  me?: string;
};

export const useOpenConversation = () => {
  const { t } = useLokaliseTranslation('chat');
  const conversationsMaintenance = useConversationsMaintenance();
  const isInternetConnected = navigator.onLine;
  const { user } = useAuth();

  const showGeneralError = useGeneralError({
    showEnqueueSnackbar: isInternetConnected,
  });
  const navigate = useNavigate();

  const openConversationMutation = useMutation(
    ({ users, me }: ParamsOpenConversation) => {
      const ulidString = ulid();
      return openConversation({ users, ulid: ulidString, me });
    },
    {
      onSuccess: (data, variables) => {
        if (conversationsMaintenance.isConversationsMaintenance) {
          conversationsMaintenance.setConversationsMaintenance(false);
        }

        const { data: channelData } = data;
        const createdAt = channelData.channel.hu_data.created_at;
        const eventProperties = {
          userId: user?.id.toString(),
          conversationType: CONVERSATION_TYPE.INDIVIDUAL,
          conversationID: channelData.channel.id,
        };
        if (createdAt && isUnixTimestampAlmostEqualToNow(createdAt)) {
          logEvent(EventName.CHATS_2_CONVERSATION_CREATED, eventProperties);
        } else {
          logEvent(EventName.CHATS_2_CONVERSATION_OPENED, eventProperties);
        }

        const baseUrl = conversationsRoutes.thread.detail(
          channelData.channel.id,
        );
        const urlWithParams = variables.me
          ? `${baseUrl}?me=${encodeURIComponent(variables.me)}`
          : baseUrl;
        navigate(urlWithParams);
      },
      onError: err => {
        if (isConversationsMaintenanceHttpError(err)) {
          conversationsMaintenance.setConversationsMaintenance(true);
          return;
        }

        showGeneralError(err, t('errors.create_group'));
      },
    },
  );

  return openConversationMutation;
};

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

export const useMarkMessages = () => {
  const { t } = useLokaliseTranslation('chat');
  const showGeneralError = useGeneralError({
    showEnqueueSnackbar: false,
  });

  const markMessageMutation = useMutation(
    ({ channelId, markedUnreadTs }: ParamsUnmarkMessage) => {
      return markMessage(channelId, markedUnreadTs);
    },
    {
      onError: error => {
        showGeneralError(error, t('errors.unmark_message'));
      },
    },
  );

  return markMessageMutation;
};

type ParamsUnreadChannels = {
  enabled?: boolean;
};

export const useGetUnreadChannels = ({
  enabled = true,
}: ParamsUnreadChannels = {}) => {
  const showGeneralError = useGeneralError({
    showEnqueueSnackbar: false,
  });

  const { t } = useLokaliseTranslation('chat');
  const channelsUnreadData = useInfiniteQuery(
    conversationKeys.unreadChannels(),
    ({ pageParam, signal }) => getUnreadChannels({ cursor: pageParam, signal }),
    {
      getNextPageParam: lastPage =>
        lastPage?.data?.response_metadata?.next_cursor || undefined,
      onError: err => {
        showGeneralError(err, t('errors.get_unread_channels'));
      },
      enabled,
      refetchOnReconnect: false,
      refetchOnWindowFocus: false,
    },
  );

  useEffect(() => {
    if (
      channelsUnreadData.hasNextPage &&
      !channelsUnreadData.isFetchingNextPage
    ) {
      channelsUnreadData.fetchNextPage();
    }
  }, [
    channelsUnreadData.hasNextPage,
    channelsUnreadData.isFetchingNextPage,
    channelsUnreadData.fetchNextPage,
  ]);

  const { channels, unreadCount } = useMemo(() => {
    const unreadCount =
      channelsUnreadData.data?.pages[0]?.data.unread_count || 0;
    if (
      channelsUnreadData.isSuccess &&
      !channelsUnreadData.isFetchingNextPage &&
      !channelsUnreadData.hasNextPage
    ) {
      return {
        channels: flatMapPages(
          channelsUnreadData.data?.pages,
          page => page?.data?.channels,
        ),
        unreadCount,
      };
    }
    return { channels: [], unreadCount };
  }, [
    channelsUnreadData.data,
    channelsUnreadData.isSuccess,
    channelsUnreadData.isFetchingNextPage,
    channelsUnreadData.hasNextPage,
  ]);

  return {
    ...channelsUnreadData,
    data: unreadCount,
    channels,
  };
};

type ParamsGetConversationInfo = {
  id: string | undefined;
  enabled?: boolean;
};

export const useGetConversationInfo = (params: ParamsGetConversationInfo) => {
  const { id = '', enabled = true } = params;
  const conversationsMaintenance = useConversationsMaintenance();
  const { user: loggedUser } = useAuth();

  const conversationData = useQuery(
    conversationKeys.info(id),
    ({ signal }) => {
      const xRequestId = ulid();

      return getConversationsInfo({
        params: { channel: id },
        headers: {
          'X-Request-Id': xRequestId,
          'X-Call-Site': 'useGetConversationInfo',
        },
        signal,
      }).then(res => {
        const existChannel = !!res?.data?.channel;
        if (!existChannel) {
          const error = new Error(
            'getConversationInfo: response returned no information',
          );
          error.name = 'ConversationInfoEmpty';
          captureException(error, {
            extra: {
              channel: id,
              response: res?.data ?? null,
              xRequestId,
            },
          });
          throw error;
        }
        return res;
      });
    },
    {
      enabled: enabled && isValidUlid(id),
      onError: err => {
        if (isConversationsMaintenanceHttpError(err)) {
          conversationsMaintenance.setConversationsMaintenance(true);
          return;
        }
      },
      onSuccess: data => {
        if (conversationsMaintenance.isConversationsMaintenance) {
          conversationsMaintenance.setConversationsMaintenance(false);
        }
        const otherMember =
          data?.data?.channel?.hu_data?.other_membership?.user?.hu_data?.member;
        if (otherMember && loggedUser) {
          addMembers([otherMember, convertUserToMember(loggedUser)]);
        }
      },
      refetchOnReconnect: false,
      refetchOnWindowFocus: false,
      staleTime: 1000 * 60 * 10,
      retryDelay: RETRY_DELAY_MS,
      retry: MAX_RETRY_ATTEMPTS,
    },
  );
  return {
    ...conversationData,
    conversation: conversationData.data?.data.channel,
  };
};

type ParamsInviteMembers = {
  channelId: string;
  users: string[];
};

export const useInviteMembers = () => {
  const conversationsMaintenance = useConversationsMaintenance();
  const inviteMembersMutation = useMutation(
    async ({ channelId, users }: ParamsInviteMembers) =>
      inviteMembers(channelId, users),
    {
      onSuccess: () => {
        if (conversationsMaintenance.isConversationsMaintenance) {
          conversationsMaintenance.setConversationsMaintenance(false);
        }
      },
      onError: err => {
        if (isConversationsMaintenanceHttpError(err)) {
          conversationsMaintenance.setConversationsMaintenance(true);
          return;
        }
      },
    },
  );
  return inviteMembersMutation;
};

type ParamsCreateGroup = {
  name: string;
  picture: FileAsset;
  users: string[];
};

export const useCreateGroup = () => {
  const { t } = useLokaliseTranslation('chat');
  const conversationsMaintenance = useConversationsMaintenance();
  const showGeneralError = useGeneralError({
    showEnqueueSnackbar: false,
  });
  const navigate = useNavigate();

  const createNewGroupMutation = useMutation(
    async ({ name, picture, users }: ParamsCreateGroup) => {
      if (users.length === 0) {
        return createGroup(name, picture);
      }
      const newGroup = await createGroup(name, picture);
      const eventProperties = {
        userId: newGroup.data.channel.latest.user,
        conversationType: CONVERSATION_TYPE.GROUP,
        conversationID: newGroup.data.channel.id,
      };
      logEvent(EventName.CHATS_2_CONVERSATION_CREATED, eventProperties);

      return inviteMembers(newGroup.data.channel.id, users);
    },
    {
      onSuccess: async data => {
        const { data: channelData } = data;
        if (conversationsMaintenance.isConversationsMaintenance) {
          conversationsMaintenance.setConversationsMaintenance(false);
        }
        navigate(conversationsRoutes.thread.detail(channelData.channel.id));
      },
      onError: err => {
        if (isConversationsMaintenanceHttpError(err)) {
          conversationsMaintenance.setConversationsMaintenance(true);
          return;
        }
        showGeneralError(err, t('errors.create_group'));
      },
    },
  );

  return createNewGroupMutation;
};

type ParamsEditGroup = {
  channelId: string;
  name?: string;
  picture?: SendImagesType | null;
  description?: string;
};

export const useEditGroup = () => {
  const createNewGroupMutation = useMutation(
    ({ channelId, name, picture, description }: ParamsEditGroup) =>
      editConversations(channelId, name, picture ?? undefined, description),
  );

  return createNewGroupMutation;
};

export const useGetCorrectAttachmentUrl = ({
  url,
  conversationId,
  enabledToShowError = true,
  enabled = false,
  onError,
  onSuccess,
  imageClass,
}: {
  url: string;
  conversationId?: string;
  enabledToShowError?: boolean;
  enabled?: boolean;
  origin?: string;
  onError?: () => void;
  onSuccess?: () => void;
  imageClass?: BunnyImageClass;
}) => {
  const { t } = useLokaliseTranslation('chat');

  const showGeneralError = useGeneralError({
    showEnqueueSnackbar: false,
  });
  const dataCorrectImg = useQuery(
    // `conversationId` may be unavailable in some early/shared flows where the file
    // is requested before the conversation context is fully resolved.
    // In that case, fall back to a generic key to keep the request cacheable/cancelable
    conversationId
      ? conversationKeys.messages.file(conversationId, url, imageClass)
      : conversationKeys.file(url, imageClass),
    ({ signal }) => getCorrectFileUrl({ url, signal, imageClass }),
    {
      enabled,
      staleTime: Infinity,
      onSuccess,
      onError: error => {
        onError?.();
        if (enabledToShowError) {
          showGeneralError(error, t(`errors.fetching_image`));
        }
      },
      retry: MAX_RETRY_ATTEMPTS,
      retryDelay: RETRY_DELAY_MS,
    },
  );

  return { ...dataCorrectImg };
};

type ParamsGrantPermissions = {
  channelId: string;
  user: string;
};

export const useGrantPermissionsAdmin = () => {
  const grantPermissionsMutation = useMutation(
    ({ channelId, user }: ParamsGrantPermissions) =>
      grantPermissionsAdmin(channelId, user),
  );
  return grantPermissionsMutation;
};

export const useRevokePermissionsAdmin = () => {
  const revokePermissionsMutation = useMutation(
    ({ channelId, user }: ParamsGrantPermissions) =>
      revokePermissionsAdmin(channelId, user),
  );
  return revokePermissionsMutation;
};

type AudioFile = {
  blob: Blob;
  duration: string;
};

const NAME_UPLOAD_AUDIO = 'humand_audio_';
export const useUploadAudio = () => {
  const uploadImgGroupMutation = useMutation((file: AudioFile) => {
    const newFile = {
      name: getDownloadName(NAME_UPLOAD_AUDIO, '.webm'),
      size: file.blob.size,
      contentType: file.blob.type,
      fileObject: file.blob,
      type: FileTypes.AUDIO,
      duration: file?.duration,
    };

    return uploadOneFile(newFile);
  });
  return uploadImgGroupMutation;
};

export const useUploadImage = () => {
  const uploadImgGroupMutation = useMutation(async (file: File) => {
    const compressedFile = await compressFile(file, { mimeType: file.type });
    const dimensions = await getDimensions(compressedFile);

    const newFile = {
      name: replaceSpacesWithPlus(compressedFile.name),
      size: compressedFile.size,
      contentType: compressedFile.type,
      fileObject: compressedFile,
      type: FileTypes.IMAGE,
      width: dimensions.width,
      height: dimensions.height,
    };

    return uploadOneFile(newFile);
  });
  return uploadImgGroupMutation;
};

export const useUploadGif = () => {
  const uploadGifMutation = useMutation<UploadGifBody, unknown, IGif>(
    async (gifData: IGif) => {
      const { height, width } = gifData.images?.original;
      const { width: widthGif, height: heightGif } =
        gifWidthAndHeightCalculator({
          width,
          height,
        });

      const fileAsset = await api.post<FileAsset>('/file-assets', {
        name: `giphy_${gifData.id}.gif`,
        type: FileTypes.GIF,
        contentType: 'image/gif',
        width: widthGif,
        height: heightGif,
        size: bytesToSize(1),
        externalReference: {
          origin: GIPHY,
          id: gifData.id,
        },
      });

      const fileData = fileAsset.data;

      const data: UploadGifBody = {
        id: fileData.id,
        type: FileTypes.GIF,
        name: fileData.name,
        width: widthGif,
        height: heightGif,
        contentType: 'image/gif',
        external_reference: {
          origin: GIPHY,
          id: String(gifData.id),
        },
      };

      return data;
    },
  );
  return uploadGifMutation;
};

export type ParamsEditMessage = {
  msgEdited: ConversationMessage;
};

type UseEditMessageParams = {
  onClose: () => void;
};

export const useEditMessage = (params: UseEditMessageParams) => {
  const { onClose } = params;
  const { t } = useLokaliseTranslation('chat');
  const showGeneralError = useGeneralError();

  const editMessageMutation = useMutation<
    EditMessageResponse,
    Error,
    ParamsEditMessage,
    unknown
  >(
    ({ msgEdited }) => {
      const { channel, text, ts, attachments, blocks } = msgEdited;
      return editMessage(channel, ts, text || '', attachments, blocks);
    },
    {
      onError: error => {
        showGeneralError(error, t('errors.edit_message'));
      },
      onMutate: ({ msgEdited }) => {
        updateMessage({
          channelId: msgEdited.channel,
          newMessage: msgEdited,
        });
      },
      onSuccess: () => {
        onClose();
      },
    },
  );

  return editMessageMutation;
};

export const useReactionsMutations = () => {
  const optionReaction = useMutation(
    ({ channel, ts, name, hasReacted }: ParamsReaction) =>
      hasReacted
        ? removeReaction({ channel, ts, name })
        : addReaction({ channel, ts, name }),
  );

  return {
    ...optionReaction,
    mutate: (
      params: ParamsReaction & {
        userId: string;
      },
    ) => {
      const { channel, ts, name, hasReacted, userId } = params;

      optionReaction.mutate(
        { channel, ts, name, hasReacted },
        {
          onSuccess: () => {
            updateMessageReactions({ channel, ts, name, hasReacted, userId });
          },
        },
      );
    },
  };
};

export const useGetReactions = ({
  channel,
  ts,
  isOpen,
  initialData,
}: {
  channel: string;
  ts: string;
  isOpen: boolean;
  initialData: MessageReaction[];
}) => {
  const reactionsData = useQuery(
    conversationKeys.reactions(channel, ts),
    ({ signal }) => getReactions({ channel, ts, signal }),
    {
      enabled: isOpen,
      retry: MAX_RETRY_ATTEMPTS,
      retryDelay: RETRY_DELAY_MS,
    },
  );

  const sortedReactions = useMemo(() => {
    const reactions = reactionsData.data || initialData;
    return [...reactions].sort((a, b) => b.count - a.count);
  }, [reactionsData.data, initialData]);
  return {
    ...reactionsData,
    data: sortedReactions,
  };
};

type ParamsDeleteMessage = {
  channel: string;
  ts: string;
};

export const useDeleteMessage = ({
  onMutate,
  onSuccess,
}: {
  onMutate?: (variables: ParamsDeleteMessage) => void;
  onSuccess?: () => void;
} = {}) => {
  const { t } = useLokaliseTranslation('chat');
  const showGeneralError = useGeneralError();
  const deleteMessageMutation = useMutation(
    ({ channel, ts }: ParamsDeleteMessage) => deleteMessage({ channel, ts }),
    {
      onError: error => {
        showGeneralError(error, t('errors.delete_message'));
      },
      onMutate,
      onSuccess,
    },
  );
  return deleteMessageMutation;
};

export const useForwardMessage = ({
  onSuccess,
}: {
  onSuccess?: () => void;
} = {}) => {
  const { t } = useLokaliseTranslation('chat');
  const showGeneralError = useGeneralError();
  const forwardMessageMutation = useMutation(
    ({
      source_message_ts,
      source_channel_ts,
      channel_destinations,
      user_destinations,
    }: ParamsForward) =>
      forwardMessage({
        source_message_ts,
        source_channel_ts,
        channel_destinations,
        user_destinations,
      }),
    {
      onError: error => {
        showGeneralError(error, t('errors.forward_message'));
      },
      onSuccess,
    },
  );
  return forwardMessageMutation;
};

const LIMIT = 10;
const fetchAllConversationEvents = async (
  startTs: string,
  signal?: AbortSignal,
): Promise<ConversationMessage[]> => {
  const allMessages: ConversationMessage[] = [];
  let counter = 0;
  let cursor: string | undefined;
  const cursorInfo: Record<number, string | undefined> = { 0: cursor };
  const responseLengths: Record<number, number> = { 0: 0 };
  const allTs: Record<number, string> = { 0: startTs };
  while (true) {
    if (counter >= LIMIT) {
      logEvent(EventName.CHATS_2_MESSAGE_RECEIVED_LIMIT_REACHED, {
        initialTs: startTs,
        nextCursor: cursor,
        cursorInfo: JSON.stringify(cursorInfo),
        responseLengths: JSON.stringify(responseLengths),
        fetchCount: counter,
        allTs: JSON.stringify(allTs),
      });
      break;
    }
    counter++;
    const response = await conversationEvents({
      oldest: startTs,
      cursor,
      signal,
    });

    if (!response?.messages?.length) {
      break;
    }

    allMessages.push(...response.messages);

    if (!response?.response_metadata?.next_cursor) {
      if (counter > 1) {
        logEvent(EventName.CHATS_2_MESSAGE_RECEIVED_LIMIT_REACHED, {
          initialTs: startTs,
          nextCursor: cursor,
          cursorInfo: JSON.stringify(cursorInfo),
          responseLengths: JSON.stringify(responseLengths),
          fetchCount: counter,
          allTs: JSON.stringify(allTs),
        });
      }
      break;
    }
    if (cursor && cursor === response.response_metadata?.next_cursor) {
      logEvent(EventName.CHATS_2_MESSAGE_RECEIVED_LIMIT_REACHED, {
        initialTs: startTs,
        nextCursor: cursor,
        cursorInfo: JSON.stringify(cursorInfo),
        responseLengths: JSON.stringify(responseLengths),
        fetchCount: counter,
        allTs: JSON.stringify(allTs),
      });
      break;
    }
    cursor = response.response_metadata?.next_cursor;
    cursorInfo[counter] = cursor;
    responseLengths[counter] = response.messages.length;
    allTs[counter] = startTs;
  }
  return allMessages;
};

export const useGetConversationEvents = ({
  initialTs,
}: {
  initialTs: string;
}) => {
  const { currentTs } = getConversationEvents(initialTs);
  const eventsData = useQuery(
    conversationKeys.events(),
    async ({ signal }) => {
      const allEvents = await fetchAllConversationEvents(currentTs, signal);
      return {
        events: allEvents,
        currentTs,
        totalCount: allEvents.length,
      };
    },
    {
      refetchOnWindowFocus: false,
      refetchOnReconnect: false,
      enabled: false,
      retry: MAX_RETRY_ATTEMPTS,
      retryDelay: RETRY_DELAY_MS,
    },
  );

  return eventsData;
};

type ParamsEditConversationProperties = {
  channel: string;
  properties: PropertiesConversation;
};

export const useEditConversationProperties = () => {
  const editConversationPropertiesMutation = useMutation(
    ({ channel, properties }: ParamsEditConversationProperties) =>
      updateConversationProperties(channel, properties),
  );
  return editConversationPropertiesMutation;
};

export const useGetActiveVideoCall = (params: {
  id?: string;
  enabled?: boolean;
}) => {
  const { id = '', enabled = true } = params;

  const activeVideoCallData = useQuery(
    videoCallKeys.activeVideoCall(id),
    ({ signal }) => getActiveVideoCall({ channelId: id }, signal),
    {
      enabled: enabled && isValidUlid(id),
      select: response => response?.data,
      retry: 0,
      onError: () => {},
    },
  );
  return activeVideoCallData;
};

export const useGetMembersReadMessages = ({
  channelId,
  messageTs,
  enabled = false,
}: {
  channelId: string;
  messageTs: string;
  enabled: boolean;
}) => {
  const membersReadMessagesData = useInfiniteQuery(
    conversationKeys.members.readMessage(channelId, messageTs),
    async ({ pageParam, signal }) =>
      getMembersReadMessage({
        channel: channelId,
        cursor: pageParam,
        ts: messageTs,
        signal,
      }),

    {
      getNextPageParam: lastPage =>
        lastPage?.data?.response_metadata?.next_cursor,
      enabled: enabled && !!channelId && !!messageTs,
      retry: MAX_RETRY_ATTEMPTS,
      retryDelay: RETRY_DELAY_MS,
    },
  );

  const responseData = {
    members: flatMapPages(
      membersReadMessagesData.data?.pages,
      p => p?.data?.read_states,
    )
      .filter(
        readyStates =>
          !!readyStates?.read_at &&
          !!readyStates?.was_in_channel_when_message_sent,
      )
      .map(readyStates => ({
        id: readyStates?.user.id,
        pictureUrl: readyStates?.user.user.hu_data.member.picture_asset_url,
        read_at: readyStates?.read_at,
        userName: `${readyStates?.user.user.hu_data.member.first_name} ${readyStates?.user.user.hu_data.member.last_name}`,
      })),
    isLoading: membersReadMessagesData?.isLoading,
    isSuccess: membersReadMessagesData?.isSuccess,
    hasNextPage: membersReadMessagesData?.hasNextPage,
    fetchNextPage: membersReadMessagesData?.fetchNextPage,
    isFetchingNextPage: membersReadMessagesData?.isFetchingNextPage,
    isEmpty:
      !membersReadMessagesData?.data?.pages &&
      !membersReadMessagesData?.isLoading,
  };

  return responseData;
};

/**
 * Hook global para manejar automáticamente el cleanup de blob URLs
 * cuando React Query elimina queries del cache.
 *
 * Detecta específicamente keys del tipo conversationKeys.file(url)
 * y ejecuta URL.revokeObjectURL() para prevenir memory leaks.
 */
export const useRevokeBlobFileInConversations = (queryClient: QueryClient) => {
  useEffect(() => {
    const handleQueryEvent = (event: any) => {
      if (event.type !== 'queryRemoved') return;

      const queryKey = event.query.queryKey;
      const data = event.query.state.data;

      const isFileQueryConversations =
        Array.isArray(queryKey) &&
        queryKey[0] === 'conversations' &&
        (queryKey[2] === 'file' || queryKey[3] === 'file');

      if (
        isFileQueryConversations &&
        data &&
        typeof data === 'string' &&
        data.startsWith('blob:')
      ) {
        URL.revokeObjectURL(data);
      }
    };

    const cancelSubscriptionFilesConversations = queryClient
      .getQueryCache()
      .subscribe(handleQueryEvent);

    return cancelSubscriptionFilesConversations;
  }, []);
};

export const useSearchMessages = ({
  channelId,
  query,
  enabled = true,
}: {
  channelId: string;
  query: string;
  enabled?: boolean;
}) => {
  const debouncedQuery = useDebounce(query, 500);
  const isValidQuery = isValidSearchQuery(debouncedQuery);
  const isEnabled =
    enabled &&
    !!channelId &&
    !!debouncedQuery &&
    debouncedQuery.length >= MIN_QUERY_LENGTH &&
    isValidQuery;

  const searchMessagesData = useInfiniteQuery(
    conversationKeys.messages.search(channelId, debouncedQuery),
    async ({ pageParam, signal }) =>
      searchMessages({
        channelId,
        query: debouncedQuery,
        cursor: pageParam,
        signal,
      }),
    {
      getNextPageParam: lastPage =>
        lastPage.data?.response_metadata?.next_cursor,
      enabled: isEnabled,
      staleTime: 10 * 60 * 1000,
      retry: MAX_RETRY_ATTEMPTS,
      retryDelay: RETRY_DELAY_MS,
    },
  );

  const matches = useMemo(
    () =>
      searchMessagesData.data?.pages.flatMap(page => page.data.matches) || [],
    [searchMessagesData.data, searchMessagesData.dataUpdatedAt],
  );

  return {
    ...searchMessagesData,
    hasNextPage: searchMessagesData.hasNextPage || false,
    matches,
  };
};

export const useGetPinnedMessages = <TData = ConversationMessage[]>({
  channelId,
  enabled = true,
  select,
}: {
  channelId: string;
  enabled?: boolean;
  select?: (data: ConversationMessage[]) => TData;
}) => {
  const pinnedMessages = useQuery(
    conversationKeys.messages.pinned(channelId),
    async ({ signal }) => {
      const response = await getPinnedMessages(channelId, signal);

      const messages = response.data.items.map(item => item.message);

      return messages;
    },
    {
      enabled,
      staleTime: 1000 * 60 * 10,
      refetchOnWindowFocus: false,
      refetchOnReconnect: false,
      retry: MAX_RETRY_ATTEMPTS,
      retryDelay: RETRY_DELAY_MS,
      select,
    },
  );

  return pinnedMessages;
};

export const useUpdatePinnedMessages = () => {
  const { t } = useLokaliseTranslation('chat');
  const showGeneralError = useGeneralError();

  const updatePinnedMessagesMutation = useMutation(
    ({
      channel,
      messageTs,
      expiresAt,
    }: {
      channel: string;
      messageTs: string;
      expiresAt?: number;
    }) => {
      if (expiresAt === undefined) {
        return removePin({ channel, message_ts: messageTs });
      }
      return addPin({
        channel,
        message_ts: messageTs,
        expires_at: expiresAt,
      });
    },
    {
      onError: error => {
        if (
          error instanceof AxiosError &&
          PIN_DUPLICATED_ACTION_ERRORS.includes(error.response?.data?.error)
        ) {
          return;
        }
        showGeneralError(error, t('errors.pin_message'));
      },
      onSuccess: (response, { channel, messageTs, expiresAt }) => {
        const messages = response.data.items.map(item => item.message);

        updatePinnedMessagesCache(channel, messages);

        if (expiresAt === undefined) {
          logEvent(EventName.CHATS_2_MESSAGE_UNPINNED, {
            conversationId: channel,
            messageId: messageTs,
          });
        } else {
          logEvent(EventName.CHATS_2_MESSAGE_PINNED, {
            conversationId: channel,
            messageId: messageTs,
            duration: expiresAt,
          });
        }
      },
    },
  );

  return updatePinnedMessagesMutation;
};
