import { useMutation } from '@tanstack/react-query';
import clsx from 'clsx';
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { escape } from 'lodash';
import React, { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';

import { openModal } from 'soapbox/actions/modals';
import { initReport } from 'soapbox/actions/reports';
import { HStack, Icon, IconButton, Stack, Text } from 'soapbox/components/ui';
import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container';
import emojify from 'soapbox/features/emoji/emoji';
import Bundle from 'soapbox/features/ui/components/bundle';
import { MediaGallery } from 'soapbox/features/ui/util/async-components';
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
import { normalizeAccount } from 'soapbox/normalizers';
import { ChatKeys, IChat, useChatActions } from 'soapbox/queries/chats';
import { queryClient } from 'soapbox/queries/client';
import { stripHTML } from 'soapbox/utils/html';
import { onlyEmoji } from 'soapbox/utils/rich-content';

import ChatMessageReaction from './chat-message-reaction';
import ChatMessageReactionWrapper from './chat-message-reaction-wrapper/chat-message-reaction-wrapper';

import type { Menu as IMenu } from 'soapbox/components/dropdown-menu';
import type { ChatMessage as ChatMessageEntity } from 'soapbox/types/entities';

const messages = defineMessages({
  copy: { id: 'chats.actions.copy', defaultMessage: 'Copy' },
  delete: { id: 'chats.actions.delete', defaultMessage: 'Delete for both' },
  deleteForMe: { id: 'chats.actions.deleteForMe', defaultMessage: 'Delete for me' },
  more: { id: 'chats.actions.more', defaultMessage: 'More' },
  report: { id: 'chats.actions.report', defaultMessage: 'Report' },
});

const BIG_EMOJI_LIMIT = 3;

const makeEmojiMap = (record: any) => record.get('emojis', ImmutableList()).reduce((map: ImmutableMap<string, any>, emoji: ImmutableMap<string, any>) => {
  return map.set(`:${emoji.get('shortcode')}:`, emoji);
}, ImmutableMap());

const parsePendingContent = (content: string) => {
  return escape(content).replace(/(?:\r\n|\r|\n)/g, '<br>');
};

const parseContent = (chatMessage: ChatMessageEntity) => {
  const content = chatMessage.content || '';
  const pending = chatMessage.pending;
  const deleting = chatMessage.deleting;
  const formatted = (pending && !deleting) ? parsePendingContent(content) : content;
  const emojiMap = makeEmojiMap(chatMessage);
  return emojify(formatted, emojiMap.toJS());
};

interface IChatMessage {
  chat: IChat
  chatMessage: ChatMessageEntity
}

const ChatMessage = (props: IChatMessage) => {
  const { chat, chatMessage } = props;

  const dispatch = useAppDispatch();
  const features = useFeatures();
  const intl = useIntl();

  const me = useAppSelector((state) => state.me);
  const { createReaction, deleteChatMessage, deleteReaction } = useChatActions(chat.id);

  const [isReactionSelectorOpen, setIsReactionSelectorOpen] = useState<boolean>(false);
  const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);

  const handleDeleteMessage = useMutation((chatMessageId: string) => deleteChatMessage(chatMessageId), {
    onSettled: () => {
      queryClient.invalidateQueries(ChatKeys.chatMessages(chat.id));
    },
  });

  const content = parseContent(chatMessage);
  const lastReadMessageDateString = chat.latest_read_message_by_account?.find((latest) => latest.id === chat.account.id)?.date;
  const lastReadMessageTimestamp = lastReadMessageDateString ? new Date(lastReadMessageDateString) : null;
  const isMyMessage = chatMessage.account_id === me;

  // did this occur before this time?
  const isRead = isMyMessage
    && lastReadMessageTimestamp
    && lastReadMessageTimestamp >= new Date(chatMessage.created_at);

  const isOnlyEmoji = useMemo(() => {
    const hiddenEl = document.createElement('div');
    hiddenEl.innerHTML = content;
    return onlyEmoji(hiddenEl, BIG_EMOJI_LIMIT, false);
  }, []);

  const emojiReactionRows = useMemo(() => {
    if (!chatMessage.emoji_reactions) {
      return [];
    }

    return chatMessage.emoji_reactions.reduce((rows: any, key: any, index) => {
      return (index % 4 === 0 ? rows.push([key])
        : rows[rows.length - 1].push(key)) && rows;
    }, []);
  }, [chatMessage.emoji_reactions]);

  const onOpenMedia = (media: any, index: number) => {
    dispatch(openModal('MEDIA', { media, index }));
  };

  const maybeRenderMedia = (chatMessage: ChatMessageEntity) => {
    if (!chatMessage.media_attachments.size) return null;

    return (
      <Bundle fetchComponent={MediaGallery}>
        {(Component: any) => (
          <Component
            media={chatMessage.media_attachments}
            onOpenMedia={onOpenMedia}
            visible
          />
        )}
      </Bundle>
    );
  };

  const handleCopyText = (chatMessage: ChatMessageEntity) => {
    if (navigator.clipboard) {
      const text = stripHTML(chatMessage.content);
      navigator.clipboard.writeText(text);
    }
  };
  const setBubbleRef = (c: HTMLDivElement) => {
    if (!c) return;
    const links = c.querySelectorAll('a[rel="ugc"]');

    links.forEach(link => {
      link.classList.add('chat-link');
      link.setAttribute('rel', 'ugc nofollow noopener');
      link.setAttribute('target', '_blank');
    });
  };

  const getFormattedTimestamp = (chatMessage: ChatMessageEntity) => {
    return intl.formatDate(new Date(chatMessage.created_at), {
      hour12: false,
      year: 'numeric',
      month: 'short',
      day: '2-digit',
      hour: '2-digit',
      minute: '2-digit',
    });
  };

  const menu = useMemo(() => {
    const menu: IMenu = [];

    if (navigator.clipboard && chatMessage.content) {
      menu.push({
        text: intl.formatMessage(messages.copy),
        action: () => handleCopyText(chatMessage),
        icon: require('@tabler/icons/copy.svg'),
      });
    }

    if (isMyMessage) {
      menu.push({
        text: intl.formatMessage(messages.delete),
        action: () => handleDeleteMessage.mutate(chatMessage.id),
        icon: require('@tabler/icons/trash.svg'),
        destructive: true,
      });
    } else {
      if (features.reportChats) {
        menu.push({
          text: intl.formatMessage(messages.report),
          action: () => dispatch(initReport(normalizeAccount(chat.account) as any, { chatMessage } as any)),
          icon: require('@tabler/icons/flag.svg'),
        });
      }
      menu.push({
        text: intl.formatMessage(messages.deleteForMe),
        action: () => handleDeleteMessage.mutate(chatMessage.id),
        icon: require('@tabler/icons/trash.svg'),
        destructive: true,
      });
    }

    return menu;
  }, [chatMessage, chat]);

  return (
    <div className='px-4 py-2'>
      <div key={chatMessage.id} className='group' data-testid='chat-message'>
        <Stack
          space={1.5}
          className={clsx({
            'ml-auto': isMyMessage,
          })}
        >
          <HStack
            alignItems='center'
            justifyContent={isMyMessage ? 'end' : 'start'}
            className={clsx({
              'opacity-50': chatMessage.pending,
            })}
          >
            {menu.length > 0 && (
              <DropdownMenuContainer
                items={menu}
                onOpen={() => setIsMenuOpen(true)}
                onClose={() => setIsMenuOpen(false)}
              >
                <IconButton
                  src={require('@tabler/icons/dots.svg')}
                  title={intl.formatMessage(messages.more)}
                  className={clsx({
                    'opacity-0 transition-opacity group-hover:opacity-100 focus:opacity-100 flex text-gray-600 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-500 focus:text-gray-700 dark:focus:text-gray-500': true,
                    'mr-2 order-2': isMyMessage,
                    'ml-2 order-2': !isMyMessage,
                    '!text-gray-700 dark:!text-gray-500': isMenuOpen,
                    '!opacity-100': isMenuOpen || isReactionSelectorOpen,
                  })}
                  data-testid='chat-message-menu'
                  iconClassName='w-4 h-4'
                />
              </DropdownMenuContainer>
            )}

            {features.chatEmojiReactions ? (
              <ChatMessageReactionWrapper
                onOpen={setIsReactionSelectorOpen}
                onSelect={(emoji) => createReaction.mutate({ emoji, messageId: chatMessage.id, chatMessage })}
              >
                <IconButton
                  src={require('@tabler/icons/mood-smile.svg')}
                  title={intl.formatMessage(messages.more)}
                  className={clsx({
                    'opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity flex text-gray-600 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-500 focus:text-gray-700 dark:focus:text-gray-500': true,
                    'mr-2 order-1': isMyMessage,
                    'ml-2 order-3': !isMyMessage,
                    '!text-gray-700 dark:!text-gray-500': isReactionSelectorOpen,
                    '!opacity-100': isMenuOpen || isReactionSelectorOpen,
                  })}
                  iconClassName='w-5 h-5'
                />
              </ChatMessageReactionWrapper>
            ) : null}

            <Stack
              space={0.5}
              className={clsx({
                'max-w-[85%]': true,
                'flex-1': !!chatMessage.media_attachments.size,
                'order-3': isMyMessage,
                'order-1': !isMyMessage,
              })}
              alignItems={isMyMessage ? 'end' : 'start'}
            >
              {maybeRenderMedia(chatMessage)}

              {content && (
                <HStack alignItems='bottom' className='max-w-full'>
                  <div
                    title={getFormattedTimestamp(chatMessage)}
                    className={
                      clsx({
                        'text-ellipsis break-words relative rounded-md py-2 px-3 max-w-full space-y-2 [&_.mention]:underline': true,
                        'rounded-tr-sm': (!!chatMessage.media_attachments.size) && isMyMessage,
                        'rounded-tl-sm': (!!chatMessage.media_attachments.size) && !isMyMessage,
                        '[&_.mention]:text-primary-600 dark:[&_.mention]:text-accent-blue': !isMyMessage,
                        '[&_.mention]:text-white dark:[&_.mention]:white': isMyMessage,
                        'bg-primary-500 text-white': isMyMessage,
                        'bg-gray-200 dark:bg-gray-800 text-gray-900 dark:text-gray-100': !isMyMessage,
                        '!bg-transparent !p-0 emoji-lg': isOnlyEmoji,
                      })
                    }
                    ref={setBubbleRef}
                    tabIndex={0}
                  >
                    <Text
                      size='sm'
                      theme='inherit'
                      className='break-word-nested'
                      dangerouslySetInnerHTML={{ __html: content }}
                    />
                  </div>
                </HStack>
              )}
            </Stack>
          </HStack>

          {(features.chatEmojiReactions && chatMessage.emoji_reactions) ? (
            <div
              className={clsx({
                'space-y-1': true,
                'ml-auto': isMyMessage,
                'mr-auto': !isMyMessage,
              })}
            >
              {emojiReactionRows?.map((emojiReactionRow: any, idx: number) => (
                <HStack
                  key={idx}
                  className={
                    clsx({
                      'flex items-center gap-1': true,
                      'flex-row-reverse': isMyMessage,
                    })
                  }
                >
                  {emojiReactionRow.map((emojiReaction: any, idx: number) => (
                    <ChatMessageReaction
                      key={idx}
                      emojiReaction={emojiReaction}
                      onAddReaction={(emoji) => createReaction.mutate({ emoji, messageId: chatMessage.id, chatMessage })}
                      onRemoveReaction={(emoji) => deleteReaction.mutate({ emoji, messageId: chatMessage.id })}
                    />
                  ))}
                </HStack>
              ))}
            </div>
          ) : null}

          <HStack
            alignItems='center'
            space={2}
            className={clsx({
              'ml-auto': isMyMessage,
            })}
          >
            <div
              className={clsx({
                'text-right': isMyMessage,
                'order-2': !isMyMessage,
              })}
            >
              <span className='flex items-center space-x-1.5'>
                <Text theme='muted' size='xs'>
                  {intl.formatTime(chatMessage.created_at)}
                </Text>

                {(isMyMessage && features.chatsReadReceipts) ? (
                  <>
                    {isRead ? (
                      <span className='flex flex-col items-center justify-center rounded-full border border-solid border-primary-500 bg-primary-500 p-0.5 text-white dark:border-primary-400 dark:bg-primary-400 dark:text-primary-900'>
                        <Icon
                          src={require('@tabler/icons/check.svg')}
                          strokeWidth={3}
                          className='h-2.5 w-2.5'
                        />
                      </span>
                    ) : (
                      <span className='flex flex-col items-center justify-center rounded-full border border-solid border-primary-500 bg-transparent p-0.5 text-primary-500 dark:border-primary-400 dark:text-primary-400'>
                        <Icon
                          src={require('@tabler/icons/check.svg')}
                          strokeWidth={3}
                          className='h-2.5 w-2.5'
                        />
                      </span>
                    )}
                  </>
                ) : null}
              </span>
            </div>
          </HStack>
        </Stack>
      </div>
    </div>
  );
};

export default ChatMessage;