import type { GroupCallConnectionData } from '../../lib/secret-sauce';
import { Api as GramJs, connection } from '../../lib/gramjs';
import type { ApiMessage, ApiUpdateConnectionStateType, OnApiUpdate } from '../types';

import { pick } from '../../util/iteratees';
import {
  buildApiMessage,
  buildApiMessageFromShort,
  buildApiMessageFromShortChat,
  buildMessageMediaContent,
  buildMessageTextContent,
  buildPoll,
  buildPollResults,
  buildApiMessageFromNotification,
  buildMessageDraft,
  buildMessageReactions,
} from './apiBuilders/messages';
import {
  buildChatMember,
  buildChatMembers,
  buildChatTypingStatus,
  buildAvatarHash,
  buildApiChatFromPreview,
  buildApiChatFolder,
  buildApiChatSettings,
} from './apiBuilders/chats';
import { buildApiUser, buildApiUserStatus } from './apiBuilders/users';
import {
  buildMessageFromUpdate,
  isMessageWithMedia,
  buildChatPhotoForLocalDb,
} from './gramjsBuilders';
import localDb from './localDb';
import { omitVirtualClassFields } from './apiBuilders/helpers';
import { DEBUG } from '../../config';
import {
  addMessageToLocalDb,
  addEntitiesWithPhotosToLocalDb,
  addPhotoToLocalDb,
  resolveMessageApiChatId,
  serializeBytes,
} from './helpers';
import { buildApiNotifyException, buildPrivacyKey, buildPrivacyRules } from './apiBuilders/misc';
import { buildApiPhoto } from './apiBuilders/common';
import {
  buildApiGroupCall,
  buildApiGroupCallParticipant,
  buildPhoneCall,
  getGroupCallId,
} from './apiBuilders/calls';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers';
import { buildApiEmojiInteraction, buildStickerSet } from './apiBuilders/symbols';
import { buildApiBotMenuButton } from './apiBuilders/bots';

type Update = (
  (GramJs.TypeUpdate | GramJs.TypeUpdates) & { _entities?: (GramJs.TypeUser | GramJs.TypeChat)[] }
) | typeof connection.UpdateConnectionState;

const DELETE_MISSING_CHANNEL_MESSAGE_DELAY = 1000;

let onUpdate: OnApiUpdate;

export function init(_onUpdate: OnApiUpdate) {
  onUpdate = _onUpdate;
}

const sentMessageIds = new Set();
let serverTimeOffset = 0;
// Workaround for a situation when an incorrect update comes with an undefined property `adminRights`
let shouldIgnoreNextChannelUpdate = false;
const IGNORE_NEXT_CHANNEL_UPDATE_TIMEOUT = 2000;

function dispatchUserAndChatUpdates(entities: (GramJs.TypeUser | GramJs.TypeChat)[]) {
  entities
    .filter((e) => e instanceof GramJs.User)
    .map(buildApiUser)
    .forEach((user) => {
      if (!user) {
        return;
      }

      onUpdate({
        '@type': 'updateUser',
        id: user.id,
        user,
      });
    });

  entities
    .filter((e) => e instanceof GramJs.Chat || e instanceof GramJs.Channel)
    .map((e) => buildApiChatFromPreview(e))
    .forEach((chat) => {
      if (!chat) {
        return;
      }

      onUpdate({
        '@type': 'updateChat',
        id: chat.id,
        chat,
      });
    });
}

