import { memo, type MouseEvent, type ReactNode, useEffect, useRef } from 'react';

import type { ChatBskyActorDefs, ChatBskyConvoDefs } from '@atcute/bluesky';

import { useQueryClient } from '@tanstack/react-query';
import { clsx } from 'clsx';

import { isBlockedOrBlocking } from '#/lib/moderation/blocked-and-muted';
import { isOnlyEmoji } from '#/lib/utils/emoji';

import { useMaybeProfileShadow } from '#/state/cache/profile-shadow';
import type { Shadow } from '#/state/cache/types';
import type { ConvoItem } from '#/state/messages/convo/types';
import { useModerationOpts } from '#/state/moderation/moderation-opts';
import { useProfileBlockMutationQueue } from '#/state/queries/profile';
import { unstableCacheProfileView } from '#/state/queries/unstable-profile-cache';
import { useSession } from '#/state/session';

import * as Dialog from '#/components/Dialog';
import { MessageActions } from '#/components/dms/MessageActions';
import { useMessageDialogs } from '#/components/dms/MessageOverlays';
import { useMessageReplies } from '#/components/dms/MessageReplies';
import { getReplyPreviewText } from '#/components/dms/replyPreview';
import * as Prompt from '#/components/Prompt';
import { RichText } from '#/components/RichText';
import { Text } from '#/components/Text';
import { InlineButton } from '#/components/web/Link';
import * as ProfileCard from '#/components/web/ProfileCard';

import ArrowCornerDownRightIcon from '#/icons/central/ArrowCornerDownRight_round_outlined_radius3_stroke2.svg';
import { m } from '#/paraglide/messages';
import { colors } from '#/styles/colors';

import { DateDivider } from './DateDivider';
import * as css from './MessageItem.css';
import { MessageItemEmbed } from './MessageItemEmbed';
import { MessageItemInviteEmbed } from './MessageItemInviteEmbed';
import { groupReactions } from './ReactionsDialog';
import { filterBlockedReactions } from './util';

