/* eslint-disable no-restricted-syntax */
import { createSelector } from '@reduxjs/toolkit';
import { filter, isEmpty, isFinite, isNumber, pick, sortBy, toNumber } from 'lodash';

import {
  ConversationLookupType,
  ConversationsStateType,
  lookupQuote,
  MentionsMembersType,
  MessageModelPropsWithConvoProps,
  MessageModelPropsWithoutConvoProps,
  PropsForQuote,
  QuoteLookupType,
  ReduxConversationType,
  SortedMessageModelProps,
} from '../ducks/conversations';
import { StateType } from '../reducer';

import { ReplyingToMessageProps } from '../../components/conversation/composition/CompositionBox';
import { MessageAttachmentSelectorProps } from '../../components/conversation/message/message-content/MessageAttachment';
import { MessageContentSelectorProps } from '../../components/conversation/message/message-content/MessageContent';
import { MessageContentWithStatusSelectorProps } from '../../components/conversation/message/message-content/MessageContentWithStatus';
import { MessageTextSelectorProps } from '../../components/conversation/message/message-content/MessageText';
import { GenericReadableMessageSelectorProps } from '../../components/conversation/message/message-item/GenericReadableMessage';
import { hasValidIncomingRequestValues } from '../../models/conversation';
import { isOpenOrClosedGroup } from '../../models/conversationAttributes';
import { getConversationController } from '../../session/conversations';
import { UserUtils } from '../../session/utils';
import { BlockedNumberController } from '../../util';
import { Storage } from '../../util/storage';
import { getIntl } from './user';

import { MessageReactsSelectorProps } from '../../components/conversation/message/message-content/MessageReactions';
import { processQuoteAttachment } from '../../models/message';
import { isUsAnySogsFromCache } from '../../session/apis/open_group_api/sogsv3/knownBlindedkeys';
import { PubKey } from '../../session/types';
import { getSelectedConversationKey } from './selectedConversation';
import { getModeratorsOutsideRedux } from './sogsRoomInfo';
import { CONVERSATION_PRIORITIES } from '../../models/types';

export const getConversations = (state: StateType): ConversationsStateType => state.conversations;

export const getConversationLookup = (state: StateType): ConversationLookupType => {
  return state.conversations.conversationLookup;
};

export const getConversationsCount = createSelector(getConversationLookup, (state): number => {
  return Object.keys(state).length;
});

const getConversationQuotes = (state: StateType): QuoteLookupType | undefined => {
  return state.conversations.quotes;
};

export const getOurPrimaryConversation = createSelector(
  getConversations,
  (state: ConversationsStateType): ReduxConversationType =>
    state.conversationLookup[Storage.get('primaryDevicePubKey') as string]
);

const getMessagesOfSelectedConversation = (
  state: StateType
): Array<MessageModelPropsWithoutConvoProps> => state.conversations.messages;

// Redux recommends to do filtered and deriving state in a selector rather than ourself
export const getSortedMessagesOfSelectedConversation = createSelector(
  getMessagesOfSelectedConversation,
  (messages: Array<MessageModelPropsWithoutConvoProps>): Array<SortedMessageModelProps> => {
    if (messages.length === 0) {
      return [];
    }

    const convoId = messages[0].propsForMessage.convoId;
    const convo = getConversationController().get(convoId);

    if (!convo) {
      return [];
    }

    const isPublic = convo.isPublic() || false;
    const sortedMessage = sortMessages(messages, isPublic);

    return updateFirstMessageOfSeries(sortedMessage);
  }
);

export const hasSelectedConversationIncomingMessages = createSelector(
  getSortedMessagesOfSelectedConversation,
  (messages: Array<MessageModelPropsWithoutConvoProps>): boolean => {
    return messages.some(m => m.propsForMessage.direction === 'incoming');
  }
);

export const getFirstUnreadMessageId = (state: StateType): string | undefined => {
  return state.conversations.firstUnreadMessageId;
};

