import {
  ChecksIcon,
  CircleNotchIcon,
  MegaphoneIcon,
  WarningCircleIcon,
} from '@phosphor-icons/react'
import classNames from 'classnames'
import {
  MessageStatus,
  useChannelStateContext,
  useMessageContext,
} from 'stream-chat-react'

import type { OutboundClickHandler, ResolvedOutbound } from '../../types'

import { hasPaidDeliveryTag } from './MessageTag'
import { isTipMessage } from './TipMessage'

/**
 * Message status stamps for sending and terminal `MessageStatus` states
 * (sent / delivered / read), plus failed messages.
 *
 * Two reasons this is more than a static label:
 *
 * 1. `MessageStatus` picks its `delivered`/`read` branches ahead of `sent`
 *    whenever Stream has `deliveredTo`/`readBy`, and — unlike `sent` — those
 *    branches aren't gated to the last message. So we override all three
 *    terminal slots with this same label (never Stream's default delivered
 *    icon / read avatars — Read is out of scope) and re-apply the "latest
 *    message in the thread" gate ourselves.
 *
 * 2. We derive "latest message" from **live channel state**, not the message
 *    context's `lastOwnMessage`/`lastReceivedId`. Stream's `Message` is
 *    memoised by `areMessagePropsEqual`, which does not compare those props;
 *    a previous own row whose message/grouping props are unchanged (e.g.
 *    after an intervening incoming message) never re-renders, so the old
 *    bubble would keep showing "Delivered" beside the new one.
 *    `useChannelStateContext` gets a fresh `messages` array on every new
 *    message and, being a context, re-renders this leaf straight through the
 *    stale `Message`/`MessageStatus` memos — so the label reliably clears
 *    once a reply lands. We still skip non-participant rows (`system`,
 *    `ephemeral`, `deleted`) when finding the last real message so a trailing
 *    system event does not clear the label.
 *
 * Rendered inside stream's `.str-chat__message-status` span, which supplies
 * the flex layout; icon and copy are styled via that span in `styles.css`.
 */
const useIsLatestRealMessage = () => {
  const { message } = useMessageContext('SentMessageDeliveryStatus')
  const { messages } = useChannelStateContext('SentMessageDeliveryStatus')

  const lastMessageId = (() => {
    if (!messages) {
      return undefined
    }

    for (let index = messages.length - 1; index >= 0; index -= 1) {
      const lastMessage = messages[index]

      if (
        lastMessage.type === 'system' ||
        lastMessage.type === 'ephemeral' ||
        lastMessage.type === 'deleted'
      ) {
        continue
      }

      return lastMessage.id
    }

    return undefined
  })()

  return message.id === lastMessageId
}

const DeliveredStatus = () => {
  if (!useIsLatestRealMessage()) return null

  return (
    <>
      <ChecksIcon weight="bold" aria-hidden="true" />
      <span data-testid="sent-message-delivery-status">Delivered</span>
    </>
  )
}

const SendingStatus = () => {
  if (!useIsLatestRealMessage()) return null

  return (
    <>
      <CircleNotchIcon
        className="sent-message-status__spinner animate-spin"
        weight="bold"
        aria-hidden="true"
      />
      <span data-testid="sent-message-sending-status">Sending</span>
    </>
  )
}

const OutboundStatus = ({
  outbound,
  onOutboundClick,
}: {
  outbound: ResolvedOutbound
  onOutboundClick?: OutboundClickHandler
}) => {
  const name = outbound.name || 'Broadcast'
  // Underline the name only in the clickable variant (it reads as a link).
  // The name's colour still comes from the `.str-chat`-scoped grid rule in
  // styles.css; we only add the underline here.
  const isLink = Boolean(onOutboundClick)
  const content = (
    <>
      <span className="sent-message-outbound-badge" aria-hidden="true">
        <MegaphoneIcon weight="fill" />
      </span>
      <span
        className={classNames('sent-message-outbound-name', {
          'underline [text-decoration-skip-ink:auto]': isLink,
        })}
        data-testid="sent-message-outbound-status"
      >
        {name}
      </span>
    </>
  )

  return onOutboundClick ? (
    <button
      type="button"
      className="inline-flex cursor-pointer items-center gap-1 border-0 bg-transparent p-0 text-inherit [font:inherit]"
      aria-label={`View broadcast ${name}`}
      onClick={() => onOutboundClick(outbound)}
    >
      {content}
    </button>
  ) : (
    content
  )
}

const FailedStatus = () => (
  <span className="str-chat__message-status sent-message-status--failed">
    <WarningCircleIcon weight="bold" aria-hidden="true" />
    <span data-testid="sent-message-failed-status">Failed to send</span>
  </span>
)

/**
 * Delivery status beneath the viewer's most recent sent message (MES-1036):
 * a right-aligned "✓✓ Delivered" that appears once Stream confirms the
 * message is stored and updates live.
 *
 * Built on stream-chat-react's `MessageStatus`, which owns the own-message /
 * not-error gating and only surfaces a terminal (sending/sent/delivered/read)
 * state once the message is stored. Stream's `threadList` guard covers
 * sent/delivered/read only — not `sending` — so we suppress the whole stamp
 * (including failed) in thread lists ourselves. Sending uses the same
 * latest-message-in-thread gate; failed messages bypass `MessageStatus`
 * because it has no failed slot and are shown on every own, non-thread
 * failed message.
 *
 * Messages whose footer tag already reads "Delivered with $10 message" skip
 * this label so delivery is only claimed once (MES-1480); failed and broadcast
 * stamps still apply.
 * Tips read as delivered while still pending; a failed tip still shows Failed.
 */
export const SentMessageDeliveryStatus = ({
  onOutboundClick,
  resolvedOutbound,
}: {
  onOutboundClick?: OutboundClickHandler
  resolvedOutbound?: ResolvedOutbound
}) => {
  const { isMyMessage, message, threadList } = useMessageContext(
    'SentMessageDeliveryStatus'
  )
  const isOwnMessage = isMyMessage()
  const isLatestRealMessage = useIsLatestRealMessage()

  // Stream's MessageStatus still mounts MessageSendingStatus when
  // threadList is true; keep all of our stamps out of thread replies.
  if (threadList) {
    return null
  }

  if (message.status === 'failed' && isOwnMessage && message.type !== 'error') {
    return <FailedStatus />
  }

  if (!isOwnMessage) {
    return null
  }

  const outboundRecordId = message.metadata?.outbound_id
  const outboundStatus =
    message.status !== 'sending' &&
    message.status !== 'failed' &&
    outboundRecordId &&
    resolvedOutbound ? (
      <OutboundStatus
        outbound={resolvedOutbound}
        onOutboundClick={onOutboundClick}
      />
    ) : null

  const messageStatus = hasPaidDeliveryTag(message) ? null : (
    <MessageStatus
      MessageSentStatus={DeliveredStatus}
      MessageDeliveredStatus={DeliveredStatus}
      MessageReadStatus={DeliveredStatus}
      MessageSendingStatus={
        isTipMessage(message) ? DeliveredStatus : SendingStatus
      }
    />
  )

  if (!outboundStatus) {
    return messageStatus
  }

  return (
    <span className="sent-message-status-row">
      {messageStatus}
      {messageStatus && isLatestRealMessage && (
        <span className="sent-message-outbound-separator" aria-hidden="true">
          •
        </span>
      )}
      {outboundStatus}
    </span>
  )
}
