import classNames from 'classnames'
import React, { useContext, useMemo, useState } from 'react'
import {
  Attachment as DefaultAttachment,
  EditMessageModal as DefaultEditMessageModal,
  MessageBounceModal,
  MessageBouncePrompt as DefaultMessageBouncePrompt,
  MessageBlocked as DefaultMessageBlocked,
  MessageDeleted as DefaultMessageDeleted,
  MessageErrorIcon,
  MessageIsThreadReplyInChannelButtonIndicator as DefaultMessageIsThreadReplyInChannelButtonIndicator,
  MessageRepliesCountButton as DefaultMessageRepliesCountButton,
  MessageText,
  Poll,
  ReminderNotification as DefaultReminderNotification,
  StreamedMessageText as DefaultStreamedMessageText,
  areMessageUIPropsEqual,
  isDateSeparatorMessage,
  isMessageBlocked,
  isMessageBounced,
  messageHasAttachments,
  messageHasReactions,
  useChannelStateContext,
  useComponentContext,
  useChatContext,
  useMessageContext,
  useMessageReminder,
  type MessageContextValue,
  type MessageUIComponentProps,
} from 'stream-chat-react'

import type { OutboundClickHandler, ResolvedOutbound } from '../../types'
import { getMessageDisplayText } from '../../utils/getMessageDisplayText'
import { Avatar } from '../Avatar'
import { isLinkAttachment } from '../MediaMessage'
import type { BubbleGroupPosition } from '../MessageAttachment/types'

import { useCustomMessage } from './context'
import LockedAttachment from './LockedAttachment'
import {
  MessageTag,
  isAttachmentMessage,
  isChatbotMessage,
  isMediaAttachmentMessage,
  isTextAttachmentMessage,
} from './MessageTag'
import { MessageTail } from './MessageTail'
import { OutboundContext } from './OutboundContext'
import { SentMessageDeliveryStatus } from './SentMessageDeliveryStatus'
import StreamAttachmentMessage, {
  buildOrderedAttachmentSegments,
} from './StreamAttachmentMessage'
import { TipMessage, isTipMessage } from './TipMessage'

type CustomMessageUIComponentProps = MessageUIComponentProps & {
  viewerLanguage?: string
  onOutboundClick?: OutboundClickHandler
}

type CustomMessageWithContextProps = MessageContextValue & {
  viewerLanguage?: string
  onOutboundClick?: OutboundClickHandler
  resolvedOutbound?: ResolvedOutbound
}

