import { useMutation, useQuery } from 'convex/react';
import { useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Avatar } from '@discord-clone/ui';
import { api } from '../../../../../convex/_generated/api';
import { useIsMobile } from '../../hooks/useIsMobile';
import { EmojiPicker, type EmojiPickerValue } from './EmojiPicker';
import { EncryptedAttachment, type AttachmentMetadata } from './EncryptedAttachment';
import { ImageLightbox } from './ImageLightbox';
import { LinkEmbed } from './LinkEmbed';
import type { DecryptedMessage } from './Messages';
import { MessageActionBar } from './MessageActionBar';
import { MessageContent } from './MessageContent';
import { MobileMessageActionsSheet } from './MobileMessageActionsSheet';
import {
	MemberProfilePopout,
	type MemberProfilePopoutMember,
} from '../member/MemberProfilePopout';
import { PinConfirmationModal } from './PinConfirmationModal';
import { ReactionsModal } from './ReactionsModal';
import { Tooltip } from '@discord-clone/ui';
import { reactionKeyToName } from '../../utils/emojiLookup';
import type { PinnedMessage } from './PinnedMessageRow';
import { TwemojiImg } from './TwemojiImg';
import { resolveReactionKeyToUnicode } from '../../utils/emojiLookup';
import styles from './MessageGroup.module.css';

interface MessageGroupProps {
	messages: DecryptedMessage[];
	channelId: string;
	onReply?: (eventId: string, username: string) => void;
}

const URL_REGEX = /https?:\/\/[^\s<>"']+/gi;

function extractUrls(text: string): string[] {
	const matches = text.match(URL_REGEX) ?? [];
	// Strip trailing punctuation that's almost never part of the URL but
	// commonly butts up against one in prose ("see https://foo.com.").
	const cleaned = matches.map((m) => m.replace(/[),.;!?]+$/, ''));
	return Array.from(new Set(cleaned));
}

/** True when a message body is entirely made up of one or more GIF
 *  URLs plus whitespace — i.e. the user posted a GIF from the
 *  picker and there's nothing worth showing as text. The render
 *  path hides the <MessageContent> block in that case so only the
 *  embedded preview appears. */
