'use client';

import clsx from 'clsx';
import { EmojiClickData } from 'emoji-picker-react';
import { throttle } from 'lodash-es';
import React, {
  memo,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useInView } from 'react-intersection-observer';

import ListItem from '@/components/listView/ListItem';
import useDeviceAttributes from '@/hooks/useDeviceAttributes';
import { ChevronDownIcon } from '@/icons';
import {
  CommentEntity,
  CommentEntityType,
  CommentReplyEntity,
} from '@/state/clipStore';
import { MENTION_CLEANUP_REGEX } from '@/utils/constants';
import { parseMentions } from '@/utils/mentions';

import { useCommentReplies } from '../../hooks/useComments';
import { useDialogModal } from '../modal/DialogModal';
import CharacterCount from '../textarea/CharacterCount';
import CommentEmojiPicker from './CommentEmojiPicker';
import CommentInput from './CommentInput';
import CommentListItem from './CommentListItem';
import CommentReportReasons from './CommentReportReasons';
import CommentSkeleton from './CommentSkeleton';
import MentionSuggestions from './MentionSuggestions';
import { extractCurrentWord, getCommentProps } from './utils';

export type Props = React.HTMLAttributes<HTMLDivElement> &
  Pick<
    React.ComponentProps<typeof CommentListItem>,
    | 'onView'
    | 'onProfileClick'
    | 'onLike'
    | 'onDelete'
    | 'onModeratorDelete'
    | 'onHide'
    | 'onReport'
    | 'onBlock'
    | 'onUnblock'
  > & {
    currentUserId?: string;
    avatarSrc?: string;
    allowComments?: boolean;
    defaultShowCommentInput?: boolean;
    defaultShowAllReplies?: boolean;
    isUserContentOwner?: boolean;
    comment: CommentEntity;
    replies?: CommentReplyEntity[] | null;
    hasMoreReplies?: boolean | number;
    currentTime?: string;
    autoPagination?: boolean;
    commentMaxLength?: number;
    readonly?: boolean;
    clipCreatorId?: string;
    originalClipCreatorId?: string;
    deeplinkedCommentId?: string;
    entityId: string;
    entityType: CommentEntityType;
    disablePortal?: boolean;
  };

const SHOW_DELETED_POST_MARKER = true;
const SHOW_THREAD_DELETED_POST = false;
const SHOW_DELETED_REPLY_MARKER = false;

