import type { ApiChat, ApiUpdateChat } from '../../../api/types';
import type { ActionReturnType } from '../../types';
import { MAIN_THREAD_ID } from '../../../api/types';

import { ARCHIVED_FOLDER_ID, MAX_ACTIVE_PINNED_CHATS, SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
import { buildCollectionByKey, omit } from '../../../util/iteratees';
import { isLocalMessageId } from '../../../util/keys/messageKey';
import { closeMessageNotifications, notifyAboutMessage } from '../../../util/notifications';
import { checkIfHasUnreadReactions, isChatChannel } from '../../helpers';
import {
  addActionHandler, getGlobal, setGlobal,
} from '../../index';
import {
  addChatListIds,
  addUnreadMentions,
  deleteChatMessages,
  deletePeerPhoto,
  leaveChat,
  removeUnreadMentions,
  replaceChatMessages,
  replacePeerPhotos,
  replacePinnedTopicIds,
  updateChat,
  updateChatFullInfo,
  updateChatListType,
  updateChatParticipantRank,
  updatePeerStoriesHidden,
  updateTopic,
} from '../../reducers';
import { removeUnreadReactions } from '../../reducers/reactions';
import { updateTabState } from '../../reducers/tabs';
import { addUnreadMessageToCounter, replaceThreadLocalStateParam } from '../../reducers/threads';
import {
  selectChat,
  selectChatFullInfo,
  selectChatListType,
  selectChatMessages,
  selectCommonBoxChatId,
  selectCurrentMessageList,
  selectIsChatListed,
  selectPeer,
  selectTabState,
} from '../../selectors';
import { selectThreadLocalStateParam, selectThreadReadState } from '../../selectors/threads';

const TYPING_STATUS_CLEAR_DELAY = 6000; // 6 seconds
const INVALIDATE_FULL_CHAT_FIELDS = new Set<keyof ApiChat>([
  'boostLevel', 'isForum', 'isLinkedInDiscussion', 'fakeType', 'restrictionReasons', 'isJoinToSend', 'isJoinRequest',
  'type',
]);

addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
  switch (update['@type']) {
    case 'updateChat': {
      const localChat = selectChat(global, update.id);
      const localReadState = selectThreadReadState(global, update.id, MAIN_THREAD_ID);
      const { isForum: prevIsForum } = localChat || {};
      const { lastReadOutboxMessageId } = localReadState || {};
      if (update.readState?.lastReadOutboxMessageId && lastReadOutboxMessageId
        && update.readState.lastReadOutboxMessageId < lastReadOutboxMessageId) {
        update = {
          ...update,
          readState: omit(update.readState, ['lastReadOutboxMessageId']),
        };
      }
      global = updateChat(global, update.id, update.chat);

      if (localChat?.areStoriesHidden !== update.chat.areStoriesHidden) {
        global = updatePeerStoriesHidden(global, update.id, update.chat.areStoriesHidden || false);
      }

      const localAdminRights = localChat?.adminRights;
      const newAdminRights = update.chat.adminRights;

      if (localAdminRights && localAdminRights.manageDirectMessages && !update.chat.isMin
        && newAdminRights?.manageDirectMessages !== localAdminRights.manageDirectMessages
        && localChat.linkedMonoforumId) {
        global = replaceChatMessages(global, localChat.linkedMonoforumId, {});
      }

      setGlobal(global);

      const updatedChat = selectChat(global, update.id);
      if (!update.noTopChatsRequest && !selectIsChatListed(global, update.id)
        && !updatedChat?.isNotJoined) {
        // Reload top chats to update chat listing
        actions.loadTopChats();
      }

      if (update.chat.id) {
        closeMessageNotifications({
          chatId: update.chat.id,
          lastReadInboxMessageId: update.readState?.lastReadInboxMessageId,
        });
      }

      Object.values(global.byTabId).forEach(({ id: tabId }) => {
        const { chatId: currentChatId } = selectCurrentMessageList(global, tabId) || {};
        const chatUpdate = update as ApiUpdateChat;
        // The property `isForum` was changed in another client
        if (currentChatId === chatUpdate.id
          && 'isForum' in chatUpdate.chat && prevIsForum !== chatUpdate.chat.isForum) {
          if (prevIsForum) {
            actions.closeForumPanel({ tabId });
          }
          actions.openChat({ id: currentChatId, tabId });
        }
      });

      if (localChat) {
        const chatUpdate = update.chat;
        const changedFields = (Object.keys(chatUpdate) as (keyof ApiChat)[])
          .filter((key) => localChat[key] !== chatUpdate[key]);
        if (changedFields.some((key) => INVALIDATE_FULL_CHAT_FIELDS.has(key))) {
          actions.invalidateFullInfo({ peerId: update.id });
        }
      }

      return undefined;
    }

    case 'updateChatJoin': {
      const listType = selectChatListType(global, update.id);
      const chat = selectChat(global, update.id);

      global = updateChat(global, update.id, { isNotJoined: false, isForbidden: false });

      setGlobal(global);

      if (chat) {
        actions.requestChatUpdate({ chatId: chat.id });
      }

      actions.loadFullChat({ chatId: update.id, force: true });

      if (!listType) {
        return undefined;
      }

      global = getGlobal();
      global = addChatListIds(global, listType, [update.id]);
      setGlobal(global);

      return undefined;
    }

    case 'updateChatLeave': {
      global = leaveChat(global, update.id);
      const chat = selectChat(global, update.id);
      if (chat && isChatChannel(chat)) {
        const chatMessages = selectChatMessages(global, update.id);
        if (chatMessages) {
          const localMessageIds = Object.keys(chatMessages).map(Number).filter(isLocalMessageId);
          global = deleteChatMessages(global, chat.id, localMessageIds);
        }
      }

      return global;
    }

    case 'updateChatTypingStatus': {
      const {
        id, threadId = MAIN_THREAD_ID, typingStatus, peerId,
      } = update;
      const currentTypingStatusByPeerId = selectThreadLocalStateParam(global, id, threadId, 'typingStatusByPeerId');

      if (!typingStatus) {
        if (!currentTypingStatusByPeerId?.[peerId]) {
          return undefined;
        }

        const nextTypingStatusByPeerId = omit(currentTypingStatusByPeerId, [peerId]);
        global = replaceThreadLocalStateParam(
          global,
          id,
          threadId,
          'typingStatusByPeerId',
          Object.keys(nextTypingStatusByPeerId).length ? nextTypingStatusByPeerId : undefined,
        );
        setGlobal(global);

        return undefined;
      }

      const updatedTypingStatusByPeerId = currentTypingStatusByPeerId
        ? { ...currentTypingStatusByPeerId, [peerId]: typingStatus }
        : { [peerId]: typingStatus };
      global = replaceThreadLocalStateParam(global, id, threadId, 'typingStatusByPeerId', updatedTypingStatusByPeerId);
      setGlobal(global);

      setTimeout(() => {
        global = getGlobal();
        const actualTypingStatusByPeerId = selectThreadLocalStateParam(global, id, threadId, 'typingStatusByPeerId');
        const currentTypingStatus = actualTypingStatusByPeerId?.[peerId];

        if (!currentTypingStatus || typingStatus.timestamp !== currentTypingStatus.timestamp) {
          return;
        }

        const nextTypingStatusByPeerId = omit(actualTypingStatusByPeerId, [peerId]);
        global = replaceThreadLocalStateParam(
          global,
          id,
          threadId,
          'typingStatusByPeerId',
          Object.keys(nextTypingStatusByPeerId).length ? nextTypingStatusByPeerId : undefined,
        );
        setGlobal(global);
      }, TYPING_STATUS_CLEAR_DELAY);

      return undefined;
    }

    case 'newMessage': {
      const { chatId, id, message } = update;

      const isOur = message.senderId ? message.senderId === global.currentUserId : message.isOutgoing;
      if (isOur && !message.isFromScheduled) {
        return undefined;
      }

      const isLocal = isLocalMessageId(id);

      const chat = selectChat(global, chatId);
      if (!chat) {
        return undefined;
      }

      const hasMention = Boolean(message.hasUnreadMention);

      if (!isLocal || chat.id === SERVICE_NOTIFICATIONS_USER_ID) {
        global = addUnreadMessageToCounter(global, chatId, message);

        if (hasMention) {
          global = addUnreadMentions({
            global,
            chatId,
            ids: [id],
          });
        }
      }

      setGlobal(global);

      notifyAboutMessage({
        chat,
        message,
      });

      return undefined;
    }

    case 'updateCommonBoxMessages':
    case 'updateChannelMessages': {
      const { ids, messageUpdate } = update;

      ids.forEach((id) => {
        const chatId = ('channelId' in update ? update.channelId : selectCommonBoxChatId(global, id))!;

        if (messageUpdate.reactions && !checkIfHasUnreadReactions(global, messageUpdate.reactions)) {
          global = removeUnreadReactions({ global, chatId, ids: [id] });
        }

        if (!messageUpdate.hasUnreadMention) {
          global = removeUnreadMentions({
            global,
            chatId,
            ids: [id],
          });
        }
      });

      return global;
    }

    case 'updateChatFullInfo': {
      return updateChatFullInfo(global, update.id, update.fullInfo);
    }

    case 'updatePinnedChatIds': {
      const { ids, folderId } = update;
      const listType = folderId === ARCHIVED_FOLDER_ID ? 'archived' : 'active';
      if (!ids) {
        actions.loadPinnedDialogs({ listType });
        return global;
      }

      return {
        ...global,
        chats: {
          ...global.chats,
          orderedPinnedIds: {
            ...global.chats.orderedPinnedIds,
            [listType]: ids.length ? ids : undefined,
          },
        },
      };
    }

    case 'updatePinnedSavedDialogIds': {
      const { ids } = update;

      return {
        ...global,
        chats: {
          ...global.chats,
          orderedPinnedIds: {
            ...global.chats.orderedPinnedIds,
            saved: ids.length ? ids : undefined,
          },
        },
      };
    }

    case 'updateChatPinned': {
      const { id, isPinned } = update;
      const listType = selectChatListType(global, id);
      if (!listType) {
        return undefined;
      }

      const { [listType]: orderedPinnedIds } = global.chats.orderedPinnedIds;

      let newOrderedPinnedIds = orderedPinnedIds || [];
      if (!isPinned) {
        newOrderedPinnedIds = newOrderedPinnedIds.filter((pinnedId) => pinnedId !== id);
      } else if (!newOrderedPinnedIds.includes(id)) {
        // When moving pinned chats to archive, active ordered pinned ids don't get updated
        // (to preserve chat pinned state when it returns from archive)
        // If user already has max pinned chats, we should check for orderedIds
        // that don't point to listed chats
        if (listType === 'active' && newOrderedPinnedIds.length >= MAX_ACTIVE_PINNED_CHATS) {
          const listIds = global.chats.listIds.active;
          newOrderedPinnedIds = newOrderedPinnedIds.filter((pinnedId) => listIds && listIds.includes(pinnedId));
        }

        newOrderedPinnedIds = [id, ...newOrderedPinnedIds];
      }

      return {
        ...global,
        chats: {
          ...global.chats,
          orderedPinnedIds: {
            ...global.chats.orderedPinnedIds,
            [listType]: newOrderedPinnedIds.length ? newOrderedPinnedIds : undefined,
          },
        },
      };
    }

    case 'updateSavedDialogPinned': {
      const { id, isPinned } = update;

      const { saved: orderedPinnedIds } = global.chats.orderedPinnedIds;

      let newOrderedPinnedIds = orderedPinnedIds || [];
      if (!isPinned) {
        newOrderedPinnedIds = newOrderedPinnedIds.filter((pinnedId) => pinnedId !== id);
      } else if (!newOrderedPinnedIds.includes(id)) {
        newOrderedPinnedIds = [id, ...newOrderedPinnedIds];
      }

      return {
        ...global,
        chats: {
          ...global.chats,
          orderedPinnedIds: {
            ...global.chats.orderedPinnedIds,
            saved: newOrderedPinnedIds.length ? newOrderedPinnedIds : undefined,
          },
        },
      };
    }

    case 'updateChatListType': {
      const { id, folderId } = update;

      return updateChatListType(global, id, folderId);
    }

    case 'updateChatFolder': {
      const { id, folder } = update;
      const { byId: chatFoldersById, orderedIds } = global.chatFolders;

      const isDeleted = folder === undefined;

      Object.values(global.byTabId).forEach(({ id: tabId }) => {
        const tabState = selectTabState(global, tabId);
        const isFolderActive = Object.values(chatFoldersById)[tabState.activeChatFolder - 1]?.id === id;

        if (isFolderActive) {
          global = updateTabState(global, { activeChatFolder: 0 }, tabId);
        }
      });

      const newChatFoldersById = !isDeleted ? { ...chatFoldersById, [id]: folder } : omit(chatFoldersById, [id]);
      const newOrderedIds = !isDeleted
        ? orderedIds?.includes(id) ? orderedIds : [...(orderedIds || []), id]
        : orderedIds?.filter((orderedId) => orderedId !== id);

      return {
        ...global,
        chatFolders: {
          ...global.chatFolders,
          byId: newChatFoldersById,
          orderedIds: newOrderedIds,
          invites: omit(global.chatFolders.invites, [id]),
        },
      };
    }

    case 'updateChatFoldersOrder': {
      const { orderedIds } = update;

      return {
        ...global,
        chatFolders: {
          ...global.chatFolders,
          orderedIds,
        },
      };
    }

    case 'updateRecommendedChatFolders': {
      const { folders } = update;

      return {
        ...global,
        chatFolders: {
          ...global.chatFolders,
          recommended: folders,
        },
      };
    }

    case 'updateChatMembers': {
      const targetChatFullInfo = selectChatFullInfo(global, update.id);
      const { replacedMembers, addedMember, deletedMemberId } = update;
      if (!targetChatFullInfo) {
        return undefined;
      }

      let shouldUpdate = false;
      let members = targetChatFullInfo?.members
        ? [...targetChatFullInfo.members]
        : [];

      if (replacedMembers) {
        members = replacedMembers;
        shouldUpdate = true;
      } else if (addedMember) {
        if (
          !members.length
          || !members.some((m) => m.userId === addedMember.userId)
        ) {
          members.push(addedMember);
          shouldUpdate = true;
        }
      } else if (members.length && deletedMemberId) {
        const deleteIndex = members.findIndex((m) => m.userId === deletedMemberId);
        if (deleteIndex > -1) {
          members.slice(deleteIndex, 1);
          shouldUpdate = true;
        }
      }

      if (shouldUpdate) {
        const adminMembers = members.filter(({ isOwner, isAdmin }) => isOwner || isAdmin);
        // TODO Kicked members?

        global = updateChat(global, update.id, { membersCount: members.length });
        global = updateChatFullInfo(global, update.id, {
          members,
          adminMembersById: buildCollectionByKey(adminMembers, 'userId'),
        });

        return global;
      }

      return undefined;
    }

    case 'updateChatParticipantRank': {
      return updateChatParticipantRank(global, update.id, update.userId, update.rank);
    }

    case 'draftMessage': {
      const {
        chatId, threadId, draft,
      } = update;
      const chat = global.chats.byId[chatId];
      if (!chat) {
        return undefined;
      }

      global = replaceThreadLocalStateParam(global, chatId, threadId || MAIN_THREAD_ID, 'draft', draft);
      return global;
    }

    case 'updatePendingJoinRequests': {
      const { chatId, requestsPending, recentRequesterIds } = update;
      const chat = global.chats.byId[chatId];
      if (!chat) {
        return undefined;
      }

      global = updateChatFullInfo(global, chatId, {
        requestsPending,
        recentRequesterIds,
      });
      setGlobal(global);

      actions.loadChatJoinRequests({ chatId });
      return undefined;
    }

    case 'updatePinnedTopic': {
      const { chatId, topicId, isPinned } = update;

      const chat = global.chats.byId[chatId];
      if (!chat) {
        return undefined;
      }

      global = updateTopic(global, chatId, topicId, {
        isPinned,
      });
      setGlobal(global);

      return undefined;
    }

    case 'updatePinnedTopicsOrder': {
      const { chatId, order } = update;

      const chat = global.chats.byId[chatId];
      if (!chat) return undefined;

      global = replacePinnedTopicIds(global, chatId, order);
      setGlobal(global);

      return undefined;
    }

    case 'updateTopic': {
      const { chatId, topicId } = update;

      const chat = selectChat(global, chatId);
      if (!chat?.isForum) return undefined;

      actions.loadTopicById({ chatId, topicId });

      return undefined;
    }

    case 'updateTopics': {
      const { chatId } = update;

      const chat = selectChat(global, chatId);
      if (!chat?.isForum) return undefined;

      actions.loadTopics({ chatId, force: true });

      return undefined;
    }

    case 'updateDiscussion': {
      const { chatId, threadId } = update;

      actions.loadDiscussion({ chatId, threadId });

      return undefined;
    }

    case 'updateViewForumAsMessages': {
      const { chatId, isEnabled } = update;

      const chat = selectChat(global, chatId);
      if (!chat?.isForum) return undefined;

      global = updateChat(global, chatId, {
        isForumAsMessages: isEnabled,
      });
      setGlobal(global);
      break;
    }

    case 'updateNewProfilePhoto': {
      const { peerId, photo } = update;

      global = updateChat(global, peerId, {
        avatarPhotoId: photo.id,
      });
      setGlobal(global);

      actions.loadMoreProfilePhotos({ peerId, shouldInvalidateCache: true });

      break;
    }

    case 'updateDeleteProfilePhoto': {
      const { peerId, photoId } = update;

      const peer = selectPeer(global, peerId);
      if (!peer) {
        return undefined;
      }

      if (!photoId || peer.avatarPhotoId === photoId) {
        global = updateChat(global, peerId, {
          avatarPhotoId: undefined,
        });
        global = replacePeerPhotos(global, peerId, undefined);
      } else {
        global = deletePeerPhoto(global, peerId, photoId);
      }
      setGlobal(global);

      actions.loadMoreProfilePhotos({ peerId, shouldInvalidateCache: true });

      break;
    }
  }

  return undefined;
});