let MessageItem = ({
	hasLargeGapFromPrev,
	isFirstInCluster,
	isGroupChat = false,
	isLastInCluster,
	item,
	relatedProfiles,
	squaredBottomCorner,
	squaredTopCorner,
}: {
	/** whether a date divider precedes this row (first of a new day/gap). */
	hasLargeGapFromPrev: boolean;
	isFirstInCluster: boolean;
	isGroupChat?: boolean;
	isLastInCluster: boolean;
	item: ConvoItem & { type: 'message' | 'pendingMessage' };
	relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>;
	squaredBottomCorner: boolean;
	squaredTopCorner: boolean;
}): ReactNode => {
	const { currentAccount } = useSession();
	const moderationOpts = useModerationOpts();
	const queryClient = useQueryClient();

	const { message } = item;
	const profile = useMaybeProfileShadow(relatedProfiles.get(message.sender.did));

	const { reactionsHandle } = useMessageDialogs();
	const { scrollToMessage, highlightedMessage } = useMessageReplies();

	// `replyTo` comes back hydrated as the referenced message, a deleted-message
	// tombstone, or a before-joined placeholder. Narrow away the open-union
	// fallback so we only render shapes we understand.
	const replyTo =
		message.replyTo?.$type === 'chat.bsky.convo.defs#messageView' ||
		message.replyTo?.$type === 'chat.bsky.convo.defs#deletedMessageView' ||
		message.replyTo?.$type === 'chat.bsky.convo.defs#messageBeforeUserJoinedGroupView'
			? message.replyTo
			: undefined;
	// A before-joined placeholder carries no id, so there's nothing to scroll to;
	// leaving `onPress` undefined disables the reply affordance for that case.
	const replyToMessageId =
		replyTo && replyTo.$type !== 'chat.bsky.convo.defs#messageBeforeUserJoinedGroupView'
			? replyTo.id
			: undefined;
	const onPressReplyTo = replyToMessageId ? () => scrollToMessage(replyToMessageId) : undefined;

	const isPending = item.type === 'pendingMessage';

	const displayName = profile?.handle ?? null;

	const isFromSelf = message.sender?.did != null && message.sender.did === currentAccount?.did;

	const visibleReactions = filterBlockedReactions(message.reactions, relatedProfiles);
	const hasReactions = visibleReactions.length > 0;

	const pendingColor = colors.primary_300;

	const rt = { text: message.text, facets: message.facets ?? [] };

	const hasEmbed =
		message.embed?.$type === 'app.bsky.embed.record#view' ||
		message.embed?.$type === 'chat.bsky.embed.joinLink#view';
	const hasEmbedAndText = hasEmbed && rt.text.length > 0;

	// an embed sitting above the text squares the bubble's top so the two butt together.
	const squaredTop = squaredTopCorner || hasEmbedAndText;

	const showDisplayName = isGroupChat && !isFromSelf && isFirstInCluster && !isOnlyEmoji(message.text);
	const showAvatar = isGroupChat && !isFromSelf && isLastInCluster;

	// Flash the message background when it's been scrolled to (e.g. by tapping a
	// reply that quotes it), so it's easy to spot. Keyed on the highlight `key`
	// so re-tapping the same message re-triggers the flash.
	const isHighlighted = highlightedMessage?.id === message.id;
	const highlightKey = isHighlighted ? highlightedMessage.key : null;
	const flashRef = useRef<HTMLDivElement | null>(null);
	useEffect(() => {
		if (highlightKey === null) {
			return;
		}
		const node = flashRef.current;
		const animation = node?.animate(
			[{ opacity: 0 }, { opacity: 1, offset: 0.15 }, { opacity: 1, offset: 0.4 }, { opacity: 0 }],
			{ duration: 1000, easing: 'ease' },
		);
		return () => animation?.cancel();
	}, [highlightKey]);

	const avatar =
		profile && moderationOpts ? (
			<ProfileCard.Avatar
				profile={profile}
				size={css.AVATAR_SIZE}
				moderationOpts={moderationOpts}
				onPress={() => unstableCacheProfileView(queryClient, profile)}
			/>
		) : (
			<ProfileCard.AvatarPlaceholder size={css.AVATAR_SIZE} />
		);

	const groupedReactions = groupReactions(visibleReactions);

	const reactions = visibleReactions;

	const hasSelfReacted = reactions.some((r) => r.sender.did === currentAccount?.did);

	let reactionsLabel = '';
	if (reactions.length === 1) {
		const reaction = reactions[0]!;
		const sender = reaction.sender;
		if (sender.did === currentAccount?.did) {
			reactionsLabel = m['components.dms.update.youReacted']({ value: reaction.value });
		} else {
			const senderDid = reaction.sender.did;
			const memberSender = relatedProfiles.get(senderDid);
			if (memberSender) {
				reactionsLabel = m['components.dms.reaction.reactedBy']({
					name: memberSender.handle,
					reaction: reaction.value,
				});
			} else {
				reactionsLabel = m['components.dms.update.someoneReacted']({ value: reaction.value });
			}
		}
	} else if (reactions.length > 1) {
		reactionsLabel = m['components.dms.reaction.summary']({
			count: reactions.length,
			values: groupedReactions.map((g) => g.value).join(' '),
		});
	}

	const reactionPillContents = (
		<>
			{groupedReactions.slice(0, 10).map((group) => (
				<Text className={css.reactionGlyph} key={group.value} leading="none" size="lg">
					{group.value}
				</Text>
			))}
			{(groupedReactions.length !== reactions.length || groupedReactions.length > 10) &&
			reactions.length > 1 ? (
				<Text
					className={css.reactionCount}
					color={hasSelfReacted ? 'primary_900' : 'textContrastMedium'}
					leading="none"
					size="md"
					weight="semiBold"
				>
					{reactions.length}
				</Text>
			) : null}
		</>
	);

	const appliedReactions = (
		<>
			{hasReactions ? (
				<div className={css.reactionsWrap({ fromSelf: isFromSelf })}>
					{isGroupChat ? (
						// detached Trigger: opens the reactions Dialog.Root owned by MessageOverlays, keyed to this message.
						<Dialog.Trigger
							handle={reactionsHandle}
							payload={message}
							type="button"
							aria-label={reactionsLabel}
							className={clsx(
								css.reactionPill,
								css.reactionPillButton,
								hasSelfReacted && css.reactionPillSelected,
							)}
						>
							{reactionPillContents}
						</Dialog.Trigger>
					) : (
						<div
							aria-label={reactionsLabel}
							className={clsx(css.reactionPill, hasSelfReacted && css.reactionPillSelected)}
						>
							{reactionPillContents}
						</div>
					)}
				</div>
			) : null}
		</>
	);

	const isEmojiOnly = isOnlyEmoji(message.text);
	const bubbleBackground = isFromSelf ? (isPending ? pendingColor : colors.primary_500) : colors.contrast_50;
	const isBlocked = !!profile && isGroupChat && isBlockedOrBlocking(profile);

	return (
		<>
			{hasLargeGapFromPrev && <DateDivider date={message.sentAt} />}
			<div
				className={css.row({ avatarGutter: !isFromSelf && isGroupChat, firstInCluster: isFirstInCluster })}
				data-message-id={message.id}
			>
				<div className={css.flash} ref={flashRef} />
				{!isFromSelf && isGroupChat ? <div className={css.avatar}>{showAvatar ? avatar : null}</div> : null}
				{replyTo ? (
					<ReplyCaption
						isFromSelf={isFromSelf}
						isGroupChat={isGroupChat}
						onPress={onPressReplyTo}
						relatedProfiles={relatedProfiles}
						replierDisplayName={displayName}
						replyTo={replyTo}
					/>
				) : displayName && showDisplayName ? (
					<Text className={css.displayName} color="textContrastMedium" size="xs">
						{displayName}
					</Text>
				) : null}
				{isBlocked ? (
					<BlockedPlaceholder
						isFromSelf={isFromSelf}
						profile={profile}
						squaredBottomCorner={squaredBottomCorner}
						squaredTopCorner={squaredTop}
					/>
				) : (
					<div className={css.bubbleRow({ fromSelf: isFromSelf })}>
						<div className={css.bubble({ fromSelf: isFromSelf })}>
							{message.embed?.$type === 'app.bsky.embed.record#view' && (
								<MessageItemEmbed
									embed={message.embed}
									isFromSelf={isFromSelf}
									squaredBottomCorner={squaredBottomCorner || hasEmbedAndText}
									squaredTopCorner={squaredTopCorner}
								/>
							)}
							{message.embed?.$type === 'chat.bsky.embed.joinLink#view' && (
								<MessageItemInviteEmbed
									embed={message.embed}
									isFromSelf={isFromSelf}
									squaredBottomCorner={squaredBottomCorner || hasEmbedAndText}
									squaredTopCorner={squaredTopCorner}
								/>
							)}
							{rt.text.length > 0 && (
								<div
									className={clsx(
										!isEmojiOnly && css.bubbleStyled,
										!isEmojiOnly &&
											css.bubbleCorners({
												fromSelf: isFromSelf,
												squaredBottom: squaredBottomCorner,
												squaredTop,
											}),
									)}
									style={
										isEmojiOnly
											? undefined
											: {
													marginTop: hasEmbedAndText ? css.CLUSTERED_MESSAGE_GAP : 0,
													backgroundColor: bubbleBackground,
												}
									}
								>
									{replyTo && !isEmojiOnly ? (
										<ReplyQuote
											isFromSelf={isFromSelf}
											onPress={onPressReplyTo}
											relatedProfiles={relatedProfiles}
											replyTo={replyTo}
										/>
									) : null}
									<RichText
										// emoji-only content is enlarged and gets tight leading to avoid clipping the glyph;
										// non-self bubbles also pull the bottom up to bottom-align the glyph with the avatar
										className={isEmojiOnly && !isFromSelf ? css.emojiBaselineNudge : undefined}
										color={isFromSelf ? 'white' : undefined}
										emojiScale="large"
										enableTags
										leading={isEmojiOnly ? 'none' : undefined}
										linkUnderline="always"
										size="md"
										value={rt}
									/>
								</div>
							)}
						</div>
						<div className={css.actions({ fromSelf: isFromSelf })}>
							<MessageActions message={message} moderationOpts={moderationOpts} senderProfile={profile} />
						</div>
					</div>
				)}
				{!isBlocked ? appliedReactions : null}
				{isLastInCluster && <MessageItemMetadata align={isFromSelf ? 'right' : 'left'} item={item} />}
			</div>
		</>
	);
};
MessageItem = memo(MessageItem);
export { MessageItem };