export type MessagePropsType =
  | 'group-notification'
  | 'group-invitation'
  | 'data-extraction'
  | 'message-request-response'
  | 'timer-notification'
  | 'regular-message'
  | 'unread-indicator'
  | 'call-notification'
  | 'interaction-notification';

export const getSortedMessagesTypesOfSelectedConversation = createSelector(
  getSortedMessagesOfSelectedConversation,
  getFirstUnreadMessageId,
  (sortedMessages, firstUnreadId) => {
    const maxMessagesBetweenTwoDateBreaks = 5;
    // we want to show the date break if there is a large jump in time
    // remember that messages are sorted from the most recent to the oldest
    return sortedMessages.map((msg, index) => {
      const isFirstUnread = Boolean(firstUnreadId === msg.propsForMessage.id);
      const messageTimestamp = msg.propsForMessage.serverTimestamp || msg.propsForMessage.timestamp;
      // do not show the date break if we are the oldest message (no previous)
      // this is to smooth a bit the loading of older message (to avoid a jump once new messages are rendered)
      const previousMessageTimestamp =
        index + 1 >= sortedMessages.length
          ? 0
          : sortedMessages[index + 1].propsForMessage.serverTimestamp ||
            sortedMessages[index + 1].propsForMessage.timestamp;

      const showDateBreak =
        messageTimestamp - previousMessageTimestamp > maxMessagesBetweenTwoDateBreaks * 60 * 1000
          ? messageTimestamp
          : undefined;

      const common = { showUnreadIndicator: isFirstUnread, showDateBreak };

      if (msg.propsForDataExtractionNotification) {
        return {
          ...common,
          message: {
            messageType: 'data-extraction',
            props: { ...msg.propsForDataExtractionNotification, messageId: msg.propsForMessage.id },
          },
        };
      }

      if (msg.propsForMessageRequestResponse) {
        return {
          ...common,
          message: {
            messageType: 'message-request-response',
            props: { ...msg.propsForMessageRequestResponse, messageId: msg.propsForMessage.id },
          },
        };
      }

      if (msg.propsForGroupInvitation) {
        return {
          ...common,
          message: {
            messageType: 'group-invitation',
            props: { ...msg.propsForGroupInvitation, messageId: msg.propsForMessage.id },
          },
        };
      }

      if (msg.propsForGroupUpdateMessage) {
        return {
          ...common,
          message: {
            messageType: 'group-notification',
            props: { ...msg.propsForGroupUpdateMessage, messageId: msg.propsForMessage.id },
          },
        };
      }

      if (msg.propsForTimerNotification) {
        return {
          ...common,
          message: {
            messageType: 'timer-notification',
            props: { ...msg.propsForTimerNotification, messageId: msg.propsForMessage.id },
          },
        };
      }

      if (msg.propsForCallNotification) {
        return {
          ...common,
          message: {
            messageType: 'call-notification',
            props: {
              ...msg.propsForCallNotification,
              messageId: msg.propsForMessage.id,
            },
          },
        };
      }

      if (msg.propsForInteractionNotification) {
        return {
          ...common,
          message: {
            messageType: 'interaction-notification',
            props: {
              ...msg.propsForInteractionNotification,
              messageId: msg.propsForMessage.id,
            },
          },
        };
      }

      return {
        showUnreadIndicator: isFirstUnread,
        showDateBreak,
        message: {
          messageType: 'regular-message',
          props: { messageId: msg.propsForMessage.id },
        },
      };
    });
  }
);

function getConversationTitle(conversation: ReduxConversationType): string {
  if (conversation.displayNameInProfile) {
    return conversation.displayNameInProfile;
  }

  if (isOpenOrClosedGroup(conversation.type)) {
    return window.i18n('unknown');
  }
  return conversation.id;
}

const collator = new Intl.Collator();