const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
  const {
    additionalMessageInputProps,
    editing,
    endOfGroup,
    firstOfGroup,
    groupStyles,
    groupedByUser,
    handleAction,
    handleOpenThread,
    handleRetry,
    highlighted,
    isMessageAIGenerated,
    isMyMessage,
    message,
    renderText,
    threadList,
    viewerLanguage,
    onOutboundClick,
    resolvedOutbound,
  } = props

  const { client } = useChatContext('CustomMessage')
  const { channel } = useChannelStateContext('CustomMessage')
  const { isUnlocking, onUnlockClick, onFetchSource, onDownloadClick } =
    useCustomMessage('LockedAttachment')
  const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false)
  const reminder = useMessageReminder(message.id)

  const {
    Attachment = DefaultAttachment,
    EditMessageModal = DefaultEditMessageModal,
    MessageActions,
    MessageBlocked = DefaultMessageBlocked,
    MessageBouncePrompt = DefaultMessageBouncePrompt,
    MessageDeleted = DefaultMessageDeleted,
    MessageIsThreadReplyInChannelButtonIndicator = DefaultMessageIsThreadReplyInChannelButtonIndicator,
    MessageRepliesCountButton = DefaultMessageRepliesCountButton,
    ReminderNotification = DefaultReminderNotification,
    StreamedMessageText = DefaultStreamedMessageText,
    PinIndicator,
  } = useComponentContext('CustomMessage')

  const hasAttachment = messageHasAttachments(message)
  const hasReactions = messageHasReactions(message)
  const isAIGenerated = useMemo(
    () => isMessageAIGenerated?.(message),
    [isMessageAIGenerated, message]
  )
  const finalAttachments = useMemo(() => {
    const attachments = message.attachments ?? []
    const raw = message.shared_location
      ? [message.shared_location, ...attachments]
      : attachments

    if (!isChatbotMessage(message)) return raw

    const filtered = raw.filter((a) => !('type' in a) || !isLinkAttachment(a))
    return filtered.length === raw.length ? raw : filtered
  }, [message])
  const attachmentSegments = useMemo(
    () => buildOrderedAttachmentSegments(finalAttachments),
    [finalAttachments]
  )
  const displayMessage = useMemo(() => {
    const displayText = getMessageDisplayText({ message, viewerLanguage })
    return displayText === message.text
      ? message
      : { ...message, text: displayText }
  }, [message, viewerLanguage])
  // Route every generic Stream-attachment message (link OG previews and
  // image/video/audio/pdf/file media) through the toolkit renderer, so
  // messaging-react owns all shared attachment rendering rather than leaving
  // media on stream-chat-react's default Attachment. App-specific types
  // (locked/paid, tips, chatbot) are handled by the branches above.
  const hasAttachmentSegments = attachmentSegments.length > 0
  // Only own the message when every attachment maps to a representable segment.
  // Otherwise a mixed message (e.g. an unsupported shared_location alongside a
  // photo) would silently drop the unrepresented attachment — those fall
  // through to the default `Attachment` renderer, which handles all of them.
  const allAttachmentsRepresented =
    attachmentSegments.reduce(
      (total, segment) => total + segment.attachments.length,
      0
    ) === (finalAttachments?.length ?? 0)
  // Stream's MessageList sets the group-style string that drives the
  // `.str-chat__li--{single,top,middle,bottom}` class; the `firstOfGroup` /
  // `endOfGroup` / `groupedByUser` booleans it forwards are unset in this render
  // path (see `renderMessages`), so read the style directly.
  const groupStyle = groupStyles?.[0] ?? ''
  const bubbleGroupPosition: BubbleGroupPosition =
    groupStyle === 'top'
      ? 'first'
      : groupStyle === 'middle'
        ? 'middle'
        : groupStyle === 'bottom'
          ? 'end'
          : 'single'
  // The trailing bubble of a same-author run carries the tail — matches the
  // `.str-chat__li--single` / `--bottom` the old `::after` keyed off.
  const showTail = groupStyle === 'single' || groupStyle === 'bottom'

  if (isDateSeparatorMessage(message)) {
    return null
  }

  if (message.deleted_at || message.type === 'deleted') {
    return <MessageDeleted message={message} />
  }

  if (isMessageBlocked(message)) {
    return <MessageBlocked />
  }

  const showReplyCountButton = !threadList && !!message.reply_count
  const showIsReplyInChannel =
    !threadList && message.show_in_channel && message.parent_id
  const allowRetry =
    message.status === 'failed' && message.error?.status !== 403
  const isBounced = isMessageBounced(message)
  let handleClick: (() => void) | undefined = undefined

  if (allowRetry) {
    handleClick = () => handleRetry(message)
  } else if (isBounced) {
    handleClick = () => setIsBounceDialogOpen(true)
  }

  const isMine = isMyMessage()
  const rootClassName = classNames(
    'str-chat__message str-chat__message-simple',
    `str-chat__message--${message.type}`,
    `str-chat__message--${message.status}`,
    isMine
      ? 'str-chat__message--me str-chat__message-simple--me'
      : 'str-chat__message--other',
    message.text ? 'str-chat__message--has-text' : 'has-no-text',
    {
      'str-chat__message--has-attachment': hasAttachment,
      'str-chat__message--highlighted': highlighted,
      'str-chat__message--pinned pinned-message': message.pinned,
      'str-chat__message--with-reactions': hasReactions,
      'str-chat__message-send-can-be-retried':
        message?.status === 'failed' && message?.error?.status !== 403,
      'str-chat__message-with-thread-link':
        showReplyCountButton || showIsReplyInChannel,
      'str-chat__virtual-message__wrapper--end': endOfGroup,
      'str-chat__virtual-message__wrapper--first': firstOfGroup,
      'str-chat__virtual-message__wrapper--group': groupedByUser,
    }
  )

  const poll = message.poll_id && client.polls.fromState(message.poll_id)
  const isTip = isTipMessage(message)
  const isChatbot = isChatbotMessage(message)
  const isAttachment = isAttachmentMessage(message)
  const hasRenderableAttachments = !!(
    finalAttachments?.length && !message.quoted_message
  )
  const useAttachmentFooterChatbotTag =
    isChatbot && isMine && hasRenderableAttachments
  // Route generic Stream attachments through the toolkit renderer only when it
  // can faithfully own the whole message. Quoted replies (attachments are
  // suppressed there), polls, AI-streamed, and chatbot messages fall through to
  // the default branch so their Poll / StreamedMessageText / message-text and
  // chatbot attribution (MessageTag) handling is preserved — the accompanying
  // message text and attribution must never be dropped.
  const canRenderAttachmentsInToolkit =
    hasAttachmentSegments &&
    allAttachmentsRepresented &&
    !message.quoted_message &&
    !poll &&
    !isAIGenerated &&
    !isChatbot

  const sharedLockedTextProps = {
    contentType: 'text' as const,
    amountText: message.metadata?.amount_text,
    paymentStatus: message.metadata?.payment_status,
    renderedText: message.text && (
      <MessageText message={displayMessage} renderText={renderText} />
    ),
  }
  const sharedLockedMediaProps = {
    contentType: 'media' as const,
    title: message.metadata?.attachment_title,
    mimeType: message.metadata?.attachment_mime_type,
    thumbnailUrl: message.metadata?.attachment_thumbnail,
    amountText: message.metadata?.amount_text,
    detail: message.metadata?.attachment_detail,
    paymentStatus: message.metadata?.payment_status,
    onFetchSource: onFetchSource
      ? () => onFetchSource(message, channel)
      : undefined,
  }
  // Shared by the tip and locked-media-attachment branches below: an
  // accompanying text bubble stacked under the pill / card.
  const accompanyingTextBubble = (
    <div
      className={classNames('str-chat__message-bubble', {
        'relative isolate': showTail,
      })}
    >
      <MessageText message={displayMessage} renderText={renderText} />
      {showTail && <MessageTail side={isMine ? 'me' : 'other'} />}
    </div>
  )
  return (
    <>
      {editing && (
        <EditMessageModal
          additionalMessageInputProps={additionalMessageInputProps}
        />
      )}
      {isBounceDialogOpen && (
        <MessageBounceModal
          MessageBouncePrompt={MessageBouncePrompt}
          onClose={() => setIsBounceDialogOpen(false)}
          open={isBounceDialogOpen}
        />
      )}
      <div
        className={rootClassName}
        key={message.id}
        data-message-id={message.id}
        data-dd-privacy="mask"
      >
        {PinIndicator && <PinIndicator />}
        {!!reminder && <ReminderNotification reminder={reminder} />}
        {message.user && (
          <Avatar
            className="str-chat__avatar str-chat__message-sender-avatar"
            id={message.user.id}
            image={message.user.image}
            name={message.user.name || message.user.id}
            size={24}
            shape="circle"
            dmAgentEnabled={isChatbot}
          />
        )}
        <div
          className={classNames('str-chat__message-inner', {
            'str-chat__simple-message--error-failed': allowRetry || isBounced,
          })}
          data-testid="message-inner"
          onClick={handleClick}
          onKeyDown={handleClick}
          role={handleClick ? 'button' : undefined}
          tabIndex={handleClick ? 0 : undefined}
          style={{
            // Force margins to 0 to prevent hover layout shift
            // Stream Chat CSS sets margin-inline-end/start to 78px, then 0 on hover
            marginInlineEnd: 0,
            marginInlineStart: 0,
          }}
        >
          {isTip ? (
            <div className="str-chat__message-bubble-wrapper message-tip-wrapper">
              <TipMessage message={message} />
              {message.text?.trim() && accompanyingTextBubble}
            </div>
          ) : isTextAttachmentMessage(message) ? (
            <div className="str-chat__message-bubble-wrapper">
              <div className="flex items-center gap-2">
                {isMine && MessageActions && <MessageActions />}
                {isMine ? (
                  <LockedAttachment {...sharedLockedTextProps} isMine={true} />
                ) : (
                  <LockedAttachment
                    {...sharedLockedTextProps}
                    isMine={false}
                    isUnlocking={isUnlocking(message.id)}
                    onUnlockClick={() => onUnlockClick?.(message, channel)}
                  />
                )}
                {!isMine && MessageActions && <MessageActions />}
              </div>
            </div>
          ) : isMediaAttachmentMessage(message) ? (
            <div className="str-chat__message-bubble-wrapper message-locked-attachment-wrapper">
              <div className="flex items-center gap-2">
                {isMine && MessageActions && <MessageActions />}
                {isMine ? (
                  <LockedAttachment
                    {...sharedLockedMediaProps}
                    isMine={true}
                    onPreviewClick={() =>
                      onUnlockClick
                        ? onUnlockClick(message, channel)
                        : undefined
                    }
                  />
                ) : (
                  <LockedAttachment
                    {...sharedLockedMediaProps}
                    isMine={false}
                    isUnlocking={isUnlocking(message.id)}
                    onUnlockClick={() =>
                      onUnlockClick
                        ? onUnlockClick(message, channel)
                        : undefined
                    }
                    onDownloadClick={() =>
                      onDownloadClick
                        ? onDownloadClick(message, channel)
                        : undefined
                    }
                  />
                )}
                {!isMine && MessageActions && <MessageActions />}
              </div>
              {message.text && accompanyingTextBubble}
            </div>
          ) : canRenderAttachmentsInToolkit ? (
            <StreamAttachmentMessage
              groupPosition={bubbleGroupPosition}
              isMyMessage={isMine}
              message={{
                ...displayMessage,
                attachments: finalAttachments,
                // MessageText prefers message.i18n[userLanguage_text] from
                // Stream's TranslationContext over message.text. Drop i18n so
                // the caption uses displayMessage.text from viewerLanguage.
                i18n: undefined,
              }}
              renderText={renderText}
            />
          ) : (
            <div className="str-chat__message-bubble-wrapper">
              <div
                className={classNames('str-chat__message-bubble', {
                  'relative isolate': showTail,
                })}
              >
                {isChatbot && !useAttachmentFooterChatbotTag && (
                  <MessageTag
                    message={message}
                    hasAttachment={hasRenderableAttachments}
                    isMyMessage={isMine}
                  />
                )}
                {poll && <Poll poll={poll} />}
                {finalAttachments?.length && !message.quoted_message ? (
                  <Attachment
                    actionHandler={handleAction}
                    attachments={finalAttachments}
                  />
                ) : null}
                {isAIGenerated ? (
                  <StreamedMessageText
                    message={displayMessage}
                    renderText={renderText}
                  />
                ) : (
                  <MessageText
                    message={displayMessage}
                    renderText={renderText}
                  />
                )}
                <MessageErrorIcon />
                {showTail && <MessageTail side={isMine ? 'me' : 'other'} />}
              </div>
            </div>
          )}
        </div>
        {!isAttachment && !isTip && (
          <div className="str-chat__message-footer">
            {(!isChatbot || useAttachmentFooterChatbotTag) && (
              <MessageTag
                message={message}
                hasAttachment={hasRenderableAttachments}
                isMyMessage={isMine}
              />
            )}
          </div>
        )}
        <SentMessageDeliveryStatus
          onOutboundClick={onOutboundClick}
          resolvedOutbound={resolvedOutbound}
        />
        {showReplyCountButton && (
          <MessageRepliesCountButton
            onClick={handleOpenThread}
            reply_count={message.reply_count}
          />
        )}
        {showIsReplyInChannel && (
          <MessageIsThreadReplyInChannelButtonIndicator />
        )}
      </div>
    </>
  )
}

const MemoizedCustomMessage = React.memo(
  CustomMessageWithContext,
  (prev, next) => {
    if (prev.viewerLanguage !== next.viewerLanguage) return false
    if (prev.onOutboundClick !== next.onOutboundClick) return false
    if (prev.resolvedOutbound?.id !== next.resolvedOutbound?.id) return false
    if (prev.resolvedOutbound?.name !== next.resolvedOutbound?.name)
      return false
    return areMessageUIPropsEqual(prev, next)
  }
) as typeof CustomMessageWithContext

export const CustomMessage = (props: CustomMessageUIComponentProps) => {
  const messageContext = useMessageContext('CustomMessage')
  const outboundsByRecordId = useContext(OutboundContext)
  const outboundRecordId = messageContext.message.metadata?.outbound_id
  const resolvedOutbound =
    typeof outboundRecordId === 'string'
      ? outboundsByRecordId?.[outboundRecordId]
      : undefined

  return (
    <MemoizedCustomMessage
      {...messageContext}
      {...props}
      resolvedOutbound={resolvedOutbound}
    />
  )
}