function MessageItemMetadata({
	item,
	align,
}: {
	item: ConvoItem & { type: 'message' | 'pendingMessage' };
	align: 'left' | 'right';
}): ReactNode {
	const handleRetry = (e: MouseEvent) => {
		if (item.type === 'pendingMessage' && item.retry) {
			e.preventDefault();
			item.retry();
		}
	};

	switch (item.type) {
		case 'pendingMessage': {
			return item.failed ? (
				<Text align={align} className={css.meta} color="negative_400" size="xs">
					<Text color="negative_400" size="xs">
						{m['components.dms.message.error.sendFailed']()}
					</Text>
					{item.retry && (
						<>
							{' '}
							<InlineButton
								color="negative_400"
								label={m['components.dms.message.action.retry']()}
								onClick={handleRetry}
								size="xs"
							>
								{m['components.dms.message.a11y.retry']()}
							</InlineButton>
							.
						</>
					)}
				</Text>
			) : null;
		}
		default: {
			return null;
		}
	}
}
export { MessageItemMetadata };

function BlockedPlaceholder({
	isFromSelf,
	profile,
	squaredBottomCorner,
	squaredTopCorner,
}: {
	isFromSelf: boolean;
	profile: Shadow<ChatBskyActorDefs.ProfileViewBasic>;
	squaredBottomCorner: boolean;
	squaredTopCorner: boolean;
}) {
	const control = Prompt.usePromptHandle();
	const [_queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile);

	const label = profile.viewer?.blocking
		? m['components.dms.block.messageHiddenYouBlocking']()
		: m['components.dms.block.messageHiddenBlockingYou']();

	return (
		<>
			<button
				aria-label={label}
				className={css.blockedButton}
				onClick={() => control.open(null)}
				type="button"
			>
				<div
					className={clsx(
						css.blockedBubble,
						css.bubbleCorners({
							fromSelf: isFromSelf,
							squaredBottom: squaredBottomCorner,
							squaredTop: squaredTopCorner,
						}),
					)}
				>
					<Text className={css.italic} color="textContrastMedium" size="sm">
						{label}
					</Text>
				</div>
			</button>
			<Prompt.Outer handle={control}>
				<Prompt.Content>
					<Prompt.TitleText>
						{profile.viewer?.blocking
							? m['components.dms.block.youAreBlocking']({ handle: `@${profile.handle}` })
							: m['components.dms.block.isBlockingYou']({ handle: `@${profile.handle}` })}
					</Prompt.TitleText>
					<Prompt.DescriptionText>
						{profile.viewer?.blocking
							? m['components.dms.block.hiddenYouBlocking']()
							: m['components.dms.block.hiddenBlockingYou']()}
					</Prompt.DescriptionText>
					<Prompt.Actions>
						<Prompt.Action onPress={() => {}} cta={m['common.action.okay']()} color="primary" />
						{profile.viewer?.blocking && !profile.viewer.blockingByList && (
							<Prompt.Action
								onPress={() => void queueUnblock()}
								cta={m['common.block.action.unblock']()}
								color="secondary"
							/>
						)}
					</Prompt.Actions>
				</Prompt.Content>
			</Prompt.Outer>
		</>
	);
}