export const _getConversationComparator = () => {
  return (left: ReduxConversationType, right: ReduxConversationType): number => {
    // Pin is the first criteria to check
    const leftPriority = left.priority || 0;
    const rightPriority = right.priority || 0;
    if (leftPriority > rightPriority) {
      return -1;
    }
    if (rightPriority > leftPriority) {
      return 1;
    }
    // Then if none are pinned, check other criteria
    const leftActiveAt = left.activeAt;
    const rightActiveAt = right.activeAt;
    if (leftActiveAt && !rightActiveAt) {
      return -1;
    }
    if (rightActiveAt && !leftActiveAt) {
      return 1;
    }
    if (leftActiveAt && rightActiveAt && leftActiveAt !== rightActiveAt) {
      return rightActiveAt - leftActiveAt;
    }
    const leftTitle = getConversationTitle(left).toLowerCase();
    const rightTitle = getConversationTitle(right).toLowerCase();

    return collator.compare(leftTitle, rightTitle);
  };
};

export const getConversationComparator = createSelector(getIntl, _getConversationComparator);

const _getLeftPaneConversationIds = (
  sortedConversations: Array<ReduxConversationType>
): Array<string> => {
  return sortedConversations
    .filter(conversation => {
      if (conversation.isBlocked) {
        return false;
      }

      // a non private conversation is always returned here
      if (!conversation.isPrivate) {
        return true;
      }

      // a private conversation not approved is a message request. Exclude them from the left pane lists
      if (!conversation.isApproved) {
        return false;
      }

      // a hidden contact conversation is only visible from the contact list, not from the global conversation list
      if (conversation.priority && conversation.priority <= CONVERSATION_PRIORITIES.default) {
        return false;
      }

      return true;
    })
    .map(m => m.id);
};

const _getContacts = (
  sortedConversations: Array<ReduxConversationType>
): Array<ReduxConversationType> => {
  return sortedConversations.filter(convo => {
    // a private conversation not approved is a message request. Include them in the list of contacts
    return !convo.isBlocked && convo.isPrivate && !convo.isMe;
  });
};

const _getGlobalUnreadCount = (sortedConversations: Array<ReduxConversationType>): number => {
  let globalUnreadCount = 0;
  for (const conversation of sortedConversations) {
    // Blocked conversation are now only visible from the settings, not in the conversation list, so don't add it neither to the contacts list nor the conversation list
    if (conversation.isBlocked) {
      continue;
    }

    // a private conversation not approved is a message request. Exclude them from the unread count
    if (conversation.isPrivate && !conversation.isApproved) {
      continue;
    }

    // a hidden contact conversation is only visible from the contact list, not from the global conversation list
    if (
      conversation.isPrivate &&
      conversation.priority &&
      conversation.priority <= CONVERSATION_PRIORITIES.default
    ) {
      // dont increase unread counter, don't push to convo list.
      continue;
    }

    if (
      isNumber(conversation.unreadCount) &&
      isFinite(conversation.unreadCount) &&
      conversation.unreadCount > 0 &&
      conversation.currentNotificationSetting !== 'disabled'
    ) {
      globalUnreadCount += conversation.unreadCount;
    }
  }
  return globalUnreadCount;
};

export const _getSortedConversations = (
  lookup: ConversationLookupType,
  comparator: (left: ReduxConversationType, right: ReduxConversationType) => number
): Array<ReduxConversationType> => {
  const values = Object.values(lookup);
  const sorted = values.sort(comparator);

  const sortedConversations: Array<ReduxConversationType> = [];

  for (const conversation of sorted) {
    // Remove all invalid conversations and conversatons of devices associated
    //  with cancelled attempted links
    if (!conversation.isPublic && !conversation.activeAt) {
      continue;
    }

    const isBlocked = BlockedNumberController.isBlocked(conversation.id);

    sortedConversations.push({
      ...conversation,
      isBlocked: isBlocked || undefined,
    });
  }

  return sortedConversations;
};

export const getSortedConversations = createSelector(
  getConversationLookup,
  getConversationComparator,
  getSelectedConversationKey,
  _getSortedConversations
);

/**
 *
 * @param sortedConversations List of conversations that are valid for both requests and regular conversation inbox
 * @returns A list of message request conversations.
 */
