import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useParams } from 'react-router';

import debounce from 'lodash-es/debounce';
import { ulid } from 'ulid';

import { useSnackbar } from '@material-hu/components/design-system/Snackbar';

import { useAuth } from 'src/contexts/JWTContext';
import { useSocket } from 'src/contexts/SocketContext';
import useNetworkStatus from 'src/hooks/useNetworkStatus';
import {
  GLOBAL_COOLDOWN,
  MAX_RETRY_ATTEMPTS,
  RETRY_DELAY_MS,
} from 'src/pages/dashboard/Conversations/constants';
import {
  type JsonEventUser,
  MessageTypeSocket,
  NotificationType,
  type PendingReadStatus,
} from 'src/pages/dashboard/Conversations/types';
import { upsertPendingReadStatus } from 'src/pages/dashboard/Conversations/utils';
import { useLokaliseTranslation } from 'src/utils/i18n';

import {
  addMessage,
  addPinnedMessageCache,
  changePositionOfConversation,
  getMessageInCache,
  refetchUnreadChannels,
  removePinnedMessageCache,
  updateChannelProperties,
  updateConversationTs,
  updateGroupData,
  updateLastRead,
  updateMarkedUnreadTs,
  updateMembers,
  updateMessage,
  updateMessageReadStatus,
  updatePinnedMessageCache,
  updateUnreadCountDisplayListInfo,
} from '../queries';

import { useGetConversationEvents } from './useConversationsQueries';

const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