/**
 * caption rendered above a reply message indicating who replied to whom.
 *
 * aligns with the sender's display name for others (left), or with the message bubble for self (right).
 */
function ReplyCaption({
	replyTo,
	isFromSelf,
	isGroupChat,
	replierDisplayName,
	relatedProfiles,
	onPress,
}: {
	replyTo:
		| ChatBskyConvoDefs.MessageView
		| ChatBskyConvoDefs.DeletedMessageView
		| ChatBskyConvoDefs.MessageBeforeUserJoinedGroupView;
	isFromSelf: boolean;
	isGroupChat: boolean;
	replierDisplayName: string | null;
	relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>;
	onPress?: () => void;
}) {
	const { currentAccount } = useSession();

	// The before-joined placeholder carries no sender, so fall back to a generic caption.
	let caption = m['components.dms.reply.someoneReplied']();
	if (
		replyTo.$type === 'chat.bsky.convo.defs#messageView' ||
		replyTo.$type === 'chat.bsky.convo.defs#deletedMessageView'
	) {
		const originalSenderIsSelf = replyTo.sender.did === currentAccount?.did;
		const originalProfile = relatedProfiles.get(replyTo.sender.did);
		const originalName = originalSenderIsSelf ? null : originalProfile ? originalProfile.handle : null;
		caption = isFromSelf
			? originalSenderIsSelf
				? m['components.dms.update.youRepliedToYourself']()
				: originalName
					? m['components.dms.update.youRepliedTo']({ originalName })
					: m['components.dms.update.youReplied']()
			: originalSenderIsSelf
				? m['components.dms.reply.repliedToYou']({ replier: replierDisplayName ?? '' })
				: originalName
					? m['components.dms.reply.repliedTo']({ original: originalName, replier: replierDisplayName ?? '' })
					: m['components.dms.reply.replied']({ replier: replierDisplayName ?? '' });
	}

	const captionAlign = isFromSelf ? 'self' : isGroupChat ? 'otherGroup' : 'otherPlain';

	return (
		<button
			aria-label={
				onPress ? m['components.dms.reply.a11y.scrollTo']() : m['components.dms.reply.a11y.beforeJoined']()
			}
			className={css.replyCaption({ align: captionAlign })}
			disabled={!onPress}
			onClick={onPress}
			type="button"
		>
			<ArrowCornerDownRightIcon className={css.arrowCornerDownRightIcon} />
			<Text className={css.replyCaptionText} color="textContrastMedium" numberOfLines={1} size="xs">
				{caption}
			</Text>
		</button>
	);
}