const _getConversationRequests = (
  sortedConversations: Array<ReduxConversationType>
): Array<ReduxConversationType> => {
  return filter(sortedConversations, conversation => {
    const { isApproved, isBlocked, isPrivate, isMe, activeAt, didApproveMe } = conversation;
    const isIncomingRequest = hasValidIncomingRequestValues({
      isApproved: isApproved || false,
      isBlocked: isBlocked || false,
      isPrivate: isPrivate || false,
      isMe: isMe || false,
      activeAt: activeAt || 0,
      didApproveMe: didApproveMe || false,
    });
    return isIncomingRequest;
  });
};

export const getConversationRequests = createSelector(
  getSortedConversations,
  _getConversationRequests
);

export const getConversationRequestsIds = createSelector(getConversationRequests, requests =>
  requests.map(m => m.id)
);

export const hasConversationRequests = (state: StateType) => {
  return !!getConversationRequests(state).length;
};

const _getUnreadConversationRequests = (
  sortedConversationRequests: Array<ReduxConversationType>
): Array<ReduxConversationType> => {
  return filter(sortedConversationRequests, conversation => {
    return Boolean(conversation && conversation.unreadCount && conversation.unreadCount > 0);
  });
};

export const getUnreadConversationRequests = createSelector(
  getConversationRequests,
  _getUnreadConversationRequests
);

/**
 * Returns all the conversation ids of private conversations which are
 * - private
 * - not me
 * - not blocked
 * - approved (or message requests are disabled)
 * - active_at is set to something truthy
 */
export const getLeftPaneConversationIds = createSelector(
  getSortedConversations,
  _getLeftPaneConversationIds
);

export const getLeftPaneConversationIdsCount = createSelector(
  getLeftPaneConversationIds,
  (convoIds: Array<string>) => {
    return convoIds.length;
  }
);

/**
 * Returns all the conversation ids of contacts which are
 * - private
 * - not me
 * - not blocked
 */
const getContacts = createSelector(getSortedConversations, _getContacts);

export const getContactsCount = createSelector(
  getContacts,
  (contacts: Array<ReduxConversationType>) => contacts.length
);

export type DirectContactsByNameType = {
  displayName?: string;
  id: string;
};

// make sure that createSelector is called here so this function is memoized
export const getSortedContacts = createSelector(
  getContacts,
  (contacts: Array<ReduxConversationType>): Array<DirectContactsByNameType> => {
    const us = UserUtils.getOurPubKeyStrFromCache();
    const extractedContacts = contacts
      .filter(m => m.id !== us)
      .map(m => {
        return {
          id: m.id,
          displayName: m.nickname || m.displayNameInProfile,
        };
      });

    const contactsStartingWithANumber = sortBy(
      extractedContacts.filter(
        m => !m.displayName || (m.displayName && m.displayName[0].match(/^[0-9]+$/))
      ),
      m => m.displayName || m.id
    );

    const contactsWithDisplayName = sortBy(
      extractedContacts.filter(m => !!m.displayName && !m.displayName[0].match(/^[0-9]+$/)),
      m => m.displayName?.toLowerCase()
    );

    return [...contactsWithDisplayName, ...contactsStartingWithANumber];
  }
);

export const getSortedContactsWithBreaks = createSelector(
  getSortedContacts,
  (contacts: Array<DirectContactsByNameType>): Array<DirectContactsByNameType | string> => {
    // add a break wherever needed
    const unknownSection = 'unknown';
    let currentChar = '';
    // if the item is a string we consider it to be a break of that string
    const contactsWithBreaks: Array<DirectContactsByNameType | string> = [];

    contacts.forEach(m => {
      if (
        !!m.displayName &&
        m.displayName[0].toLowerCase() !== currentChar &&
        !m.displayName[0].match(/^[0-9]+$/)
      ) {
        currentChar = m.displayName[0].toLowerCase();
        contactsWithBreaks.push(currentChar.toUpperCase());
      } else if (
        ((m.displayName && m.displayName[0].match(/^[0-9]+$/)) || !m.displayName) &&
        currentChar !== unknownSection
      ) {
        currentChar = unknownSection;
        contactsWithBreaks.push('#');
      }

      contactsWithBreaks.push(m);
    });

    contactsWithBreaks.unshift({
      id: UserUtils.getOurPubKeyStrFromCache(),
      displayName: window.i18n('noteToSelf'),
    });

    return contactsWithBreaks;
  }
);

