import type { ReactNode } from 'react';
import { getTwemojiUrl } from '../../utils/twemoji';
import styles from './MessageContent.module.css';

interface MentionMember {
	displayName: string;
	username: string;
	userId: string;
}

export interface CustomEmojiEntry {
	name: string;
	url: string;
}

interface MessageContentProps {
	content: string;
	members?: MentionMember[];
	customEmojis?: CustomEmojiEntry[];
}

const EMOJI_REGEX =
	/(?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F)(?:\u200D(?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F))*|\p{Regional_Indicator}{2}/gu;

const CUSTOM_EMOJI_REGEX = /:([a-z0-9_]+):/gi;

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

// Escape user-supplied strings for safe inclusion in a regex.
function escapeRegex(s: string): string {
	return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

interface Token {
	type: 'emoji' | 'mention' | 'customEmoji' | 'url';
	index: number;
	length: number;
	text: string;
	// For customEmoji / url: the resolved URL.
	url?: string;
}

// Find the earliest token (emoji or mention) in `text` starting at `from`.
function findNextToken(
	text: string,
	from: number,
	members: MentionMember[],
	customEmojiMap: Map<string, string>,
): Token | null {
	let best: Token | null = null;

	// URL — earliest match at or after `from`. Trailing punctuation
	// is commonly prose, not part of the URL.
	URL_REGEX.lastIndex = from;
	const urlMatch = URL_REGEX.exec(text);
	if (urlMatch) {
		let matchText = urlMatch[0];
		const trimmed = matchText.replace(/[),.;!?]+$/, '');
		matchText = trimmed;
		best = {
			type: 'url',
			index: urlMatch.index,
			length: matchText.length,
			text: matchText,
			url: matchText,
		};
	}

	// Emoji — next match at or after `from`.
	EMOJI_REGEX.lastIndex = from;
	const emojiMatch = EMOJI_REGEX.exec(text);
	if (emojiMatch && (!best || emojiMatch.index < best.index)) {
		best = {
			type: 'emoji',
			index: emojiMatch.index,
			length: emojiMatch[0].length,
			text: emojiMatch[0],
		};
	}

	// Custom emoji `:shortcode:` — match only when we have a registered
	// emoji with that name. Unknown shortcodes fall through as plain text.
	if (customEmojiMap.size > 0) {
		CUSTOM_EMOJI_REGEX.lastIndex = from;
		let m: RegExpExecArray | null;
		while ((m = CUSTOM_EMOJI_REGEX.exec(text)) !== null) {
			const name = m[1].toLowerCase();
			const url = customEmojiMap.get(name);
			if (url) {
				if (!best || m.index < best.index) {
					best = {
						type: 'customEmoji',
						index: m.index,
						length: m[0].length,
						text: name,
						url,
					};
				}
				break;
			}
			// Unknown shortcode — keep searching past this match.
		}
	}

	// @everyone
	const everyoneIdx = text.indexOf('@everyone', from);
	if (everyoneIdx !== -1 && (!best || everyoneIdx < best.index)) {
		best = { type: 'mention', index: everyoneIdx, length: '@everyone'.length, text: '@everyone' };
	}

	// @{DisplayName} — prefer longest display name match so "@Alice Smith"
	// beats "@Alice". Sort members by descending display-name length.
	const sorted = [...members].sort(
		(a, b) => (b.displayName?.length ?? 0) - (a.displayName?.length ?? 0),
	);
	for (const m of sorted) {
		const name = m.displayName || m.username;
		if (!name) continue;
		const needle = `@${name}`;
		const idx = text.indexOf(needle, from);
		if (idx !== -1 && (!best || idx < best.index)) {
			best = { type: 'mention', index: idx, length: needle.length, text: needle };
		}
	}

	// Generic @word fallback — single run of word chars after an @.
	const genericRe = /@[\w]+/g;
	genericRe.lastIndex = from;
	const gm = genericRe.exec(text);
	if (gm && (!best || gm.index < best.index)) {
		best = { type: 'mention', index: gm.index, length: gm[0].length, text: gm[0] };
	}

	return best;
}

function renderContent(
	text: string,
	members: MentionMember[],
	customEmojiMap: Map<string, string>,
	keyPrefix: string,
): ReactNode[] {
	const parts: ReactNode[] = [];
	let cursor = 0;
	let safety = 0;
	while (cursor < text.length && safety++ < 10000) {
		const tok = findNextToken(text, cursor, members, customEmojiMap);
		if (!tok) {
			parts.push(<span key={`${keyPrefix}t${cursor}`}>{text.slice(cursor)}</span>);
			break;
		}
		if (tok.index > cursor) {
			parts.push(<span key={`${keyPrefix}t${cursor}`}>{text.slice(cursor, tok.index)}</span>);
		}
		if (tok.type === 'emoji') {
			parts.push(
				<img
					key={`${keyPrefix}e${tok.index}`}
					src={getTwemojiUrl(tok.text)}
					alt={tok.text}
					className={styles.emoji}
					draggable={false}
				/>,
			);
		} else if (tok.type === 'customEmoji') {
			parts.push(
				<img
					key={`${keyPrefix}c${tok.index}`}
					src={tok.url}
					alt={`:${tok.text}:`}
					title={`:${tok.text}:`}
					className={`${styles.customEmoji} ${styles.emoji}`}
					draggable={false}
				/>,
			);
		} else if (tok.type === 'url') {
			parts.push(
				<a
					key={`${keyPrefix}u${tok.index}`}
					className={styles.link}
					href={tok.url}
					target="_blank"
					rel="noopener noreferrer"
				>
					{tok.text}
				</a>,
			);
		} else {
			parts.push(
				<span key={`${keyPrefix}m${tok.index}`} className={styles.mention}>
					{tok.text}
				</span>,
			);
		}
		cursor = tok.index + tok.length;
	}
	if (parts.length === 0) {
		parts.push(<span key={`${keyPrefix}t0`}>{text}</span>);
	}
	// Silence unused escapeRegex in case linter complains.
	void escapeRegex;
	return parts;
}

export function MessageContent({
	content,
	members = [],
	customEmojis = [],
}: MessageContentProps) {
	const map = new Map<string, string>();
	for (const e of customEmojis) map.set(e.name.toLowerCase(), e.url);
	return <>{renderContent(content, members, map, 'mc')}</>;
}