export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
  if (update instanceof connection.UpdateServerTimeOffset) {
    serverTimeOffset = update.timeOffset;
  } else if (update instanceof connection.UpdateConnectionState) {
    let connectionState: ApiUpdateConnectionStateType;

    switch (update.state) {
      case connection.UpdateConnectionState.disconnected:
        connectionState = 'connectionStateConnecting';
        break;
      case connection.UpdateConnectionState.broken:
        connectionState = 'connectionStateBroken';
        break;
      case connection.UpdateConnectionState.connected:
      default:
        connectionState = 'connectionStateReady';
        break;
    }

    onUpdate({
      '@type': 'updateConnectionState',
      connectionState,
    });

    // Messages
  } else if (
    update instanceof GramJs.UpdateNewMessage
    || update instanceof GramJs.UpdateNewScheduledMessage
    || update instanceof GramJs.UpdateNewChannelMessage
    || update instanceof GramJs.UpdateShortChatMessage
    || update instanceof GramJs.UpdateShortMessage
  ) {
    let message: ApiMessage | undefined;
    let shouldForceReply: boolean | undefined;

    if (update instanceof GramJs.UpdateShortChatMessage) {
      message = buildApiMessageFromShortChat(update);
    } else if (update instanceof GramJs.UpdateShortMessage) {
      message = buildApiMessageFromShort(update);
    } else {
      // TODO Remove if proven not reproducing
      if (update.message instanceof GramJs.MessageEmpty) {
        if (DEBUG) {
          // eslint-disable-next-line no-console
          console.error('Unexpected update:', update.className, update);
        }

        return;
      }

      if (update.message instanceof GramJs.Message && isMessageWithMedia(update.message)) {
        addMessageToLocalDb(update.message);
      }

      message = buildApiMessage(update.message)!;
      shouldForceReply = 'replyMarkup' in update.message
        && update.message?.replyMarkup instanceof GramJs.ReplyKeyboardForceReply
        && (!update.message.replyMarkup.selective || message.isMentioned);
    }

    // eslint-disable-next-line no-underscore-dangle
    const entities = update._entities;
    if (entities) {
      addEntitiesWithPhotosToLocalDb(entities);
      dispatchUserAndChatUpdates(entities);
    }

    if (update instanceof GramJs.UpdateNewScheduledMessage) {
      onUpdate({
        '@type': sentMessageIds.has(message.id) ? 'updateScheduledMessage' : 'newScheduledMessage',
        id: message.id,
        chatId: message.chatId,
        message,
      });
    } else {
      onUpdate({
        '@type': sentMessageIds.has(message.id) ? 'updateMessage' : 'newMessage',
        id: message.id,
        chatId: message.chatId,
        message,
        shouldForceReply,
      });
    }

    // Some updates to a Chat/Channel don't have a dedicated update class.
    // We can get info on some updates from Service Messages.
    if (update.message instanceof GramJs.MessageService) {
      const { action } = update.message;

      if (action instanceof GramJs.MessageActionPaymentSent) {
        onUpdate({
          '@type': 'updatePaymentStateCompleted',
        });
      } else if (action instanceof GramJs.MessageActionChatEditTitle) {
        onUpdate({
          '@type': 'updateChat',
          id: message.chatId,
          chat: {
            title: action.title,
          },
        });
      } else if (action instanceof GramJs.MessageActionChatEditPhoto) {
        const photo = buildChatPhotoForLocalDb(action.photo);
        const avatarHash = buildAvatarHash(photo);

        const localDbChatId = resolveMessageApiChatId(update.message)!;
        if (localDb.chats[localDbChatId]) {
          localDb.chats[localDbChatId].photo = photo;
        }
        addPhotoToLocalDb(action.photo);

        if (avatarHash) {
          onUpdate({
            '@type': 'updateChat',
            id: message.chatId,
            chat: {
              avatarHash,
            },
            ...(action.photo instanceof GramJs.Photo && { newProfilePhoto: buildApiPhoto(action.photo) }),
          });
        }
      } else if (action instanceof GramJs.MessageActionChatDeletePhoto) {
        const localDbChatId = resolveMessageApiChatId(update.message)!;
        if (localDb.chats[localDbChatId]) {
          localDb.chats[localDbChatId].photo = new GramJs.ChatPhotoEmpty();
        }

        onUpdate({
          '@type': 'updateChat',
          id: message.chatId,
          chat: { avatarHash: undefined },
        });
      } else if (action instanceof GramJs.MessageActionChatDeleteUser) {
        // eslint-disable-next-line no-underscore-dangle
        if (update._entities && update._entities.some((e): e is GramJs.User => (
          e instanceof GramJs.User && Boolean(e.self) && e.id === action.userId
        ))) {
          onUpdate({
            '@type': 'updateChat',
            id: message.chatId,
            chat: {
              isForbidden: true,
              isNotJoined: true,
            },
          });
        }
      } else if (action instanceof GramJs.MessageActionChatAddUser) {
        // eslint-disable-next-line no-underscore-dangle
        if (update._entities && update._entities.some((e): e is GramJs.User => (
          e instanceof GramJs.User && Boolean(e.self) && action.users.includes(e.id)
        ))) {
          onUpdate({
            '@type': 'updateChatJoin',
            id: message.chatId,
          });
        }
      } else if (action instanceof GramJs.MessageActionGroupCall) {
        if (!action.duration && action.call) {
          onUpdate({
            '@type': 'updateGroupCallChatId',
            chatId: message.chatId,
            call: {
              id: action.call.id.toString(),
              accessHash: action.call.accessHash.toString(),
            },
          });
        }
      }
    }
  } else if (
    update instanceof GramJs.UpdateEditMessage
    || update instanceof GramJs.UpdateEditChannelMessage
  ) {
    // TODO Remove if proven not reproducing
    if (update.message instanceof GramJs.MessageEmpty) {
      if (DEBUG) {
        // eslint-disable-next-line no-console
        console.error('Unexpected update:', update.className, update);
      }

      return;
    }

    if (update.message instanceof GramJs.Message && isMessageWithMedia(update.message)) {
      addMessageToLocalDb(update.message);
    }

    const message = buildApiMessage(update.message)!;

    onUpdate({
      '@type': 'updateMessage',
      id: message.id,
      chatId: message.chatId,
      message,
    });
  } else if (update instanceof GramJs.UpdateMessageReactions) {
    onUpdate({
      '@type': 'updateMessageReactions',
      id: update.msgId,
      chatId: getApiChatIdFromMtpPeer(update.peer),
      reactions: buildMessageReactions(update.reactions),
    });
  } else if (update instanceof GramJs.UpdateDeleteMessages) {
    onUpdate({
      '@type': 'deleteMessages',
      ids: update.messages,
    });
  } else if (update instanceof GramJs.UpdateDeleteScheduledMessages) {
    onUpdate({
      '@type': 'deleteScheduledMessages',
      ids: update.messages,
      chatId: getApiChatIdFromMtpPeer(update.peer),
    });
  } else if (update instanceof GramJs.UpdateDeleteChannelMessages) {
    const chatId = buildApiPeerId(update.channelId, 'channel');
    const ids = update.messages;
    const existingIds = ids.filter((id) => localDb.messages[`${chatId}-${id}`]);
    const missingIds = ids.filter((id) => !localDb.messages[`${chatId}-${id}`]);
    const profilePhotoIds = ids.map((id) => {
      const message = localDb.messages[`${chatId}-${id}`];

      return message && message instanceof GramJs.MessageService && 'photo' in message.action
        ? String(message.action.photo.id)
        : undefined;
    }).filter<string>(Boolean as any);

    if (existingIds.length) {
      onUpdate({
        '@type': 'deleteMessages',
        ids: existingIds,
        chatId,
      });
    }

    if (profilePhotoIds.length) {
      onUpdate({
        '@type': 'deleteProfilePhotos',
        ids: profilePhotoIds,
        chatId,
      });
    }

    // For some reason delete message update sometimes comes before new message update
    if (missingIds.length) {
      setTimeout(() => {
        onUpdate({
          '@type': 'deleteMessages',
          ids: missingIds,
          chatId,
        });
      }, DELETE_MISSING_CHANNEL_MESSAGE_DELAY);
    }
  } else if (update instanceof GramJs.UpdateServiceNotification) {
    if (update.popup) {
      onUpdate({
        '@type': 'error',
        error: {
          message: update.message,
        },
      });
    } else {
      const currentDate = Date.now() / 1000 + serverTimeOffset;
      const message = buildApiMessageFromNotification(update, currentDate);

      if (isMessageWithMedia(update)) {
        addMessageToLocalDb(buildMessageFromUpdate(message.id, message.chatId, update));
      }

      onUpdate({
        '@type': 'updateServiceNotification',
        message,
      });
    }
  } else if ((
    originRequest instanceof GramJs.messages.SendMessage
    || originRequest instanceof GramJs.messages.SendMedia
    || originRequest instanceof GramJs.messages.SendMultiMedia
    || originRequest instanceof GramJs.messages.ForwardMessages
  ) && (
    update instanceof GramJs.UpdateMessageID || update instanceof GramJs.UpdateShortSentMessage
  )) {
    let randomId;
    if ('randomId' in update) {
      randomId = update.randomId;
    } else if ('randomId' in originRequest) {
      randomId = originRequest.randomId;
    }

    const localMessage = randomId && localDb.localMessages[String(randomId)];
    if (!localMessage) {
      throw new Error('Local message not found');
    }

    let newContent: ApiMessage['content'] | undefined;
    if (update instanceof GramJs.UpdateShortSentMessage) {
      if (localMessage.content.text && update.entities) {
        newContent = {
          text: buildMessageTextContent(localMessage.content.text.text, update.entities),
        };
      }
      if (update.media) {
        newContent = {
          ...newContent,
          ...buildMessageMediaContent(update.media),
        };
      }

      const mtpMessage = buildMessageFromUpdate(update.id, localMessage.chatId, update);
      if (isMessageWithMedia(mtpMessage)) {
        addMessageToLocalDb(mtpMessage);
      }
    }

    sentMessageIds.add(update.id);

    // Edge case for "Send When Online"
    const isAlreadySent = 'date' in update && update.date * 1000 < Date.now() + serverTimeOffset * 1000;

    onUpdate({
      '@type': localMessage.isScheduled && !isAlreadySent
        ? 'updateScheduledMessageSendSucceeded'
        : 'updateMessageSendSucceeded',
      chatId: localMessage.chatId,
      localId: localMessage.id,
      message: {
        ...localMessage,
        ...(newContent && {
          content: {
            ...localMessage.content,
            ...newContent,
          },
        }),
        id: update.id,
        sendingState: undefined,
        ...('date' in update && { date: update.date }),
      },
    });
  } else if (update instanceof GramJs.UpdateReadMessagesContents) {
    onUpdate({
      '@type': 'updateCommonBoxMessages',
      ids: update.messages,
      messageUpdate: {
        hasUnreadMention: false,
        isMediaUnread: false,
      },
    });
  } else if (update instanceof GramJs.UpdateChannelReadMessagesContents) {
    onUpdate({
      '@type': 'updateChannelMessages',
      channelId: buildApiPeerId(update.channelId, 'channel'),
      ids: update.messages,
      messageUpdate: {
        hasUnreadMention: false,
        isMediaUnread: false,
      },
    });
  } else if (update instanceof GramJs.UpdateMessagePoll) {
    const { pollId, poll, results } = update;
    if (poll) {
      const apiPoll = buildPoll(poll, results);

      onUpdate({
        '@type': 'updateMessagePoll',
        pollId: String(pollId),
        pollUpdate: apiPoll,
      });
    } else {
      const pollResults = buildPollResults(results);
      onUpdate({
        '@type': 'updateMessagePoll',
        pollId: String(pollId),
        pollUpdate: { results: pollResults },
      });
    }
  } else if (update instanceof GramJs.UpdateMessagePollVote) {
    onUpdate({
      '@type': 'updateMessagePollVote',
      pollId: String(update.pollId),
      userId: buildApiPeerId(update.userId, 'user'),
      options: update.options.map(serializeBytes),
    });
  } else if (update instanceof GramJs.UpdateChannelMessageViews) {
    onUpdate({
      '@type': 'updateMessage',
      chatId: buildApiPeerId(update.channelId, 'channel'),
      id: update.id,
      message: { views: update.views },
    });

    // Chats
  } else if (update instanceof GramJs.UpdateReadHistoryInbox) {
    onUpdate({
      '@type': 'updateChatInbox',
      id: getApiChatIdFromMtpPeer(update.peer),
      chat: {
        lastReadInboxMessageId: update.maxId,
        unreadCount: update.stillUnreadCount,
      },
    });
  } else if (update instanceof GramJs.UpdateReadHistoryOutbox) {
    onUpdate({
      '@type': 'updateChat',
      id: getApiChatIdFromMtpPeer(update.peer),
      chat: {
        lastReadOutboxMessageId: update.maxId,
      },
    });
  } else if (update instanceof GramJs.UpdateReadChannelInbox) {
    onUpdate({
      '@type': 'updateChat',
      id: buildApiPeerId(update.channelId, 'channel'),
      chat: {
        lastReadInboxMessageId: update.maxId,
        unreadCount: update.stillUnreadCount,
      },
    });
  } else if (update instanceof GramJs.UpdateReadChannelOutbox) {
    onUpdate({
      '@type': 'updateChat',
      id: buildApiPeerId(update.channelId, 'channel'),
      chat: {
        lastReadOutboxMessageId: update.maxId,
      },
    });
  } else if (
    update instanceof GramJs.UpdateDialogPinned
    && update.peer instanceof GramJs.DialogPeer
  ) {
    onUpdate({
      '@type': 'updateChatPinned',
      id: getApiChatIdFromMtpPeer(update.peer.peer),
      isPinned: update.pinned || false,
    });
  } else if (update instanceof GramJs.UpdatePinnedDialogs) {
    const ids = update.order
      ? update.order
        .filter((dp): dp is GramJs.DialogPeer => dp instanceof GramJs.DialogPeer)
        .map((dp) => getApiChatIdFromMtpPeer(dp.peer))
      : [];

    onUpdate({
      '@type': 'updatePinnedChatIds',
      ids,
      folderId: update.folderId || undefined,
    });
  } else if (update instanceof GramJs.UpdateFolderPeers) {
    update.folderPeers.forEach((folderPeer) => {
      const { folderId, peer } = folderPeer;

      onUpdate({
        '@type': 'updateChatListType',
        id: getApiChatIdFromMtpPeer(peer),
        folderId,
      });
    });
  } else if (update instanceof GramJs.UpdateDialogFilter) {
    const { id, filter } = update;
    const folder = filter ? buildApiChatFolder(filter) : undefined;

    onUpdate({
      '@type': 'updateChatFolder',
      id,
      folder,
    });
  } else if (update instanceof GramJs.UpdateDialogFilterOrder) {
    onUpdate({
      '@type': 'updateChatFoldersOrder',
      orderedIds: update.order,
    });
  } else if (update instanceof GramJs.UpdateChatParticipants) {
    const replacedMembers = buildChatMembers(update.participants);

    onUpdate({
      '@type': 'updateChatMembers',
      id: buildApiPeerId(update.participants.chatId, 'chat'),
      replacedMembers,
    });
  } else if (update instanceof GramJs.UpdateChatParticipantAdd) {
    const addedMember = buildChatMember(
      pick(update, ['userId', 'inviterId', 'date']) as GramJs.ChatParticipant,
    );

    onUpdate({
      '@type': 'updateChatMembers',
      id: buildApiPeerId(update.chatId, 'chat'),
      addedMember,
    });
  } else if (update instanceof GramJs.UpdateChatParticipantDelete) {
    onUpdate({
      '@type': 'updateChatMembers',
      id: buildApiPeerId(update.chatId, 'chat'),
      deletedMemberId: buildApiPeerId(update.userId, 'user'),
    });
  } else if (
    update instanceof GramJs.UpdatePinnedMessages
    || update instanceof GramJs.UpdatePinnedChannelMessages
  ) {
    const chatId = update instanceof GramJs.UpdatePinnedMessages
      ? getApiChatIdFromMtpPeer(update.peer)
      : buildApiPeerId(update.channelId, 'channel');

    onUpdate({
      '@type': 'updatePinnedIds',
      chatId,
      messageIds: update.messages,
      isPinned: update.pinned,
    });
  } else if (
    update instanceof GramJs.UpdateNotifySettings
    && update.peer instanceof GramJs.NotifyPeer
  ) {
    onUpdate({
      '@type': 'updateNotifyExceptions',
      ...buildApiNotifyException(update.notifySettings, update.peer.peer, serverTimeOffset),
    });
  } else if (
    update instanceof GramJs.UpdateUserTyping
    || update instanceof GramJs.UpdateChatUserTyping
  ) {
    const id = update instanceof GramJs.UpdateUserTyping
      ? buildApiPeerId(update.userId, 'user')
      : buildApiPeerId(update.chatId, 'chat');

    if (update.action instanceof GramJs.SendMessageEmojiInteraction) {
      onUpdate({
        '@type': 'updateStartEmojiInteraction',
        id,
        emoji: update.action.emoticon,
        messageId: update.action.msgId,
        interaction: buildApiEmojiInteraction(JSON.parse(update.action.interaction.data)),
      });
    } else {
      onUpdate({
        '@type': 'updateChatTypingStatus',
        id,
        typingStatus: buildChatTypingStatus(update, serverTimeOffset),
      });
    }
  } else if (update instanceof GramJs.UpdateChannelUserTyping) {
    const id = buildApiPeerId(update.channelId, 'channel');

    onUpdate({
      '@type': 'updateChatTypingStatus',
      id,
      typingStatus: buildChatTypingStatus(update, serverTimeOffset),
    });
  } else if (update instanceof GramJs.UpdateChannel) {
    // eslint-disable-next-line @typescript-eslint/naming-convention
    const { _entities } = update;
    if (!_entities) {
      return;
    }

    const channel = _entities.find((e): e is GramJs.Channel | GramJs.ChannelForbidden => (
      e instanceof GramJs.Channel || e instanceof GramJs.ChannelForbidden
    ));

    if (channel instanceof GramJs.Channel) {
      if (shouldIgnoreNextChannelUpdate) {
        shouldIgnoreNextChannelUpdate = false;
        return;
      }

      if (originRequest instanceof GramJs.messages.ToggleNoForwards) {
        shouldIgnoreNextChannelUpdate = true;
        setTimeout(() => { shouldIgnoreNextChannelUpdate = false; }, IGNORE_NEXT_CHANNEL_UPDATE_TIMEOUT);
      }

      const chat = buildApiChatFromPreview(channel);
      if (chat) {
        onUpdate({
          '@type': 'updateChat',
          id: chat.id,
          chat,
        });

        onUpdate({
          '@type': chat.isNotJoined ? 'updateChatLeave' : 'updateChatJoin',
          id: buildApiPeerId(update.channelId, 'channel'),
        });
      }
    } else if (channel instanceof GramJs.ChannelForbidden) {
      const chatId = buildApiPeerId(update.channelId, 'channel');

      onUpdate({
        '@type': 'updateChat',
        id: chatId,
        chat: {
          isRestricted: true,
        },
      });

      onUpdate({
        '@type': 'updateChatLeave',
        id: chatId,
      });
    } else if (_entities.length === 0) {
      // The link to the discussion group may have been changed.
      // No corresponding update available at this moment https://core.telegram.org/type/Updates
      onUpdate({
        '@type': 'resetMessages',
        id: buildApiPeerId(update.channelId, 'channel'),
      });
    }
  } else if (
    update instanceof GramJs.UpdateDialogUnreadMark
    && update.peer instanceof GramJs.DialogPeer
  ) {
    onUpdate({
      '@type': 'updateChat',
      id: getApiChatIdFromMtpPeer(update.peer.peer),
      chat: {
        hasUnreadMark: update.unread,
      },
    });
  } else if (update instanceof GramJs.UpdateChatDefaultBannedRights) {
    onUpdate({
      '@type': 'updateChat',
      id: getApiChatIdFromMtpPeer(update.peer),
      chat: {
        defaultBannedRights: omitVirtualClassFields(update.defaultBannedRights),
      },
    });

    // Users
  } else if (update instanceof GramJs.UpdateUserStatus) {
    onUpdate({
      '@type': 'updateUserStatus',
      userId: buildApiPeerId(update.userId, 'user'),
      status: buildApiUserStatus(update.status),
    });
  } else if (update instanceof GramJs.UpdateUserName) {
    const apiUserId = buildApiPeerId(update.userId, 'user');
    const updatedUser = localDb.users[apiUserId];
    const user = updatedUser?.mutualContact && !updatedUser.self
      ? pick(update, ['username'])
      : pick(update, ['firstName', 'lastName', 'username']);

    onUpdate({
      '@type': 'updateUser',
      id: apiUserId,
      user,
    });
  } else if (update instanceof GramJs.UpdateUserPhoto) {
    const { userId, photo } = update;
    const apiUserId = buildApiPeerId(userId, 'user');
    const avatarHash = buildAvatarHash(photo);

    if (localDb.users[apiUserId]) {
      localDb.users[apiUserId].photo = photo;
    }

    onUpdate({
      '@type': 'updateUser',
      id: apiUserId,
      user: { avatarHash },
    });
  } else if (update instanceof GramJs.UpdateUserPhone) {
    const { userId, phone } = update;

    onUpdate({
      '@type': 'updateUser',
      id: buildApiPeerId(userId, 'user'),
      user: { phoneNumber: phone },
    });
  } else if (update instanceof GramJs.UpdatePeerSettings) {
    // eslint-disable-next-line @typescript-eslint/naming-convention
    const { _entities, settings } = update;
    if (!_entities) {
      return;
    }

    if (_entities?.length) {
      _entities
        .filter((e) => e instanceof GramJs.User && !e.contact)
        .forEach((user) => {
          onUpdate({
            '@type': 'deleteContact',
            id: buildApiPeerId(user.id, 'user'),
          });
        });

      _entities
        .filter((e) => e instanceof GramJs.User && e.contact)
        .map(buildApiUser)
        .forEach((user) => {
          if (!user) {
            return;
          }

          onUpdate({
            '@type': 'updateUser',
            id: user.id,
            user: {
              ...user,
              ...(settings && { settings: buildApiChatSettings(settings) }),
            },
          });
        });
    }

    // Settings
  } else if (update instanceof GramJs.UpdateNotifySettings) {
    const {
      notifySettings: {
        showPreviews, silent, muteUntil,
      },
      peer: { className },
    } = update;

    const peerType = className === 'NotifyUsers'
      ? 'contact'
      : (className === 'NotifyChats'
        ? 'group'
        : (className === 'NotifyBroadcasts'
          ? 'broadcast'
          : undefined
        )
      );

    if (!peerType) {
      return;
    }

    onUpdate({
      '@type': 'updateNotifySettings',
      peerType,
      isSilent: Boolean(silent
        || (typeof muteUntil === 'number' && Date.now() + serverTimeOffset * 1000 < muteUntil * 1000)),
      shouldShowPreviews: Boolean(showPreviews),
    });
  } else if (update instanceof GramJs.UpdatePeerBlocked) {
    onUpdate({
      '@type': 'updatePeerBlocked',
      id: getApiChatIdFromMtpPeer(update.peerId),
      isBlocked: update.blocked,
    });
  } else if (update instanceof GramJs.UpdatePrivacy) {
    const key = buildPrivacyKey(update.key);
    if (key) {
      onUpdate({
        '@type': 'updatePrivacy',
        key,
        rules: buildPrivacyRules(update.rules),
      });
    }

    // Misc
  } else if (update instanceof GramJs.UpdateDraftMessage) {
    onUpdate({
      '@type': 'draftMessage',
      chatId: getApiChatIdFromMtpPeer(update.peer),
      ...buildMessageDraft(update.draft),
    });
  } else if (update instanceof GramJs.UpdateContactsReset) {
    onUpdate({ '@type': 'updateResetContactList' });
  } else if (update instanceof GramJs.UpdateFavedStickers) {
    onUpdate({ '@type': 'updateFavoriteStickers' });
  } else if (update instanceof GramJs.UpdateRecentStickers) {
    onUpdate({ '@type': 'updateRecentStickers' });
  } else if (update instanceof GramJs.UpdateStickerSets) {
    onUpdate({ '@type': 'updateStickerSets' });
  } else if (update instanceof GramJs.UpdateStickerSetsOrder) {
    onUpdate({ '@type': 'updateStickerSetsOrder', order: update.order.map((n) => n.toString()) });
  } else if (update instanceof GramJs.UpdateNewStickerSet) {
    if (update.stickerset instanceof GramJs.messages.StickerSet) {
      const stickerSet = buildStickerSet(update.stickerset.set);
      onUpdate({
        '@type': 'updateStickerSet',
        id: stickerSet.id,
        stickerSet,
      });
    }
  } else if (update instanceof GramJs.UpdateSavedGifs) {
    onUpdate({ '@type': 'updateSavedGifs' });
  } else if (update instanceof GramJs.UpdateGroupCall) {
    onUpdate({
      '@type': 'updateGroupCall',
      call: buildApiGroupCall(update.call),
    });
  } else if (update instanceof GramJs.UpdateGroupCallConnection) {
    onUpdate({
      '@type': 'updateGroupCallConnection',
      data: JSON.parse(update.params.data) as GroupCallConnectionData,
      presentation: Boolean(update.presentation),
    });
  } else if (update instanceof GramJs.UpdateGroupCallParticipants) {
    // eslint-disable-next-line no-underscore-dangle
    const entities = update._entities;
    if (entities) {
      addEntitiesWithPhotosToLocalDb(entities);
      dispatchUserAndChatUpdates(entities);
    }

    onUpdate({
      '@type': 'updateGroupCallParticipants',
      groupCallId: getGroupCallId(update.call),
      participants: update.participants.map(buildApiGroupCallParticipant),
    });
  } else if (update instanceof GramJs.UpdatePendingJoinRequests) {
    // eslint-disable-next-line no-underscore-dangle
    const entities = update._entities;
    if (entities) {
      addEntitiesWithPhotosToLocalDb(entities);
      dispatchUserAndChatUpdates(entities);
    }

    onUpdate({
      '@type': 'updatePendingJoinRequests',
      chatId: getApiChatIdFromMtpPeer(update.peer),
      recentRequesterIds: update.recentRequesters.map((id) => buildApiPeerId(id, 'user')),
      requestsPending: update.requestsPending,
    });
  } else if (update instanceof GramJs.UpdatePhoneCall) {
    // eslint-disable-next-line no-underscore-dangle
    const entities = update._entities;
    if (entities) {
      addEntitiesWithPhotosToLocalDb(entities);
      dispatchUserAndChatUpdates(entities);
    }

    onUpdate({
      '@type': 'updatePhoneCall',
      call: buildPhoneCall(update.phoneCall),
    });
  } else if (update instanceof GramJs.UpdatePhoneCallSignalingData) {
    onUpdate({
      '@type': 'updatePhoneCallSignalingData',
      callId: update.phoneCallId.toString(),
      data: Array.from(update.data),
    });
  } else if (update instanceof GramJs.UpdateWebViewResultSent) {
    const { queryId } = update;

    onUpdate({
      '@type': 'updateWebViewResultSent',
      queryId: queryId.toString(),
    });
  } else if (update instanceof GramJs.UpdateBotMenuButton) {
    const { botId, button } = update;

    const id = buildApiPeerId(botId, 'user');

    onUpdate({
      '@type': 'updateBotMenuButton',
      botId: id,
      button: buildApiBotMenuButton(button),
    });
  } else if (DEBUG) {
    const params = typeof update === 'object' && 'className' in update ? update.className : update;
    // eslint-disable-next-line no-console
    console.warn('[GramJs/updater] Unexpected update:', params);
  }
}