export const getPrivateContactsPubkeys = createSelector(getSortedContacts, state =>
  state.map(m => m.id)
);

export const getGlobalUnreadMessageCount = createSelector(
  getSortedConversations,
  _getGlobalUnreadCount
);

export const getMessageInfoId = (state: StateType) => state.conversations.messageInfoId;

export const isRightPanelShowing = (state: StateType): boolean =>
  state.conversations.showRightPanel;

export const isMessageSelectionMode = (state: StateType): boolean =>
  state.conversations.selectedMessageIds.length > 0;

export const getSelectedMessageIds = (state: StateType): Array<string> =>
  state.conversations.selectedMessageIds;

export const getIsMessageSelectionMode = (state: StateType): boolean =>
  Boolean(getSelectedMessageIds(state).length);

export const getQuotedMessage = (state: StateType): ReplyingToMessageProps | undefined =>
  state.conversations.quotedMessage;

export const areMoreMessagesBeingFetched = (state: StateType): boolean =>
  state.conversations.areMoreMessagesBeingFetched || false;

export const getShowScrollButton = (state: StateType): boolean =>
  state.conversations.showScrollButton || false;

export const getQuotedMessageToAnimate = (state: StateType): string | undefined =>
  state.conversations.animateQuotedMessageId || undefined;

export const getShouldHighlightMessage = (state: StateType): boolean =>
  Boolean(state.conversations.animateQuotedMessageId && state.conversations.shouldHighlightMessage);

export const getNextMessageToPlayId = (state: StateType): string | undefined =>
  state.conversations.nextMessageToPlayId || undefined;

export const getMentionsInput = (state: StateType): MentionsMembersType =>
  state.conversations.mentionMembers;

/// Those calls are just related to ordering messages in the redux store.

function updateFirstMessageOfSeries(
  messageModelsProps: Array<MessageModelPropsWithoutConvoProps>
): Array<SortedMessageModelProps> {
  // messages are got from the more recent to the oldest, so we need to check if
  // the next messages in the list is still the same author.
  // The message is the first of the series if the next message is not from the same author
  const sortedMessageProps: Array<SortedMessageModelProps> = [];

  for (let i = 0; i < messageModelsProps.length; i++) {
    const currentSender = messageModelsProps[i].propsForMessage?.sender;
    // most recent message is at index 0, so the previous message sender is 1+index
    const previousSender =
      i < messageModelsProps.length - 1
        ? messageModelsProps[i + 1].propsForMessage?.sender
        : undefined;
    const nextSender = i > 0 ? messageModelsProps[i - 1].propsForMessage?.sender : undefined;
    // Handle firstMessageOfSeries for conditional avatar rendering

    sortedMessageProps.push({
      ...messageModelsProps[i],
      firstMessageOfSeries: !(i >= 0 && currentSender === previousSender),
      lastMessageOfSeries: currentSender !== nextSender,
    });
  }
  return sortedMessageProps;
}