const useConversationsEvents = () => {
  const socket = useSocket();
  const { id } = useParams();
  const { t } = useLokaliseTranslation('chat');
  const { enqueueSnackbar } = useSnackbar();
  const initialTs = useRef(ulid());
  const lastExecutionTime = useRef<number>(0);
  const { user } = useAuth();
  const { refetch } = useGetConversationEvents({
    initialTs: initialTs.current,
  });
  const wasDisconnectedRef = useRef(false);

  const handleSocketDisconnect = useCallback(() => {
    wasDisconnectedRef.current = true;
  }, []);

  const callbackErrorUpdateConversation = () =>
    enqueueSnackbar({
      title: t('errors.update_conversations'),
      variant: 'error',
    });

  const isReadyToUpdateEvents = () => {
    const currentTime = Date.now();

    if (currentTime - lastExecutionTime.current < GLOBAL_COOLDOWN) {
      return false;
    }

    lastExecutionTime.current = currentTime;
    return true;
  };

  const updateEvents = async () => {
    if (!isReadyToUpdateEvents()) {
      return;
    }

    for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
      try {
        await executeUpdateEvents();
        return;
      } catch {
        if (attempt < MAX_RETRY_ATTEMPTS) {
          await sleep(RETRY_DELAY_MS);
        }
      }
    }

    lastExecutionTime.current = 0;
  };

  const executeUpdateEvents = async () => {
    const { data } = await refetch();
    const pendingReadStatus: Record<string, PendingReadStatus> = {};
    let unreadChannels: Record<string, number> = {};
    if (data && data.events.length > 0) {
      const events = data.events;
      for (const event of events) {
        if (event.type === MessageTypeSocket.MESSAGE) {
          if (event.edited) {
            event.subtype = MessageTypeSocket.UPDATE;
          }
          if (event.hidden) {
            event.subtype = MessageTypeSocket.DELETED;
            event.text = '';
          }
          const messageInCache = getMessageInCache(
            event.channel,
            event.hu_data.message_ts,
          );
          if (event.hu_data.count_unread) {
            unreadChannels[event.channel] =
              event.hu_data.membership?.unread_count || 0;
          }
          if (messageInCache) {
            updateMessage({
              channelId: event.channel,
              newMessage: event,
            });
            updatePinnedMessageCache(event.channel, event);
          } else {
            addMessage({ id: event.channel, message: event });
            await changePositionOfConversation({
              channelId: event.channel,
              message: event,
              callback: callbackErrorUpdateConversation,
              userId: user?.id,
            });
            if (Number(event.user) === user?.id) {
              updateLastRead(event.channel, event.hu_data.message_ts);
              updateMarkedUnreadTs({
                channelId: event.channel,
                markedUnreadTs: event.hu_data.message_ts,
              });
            }
          }
        }
        if (event.type === MessageTypeSocket.MARKED_READ) {
          const payload = event.metadata?.event_payload;
          if (payload?.to_message_ts) {
            upsertPendingReadStatus(pendingReadStatus, event.channel, {
              toMessageTs: payload.to_message_ts,
              readAt: payload.read_at,
              receivedAt: payload.received_at,
            });
          }
        }
        if (event.type === MessageTypeSocket.MESSAGE_READ_BY_ALL) {
          const payload = event.metadata?.event_payload;
          if (payload?.message_ts) {
            const now = Date.now();
            upsertPendingReadStatus(pendingReadStatus, event.channel, {
              toMessageTs: payload.message_ts,
              readAt: now,
              receivedAt: now,
            });
          }
        }
        if (event.type === MessageTypeSocket.CHANGED) {
          const { metadata, channel, hu_data: huData } = event;
          const payload = metadata?.event_payload;
          updateGroupData({
            id: channel,
            newName: payload?.newName || '',
            newPictureId: payload?.newPictureId || '',
            newDescription: payload?.newDescription || '',
            version: huData.version,
          });
        }
        if (
          event.type === MessageTypeSocket.JOINED ||
          event.type === MessageTypeSocket.LEFT ||
          event.type === MessageTypeSocket.ADMIN_ADD ||
          event.type === MessageTypeSocket.ADMIN_REVOKE
        ) {
          const { channel } = event;
          updateMembers({
            channelId: channel,
            type: event.type,
            membership: event.metadata?.event_payload.membership,
            memberships: event.metadata?.event_payload.memberships,
            userId: user?.id,
          });
        }
        if (event.type === MessageTypeSocket.CHANNEL_INFO_CHANGED) {
          const { metadata, channel: channelId } =
            event as unknown as JsonEventUser;
          const payload = metadata?.event_payload.notification_type;
          const pinnedAt = metadata?.event_payload.pinned_at;
          const markedUnreadAt = metadata?.event_payload.marked_unread_at;
          const version = metadata?.event_payload.version;
          updateChannelProperties({
            pinnedAt,
            channelId,
            version,
            updateFn: channel => ({
              ...channel,
              properties: {
                notification_status: {
                  notification_type: payload || NotificationType.ALL,
                },
                pinned_at: pinnedAt,
                marked_unread_at: markedUnreadAt,
              },
            }),
          });
        }
        if (
          event.type === MessageTypeSocket.PIN_MESSAGE &&
          event.subtype === MessageTypeSocket.PIN_REMOVED
        ) {
          removePinnedMessageCache(event.channel, event.hu_data.message_ts);
        }
        if (
          event.type === MessageTypeSocket.PIN_MESSAGE &&
          event.subtype === MessageTypeSocket.PIN_ADDED
        ) {
          addPinnedMessageCache(event.channel, event);
        }
      }
      if (Object.keys(pendingReadStatus).length > 0) {
        for (const channel in pendingReadStatus) {
          const { toMessageTs, readAt, receivedAt } =
            pendingReadStatus[channel];
          updateMessageReadStatus(channel, toMessageTs, readAt, receivedAt);
        }
      }
      if (Object.keys(unreadChannels).length > 0) {
        for (const channel in unreadChannels) {
          const unreadCount = unreadChannels[channel];

          updateUnreadCountDisplayListInfo({
            channelId: channel,
            count: unreadCount,
            replace: true,
          });
        }
        await refetchUnreadChannels();
      }
      updateConversationTs(data?.events[data?.events.length - 1].ts);
      unreadChannels = {};
    }
  };

  const debouncedUpdateEvents = useMemo(() => debounce(updateEvents, 500), []);

  const handleSocketReconnection = useCallback(() => {
    if (wasDisconnectedRef.current) {
      wasDisconnectedRef.current = false;
      debouncedUpdateEvents();
    }
  }, [id, debouncedUpdateEvents]);

  useEffect(() => {
    debouncedUpdateEvents();

    const intervalId = setInterval(() => {
      debouncedUpdateEvents();
    }, GLOBAL_COOLDOWN);

    return () => {
      clearInterval(intervalId);
    };
  }, [debouncedUpdateEvents]);

  const handleInternetReconnection = useCallback(() => {
    debouncedUpdateEvents();
  }, [id, debouncedUpdateEvents]);

  useNetworkStatus({
    onOnline: handleInternetReconnection,
  });

  useEffect(() => {
    return () => {
      debouncedUpdateEvents.cancel();
    };
  }, [debouncedUpdateEvents]);

  useEffect(() => {
    if (!socket) return;
    socket.listenEvent('disconnect', handleSocketDisconnect);
    socket.listenEvent('connect', handleSocketReconnection);
    socket.listenEvent('reconnect', handleSocketReconnection);
  }, [socket, handleSocketReconnection, handleSocketDisconnect]);
};

export default useConversationsEvents;