/**
 * The nested quote of the original message, rendered at the top of a reply bubble. Tapping it scrolls to the
 * original (if loaded).
 */
function ReplyQuote({
	replyTo,
	isFromSelf,
	relatedProfiles,
	onPress,
}: {
	replyTo:
		| ChatBskyConvoDefs.MessageView
		| ChatBskyConvoDefs.DeletedMessageView
		| ChatBskyConvoDefs.MessageBeforeUserJoinedGroupView;
	isFromSelf: boolean;
	relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>;
	onPress?: () => void;
}) {
	const senderDid =
		replyTo.$type === 'chat.bsky.convo.defs#messageView' ||
		replyTo.$type === 'chat.bsky.convo.defs#deletedMessageView'
			? replyTo.sender.did
			: undefined;
	const senderProfile = useMaybeProfileShadow(senderDid ? relatedProfiles.get(senderDid) : undefined);
	// Hide the quoted content if we block, or are blocked by, the original
	// sender - mirroring how the message bubble itself is hidden.
	const isBlocked = senderProfile ? isBlockedOrBlocking(senderProfile) : false;
	const senderName = senderProfile && !isBlocked ? senderProfile.handle : null;

	const tintColor = isFromSelf ? 'white' : 'text';
	const subtleColor = isFromSelf ? 'white' : 'textContrastHigh';
	const borderColor = isFromSelf ? 'rgba(255, 255, 255, 0.5)' : colors.borderContrastHigh;

	let text: string;
	let subtle = false;
	if (isBlocked) {
		text = m['components.dms.block.messageHidden']();
		subtle = true;
	} else if (replyTo.$type === 'chat.bsky.convo.defs#messageView') {
		({ subtle, text } = getReplyPreviewText(replyTo));
	} else if (replyTo.$type === 'chat.bsky.convo.defs#messageBeforeUserJoinedGroupView') {
		text = m['components.dms.reply.beforeJoined']();
		subtle = true;
	} else {
		text = m['components.dms.message.deleted']();
		subtle = true;
	}

	return (
		<button
			aria-label={
				!onPress
					? m['components.dms.reply.a11y.repliedToBeforeJoined']()
					: senderName
						? m['components.dms.reply.a11y.repliedToFrom']({ name: senderName })
						: m['components.dms.reply.a11y.repliedTo']()
			}
			className={css.replyQuote}
			disabled={!onPress}
			onClick={onPress}
			style={{ borderColor }}
			type="button"
		>
			{senderName ? (
				<Text color={subtleColor} numberOfLines={1} size="xs">
					{senderName}
				</Text>
			) : null}
			<Text
				className={clsx(subtle && css.italic)}
				color={subtle ? subtleColor : tintColor}
				numberOfLines={2}
				size="sm"
			>
				{text}
			</Text>
		</button>
	);
}