function sortMessages(
  messages: Array<MessageModelPropsWithoutConvoProps>,
  isPublic: boolean
): Array<MessageModelPropsWithoutConvoProps> {
  // we order by serverTimestamp for public convos
  // be sure to update the sorting order to fetch messages from the DB too at getMessagesByConversation
  if (isPublic) {
    return messages.slice().sort((a, b) => {
      return (b.propsForMessage.serverTimestamp || 0) - (a.propsForMessage.serverTimestamp || 0);
    });
  }
  if (messages.some(n => !n.propsForMessage.timestamp && !n.propsForMessage.receivedAt)) {
    throw new Error('Found some messages without any timestamp set');
  }

  // for non public convos, we order by sent_at or received_at timestamp.
  // we assume that a message has either a sent_at or a received_at field set.
  const messagesSorted = messages
    .slice()
    .sort(
      (a, b) =>
        (b.propsForMessage.timestamp || b.propsForMessage.receivedAt || 0) -
        (a.propsForMessage.timestamp || a.propsForMessage.receivedAt || 0)
    );

  return messagesSorted;
}

/**
 * This returns the most recent message id in the database. This is not the most recent message shown,
 * but the most recent one, which could still not be loaded.
 */
export const getMostRecentMessageId = (state: StateType): string | null => {
  return state.conversations.mostRecentMessageId;
};

export const getMostRecentOutgoingMessageId = createSelector(
  getSortedMessagesOfSelectedConversation,
  (messages: Array<MessageModelPropsWithoutConvoProps>): string | undefined => {
    return messages.find(m => m.propsForMessage.direction === 'outgoing')?.propsForMessage.id;
  }
);

export const getOldestMessageId = createSelector(
  getSortedMessagesOfSelectedConversation,
  (messages: Array<MessageModelPropsWithoutConvoProps>): string | undefined => {
    const oldest =
      messages.length > 0 ? messages[messages.length - 1].propsForMessage.id : undefined;

    return oldest;
  }
);

export const getYoungestMessageId = createSelector(
  getSortedMessagesOfSelectedConversation,
  (messages: Array<MessageModelPropsWithoutConvoProps>): string | undefined => {
    const youngest = messages.length > 0 ? messages[0].propsForMessage.id : undefined;

    return youngest;
  }
);

function getMessagesFromState(state: StateType) {
  return state.conversations.messages;
}

export function getLoadedMessagesLength(state: StateType) {
  return getMessagesFromState(state).length;
}

export function getSelectedHasMessages(state: StateType): boolean {
  return !isEmpty(getMessagesFromState(state));
}

export const isFirstUnreadMessageIdAbove = createSelector(
  getConversations,
  (state: ConversationsStateType): boolean => {
    if (!state.firstUnreadMessageId) {
      return false;
    }

    const isNotPresent = !state.messages.some(
      m => m.propsForMessage.id === state.firstUnreadMessageId
    );

    return isNotPresent;
  }
);

const getMessageId = (_whatever: any, id: string | undefined) => id;

/**
 * A lot of our UI changes on the main panel need to happen quickly (composition box).
 */
export const getSelectedConversation = createSelector(
  getConversationLookup,
  getSelectedConversationKey,
  (lookup, selectedConvo) => {
    return selectedConvo ? lookup[selectedConvo] : undefined;
  }
);

export const getMessagePropsByMessageId = createSelector(
  getSortedMessagesOfSelectedConversation,
  getSelectedConversation,
  getMessageId,
  (
    messages: Array<SortedMessageModelProps>,
    selectedConvo,
    id
  ): MessageModelPropsWithConvoProps | undefined => {
    if (!id) {
      return undefined;
    }
    const foundMessageProps: SortedMessageModelProps | undefined = messages?.find(
      m => m?.propsForMessage?.id === id
    );

    if (!foundMessageProps || !foundMessageProps.propsForMessage.convoId) {
      return undefined;
    }
    const sender = foundMessageProps?.propsForMessage?.sender;

    // we can only show messages when the convo is selected.
    if (!selectedConvo || !sender) {
      return undefined;
    }

    const ourPubkey = UserUtils.getOurPubKeyStrFromCache();
    const isGroup = !selectedConvo.isPrivate;
    const isPublic = selectedConvo.isPublic;

    const groupAdmins = (isGroup && selectedConvo.groupAdmins) || [];
    const weAreAdmin = groupAdmins.includes(ourPubkey) || false;

    const weAreModerator =
      (isPublic && getModeratorsOutsideRedux(selectedConvo.id).includes(ourPubkey)) || false;
    // A message is deletable if
    // either we sent it,
    // or the convo is not a public one (in this case, we will only be able to delete for us)
    // or the convo is public and we are an admin or moderator
    const isDeletable =
      sender === ourPubkey || !isPublic || (isPublic && (weAreAdmin || weAreModerator));

    // A message is deletable for everyone if
    // either we sent it no matter what the conversation type,
    // or the convo is public and we are an admin or moderator
    const isDeletableForEveryone =
      sender === ourPubkey || (isPublic && (weAreAdmin || weAreModerator)) || false;

    const isSenderAdmin = groupAdmins.includes(sender);

    const messageProps: MessageModelPropsWithConvoProps = {
      ...foundMessageProps,
      propsForMessage: {
        ...foundMessageProps.propsForMessage,
        isBlocked: !!selectedConvo.isBlocked,
        isPublic: !!isPublic,
        isSenderAdmin,
        isDeletable,
        isDeletableForEveryone,
        weAreAdmin,
        conversationType: selectedConvo.type,
        sender,
        isKickedFromGroup: selectedConvo.isKickedFromGroup || false,
      },
    };

    return messageProps;
  }
);