const CommentThread: React.FC<Props> = (props) => {
  const {
    currentUserId,
    avatarSrc,
    allowComments,
    autoPagination = true,
    defaultShowCommentInput = false,
    defaultShowAllReplies = false,
    isUserContentOwner = false,
    currentTime,
    comment,
    replies: commentTopReplies,
    hasMoreReplies,
    commentMaxLength,
    readonly,
    onView,
    onProfileClick,
    onLike,
    onDelete,
    onModeratorDelete,
    onHide,
    onReport,
    onBlock,
    onUnblock,
    clipCreatorId,
    originalClipCreatorId,
    deeplinkedCommentId,
    entityId,
    entityType,
    disablePortal = false,
    ...restProps
  } = props;
  const { t } = useTranslation();

  const [showCommentInput, setShowCommentInput] = useState(
    defaultShowCommentInput
  );
  const [showAllReplies, setShowAllReplies] = useState(defaultShowAllReplies);
  const [inputValue, setInputValue] = useState('');
  const [mentionQuery, setMentionQuery] = useState<string | null>(null);

  const stateRef = useRef({
    inputValue,
    mentionQuery,
    mentionSuggestion: {} as {
      handle: string | null;
      displayName: string | null;
      isExact: boolean;
    },
    mentionsUsedMap: new Map<string, string>(),
  });

  useEffect(() => {
    stateRef.current.inputValue = inputValue;
  }, [inputValue]);
  useEffect(() => {
    stateRef.current.mentionQuery = mentionQuery;
  }, [mentionQuery]);

  const mentionCursorRef = useRef<[word: string, start: number, end: number]>([
    '',
    0,
    0,
  ]);
  const commentInputRef = useRef<HTMLTextAreaElement>(null);
  const parseMention = useMemo(
    () =>
      throttle((value: string) => {
        stateRef.current.mentionSuggestion = {
          handle: null,
          displayName: null,
          isExact: false,
        };
        if (
          commentInputRef.current &&
          commentInputRef.current.selectionStart ===
            commentInputRef.current.selectionEnd
        ) {
          mentionCursorRef.current = extractCurrentWord(
            value,
            commentInputRef.current.selectionStart
          );
          const [currentWord] = mentionCursorRef.current;
          if (currentWord.startsWith('@')) {
            setMentionQuery(
              currentWord.replace(MENTION_CLEANUP_REGEX, '').slice(1)
            );
          } else {
            setMentionQuery(null);
          }
        }
      }, 250),
    []
  );
  // @TODO: This would probably be less janky if it was done on keyup
  useEffect(() => {
    parseMention(inputValue);
  }, [inputValue, parseMention]);

  const handleSuggestion = useCallback(
    (handle: string, displayName: string) => {
      stateRef.current.mentionSuggestion = {
        handle,
        displayName,
        isExact: stateRef.current.mentionQuery === handle,
      };
    },
    []
  );

  const handleMentionSelect = useCallback(
    (handle: string, displayName: string) => {
      // Keep track of the mention we used so we can replace it later on
      stateRef.current.mentionsUsedMap.set(handle, displayName);

      const [currentWord, start, end] = mentionCursorRef.current;
      const replacementWord = currentWord.startsWith('@')
        ? `@${handle}`
        : handle;
      setInputValue((prevInputValue) =>
        [
          prevInputValue.slice(0, start),
          replacementWord,
          prevInputValue.slice(end),
        ].join('')
      );
      const cursorPosition = start + replacementWord.length;
      // Wait a frame
      setTimeout(() => {
        if (commentInputRef.current) {
          commentInputRef.current.focus();
          commentInputRef.current.setSelectionRange(
            cursorPosition,
            cursorPosition,
            'none'
          );
        }
        // Hide the suggestions
        setMentionQuery(null);
      }, 0);
    },
    []
  );

  const handleReplyClick = useCallback(() => {
    setShowCommentInput(true);
  }, []);

  const { numReplies: numCommentReplies } = comment;

  const {
    topComments: topReplies,
    comments: allReplies,
    query: repliesQuery,
    reactionMutation,
    commentMutation,
    deleteMutation,
    reportMutation,
    blockMutation,
    unblockMutation,
  } = useCommentReplies({
    entityId,
    entityType,
    commentId: comment.id,
    initialData: !commentTopReplies?.length
      ? undefined
      : {
          replies: commentTopReplies,
        },
    enabled: showAllReplies,
    refetchOnMount: 'always',
    deeplinkedCommentId: deeplinkedCommentId ?? undefined,
  });

  const { isMobile, isSmallScreen } = useDeviceAttributes();

  const { launchDialog } = useDialogModal();

  let numReplies = numCommentReplies;
  // Sanity check that the total number of replies makes sense.
  // If it doesn't, treat the count as invalid and display "Read all replies" instead
  if (
    !deeplinkedCommentId &&
    typeof numReplies === 'number' &&
    commentTopReplies?.length &&
    (hasMoreReplies ? numReplies - 1 : numReplies) < commentTopReplies.length
  ) {
    numReplies = null;
  }
  const displayedRepliesCount = showAllReplies
    ? allReplies.length
    : topReplies.length;
  const numMoreReplies = numReplies && numReplies - displayedRepliesCount;

  const handleEmojiClick = useCallback((emojiData: EmojiClickData) => {
    setInputValue((prevValue) => prevValue + emojiData.emoji);
  }, []);

  const handleInputChange = useCallback((_e: any, value: string) => {
    setInputValue(value);
  }, []);

  const handleKeyDown = useCallback(
    (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
      switch (true) {
        case e.key === 'Escape':
          setMentionQuery(null);
          break;
        case e.key === 'Tab' && !!stateRef.current.mentionSuggestion.handle:
          const { handle, displayName } = stateRef.current.mentionSuggestion;

          // Keep track of the mention we used so we can replace it later on
          stateRef.current.mentionsUsedMap.set(
            handle,
            displayName || `@${handle}`
          );

          const [currentWord, start, end] = mentionCursorRef.current;
          const replacementWord = currentWord.startsWith('@')
            ? `@${handle}`
            : handle;
          setInputValue((prevInputValue) =>
            [
              prevInputValue.slice(0, start),
              replacementWord,
              ' ', // Add a space after the suggestion
              prevInputValue.slice(end),
            ].join('')
          );
          const cursorPosition = start + replacementWord.length + 1;
          // Wait a frame
          setTimeout(() => {
            if (commentInputRef.current) {
              commentInputRef.current.focus();
              commentInputRef.current.setSelectionRange(
                cursorPosition,
                cursorPosition,
                'none'
              );
            }
            // Hide the suggestions
            setMentionQuery(null);
          }, 0);
          e.preventDefault();
          break;
        default:
          break;
      }
    },
    []
  );

  const handleClear = useCallback(() => {
    setInputValue('');
    setShowCommentInput(false);
  }, []);

  const handleSend = useCallback(() => {
    async function sendComment() {
      await commentMutation.mutateAsync({
        content: parseMentions(
          stateRef.current.inputValue,
          stateRef.current.mentionsUsedMap
        ),
        commentId: comment.id,
        commentEntityType: comment.entityType,
      });
      setInputValue('');
      setShowCommentInput(false);
    }
    // NO BLANK COMMENTS ALLOWED!
    if (stateRef.current.inputValue.trim()) {
      sendComment();
    }
  }, [comment, commentMutation]);

  const handleClickShowAllReplies = useCallback(() => {
    setShowAllReplies(true);
  }, []);

  const handleReplyLike = useCallback<
    NonNullable<React.ComponentProps<typeof CommentListItem>['onLike']>
  >(
    (payload) => {
      reactionMutation.mutate({
        commentId: payload.id,
        commentEntityType: payload.type,
        isLike: payload.like ?? true,
      });
    },
    [reactionMutation]
  );

  const handleReplyDelete = useCallback<
    NonNullable<React.ComponentProps<typeof CommentListItem>['onDelete']>
  >(
    (payload) => {
      async function deleteReply() {
        const action = await launchDialog<boolean>(
          t('comments.confirmDelete'),
          [
            { label: t('cta.confirm'), action: true },
            { label: t('cta.cancel'), action: false },
          ]
        );
        if (action === true) {
          deleteMutation.mutateAsync({
            commentId: payload.id,
            commentEntityType: payload.type,
          });
        }
      }
      deleteReply();
    },
    [deleteMutation, launchDialog, t]
  );

  const handleReplyReport = useCallback<
    NonNullable<React.ComponentProps<typeof CommentListItem>['onReport']>
  >(
    (payload) => {
      async function reportReply() {
        let reason: string | undefined = undefined;
        const action = await launchDialog<boolean>(
          <CommentReportReasons
            defaultValue={reason}
            onChange={(nextReason) => {
              reason = nextReason;
            }}
          />,
          [
            { label: t('actions.report'), action: true },
            { label: t('cta.cancel'), action: false },
          ]
        );
        if (action === true) {
          reportMutation.mutateAsync({
            commentId: payload.id,
            commentEntityType: payload.type,
            reason,
          });
        }
      }
      reportReply();
    },
    [reportMutation, launchDialog, t]
  );

  const handleReplyBlock = useCallback<
    NonNullable<React.ComponentProps<typeof CommentListItem>['onBlock']>
  >(
    (payload) => {
      async function blockUser() {
        const action = await launchDialog<boolean>(
          <>
            <h4 className='text-base'>
              <Trans
                t={t}
                i18nKey={'comments.confirmBlockName'}
                values={{ name: payload.handle }}
                components={{ nameLink: <b /> }}
              />
            </h4>
            <p className='text-xs text-secondary'>
              {t('comments.confirmBlockBody')}
            </p>
          </>,
          [
            {
              label: payload.handle
                ? t('actions.blockName', { name: payload.handle })
                : t('actions.block'),
              action: true,
            },
            { label: t('cta.cancel'), action: false },
          ]
        );
        if (action === true) {
          blockMutation.mutateAsync({
            handle: payload.handle,
            reason: payload.reason,
          });
        }
      }
      blockUser();
    },
    [blockMutation, launchDialog, t]
  );

  const handleReplyUnblock = useCallback<
    NonNullable<React.ComponentProps<typeof CommentListItem>['onUnblock']>
  >(
    (payload) => {
      async function unblockUser() {
        const action = await launchDialog<boolean>(
          <>
            <h4 className='text-base'>
              <Trans
                t={t}
                i18nKey={'comments.confirmUnblockName'}
                values={{ name: payload.handle }}
                components={{ nameLink: <b /> }}
              />
            </h4>
            <p className='text-xs text-secondary'>
              {t('comments.confirmUnblockBody')}
            </p>
          </>,
          [
            {
              label: payload.handle
                ? t('actions.unblockName', { name: payload.handle })
                : t('actions.unblock'),
              action: true,
            },
            { label: t('cta.cancel'), action: false },
          ]
        );
        if (action === true) {
          unblockMutation.mutateAsync({
            handle: payload.handle,
          });
        }
      }
      unblockUser();
    },
    [unblockMutation, launchDialog, t]
  );

  const [paginationRef] = useInView({
    onChange: async (inView) => {
      if (inView && !repliesQuery.isFetchingNextPage) {
        repliesQuery.fetchNextPage();
      }
    },
  });

  const handleLoadMoreClick = useCallback(() => {
    repliesQuery.fetchNextPage();
  }, [repliesQuery]);

  const getReplyProps = useCallback(
    (reply: CommentReplyEntity) =>
      ({
        className: clsx('pl-12', {
          'opacity-50': reply.isReported,
          'animate-bg-fade-out bg-accent-pink-on-primary/20':
            deeplinkedCommentId === reply.id,
        }),
        readonly,
        onView,
        onProfileClick,
        onLike: handleReplyLike,
        onDelete:
          isUserContentOwner || reply.userId === currentUserId
            ? handleReplyDelete
            : undefined,
        onModeratorDelete: onModeratorDelete ? handleReplyDelete : undefined,
        onReport:
          reply.userId === currentUserId ? undefined : handleReplyReport,
        onBlock:
          !isUserContentOwner ||
          reply.userCommentsBlocked ||
          reply.userId === currentUserId
            ? undefined
            : handleReplyBlock,
        onUnblock:
          !isUserContentOwner ||
          !reply.userCommentsBlocked ||
          reply.userId === currentUserId
            ? undefined
            : handleReplyUnblock,
        isCreator: reply.userId === clipCreatorId,
        isOriginalCreator:
          reply.userId === originalClipCreatorId &&
          reply.userId !== clipCreatorId,
      }) satisfies Pick<
        React.ComponentProps<typeof CommentListItem>,
        | 'className'
        | 'readonly'
        | 'onView'
        | 'onProfileClick'
        | 'onLike'
        | 'onDelete'
        | 'onModeratorDelete'
        | 'onHide'
        | 'onReport'
        | 'onBlock'
        | 'onUnblock'
        | 'isCreator'
        | 'isOriginalCreator'
      >,
    [
      currentUserId,
      readonly,
      isUserContentOwner,
      onView,
      onProfileClick,
      handleReplyLike,
      handleReplyDelete,
      onModeratorDelete,
      handleReplyReport,
      handleReplyBlock,
      handleReplyUnblock,
      clipCreatorId,
      originalClipCreatorId,
      deeplinkedCommentId,
    ]
  );

  // Deleted thread - don't show if comment is deleted and there are no replies
  const hasAnyReplies =
    hasMoreReplies ||
    topReplies.length ||
    (deeplinkedCommentId && numReplies && numReplies > 0);
  if (comment.isDeleted && !hasAnyReplies) {
    return null;
  }

  return (
    <div {...restProps}>
      {!comment.isDeleted ? (
        <CommentListItem
          key={comment.id}
          className={clsx({
            'opacity-50': comment.isReported,
            'animate-bg-fade-out bg-accent-pink-on-primary/20':
              deeplinkedCommentId === comment.id,
          })}
          currentTime={currentTime}
          {...getCommentProps(comment)}
          readonly={readonly}
          onView={onView}
          onProfileClick={onProfileClick}
          onReplyClick={
            !readonly && allowComments && !showCommentInput
              ? handleReplyClick
              : undefined
          }
          onLike={onLike}
          onDelete={onDelete}
          onModeratorDelete={onDelete || onModeratorDelete}
          onHide={onHide}
          onReport={onReport}
          onBlock={comment.userCommentsBlocked ? undefined : onBlock}
          onUnblock={comment.userCommentsBlocked ? onUnblock : undefined}
          isCreator={comment.userId === clipCreatorId}
          isOriginalCreator={
            comment.userId === originalClipCreatorId &&
            comment.userId !== clipCreatorId
          }
          disablePortal={disablePortal}
          parentEntityType={entityType}
        />
      ) : SHOW_DELETED_POST_MARKER ? (
        <p className='p-4 text-sm text-foreground-inactive'>
          {t('comments.commentDeleted')}
        </p>
      ) : null}
      {!readonly && allowComments && showCommentInput && (
        <div className='pl-12'>
          <CommentEmojiPicker
            onEmojiClick={handleEmojiClick}
            allowExpandReactions={!isMobile && !isSmallScreen}
            disablePortal={disablePortal}
          />
          <div className='relative'>
            <CommentInput
              avatarSrc={avatarSrc}
              autoFocus
              value={inputValue}
              placeholder={t('comments.placeholder')}
              maxLength={commentMaxLength}
              onChange={handleInputChange}
              onKeyDown={handleKeyDown}
              onClearClick={handleClear}
              onSend={handleSend}
              alwaysShowSend
              disabled={commentMutation.isPending}
              ref={commentInputRef}
            />
            {commentMaxLength && (
              <CharacterCount
                className='px-4 py-1 text-right'
                okayClassName='text-foreground-inactive'
                length={inputValue.length}
                maxLength={commentMaxLength}
                visibleThreshold={0.7}
                warningThreshold={0.95}
              />
            )}
            <MentionSuggestions
              className='absolute inset-x-0 top-[calc(100%-1rem)]'
              query={mentionQuery}
              onSelect={handleMentionSelect}
              onSuggestion={handleSuggestion}
            />
          </div>
        </div>
      )}
      {!SHOW_THREAD_DELETED_POST &&
      comment.isDeleted ? null : showAllReplies === true ? (
        <>
          {/* Show all replies when showAllReplies is true */}
          {(allReplies?.length ? allReplies : topReplies || []).map((reply) =>
            !reply.isDeleted ? (
              <CommentListItem
                key={reply.id}
                {...getReplyProps(reply)}
                {...getCommentProps(reply, comment)}
                disablePortal={disablePortal}
                parentEntityType={entityType}
              />
            ) : SHOW_DELETED_REPLY_MARKER ? (
              <p className='p-4 pl-14 text-sm text-foreground-inactive'>
                {t('comments.commentDeleted')}
              </p>
            ) : null
          )}
          {/* Show loading skeleton while fetching additional replies */}
          {repliesQuery.isFetching && <CommentSkeleton className='pl-12' />}
          {/* Show load more button for pagination */}
          {repliesQuery.isFetched && (
            <>
              {repliesQuery.isFetchingNextPage && (
                <CommentSkeleton className='pl-12' />
              )}
              {!repliesQuery.hasNextPage ||
              repliesQuery.isFetchingNextPage ? null : autoPagination ? (
                <div ref={paginationRef} />
              ) : (
                <ListItem className='ml-12 border-none px-10 py-2 text-xs'>
                  <button
                    className='flex flex-row items-center gap-1 text-sm font-medium text-secondary hover:underline'
                    onClick={handleLoadMoreClick}
                  >
                    <span>
                      {typeof numMoreReplies === 'number'
                        ? t('comments.readMoreReplies', {
                            count: Math.max(1, numMoreReplies),
                          })
                        : t('comments.loadMore')}
                    </span>
                    {<ChevronDownIcon className='h-3 w-3 text-current' />}
                  </button>
                </ListItem>
              )}
            </>
          )}
        </>
      ) : (
        <>
          {/* Show only top replies when showAllReplies is false */}
          {topReplies?.map((reply) =>
            !reply.isDeleted ? (
              <CommentListItem
                key={reply.id}
                {...getReplyProps(reply)}
                {...getCommentProps(reply, comment)}
                disablePortal={disablePortal}
                parentEntityType={entityType}
              />
            ) : SHOW_DELETED_REPLY_MARKER ? (
              <p className='p-4 pl-14 text-sm text-foreground-inactive'>
                {t('comments.commentDeleted')}
              </p>
            ) : null
          )}
          {/* Show "read more replies" button */}
          {hasMoreReplies ||
          (deeplinkedCommentId && numMoreReplies && numMoreReplies > 0) ? (
            <ListItem className='ml-12 border-none px-10 py-2 text-xs'>
              <button
                className='flex flex-row items-center gap-1 text-sm font-medium text-secondary hover:underline'
                onClick={handleClickShowAllReplies}
                disabled={repliesQuery.isFetching}
              >
                <span>
                  {typeof numMoreReplies === 'number'
                    ? t('comments.readMoreReplies', {
                        count: Math.max(1, numMoreReplies),
                      })
                    : t('comments.readAllReplies')}
                </span>
                {<ChevronDownIcon className='h-3 w-3 text-current' />}
              </button>
            </ListItem>
          ) : null}
        </>
      )}
    </div>
  );
};

export default memo(CommentThread);