function isGifOnlyContent(text: string): boolean {
	const urls = extractUrls(text);
	if (urls.length === 0) return false;
	if (!urls.every((u) => /\.gif(\?|#|$)/i.test(u))) return false;
	let remainder = text;
	for (const u of urls) {
		remainder = remainder.split(u).join('');
	}
	return remainder.trim().length === 0;
}

/**
 * Discord-style relative timestamp:
 *   - Same calendar day → `Today at 7:08 PM`
 *   - Previous calendar day → `Yesterday at 7:08 PM`
 *   - Anything else → `4/11/2026, 7:08 PM`
 */
function formatTime(ts: number): string {
	const date = new Date(ts);
	const now = new Date();
	const time = date.toLocaleTimeString([], {
		hour: 'numeric',
		minute: '2-digit',
	});
	const isToday = date.toDateString() === now.toDateString();
	if (isToday) return `Today at ${time}`;
	const yesterday = new Date(now);
	yesterday.setDate(yesterday.getDate() - 1);
	if (date.toDateString() === yesterday.toDateString()) {
		return `Yesterday at ${time}`;
	}
	return `${date.toLocaleDateString()}, ${time}`;
}

function formatFullTime(ts: number): string {
	const date = new Date(ts);
	return date.toLocaleString([], {
		month: 'short',
		day: 'numeric',
		year: 'numeric',
		hour: 'numeric',
		minute: '2-digit',
	});
}

export function MessageGroup({ messages, channelId, onReply }: MessageGroupProps) {
	const first = messages[0];
	const myUserId =
		typeof localStorage !== 'undefined' ? localStorage.getItem('userId') : null;
	const channelMembers = useQuery(api.members.getChannelMembers, {
		channelId: channelId as any,
	});
	const mentionMembers = Array.isArray(channelMembers)
		? channelMembers.map((m: any) => ({
				displayName: m.displayName || m.username || '',
				username: m.username || '',
				userId: m.id,
			}))
		: [];
	const removeMessage = useMutation(api.messages.remove);
	const addReaction = useMutation(api.reactions.add);
	const removeReaction = useMutation(api.reactions.remove);

	// Custom emoji catalog — powers `:shortcode:` rendering inside
	// message bodies and custom-emoji reaction chips. One query per
	// group keeps the subscription count reasonable.
	const customEmojiDocs = (useQuery(api.customEmojis.list, {}) ?? []) as Array<{
		_id: string;
		name: string;
		src: string;
	}>;
	const customEmojiList = customEmojiDocs.map((e) => ({ name: e.name, url: e.src }));
	const customEmojiByName = new Map<string, string>();
	for (const e of customEmojiDocs) customEmojiByName.set(e.name.toLowerCase(), e.src);

	// Reaction picker state — a single picker shared across the group.
	// Records which message was clicked so the chosen emoji is bound
	// to the right target.
	const [reactPicker, setReactPicker] = useState<{
		messageId: string;
		pos: { top: number; left: number };
	} | null>(null);

	// Image lightbox state — tracks the decrypted blob URL + the
	// optional attachment metadata of the image that was clicked so
	// the lightbox info card can render filename / size / dimensions.
	// Null means the lightbox is closed. For inline GIFs posted via
	// a URL (no encrypted attachment), the metadata is absent — the
	// lightbox gracefully hides the star / detail chrome in that
	// case.
	const [lightboxItem, setLightboxItem] = useState<{
		src: string;
		attachment?: AttachmentMetadata;
		filename?: string;
		mimeType?: string;
	} | null>(null);

	// Right-click context menu state. When set, the MessageActionBar for
	// the target message opens its More dropdown at the click coordinates
	// and the parent row gets `actionBarForceVisible` so the hover bar
	// stays pinned regardless of cursor position.
	const [contextMenu, setContextMenu] = useState<{
		messageId: string;
		x: number;
		y: number;
	} | null>(null);
	// Tracks which message currently has its local More dropdown open
	// so we can pin that row's hover bar visible even when the pointer
	// leaves the row. Flips back to null when the dropdown closes.
	const [localMenuOpenFor, setLocalMenuOpenFor] = useState<string | null>(
		null,
	);
	const [mobileSheetForMsg, setMobileSheetForMsg] =
		useState<DecryptedMessage | null>(null);

	// Author profile popout — opens when the user clicks the avatar
	// or username in the message header, same component MemberListContainer
	// uses so profile cards look identical regardless of entry point.
	const [authorPopout, setAuthorPopout] = useState<{
		anchorRect: DOMRect;
		member: MemberProfilePopoutMember;
	} | null>(null);

	// Full reactions breakdown modal — opens when the user clicks a
	// reaction chip's tooltip or the chip itself. Tracks the target
	// message id so the modal can read its latest reaction rows.
	const [reactionsModalMsgId, setReactionsModalMsgId] = useState<
		string | null
	>(null);

	const longPressTimerRef = useRef<number | null>(null);
	const isMobile = useIsMobile();

	const openAuthorPopout = (msg: DecryptedMessage, rect: DOMRect) => {
		setAuthorPopout({
			anchorRect: rect,
			member: {
				userId: msg.senderId,
				displayName: msg.authorName,
				avatarUrl: msg.authorAvatarUrl,
			},
		});
	};

	const startLongPress = (msg: DecryptedMessage) => {
		if (!isMobile) return;
		if (longPressTimerRef.current !== null) {
			window.clearTimeout(longPressTimerRef.current);
		}
		longPressTimerRef.current = window.setTimeout(() => {
			setMobileSheetForMsg(msg);
		}, 450);
	};
	const cancelLongPress = () => {
		if (longPressTimerRef.current !== null) {
			window.clearTimeout(longPressTimerRef.current);
			longPressTimerRef.current = null;
		}
	};

	const handleCopyText = (content: string) => {
		if (typeof navigator !== 'undefined' && navigator.clipboard) {
			void navigator.clipboard.writeText(content);
		}
	};

	const handleCopyLink = (messageId: string) => {
		if (typeof navigator !== 'undefined' && navigator.clipboard) {
			const url = `${window.location.origin}${window.location.pathname}#msg-${messageId}`;
			void navigator.clipboard.writeText(url);
		}
	};

	// Pin / unpin both flow through the PinConfirmationModal so the
	// user gets the same confirmation step the new UI uses. The
	// variant flips based on whether the target message is already
	// pinned.
	const [pinTarget, setPinTarget] = useState<{
		message: PinnedMessage;
		variant: 'pin' | 'unpin';
	} | null>(null);
	const handlePin = (messageId: string) => {
		const msg = messages.find((m) => m.id === messageId);
		if (!msg) return;
		setPinTarget({
			message: {
				id: msg.id,
				authorName: msg.authorName,
				authorAvatarUrl: msg.authorAvatarUrl,
				content: msg.content,
				timestamp: msg.timestamp,
				attachments: msg.attachments,
			},
			variant: msg.pinned ? 'unpin' : 'pin',
		});
	};

	const openReactPicker = (messageId: string, anchorEl: Element | null) => {
		const rect = anchorEl?.getBoundingClientRect();
		if (!rect) return;
		setReactPicker({
			messageId,
			pos: {
				top: rect.bottom + 8,
				left: Math.max(8, rect.right - 480),
			},
		});
	};

	const handlePickReaction = async (value: EmojiPickerValue) => {
		if (!reactPicker || !myUserId) return;
		try {
			// For unicode we store the raw surrogates as the reaction key.
			// For custom server emojis we store the shortcode — the render
			// path below rehydrates it via `customEmojiByName` and falls
			// back through `resolveReactionKeyToUnicode` for legacy /
			// unknown keys.
			const emojiKey =
				value.kind === 'custom' ? value.shortcode : value.surrogates;
			await addReaction({
				messageId: reactPicker.messageId as any,
				userId: myUserId as any,
				emoji: emojiKey,
			});
		} catch (err) {
			console.error('Failed to add reaction:', err);
		}
		setReactPicker(null);
	};

	const handleDelete = async (messageId: string) => {
		if (!myUserId) return;
		try {
			await removeMessage({ id: messageId as any, userId: myUserId as any });
		} catch (err) {
			console.error('Failed to delete message:', err);
		}
	};

	const handleToggleReaction = async (messageId: string, emoji: string, me: boolean) => {
		if (!myUserId) return;
		try {
			if (me) {
				await removeReaction({
					messageId: messageId as any,
					userId: myUserId as any,
					emoji,
				});
			} else {
				await addReaction({
					messageId: messageId as any,
					userId: myUserId as any,
					emoji,
				});
			}
		} catch (err) {
			console.error('Failed to toggle reaction:', err);
		}
	};

	return (
		<div className={styles.group}>
			<div className={styles.contentColumn}>
				{messages.map((msg, i) => {
					const isFirst = i === 0;
					const isOwn = msg.senderId === myUserId;
					const menuOpenForThis = contextMenu?.messageId === msg.id;
					return (
						<div
							key={msg.id}
							data-message-id={msg.id}
							className={`${styles.messageContent} ${isFirst ? styles.firstMessage : ''} messageHoverable ${menuOpenForThis || localMenuOpenFor === msg.id ? 'actionBarForceVisible' : ''}`}
							onContextMenu={(e) => {
								if (isMobile) return;
								e.preventDefault();
								setContextMenu({ messageId: msg.id, x: e.clientX, y: e.clientY });
							}}
							onTouchStart={() => startLongPress(msg)}
							onTouchEnd={cancelLongPress}
							onTouchMove={cancelLongPress}
							onTouchCancel={cancelLongPress}
						>
							<MessageActionBar
								isOwnMessage={isOwn}
								onMenuOpenChange={(open) =>
									setLocalMenuOpenFor(open ? msg.id : null)
								}
								onReply={() => onReply?.(msg.id, first.authorName)}
								onDelete={() => handleDelete(msg.id)}
								onReact={(e) => openReactPicker(msg.id, e?.currentTarget ?? null)}
								onQuickReact={async (emoji) => {
									if (!myUserId) return;
									try {
										await addReaction({
											messageId: msg.id as any,
											userId: myUserId as any,
											emoji,
										});
									} catch (err) {
										console.error('Failed to add quick reaction:', err);
									}
								}}
								onPin={() => handlePin(msg.id)}
								onCopyText={() => handleCopyText(msg.content)}
								onCopyLink={() => handleCopyLink(msg.id)}
								externalMenuAt={
									menuOpenForThis ? { x: contextMenu.x, y: contextMenu.y } : null
								}
								onExternalMenuClose={() => setContextMenu(null)}
							/>
							{isFirst && msg.replyToId && (
								<div
									className={styles.replyContext}
									role="button"
									tabIndex={0}
									onClick={() => {
										if (!msg.replyToId) return;
										window.dispatchEvent(
											new CustomEvent('brycord:scroll-to-message', {
												detail: {
													channelId,
													messageId: msg.replyToId,
												},
											}),
										);
									}}
									onKeyDown={(e) => {
										if (e.key === 'Enter' || e.key === ' ') {
											e.preventDefault();
											if (!msg.replyToId) return;
											window.dispatchEvent(
												new CustomEvent('brycord:scroll-to-message', {
													detail: {
														channelId,
														messageId: msg.replyToId,
													},
												}),
											);
										}
									}}
									style={{ cursor: 'pointer' }}
								>
									<div className={styles.replySpine} />
									<span className={styles.replyAuthor}>
										{msg.replyToAuthorName ?? 'Unknown'}
									</span>
									<span className={styles.replyText}>
										{msg.replyToContent ?? <em>(missing context)</em>}
									</span>
								</div>
							)}
							{isFirst && (
								<>
									<div
										className={styles.avatarSlot}
										role="button"
										tabIndex={0}
										onClick={(e) => {
											e.stopPropagation();
											openAuthorPopout(
												first,
												(e.currentTarget as HTMLElement).getBoundingClientRect(),
											);
										}}
										onKeyDown={(e) => {
											if (e.key === 'Enter' || e.key === ' ') {
												e.preventDefault();
												openAuthorPopout(
													first,
													(e.currentTarget as HTMLElement).getBoundingClientRect(),
												);
											}
										}}
										style={{ cursor: 'pointer' }}
									>
										<Avatar
											src={first.authorAvatarUrl}
											size={40}
											fallback={first.authorName}
										/>
									</div>
									<div className={styles.header}>
										<span
											className={styles.username}
											role="button"
											tabIndex={0}
											onClick={(e) => {
												e.stopPropagation();
												openAuthorPopout(
													first,
													(e.currentTarget as HTMLElement).getBoundingClientRect(),
												);
											}}
											onKeyDown={(e) => {
												if (e.key === 'Enter' || e.key === ' ') {
													e.preventDefault();
													openAuthorPopout(
														first,
														(e.currentTarget as HTMLElement).getBoundingClientRect(),
													);
												}
											}}
											style={{
												cursor: 'pointer',
												color: first.authorRoleColor ?? undefined,
											}}
										>
											{first.authorName}
										</span>
										<span
											className={styles.timestamp}
											title={formatFullTime(first.timestamp)}
										>
											{formatTime(first.timestamp)}
										</span>
									</div>
								</>
							)}
							{!isFirst && (
								<span
									className={styles.gutterTimestamp}
									title={formatFullTime(msg.timestamp)}
								>
									{new Date(msg.timestamp).toLocaleTimeString([], {
										hour: 'numeric',
										minute: '2-digit',
										hour12: true,
									})}
								</span>
							)}
							{msg.content && !isGifOnlyContent(msg.content) && (
								<div className={styles.text}>
									<MessageContent
										content={msg.content}
										members={mentionMembers}
										customEmojis={customEmojiList}
									/>
									{msg.editedTimestamp && (
										<span className={styles.editedTag}> (edited)</span>
									)}
								</div>
							)}
							{msg.content &&
								extractUrls(msg.content)
									.slice(0, 3)
									.map((url, idx) => (
										<LinkEmbed
											key={`embed-${idx}-${url}`}
											url={url}
											onOpenGif={(gifUrl) =>
												setLightboxItem({
													src: gifUrl,
													filename: gifUrl.split('/').pop() || 'gif',
													mimeType: 'image/gif',
												})
											}
										/>
									))}
							{msg.attachments.length > 0 && (
								<div className={styles.attachments}>
									{msg.attachments.map((att, j) => {
										const kind = att.mimeType.split('/')[0];
										const cls =
											kind === 'image' || kind === 'video'
												? styles.imageAttachment
												: styles.fileAttachment;
										return (
											<EncryptedAttachment
												key={`${msg.id}-${j}`}
												metadata={att}
												className={cls}
												onImageClick={(url, attachment) => {
													setLightboxItem({ src: url, attachment });
												}}
											/>
										);
									})}
								</div>
							)}
							{msg.reactions.length > 0 && (
								<div className={styles.reactions}>
									{msg.reactions.map((r) => {
										// Custom emoji wins over unicode lookup. The reaction
										// key is a plain shortcode, so check the custom map
										// first; if no match, fall through to the existing
										// shortcode→unicode path.
										const customUrl = /^[a-zA-Z0-9_]+$/.test(r.emoji)
											? customEmojiByName.get(r.emoji.toLowerCase())
											: undefined;
										// Tooltip body: emoji glyph + ":name: reacted by
										// A, B, C, and N others" + hint line. Names come
										// straight from the enriched payload so we don't
										// refetch on hover. Marked interactive so the user
										// can move onto the panel and click it to open the
										// full reactions modal.
										const emojiLabel = `:${reactionKeyToName(r.emoji)}:`;
										const displayNames = r.users
											.slice(0, 3)
											.map((u) => u.displayName || u.username);
										const extras = Math.max(0, r.count - displayNames.length);
										const namesText =
											displayNames.length === 0
												? ''
												: extras === 0
													? displayNames.length === 1
														? displayNames[0]
														: displayNames.length === 2
															? `${displayNames[0]} and ${displayNames[1]}`
															: `${displayNames.slice(0, -1).join(', ')}, and ${displayNames[displayNames.length - 1]}`
													: `${displayNames.join(', ')}, and ${extras} ${extras === 1 ? 'other' : 'others'}`;
										const tooltipContent = (
											<div
												style={{
													display: 'flex',
													flexDirection: 'column',
													alignItems: 'center',
													gap: 6,
													textAlign: 'center',
													maxWidth: 240,
												}}
											>
												<div
													style={{
														display: 'flex',
														alignItems: 'center',
														justifyContent: 'center',
													}}
												>
													{customUrl ? (
														<img
															src={customUrl}
															alt={emojiLabel}
															draggable={false}
															style={{
																width: 32,
																height: 32,
																objectFit: 'contain',
															}}
														/>
													) : (
														<TwemojiImg
															emoji={resolveReactionKeyToUnicode(r.emoji)}
															size={32}
														/>
													)}
												</div>
												<span style={{ fontWeight: 600 }}>
													{emojiLabel} reacted by
												</span>
												<span style={{ fontWeight: 500 }}>{namesText}</span>
												<span
													style={{
														fontSize: '0.6875rem',
														color: 'var(--text-tertiary)',
														marginTop: 2,
													}}
												>
													Click to view all reactions
												</span>
											</div>
										);
										return (
											<Tooltip
												key={r.emoji}
												content={tooltipContent}
												placement="top"
												delay={200}
												interactive
												onContentClick={() => setReactionsModalMsgId(msg.id)}
											>
												<button
													type="button"
													className={`${styles.reactionChip} ${r.me ? styles.reactionMe : ''}`}
													onClick={() => setReactionsModalMsgId(msg.id)}
												>
													{customUrl ? (
														<img
															src={customUrl}
															alt={`:${r.emoji}:`}
															title={`:${r.emoji}:`}
															draggable={false}
															className={styles.reactionEmoji}
														/>
													) : (
														<TwemojiImg
															emoji={resolveReactionKeyToUnicode(r.emoji)}
															size={16}
															className={styles.reactionEmoji}
														/>
													)}
													<span className={styles.reactionCount}>{r.count}</span>
												</button>
											</Tooltip>
										);
									})}
								</div>
							)}
						</div>
					);
				})}
			</div>
			{reactPicker &&
				createPortal(
					<div
						style={{
							position: 'fixed',
							top: reactPicker.pos.top,
							left: reactPicker.pos.left,
							zIndex: 15000,
						}}
						onClick={(e) => e.stopPropagation()}
					>
						<EmojiPicker
							onSelect={handlePickReaction}
							onClose={() => setReactPicker(null)}
						/>
					</div>,
					document.body,
				)}
			<ImageLightbox
				isOpen={!!lightboxItem}
				src={lightboxItem?.src ?? ''}
				filename={
					lightboxItem?.attachment?.filename ?? lightboxItem?.filename
				}
				mimeType={
					lightboxItem?.attachment?.mimeType ?? lightboxItem?.mimeType
				}
				size={lightboxItem?.attachment?.size}
				width={lightboxItem?.attachment?.width}
				height={lightboxItem?.attachment?.height}
				attachment={lightboxItem?.attachment}
				onClose={() => setLightboxItem(null)}
			/>

			<PinConfirmationModal
				isOpen={!!pinTarget}
				onClose={() => setPinTarget(null)}
				channelId={channelId}
				messageId={pinTarget?.message.id ?? null}
				message={pinTarget?.message ?? null}
				variant={pinTarget?.variant ?? 'pin'}
			/>

			{authorPopout && (
				<MemberProfilePopout
					anchorRect={authorPopout.anchorRect}
					member={authorPopout.member}
					onClose={() => setAuthorPopout(null)}
				/>
			)}

			{(() => {
				// Look up the target message by id from the current group
				// so the modal always renders against the latest reaction
				// rows (the group array is re-created on every parent
				// re-render by Messages.tsx's memoized decrypt step).
				const targetMsg = reactionsModalMsgId
					? messages.find((m) => m.id === reactionsModalMsgId) ?? null
					: null;
				return (
					<ReactionsModal
						isOpen={!!targetMsg}
						onClose={() => setReactionsModalMsgId(null)}
						reactions={targetMsg?.reactions ?? []}
						myUserId={myUserId}
						customEmojiByName={customEmojiByName}
						onRemoveOwnReaction={async (emoji) => {
							if (!targetMsg || !myUserId) return;
							try {
								await removeReaction({
									messageId: targetMsg.id as any,
									userId: myUserId as any,
									emoji,
								});
							} catch (err) {
								console.error('Failed to remove reaction:', err);
							}
						}}
					/>
				);
			})()}

			{mobileSheetForMsg && (
				<MobileMessageActionsSheet
					isOpen={!!mobileSheetForMsg}
					onClose={() => setMobileSheetForMsg(null)}
					isOwnMessage={mobileSheetForMsg.senderId === myUserId}
					canDelete={mobileSheetForMsg.senderId === myUserId}
					hasContent={!!mobileSheetForMsg.content}
					onQuickReact={async (emoji) => {
						if (!myUserId) return;
						try {
							await addReaction({
								messageId: mobileSheetForMsg.id as any,
								userId: myUserId as any,
								emoji,
							});
						} catch (err) {
							console.error('Failed to add quick reaction:', err);
						}
					}}
					onAddReaction={() => {
						// No anchor on mobile — just open a centered picker
						// by passing `null` as the anchor.
						setReactPicker({
							messageId: mobileSheetForMsg.id,
							anchor: null as any,
						});
					}}
					onReply={() => {
						onReply?.(mobileSheetForMsg.id, first.authorName);
					}}
					onForward={() => {
						/* forward not implemented yet */
					}}
					onEdit={
						mobileSheetForMsg.senderId === myUserId
							? () => {
									/* edit not implemented via sheet yet */
								}
							: undefined
					}
					onPin={() => handlePin(mobileSheetForMsg.id)}
					isPinned={mobileSheetForMsg.pinned}
					onCopyText={() => handleCopyText(mobileSheetForMsg.content)}
					onDelete={
						mobileSheetForMsg.senderId === myUserId
							? () => handleDelete(mobileSheetForMsg.id)
							: undefined
					}
				/>
			)}
		</div>
	);
}