export const getMessageReactsProps = createSelector(
  getMessagePropsByMessageId,
  (props): MessageReactsSelectorProps | undefined => {
    if (!props || isEmpty(props)) {
      return undefined;
    }

    const msgProps: MessageReactsSelectorProps = pick(props.propsForMessage, [
      'convoId',
      'conversationType',
      'reacts',
      'serverId',
    ]);

    if (msgProps.reacts) {
      // NOTE we don't want to render reactions that have 'senders' as an object this is a deprecated type used during development 25/08/2022
      const oldReactions = Object.values(msgProps.reacts).filter(
        reaction => !Array.isArray(reaction.senders)
      );

      if (oldReactions.length > 0) {
        msgProps.reacts = undefined;
        return msgProps;
      }

      const sortedReacts = Object.entries(msgProps.reacts).sort((a, b) => {
        return a[1].index < b[1].index ? -1 : a[1].index > b[1].index ? 1 : 0;
      });
      msgProps.sortedReacts = sortedReacts;
    }

    return msgProps;
  }
);

export const getMessageQuoteProps = createSelector(
  getConversationLookup,
  getMessagesOfSelectedConversation,
  getConversationQuotes,
  getMessagePropsByMessageId,
  (
    conversationLookup,
    messagesProps,
    quotesProps,
    msgGlobalProps
  ): { quote: PropsForQuote } | undefined => {
    if (!msgGlobalProps || isEmpty(msgGlobalProps)) {
      return undefined;
    }

    const msgProps = msgGlobalProps.propsForMessage;

    if (!msgProps.quote || isEmpty(msgProps.quote)) {
      return undefined;
    }

    const { id } = msgProps.quote;
    let { author } = msgProps.quote;

    if (!id || !author) {
      return undefined;
    }

    const isFromMe = isUsAnySogsFromCache(author) || false;

    // NOTE the quote lookup map always stores our messages using the unblinded pubkey
    if (isFromMe && PubKey.isBlinded(author)) {
      author = UserUtils.getOurPubKeyStrFromCache();
    }

    // NOTE: if the message is not found, we still want to render the quote
    const quoteNotFound = {
      quote: {
        id,
        author,
        isFromMe,
        referencedMessageNotFound: true,
      },
    };

    if (!quotesProps || isEmpty(quotesProps)) {
      return quoteNotFound;
    }

    const sourceMessage = lookupQuote(quotesProps, messagesProps, toNumber(id), author);
    if (!sourceMessage) {
      return quoteNotFound;
    }

    const sourceMsgProps = sourceMessage.propsForMessage;
    if (!sourceMsgProps || sourceMsgProps.isDeleted) {
      return quoteNotFound;
    }

    const convo = conversationLookup[sourceMsgProps.convoId];
    if (!convo) {
      return quoteNotFound;
    }

    const attachment = sourceMsgProps.attachments && sourceMsgProps.attachments[0];

    const quote: PropsForQuote = {
      text: sourceMsgProps.text,
      attachment: attachment ? processQuoteAttachment(attachment) : undefined,
      isFromMe,
      author: sourceMsgProps.sender,
      id: sourceMsgProps.id,
      referencedMessageNotFound: false,
      convoId: convo.id,
    };

    return {
      quote,
    };
  }
);

export const getMessageTextProps = createSelector(
  getMessagePropsByMessageId,
  (props): MessageTextSelectorProps | undefined => {
    if (!props || isEmpty(props)) {
      return undefined;
    }

    const msgProps: MessageTextSelectorProps = pick(props.propsForMessage, [
      'direction',
      'status',
      'text',
      'isDeleted',
      'conversationType',
    ]);

    return msgProps;
  }
);

export const getMessageAttachmentProps = createSelector(
  getMessagePropsByMessageId,
  (props): MessageAttachmentSelectorProps | undefined => {
    if (!props || isEmpty(props)) {
      return undefined;
    }

    const msgProps: MessageAttachmentSelectorProps = {
      attachments: props.propsForMessage.attachments || [],
      ...pick(props.propsForMessage, [
        'direction',
        'isTrustedForAttachmentDownload',
        'timestamp',
        'serverTimestamp',
        'sender',
        'convoId',
      ]),
    };

    return msgProps;
  }
);

export const getIsMessageSelected = createSelector(
  getMessagePropsByMessageId,
  getSelectedMessageIds,
  (props, selectedIds): boolean => {
    if (!props || isEmpty(props)) {
      return false;
    }

    const { id } = props.propsForMessage;

    return selectedIds.includes(id);
  }
);

export const getMessageContentSelectorProps = createSelector(
  getMessagePropsByMessageId,
  (props): MessageContentSelectorProps | undefined => {
    if (!props || isEmpty(props)) {
      return undefined;
    }

    const msgProps: MessageContentSelectorProps = {
      ...pick(props.propsForMessage, [
        'direction',
        'serverTimestamp',
        'text',
        'timestamp',
        'previews',
        'quote',
        'attachments',
      ]),
    };

    return msgProps;
  }
);

export const getMessageContentWithStatusesSelectorProps = createSelector(
  getMessagePropsByMessageId,
  (props): MessageContentWithStatusSelectorProps | undefined => {
    if (!props || isEmpty(props)) {
      return undefined;
    }

    const isGroup =
      props.propsForMessage.conversationType !== 'private' && !props.propsForMessage.isPublic;

    const msgProps: MessageContentWithStatusSelectorProps = {
      ...pick(props.propsForMessage, ['conversationType', 'direction', 'isDeleted']),
      isGroup,
    };

    return msgProps;
  }
);

export const getGenericReadableMessageSelectorProps = createSelector(
  getMessagePropsByMessageId,
  (props): GenericReadableMessageSelectorProps | undefined => {
    if (!props || isEmpty(props)) {
      return undefined;
    }

    const msgProps: GenericReadableMessageSelectorProps = pick(props.propsForMessage, [
      'convoId',
      'direction',
      'conversationType',
      'expirationDurationMs',
      'expirationTimestamp',
      'isExpired',
      'isUnread',
      'receivedAt',
      'isKickedFromGroup',
      'isDeleted',
    ]);

    return msgProps;
  }
);

export const getOldTopMessageId = (state: StateType): string | null =>
  state.conversations.oldTopMessageId || null;

export const getOldBottomMessageId = (state: StateType): string | null =>
  state.conversations.oldBottomMessageId || null;

export const getIsSelectedConvoInitialLoadingInProgress = (state: StateType): boolean =>
  Boolean(getSelectedConversation(state)?.isInitialFetchingInProgress);

export function getCurrentlySelectedConversationOutsideRedux() {
  return window?.inboxStore?.getState().conversations.selectedConversation as string | undefined;
}
