import React, { useState, useEffect, useLayoutEffect, useMemo, useCallback, memo, createContext, useContext, useRef } from 'react';
import type { AnimationItem } from 'lottie-web';
import { ProgressSpinner } from 'primereact/progressspinner';
import { authFetch } from '@/utils/authFetch';
import DOMPurify from 'isomorphic-dompurify';

// ============================================================================
// Type Definitions
// ============================================================================

interface DiscordUser {
    id: string;
    username: string;
    displayName?: string;
    avatar?: string;
    avatarUrl?: string;
    color?: string | number;
    isArchiveResolved?: boolean;
    bot?: boolean;
    isWebhook?: boolean;
    isServerForwarded?: boolean;
}

interface DiscordRole {
    id: string;
    name: string;
    color?: string | number;
}

interface DiscordChannel {
    id: string;
    name: string;
    type?: number;
    guildId?: string;
    guildName?: string;
    guildIcon?: string;
    parentId?: string;
    position?: number;
    topic?: string;
    messageCount?: number;
    threads?: DiscordChannel[];
    categoryName?: string | null;
    categoryPosition?: number;
    guild?: DiscordGuild;
}

interface DiscordEmoji {
    id: string;
    name: string;
    url?: string;
    animated?: boolean;
}

interface DiscordAttachment {
    id: string;
    filename?: string;
    url: string;
    size?: number;
    content_type?: string;
    width?: number;
    height?: number;
    originalUrl?: string;
}

interface DiscordEmbed {
    url?: string;
    title?: string;
    description?: string;
    color?: number;
    author?: { name?: string; url?: string; icon_url?: string };
    thumbnail?: { url?: string; width?: number; height?: number };
    image?: { url?: string; width?: number; height?: number };
    video?: { url?: string; width?: number; height?: number };
    footer?: { text?: string; icon_url?: string };
    fields?: Array<{ name: string; value: string; inline?: boolean }>;
    timestamp?: string;
    provider?: { name?: string; url?: string };
    type?: string;
}

interface DiscordReaction {
    emoji: { id?: string; name: string; animated?: boolean; url?: string };
    count: number;
    me?: boolean;
}

interface DiscordPollAnswer {
    answer_id: number;
    poll_media: { text?: string; emoji?: DiscordEmoji };
    voteCount?: number;
    // Runtime convenience properties
    id?: number;
    text?: string;
    emoji?: DiscordEmoji;
    voters?: Array<{ id: string; username?: string; displayName?: string; avatar?: string }>;
}

interface DiscordPoll {
    question: { text: string; emoji?: DiscordEmoji };
    question_text?: string;
    answers: DiscordPollAnswer[];
    expiry?: string;
    allow_multiselect?: boolean;
    allowMultiselect?: boolean;
    isFinalized?: boolean;
    totalVotes?: number;
    results?: {
        answer_counts: Array<{ id: number; count: number; me_voted?: boolean }>;
    };
}

interface DiscordSticker {
    id: string;
    name: string;
    format_type: number;
    formatType?: number;
    data?: unknown;
    lottieData?: unknown;
    url?: string;
}

interface DiscordMessage {
    id: string;
    author: DiscordUser;
    content: string;
    timestamp: string;
    edited_timestamp?: string;
    attachments?: DiscordAttachment[];
    embeds?: DiscordEmbed[];
    stickers?: DiscordSticker[];
    reactions?: DiscordReaction[];
    poll?: DiscordPoll | null;
    referenced_message?: DiscordMessage | null;
    type: number;
    mentions?: DiscordUser[];
    mention_roles?: string[];
    pinned?: boolean;
    tts?: boolean;
}

interface MemberSummary {
    id: string;
    username?: string;
    displayName?: string;
    avatar?: string;
    color?: string | number;
    isBot?: boolean;
}

interface MemberGroup {
    role?: DiscordRole;
    members?: MemberSummary[];
}

interface MembersData {
    groups?: MemberGroup[];
    roles?: Map<string, DiscordRole> | Record<string, DiscordRole>;
    totalMembers?: number;
}

interface DiscordGuild {
    id: string;
    name: string;
    guildId?: string;
    guildName?: string;
    guildIcon?: string;
    icon?: string | null;
    roles?: DiscordRole[];
}

interface LinkPreviewData {
    url: string;
    type?: string;
    title?: string;
    description?: string;
    image?: string;
    video?: string;
    videoId?: string;
    siteName?: string;
    trusted?: boolean;
    themeColor?: string;
    thumbnail?: string;
}

interface ParseOptions {
    channelMap?: Map<string, DiscordChannel>;
    usersMap?: Record<string, DiscordUser>;
    rolesMap?: Map<string, DiscordRole> | Record<string, DiscordRole>;
    emojiCache?: Record<string, DiscordEmoji>;
    onChannelClick?: (channelId: string) => void;
}

interface MessageProps {
    message: DiscordMessage;
    isFirstInGroup: boolean;
    showTimestamp: boolean;
    previewCache: Record<string, LinkPreviewData>;
    onPreviewLoad: (url: string, preview: LinkPreviewData) => void;
    channelMap: Map<string, DiscordChannel>;
    usersMap: Record<string, DiscordUser>;
    emojiCache: Record<string, DiscordEmoji>;
    members?: MembersData;
    onChannelSelect?: (channelId: string | DiscordChannel) => void;
    channelName?: string;
    onReactionClick?: (e: React.MouseEvent, messageId: string, reaction: DiscordReaction) => void;
    onPollVoterClick?: (e: React.MouseEvent, answer: DiscordPollAnswer) => void;
    onContextMenu?: (e: React.MouseEvent, messageId: string, content: string) => void;
    isSearchResult?: boolean;
    onJumpToMessage?: (messageId: string, channelId?: string) => void;
}

interface AttachmentProps {
    attachment: DiscordAttachment;
}

interface LinkPreviewProps {
    url: string;
    cachedPreview?: LinkPreviewData | null;
    onPreviewLoad?: (url: string, preview: LinkPreviewData) => void;
}

interface LottieStickerProps {
    data: unknown;
    name: string;
}

interface ArchivedMessageResult {
    originalAuthor: string;
    originalTimestamp: string | null;
    originalContent: string;
    topic?: string;
}

// State types for popups
interface ReactionPopupState {
  x: number;
  y: number;
    emoji: { id?: string; name: string; animated?: boolean; url?: string };
  users: Array<{ id: string; username?: string; displayName?: string; avatar?: string }>;
  loading?: boolean;
}

interface PollVoterPopupState {
  x: number;
  y: number;
  answer: DiscordPollAnswer;
  voters: Array<{ id: string; username?: string; displayName?: string; avatar?: string }>;
}

interface ContextMenuState {
  x: number;
  y: number;
  messageId: string;
  content: string;
}

interface ChannelContextMenuState {
  x: number;
  y: number;
  channel: DiscordChannel;
}

interface ImageModalState {
  url: string;
  alt?: string;
}

interface SearchResult {
  id: string;
  content: string;
  author: DiscordUser;
  timestamp: string;
  channel_id?: string;
}

// ============================================================================
// Discord Message Type Constants
// https://discord.com/developers/docs/resources/channel#message-object-message-types
const MESSAGE_TYPE_DEFAULT = 0;
const MESSAGE_TYPE_RECIPIENT_ADD = 1;
const MESSAGE_TYPE_RECIPIENT_REMOVE = 2;
const MESSAGE_TYPE_CALL = 3;
const MESSAGE_TYPE_CHANNEL_NAME_CHANGE = 4;
const MESSAGE_TYPE_CHANNEL_ICON_CHANGE = 5;
const MESSAGE_TYPE_CHANNEL_PINNED_MESSAGE = 6;
const MESSAGE_TYPE_USER_JOIN = 7;
const MESSAGE_TYPE_GUILD_BOOST = 8;
const MESSAGE_TYPE_GUILD_BOOST_TIER_1 = 9;
const MESSAGE_TYPE_GUILD_BOOST_TIER_2 = 10;
const MESSAGE_TYPE_GUILD_BOOST_TIER_3 = 11;
const MESSAGE_TYPE_CHANNEL_FOLLOW_ADD = 12;
const MESSAGE_TYPE_GUILD_DISCOVERY_DISQUALIFIED = 14;
const MESSAGE_TYPE_GUILD_DISCOVERY_REQUALIFIED = 15;
const MESSAGE_TYPE_GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING = 16;
const MESSAGE_TYPE_GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING = 17;
const MESSAGE_TYPE_THREAD_CREATED = 18;
const MESSAGE_TYPE_REPLY = 19;
const MESSAGE_TYPE_CHAT_INPUT_COMMAND = 20;
const MESSAGE_TYPE_THREAD_STARTER_MESSAGE = 21;
const MESSAGE_TYPE_GUILD_INVITE_REMINDER = 22;
const MESSAGE_TYPE_CONTEXT_MENU_COMMAND = 23;
const MESSAGE_TYPE_AUTO_MODERATION_ACTION = 24;
const MESSAGE_TYPE_ROLE_SUBSCRIPTION_PURCHASE = 25;
const MESSAGE_TYPE_INTERACTION_PREMIUM_UPSELL = 26;
const MESSAGE_TYPE_STAGE_START = 27;
const MESSAGE_TYPE_STAGE_END = 28;
const MESSAGE_TYPE_STAGE_SPEAKER = 29;
const MESSAGE_TYPE_STAGE_TOPIC = 31;
const MESSAGE_TYPE_GUILD_APPLICATION_PREMIUM_SUBSCRIPTION = 32;
const MESSAGE_TYPE_POLL_RESULT = 46;

// Image modal context for child components to trigger modal
interface ImageModalData {
    url: string;
    alt?: string;
}
const ImageModalContext = createContext<((data: ImageModalData) => void) | null>(null);

// Trusted domains that can be embedded directly without server-side proxy
const TRUSTED_DOMAINS = new Set([
    'youtube.com', 'www.youtube.com', 'youtu.be',
    'instagram.com', 'www.instagram.com',
    'twitter.com', 'x.com', 'www.twitter.com',
    'twitch.tv', 'www.twitch.tv', 'clips.twitch.tv',
    'spotify.com', 'open.spotify.com',
    'soundcloud.com', 'www.soundcloud.com',
    'vimeo.com', 'www.vimeo.com',
    'imgur.com', 'i.imgur.com',
    'giphy.com', 'media.giphy.com',
    'tenor.com', 'media.tenor.com',
    'gfycat.com',
    'reddit.com', 'www.reddit.com', 'v.redd.it', 'i.redd.it',
    'github.com', 'gist.github.com',
    'raw.githubusercontent.com', 'avatars.githubusercontent.com',
    'user-images.githubusercontent.com', 'camo.githubusercontent.com',
    'opengraph.githubassets.com',
    'codepen.io', 'codesandbox.io',
    'streamable.com', 'medal.tv',
    'discord.com', 'cdn.discordapp.com', 'media.discordapp.net',
    // Common image CDNs
    'picsum.photos', 'images.unsplash.com', 'unsplash.com',
    'pbs.twimg.com', 'abs.twimg.com',
    'img.youtube.com', 'i.ytimg.com',
    'w3schools.com', 'www.w3schools.com', // for demo video
]);

// Image extensions
const IMAGE_EXTENSIONS = /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?.*)?$/i;
// Video extensions
const VIDEO_EXTENSIONS = /\.(mp4|webm|mov|avi|mkv|m4v)(\?.*)?$/i;
// Audio extensions
const AUDIO_EXTENSIONS = /\.(mp3|wav|ogg|flac|m4a|aac)(\?.*)?$/i;

/**
 * Check if URL is from a trusted domain
 */
function isTrustedDomain(url: string): boolean {
    try {
        const parsed = new URL(url);
        return TRUSTED_DOMAINS.has(parsed.hostname);
    } catch {
        return false;
    }
}

/**
 * Check if a URL is already a proxy URL (from our server)
 */
function isProxyUrl(url: string | undefined | null): boolean {
    if (!url) return false;
    return url.startsWith('/api/image-proxy');
}

/**
 * Extract YouTube video ID
 */
function getYouTubeId(url: string): string | null {
    try {
        const parsed = new URL(url);
        if (parsed.hostname === 'youtu.be') {
            return parsed.pathname.slice(1);
        }
        if (parsed.hostname.includes('youtube.com')) {
            return parsed.searchParams.get('v') || parsed.pathname.split('/').pop() || null;
        }
    } catch {
        return null;
    }
    return null;
}

/**
 * Convert color (string or number) to CSS color string
 */
function toColorString(color: string | number | undefined): string | undefined {
    if (color === undefined || color === null) return undefined;
    if (typeof color === 'string') return color;
    // Discord integer colors are decimal RGB values
    return `#${color.toString(16).padStart(6, '0')}`;
}

/**
 * Format file size
 */
function formatFileSize(bytes: number | undefined): string {
    if (!bytes) return '';
    const units = ['B', 'KB', 'MB', 'GB'];
    let size = bytes;
    let unitIndex = 0;
    while (size >= 1024 && unitIndex < units.length - 1) {
        size /= 1024;
        unitIndex++;
    }
    return `${size.toFixed(1)} ${units[unitIndex]}`;
}

/**
 * Format Discord timestamp
 */
function formatTimestamp(timestamp: string | Date, format: 'full' | 'time' | 'short' = 'full'): string {
    const date = new Date(timestamp);
    const now = new Date();
    const isToday = date.toDateString() === now.toDateString();
    const isYesterday = new Date(now.getTime() - 86400000).toDateString() === date.toDateString();

    const time = date.toLocaleTimeString(undefined, {
        hour: 'numeric',
        minute: '2-digit',
        hour12: true
    });

    if (format === 'time') return time;
    
    if (format === 'short') {
        return date.toLocaleDateString(undefined, {
            month: 'short',
            day: 'numeric',
            year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined
        });
    }

    if (isToday) return `Today at ${time}`;
    if (isYesterday) return `Yesterday at ${time}`;

    return date.toLocaleDateString(undefined, {
        month: 'short',
        day: 'numeric',
        year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined
    }) + ` at ${time}`;
}

/**
 * Format date for divider
 */
function formatDateDivider(timestamp: string | Date): string {
    const date = new Date(timestamp);
    return date.toLocaleDateString(undefined, {
        weekday: 'long',
        month: 'long',
        day: 'numeric',
        year: 'numeric'
    });
}

/**
 * Decode HTML entities safely. Uses the DOM if available (client-side), otherwise
 * falls back to a robust regex for SSR environments.
 */
function decodeHtmlEntities(str: string | null | undefined): string {
    if (!str) return '';
    try {
        if (typeof document !== 'undefined') {
            const tx = document.createElement('textarea');
            tx.innerHTML = str;
            return tx.value;
        }
    } catch (e) {
        // fall through to fallback
    }

    return str.replace(/&(#(?:x[0-9a-fA-F]+|\d+)|[a-zA-Z]+);/g, (m, e) => {
        if (e[0] === '#') return e[1] === 'x' ? String.fromCharCode(parseInt(e.slice(2), 16)) : String.fromCharCode(parseInt(e.slice(1), 10));
        const map = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ', ndash: '–', mdash: '—', rsquo: '’', lsquo: '‘', hellip: '…', rdquo: '”', ldquo: '“' };
        return map[e] || m;
    });
}

/**
 * Parse archived messages from #dds-archive channel
 * These messages were re-sent by a bot with this format:
 * 
 * **Topic/Category**
 * **YYYY-MM-DD HH:MM:SS.ssssss+00:00 (UTC)
 * username**: message content
 * 
 * Note: The ** wraps from the timestamp line through the username
 * Some messages are nested (archive of an archive) and need recursive parsing
 * 
 * Returns { originalAuthor, originalTimestamp, originalContent, topic } or null if not parseable
 */
function parseArchivedMessage(content: string | null | undefined, depth: number = 0): ArchivedMessageResult | null {
    if (!content || depth > 3) return null; // Prevent infinite recursion

    // The format is:
    // **Topic**
    // **Timestamp (UTC)
    // username**: content

    // Username cannot contain * or : characters, and ends with **:
    // This prevents matching nested archive content
    const boldPattern = /^\*\*(.+?)\*\*\s*\n\*\*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[+-]\d{2}:\d{2})?)\s*\(UTC\)\s*\n([^*:]+)\*\*:\s*([\s\S]*)$/;
    const boldMatch = content.match(boldPattern);

    if (boldMatch) {
        const topic = boldMatch[1].trim();
        const timestampStr = boldMatch[2].trim();
        let originalAuthor = boldMatch[3].trim();
        let originalContent = boldMatch[4].trim();

        // Parse the timestamp
        let originalTimestamp;
        try {
            const tsString = timestampStr.replace(' ', 'T');
            originalTimestamp = new Date(tsString);
            if (isNaN(originalTimestamp.getTime())) {
                originalTimestamp = new Date(tsString + 'Z');
            }
        } catch {
            return null;
        }

        if (isNaN(originalTimestamp.getTime())) {
            return null;
        }

        // Check if the content is itself another archive format (nested archive)
        // Pattern: **Topic**\n**username**: content (simpler format without timestamp)
        const nestedPattern = /^\*\*(.+?)\*\*\s*\n\*\*([^*:]+)\*\*:\s*([\s\S]*)$/;
        const nestedMatch = originalContent.match(nestedPattern);
        if (nestedMatch) {
            // This is a nested archive - use the inner author and content
            originalAuthor = nestedMatch[2].trim();
            originalContent = nestedMatch[3].trim();
        }

        // Also try recursive parsing for deeply nested archives
        const recursiveParsed = parseArchivedMessage(originalContent, depth + 1);
        if (recursiveParsed) {
            return {
                originalAuthor: recursiveParsed.originalAuthor,
                originalTimestamp: recursiveParsed.originalTimestamp || originalTimestamp.toISOString(),
                originalContent: recursiveParsed.originalContent,
                topic: recursiveParsed.topic || topic
            };
        }

        return {
            originalAuthor,
            originalTimestamp: originalTimestamp.toISOString(),
            originalContent,
            topic
        };
    }

    // Try simpler format: **Topic**\n**username**: content (no timestamp)
    const simplePattern = /^\*\*(.+?)\*\*\s*\n\*\*([^*:]+)\*\*:\s*([\s\S]*)$/;
    const simpleMatch = content.match(simplePattern);
    if (simpleMatch) {
        const topic = simpleMatch[1].trim();
        const originalAuthor = simpleMatch[2].trim();
        const originalContent = simpleMatch[3].trim();

        return {
            originalAuthor,
            originalTimestamp: null, // No timestamp in this format
            originalContent,
            topic
        };
    }

    // Fallback: try without bold markers (plain text format)
    const lines = content.split('\n');
    if (lines.length < 3) return null;

    const topic = lines[0].replace(/^\*\*|\*\*$/g, '').trim();
    const timestampLine = lines[1].replace(/^\*\*/, '').trim();

    // Parse timestamp - format: "2024-11-08 18:41:34.031000+00:00 (UTC)"
    const timestampMatch = timestampLine.match(/^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[+-]\d{2}:\d{2})?)\s*(?:\(UTC\))?$/);
    if (!timestampMatch) return null;

    // Get the rest as author**: message or author: message
    const messagePart = lines.slice(2).join('\n').trim();

    // Parse "author**: message" or "author: message" - author cannot contain * or :
    const authorMatch = messagePart.match(/^([^*:]+)\*?\*?:\s*([\s\S]*)$/);
    if (!authorMatch) return null;

    const originalAuthor = authorMatch[1].trim();
    const originalContent = authorMatch[2].trim();

    // Parse the timestamp
    let originalTimestamp;
    try {
        const tsString = timestampMatch[1].replace(' ', 'T');
        originalTimestamp = new Date(tsString);
        if (isNaN(originalTimestamp.getTime())) {
            originalTimestamp = new Date(timestampMatch[1].replace(' ', 'T') + 'Z');
        }
    } catch {
        return null;
    }

    if (isNaN(originalTimestamp.getTime())) {
        return null;
    }

    return {
        originalAuthor,
        originalTimestamp: originalTimestamp.toISOString(),
        originalContent,
        topic
    };
}

/**
 * Hardcoded user data for archived messages
 * Since these users may not be mentioned in current messages, we store their data directly
 * This includes avatar URLs, display names, and colors from the database
 */
const ARCHIVE_USERS = {
    // kriegerin -> cyberkriegerin (user_id: 992437729927376996)
    'kriegerin': {
        id: '992437729927376996',
        username: 'cyberkriegerin',
        displayName: 'kriegerin',
        avatar: 'https://cdn.discordapp.com/avatars/992437729927376996/3c4030cf3a210db4a180eab76e559ea2.png?size=1024',
        color: null,
    },
    // codey/Chris -> gizmo_a (user_id: 1172340700663255091)
    'codey': {
        id: '1172340700663255091',
        username: 'gizmo_a',
        displayName: 'Chris',
        avatar: 'https://cdn.discordapp.com/avatars/1172340700663255091/05b2a61faeba2363943a175df4ecb701.png?size=1024',
        color: null,
    },
    'Chris': {
        id: '1172340700663255091',
        username: 'gizmo_a',
        displayName: 'Chris',
        avatar: 'https://cdn.discordapp.com/avatars/1172340700663255091/05b2a61faeba2363943a175df4ecb701.png?size=1024',
        color: null,
    },
    // Havoc bot (user_id: 1175471063438737519)
    'Havoc': {
        id: '1175471063438737519',
        username: 'Havoc',
        displayName: 'Havoc',
        avatar: 'https://cdn.discordapp.com/avatars/1175471063438737519/5e70b92d710a8584d27ca76220f93d67.png?size=1024',
        color: null,
        bot: true,
    },
    // Deleted User / slip (user_id: 456226577798135808)
    'Deleted User': {
        id: '456226577798135808',
        username: 'slip',
        displayName: 'poopboy',
        avatar: 'https://codey.horse/images/456226577798135808.png',
        color: null,
    },
};

/**
 * Look up a real user from an archived username
 * First checks hardcoded ARCHIVE_USERS, then falls back to usersMap
 * If members data is provided, looks up color from there
 * Returns user data if found, otherwise returns a basic object with just the username
 */
function resolveArchivedUser(archivedUsername: string, usersMap?: Record<string, DiscordUser>, members?: MembersData | null): DiscordUser {
    // First check hardcoded archive users
    if (ARCHIVE_USERS[archivedUsername]) {
        const archivedUser = ARCHIVE_USERS[archivedUsername];
        // Look up color from members data if available
        let color = archivedUser.color;
        if (!color && members?.groups && archivedUser.id) {
            for (const group of members.groups) {
                const member = group.members?.find(m => m.id === archivedUser.id);
                if (member?.color) {
                    color = member.color;
                    break;
                }
            }
        }
        return {
            ...archivedUser,
            color,
            isArchiveResolved: true,
        };
    }

    // Fall back to usersMap lookup by username match
    if (usersMap) {
        for (const [userId, userData] of Object.entries(usersMap)) {
            if (userData.username === archivedUsername || userData.displayName === archivedUsername) {
                // Look up color from members data if not in usersMap
                let color = userData.color;
                if (!color && members?.groups) {
                    for (const group of members.groups) {
                        const member = group.members?.find(m => m.id === userId);
                        if (member?.color) {
                            color = member.color;
                            break;
                        }
                    }
                }
                return {
                    ...userData,
                    id: userId,
                    color,
                    isArchiveResolved: true,
                };
            }
        }
    }

    // Return basic user object if not found
    return {
        username: archivedUsername,
        displayName: archivedUsername,
        id: `archive-${archivedUsername}`,
        isArchiveResolved: false,
    };
}

/**
 * Parse Discord markdown-like formatting
 * @param {string} text - The text to parse
 * @param {Object} options - Options for parsing
 * @param {Map} options.channelMap - Map of channel IDs to channel objects
 * @param {Object} options.usersMap - Map of user IDs to user objects { displayName, username, color }
 * @param {Object} options.rolesMap - Map of role IDs to role objects { name, color }
 * @param {Object} options.emojiCache - Map of emoji IDs to cached emoji objects { url, animated }
 * @param {Function} options.onChannelClick - Callback when channel is clicked
 */
function parseDiscordMarkdown(text: string | null | undefined, options: ParseOptions = {}): string {
    if (!text) return '';

    try {
        // Normalize HTML entities that sometimes make it into messages/embed fields
        // We decode before we escape so strings like "A &amp; B" become "A & B"
        // and avoid double-encoding when we later run an escape pass.

        const { channelMap = new Map(), usersMap = {}, rolesMap = new Map(), emojiCache = {}, onChannelClick } = options;

        // Normalize entities then escape HTML to avoid XSS while ensuring
        // already-encoded entities don't become double-encoded in the UI.
        const normalized = decodeHtmlEntities(text);

        // Escape HTML first
        let parsed = normalized
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;');

        // Code blocks (``` ```) - add data-lenis-prevent for independent scrolling
        // Must be processed first to prevent other formatting inside code
        // Don't trim - preserve whitespace for ASCII art
        parsed = parsed.replace(/```(\w+)?\n?([\s\S]*?)```/g, (_, lang, code) => {
            // Only trim trailing newline, preserve all other whitespace
            const trimmedCode = code.replace(/\n$/, '');
            return `<pre class="discord-code-block" data-lenis-prevent><code>${trimmedCode}</code></pre>`;
        });

        // Inline code (`) - must be early to prevent formatting inside code
        parsed = parsed.replace(/`([^`]+)`/g, '<code>$1</code>');


        // Blockquotes (> at start of line) - process before newline conversion
        // Group consecutive > lines into a single blockquote
        parsed = parsed.replace(/(^|\n)((?:&gt; .+(?:\n|$))+)/gm, (_, before, block) => {
            const content = block
                .split('\n')
                .map(line => line.replace(/^&gt; /, ''))
                .join('\n');
            return `${before}<blockquote class="discord-blockquote" data-lenis-prevent>${content}</blockquote>`;
        });

        // Headings (# ## ###) - must be at start of line
        // Process before other inline formatting
        parsed = parsed.replace(/(^|\n)### (.+?)(?=\n|$)/gm, (_, before, content) => {
            return `${before}<span class="discord-heading discord-heading-3">${content}</span>`;
        });
        parsed = parsed.replace(/(^|\n)## (.+?)(?=\n|$)/gm, (_, before, content) => {
            return `${before}<span class="discord-heading discord-heading-2">${content}</span>`;
        });
        parsed = parsed.replace(/(^|\n)# (.+?)(?=\n|$)/gm, (_, before, content) => {
            return `${before}<span class="discord-heading discord-heading-1">${content}</span>`;
        });

        // Subtext/small text (-# at start of line) - process before newline conversion
        parsed = parsed.replace(/(^|\n)-# (.+?)(?=\n|$)/gm, (_, before, content) => {
            return `${before}<span class="discord-subtext">${content}</span>`;
        });

        // Unordered lists (- or * at start of line, but not ---)
        parsed = parsed.replace(/(^|\n)[-*] (.+?)(?=\n|$)/gm, (_, before, content) => {
            return `${before}<span class="discord-list-item">• ${content}</span>`;
        });

        // Ordered lists (1. 2. etc at start of line)
        parsed = parsed.replace(/(^|\n)(\d+)\. (.+?)(?=\n|$)/gm, (_, before, num, content) => {
            return `${before}<span class="discord-list-item">${num}. ${content}</span>`;
        });

        // Bold + Italic + Underline combinations (most specific first)
        // ___***text***___ or ***___text___***
        parsed = parsed.replace(/(\*\*\*|___)(\*\*\*|___)([^*_]+)\2\1/g, '<strong><em><u>$3</u></em></strong>');

        // Bold + Italic (***text***)
        parsed = parsed.replace(/\*\*\*([^*]+)\*\*\*/g, '<strong><em>$1</em></strong>');

        // Bold + Underline (__**text**__ or **__text__**)
        parsed = parsed.replace(/__\*\*([^*_]+)\*\*__/g, '<u><strong>$1</strong></u>');
        parsed = parsed.replace(/\*\*__([^*_]+)__\*\*/g, '<strong><u>$1</u></strong>');

        // Italic + Underline (__*text*__ or *__text__* or ___text___)
        parsed = parsed.replace(/__\*([^*_]+)\*__/g, '<u><em>$1</em></u>');
        parsed = parsed.replace(/\*__([^*_]+)__\*/g, '<em><u>$1</u></em>');
        parsed = parsed.replace(/___([^_]+)___/g, '<u><em>$1</em></u>');

        // Bold (**)
        parsed = parsed.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');

        // Underline (__) - must come before italic _ handling
        parsed = parsed.replace(/__([^_]+)__/g, '<u>$1</u>');

        // Italic (* or _)
        parsed = parsed.replace(/\*([^*]+)\*/g, '<em>$1</em>');
        parsed = parsed.replace(/\b_([^_]+)_\b/g, '<em>$1</em>');

        // Strikethrough (~~)
        parsed = parsed.replace(/~~([^~]+)~~/g, '<del>$1</del>');

        // Spoiler (||)
        parsed = parsed.replace(/\|\|([^|]+)\|\|/g, '<span class="discord-spoiler">$1</span>');

        // Discord Timestamps (<t:1234567890:F>)
        parsed = parsed.replace(/&lt;t:(\d+)(?::([tTdDfFR]))?&gt;/g, (_, timestamp, format) => {
            const date = new Date(parseInt(timestamp) * 1000);
            let formatted;
            switch (format) {
                case 't': // Short time (9:30 PM)
                    formatted = date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
                    break;
                case 'T': // Long time (9:30:00 PM)
                    formatted = date.toLocaleTimeString();
                    break;
                case 'd': // Short date (11/28/2024)
                    formatted = date.toLocaleDateString();
                    break;
                case 'D': // Long date (November 28, 2024)
                    formatted = date.toLocaleDateString([], { dateStyle: 'long' });
                    break;
                case 'f': // Short date/time (November 28, 2024 9:30 PM)
                default:
                    formatted = date.toLocaleString([], { dateStyle: 'long', timeStyle: 'short' });
                    break;
                case 'F': // Long date/time (Thursday, November 28, 2024 9:30 PM)
                    formatted = date.toLocaleString([], { dateStyle: 'full', timeStyle: 'short' });
                    break;
                case 'R': // Relative (2 hours ago)
                    const now = Date.now();
                    const diff = now - date.getTime();
                    const seconds = Math.floor(Math.abs(diff) / 1000);
                    const minutes = Math.floor(seconds / 60);
                    const hours = Math.floor(minutes / 60);
                    const days = Math.floor(hours / 24);
                    const months = Math.floor(days / 30);
                    const years = Math.floor(days / 365);

                    if (diff < 0) {
                        // Future
                        if (years > 0) formatted = `in ${years} year${years > 1 ? 's' : ''}`;
                        else if (months > 0) formatted = `in ${months} month${months > 1 ? 's' : ''}`;
                        else if (days > 0) formatted = `in ${days} day${days > 1 ? 's' : ''}`;
                        else if (hours > 0) formatted = `in ${hours} hour${hours > 1 ? 's' : ''}`;
                        else if (minutes > 0) formatted = `in ${minutes} minute${minutes > 1 ? 's' : ''}`;
                        else formatted = `in ${seconds} second${seconds > 1 ? 's' : ''}`;
                    } else {
                        // Past
                        if (years > 0) formatted = `${years} year${years > 1 ? 's' : ''} ago`;
                        else if (months > 0) formatted = `${months} month${months > 1 ? 's' : ''} ago`;
                        else if (days > 0) formatted = `${days} day${days > 1 ? 's' : ''} ago`;
                        else if (hours > 0) formatted = `${hours} hour${hours > 1 ? 's' : ''} ago`;
                        else if (minutes > 0) formatted = `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
                        else formatted = `${seconds} second${seconds > 1 ? 's' : ''} ago`;
                    }
                    break;
            }
            return `<span class="discord-timestamp" title="${date.toLocaleString()}">${formatted}</span>`;
        });

        // User mentions (<@123456789>)
        parsed = parsed.replace(/&lt;@!?(\d+)&gt;/g, (_, userId) => {
            const user = usersMap[userId];
            const displayName = user?.displayName || user?.username || 'User';
            const colorStyle = user?.color ? ` style="color: ${user.color}"` : '';
            return `<span class="discord-mention"${colorStyle}>@${displayName}</span>`;
        });

        // @everyone and @here mentions
        parsed = parsed.replace(/@(everyone|here)/g, '<span class="discord-mention discord-mention-everyone">@$1</span>');

        // Channel mentions (<#123456789>)
        parsed = parsed.replace(/&lt;#(\d+)&gt;/g, (_, channelId) => {
            const channel = channelMap.get(channelId);
            const channelName = channel?.name || 'channel';
            if (channel) {
                return `<a href="#" class="discord-mention discord-channel-link" data-channel-id="${channelId}">#${channelName}</a>`;
            }
            return `<span class="discord-mention">#${channelName}</span>`;
        });

        // Role mentions (<@&123456789>) - robust lookup to avoid errors when rolesMap is missing or malformed
        parsed = parsed.replace(/&lt;@&amp;(\d+)&gt;/g, (_, roleId) => {
            try {
                let role: DiscordRole | null | undefined = null;
                if (rolesMap) {
                    if (typeof (rolesMap as Map<string, DiscordRole>).get === 'function') {
                        role = (rolesMap as Map<string, DiscordRole>).get(roleId);
                    } else if ((rolesMap as Record<string, DiscordRole>)[roleId]) {
                        role = (rolesMap as Record<string, DiscordRole>)[roleId];
                    } else if ((rolesMap as Record<string, DiscordRole>)[String(roleId)]) {
                        role = (rolesMap as Record<string, DiscordRole>)[String(roleId)];
                    }
                }
                const roleName = role?.name || 'role';
                const roleColor = role?.color || null;
                const style = roleColor ? ` style="color: ${roleColor}; background-color: ${roleColor}20;"` : '';
                return `<span class="discord-mention discord-role-mention"${style}>@${roleName}</span>`;
            } catch (err) {
                // Defensive: log for telemetry/debug and return safe fallback
                try { console.error('parseDiscordMarkdown: role mention parse failed', { roleId, err, rolesMapType: rolesMap && typeof rolesMap }); } catch (e) { /* ignore logging errors */ }
                return `<span class="discord-mention discord-role-mention">@role</span>`;
            }
        });

        // Slash command mentions (</command:123456789>)
        parsed = parsed.replace(/&lt;\/([^:]+):(\d+)&gt;/g, '<span class="discord-slash-command">/$1</span>');

        // Custom emoji (<:name:123456789> or <a:name:123456789>)
        // Use cached emoji URL if available, otherwise fall back to Discord CDN
        parsed = parsed.replace(/&lt;(a)?:(\w+):(\d+)&gt;/g, (_, animated, name, id) => {
            const cached = emojiCache[id];
            const url = cached?.url || `https://cdn.discordapp.com/emojis/${id}.${animated ? 'gif' : 'png'}`;
            return `<img class="discord-emoji" src="${url}" alt=":${name}:" title=":${name}:">`;
        });

        // Unicode emoji (keep as-is, they render natively)

        // Masked/Markdown links [text](url) or [text](url "title") - process before bare URL detection
        parsed = parsed.replace(
            /\[([^\]]+)\]\((https?:\/\/[^\s)"]+)(?:\s+"([^"]+)")?\)/g,
            (_, text, url, title) => {
                const titleAttr = title ? ` title="${title}"` : '';
                return `<a href="${url}" target="_blank" rel="noopener noreferrer" class="discord-link"${titleAttr}>${text}</a>`;
            }
        );

        // Links (URLs) - use negative lookbehind to skip URLs in HTML attributes (src=", href=")
        // Match URLs that are NOT preceded by =" or ='
        parsed = parsed.replace(
            /(?<![="'])(?<![=]["'])(https?:\/\/[^\s<>"']+)/g,
            '<a href="$1" target="_blank" rel="noopener noreferrer" class="discord-link">$1</a>'
        );

        // Newlines
        parsed = parsed.replace(/\n/g, '<br>');

        // Unescape Discord markdown escape sequences (\\_  \\*  \\~  \\`  \\|  \\\\)
        // Must be done after all markdown processing
        parsed = parsed.replace(/\\([_*~`|\\])/g, '$1');

        // Final sanitization pass with DOMPurify to prevent XSS
        return DOMPurify.sanitize(parsed, {
            ALLOWED_TAGS: ['strong', 'em', 'u', 's', 'span', 'code', 'pre', 'br', 'a', 'img', 'blockquote'],
            ALLOWED_ATTR: ['class', 'href', 'target', 'rel', 'src', 'alt', 'title', 'style', 'data-lenis-prevent', 'data-channel-id', 'data-user-id', 'data-role-id'],
            ALLOW_DATA_ATTR: true,
        });
    } catch (err) {
        try { console.error('parseDiscordMarkdown failed', err); } catch (e) { /* ignore logging errors */ }
        // Fallback: return a safely-escaped version of the input to avoid crashing the UI
        const safe = String(text)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;');
        return safe;
    }
}

/**
 * Extract URLs from text
 */
function extractUrls(text: string | null | undefined): string[] {
    if (!text) return [];
    const urlRegex = /(https?:\/\/[^\s<]+)/g;
    const matches = text.match(urlRegex);
    return matches || [];
}

// ============================================================================
// Link Preview Component
// ============================================================================

const LinkPreview = memo(function LinkPreview({ url, cachedPreview, onPreviewLoad }: LinkPreviewProps) {
    const [preview, setPreview] = useState<LinkPreviewData | null>(cachedPreview || null);
    const [loading, setLoading] = useState(!cachedPreview);
    const [error, setError] = useState(false);
    const [imageError, setImageError] = useState(false);

    useEffect(() => {
        if (cachedPreview) {
            setPreview(cachedPreview);
            setLoading(false);
            return;
        }

        // For direct media URLs, only allow trusted domains
        if (IMAGE_EXTENSIONS.test(url)) {
            if (isTrustedDomain(url)) {
                const previewData = { url, type: 'image', image: url, trusted: true };
                setPreview(previewData);
                setLoading(false);
                onPreviewLoad?.(url, previewData);
            } else {
                // Fetch through server to get metadata without exposing user IP
                fetchPreviewFromServer();
            }
            return;
        }
        if (VIDEO_EXTENSIONS.test(url)) {
            if (isTrustedDomain(url)) {
                const previewData = { url, type: 'video', video: url, trusted: true };
                setPreview(previewData);
                setLoading(false);
                onPreviewLoad?.(url, previewData);
            } else {
                // Don't show untrusted video embeds - too risky
                setError(true);
                setLoading(false);
            }
            return;
        }

        // Check for YouTube - trusted domain, can embed directly
        const ytId = getYouTubeId(url);
        if (ytId) {
            const previewData = {
                url,
                type: 'youtube',
                videoId: ytId,
                title: 'YouTube Video',
                siteName: 'YouTube',
                themeColor: '#FF0000',
                trusted: true,
            };
            setPreview(previewData);
            setLoading(false);
            onPreviewLoad?.(url, previewData);
            return;
        }

        // All other URLs: fetch preview from server
        fetchPreviewFromServer();

        async function fetchPreviewFromServer() {
            try {
                const response = await authFetch(`/api/link-preview?url=${encodeURIComponent(url)}`);
                if (!response.ok) throw new Error('Failed to fetch');
                const data = await response.json();
                if (data.error) throw new Error(data.error);
                setPreview(data);
                onPreviewLoad?.(url, data);
            } catch (err) {
                console.warn('Failed to fetch link preview:', url, err);
                setError(true);
            } finally {
                setLoading(false);
            }
        }
    }, [url, cachedPreview, onPreviewLoad]);

    if (loading) {
        return (
            <div className="discord-embed discord-embed-loading">
                <ProgressSpinner style={{ width: '20px', height: '20px' }} strokeWidth="4" />
            </div>
        );
    }

    if (error || !preview) {
        return null; // Don't show anything for failed previews
    }

    // YouTube embed - use click-to-play thumbnail
    if (preview.type === 'youtube' && preview.videoId) {
        const thumbnailUrl = `https://img.youtube.com/vi/${preview.videoId}/maxresdefault.jpg`;
        const watchUrl = `https://www.youtube.com/watch?v=${preview.videoId}`;

        return (
            <div className="discord-embed discord-embed-video" style={{ borderColor: preview.themeColor || '#FF0000' }}>
                <div className="discord-embed-content">
                    <div className="discord-embed-provider">YouTube</div>
                    {preview.title && (
                        <a href={watchUrl} target="_blank" rel="noopener noreferrer" className="discord-embed-title">
                            {decodeHtmlEntities(preview.title)}
                        </a>
                    )}
                </div>
                <a
                    href={watchUrl}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="discord-embed-video-thumbnail-link"
                    title="Watch on YouTube"
                >
                    <div className="discord-embed-video-container discord-embed-video-thumbnail">
                        <img
                            src={thumbnailUrl}
                            alt={decodeHtmlEntities(preview.title) || 'YouTube video'}
                            className="discord-embed-video-thumbnail-img"
                        />
                        <div className="discord-embed-video-play-overlay">
                            <svg viewBox="0 0 68 48" className="discord-embed-video-play-icon">
                                <path d="M66.52,7.74c-0.78-2.93-2.49-5.41-5.42-6.19C55.79,.13,34,0,34,0S12.21,.13,6.9,1.55 C3.97,2.33,2.27,4.81,1.48,7.74C0.06,13.05,0,24,0,24s0.06,10.95,1.48,16.26c0.78,2.93,2.49,5.41,5.42,6.19 C12.21,47.87,34,48,34,48s21.79-0.13,27.1-1.55c2.93-0.78,4.64-3.26,5.42-6.19C67.94,34.95,68,24,68,24S67.94,13.05,66.52,7.74z" fill="#f00"></path>
                                <path d="M 45,24 27,14 27,34" fill="#fff"></path>
                            </svg>
                        </div>
                    </div>
                </a>
            </div>
        );
    }

    // Direct video - only show if trusted
    if (preview.type === 'video' || preview.video) {
        const videoUrl = preview.video || url;
        if (!isTrustedDomain(videoUrl)) {
            return null; // Don't embed untrusted videos
        }
        return (
            <div className="discord-embed discord-embed-video" style={{ borderColor: preview.themeColor || '#5865f2' }}>
                <video
                    src={videoUrl}
                    controls
                    className="discord-embed-video-player"
                    preload="metadata"
                />
            </div>
        );
    }

    // Direct image - server already returns safe (proxied) URLs
    if (preview.type === 'image') {
        const imageUrl = preview.image || url;
        return (
            <div className="discord-attachment-image-wrapper">
                <img
                    src={imageUrl}
                    alt="Linked image"
                    className="discord-attachment-image"
                    loading="lazy"
                    onError={(e) => (e.target as HTMLElement).style.display = 'none'}
                />
            </div>
        );
    }

    // Standard link preview - server already returns safe (proxied) image URLs
    const hasLargeImage = preview.image && !preview.thumbnail;

    return (
        <div
            className="discord-embed"
            style={{ borderColor: preview.themeColor || '#5865f2' }}
        >
            <div className="discord-embed-content-wrapper">
                <div className="discord-embed-content">
                    {preview.siteName && (
                        <div className="discord-embed-provider">{decodeHtmlEntities(preview.siteName)}</div>
                    )}
                    {preview.title && (
                        <a
                            href={url}
                            target="_blank"
                            rel="noopener noreferrer"
                            className="discord-embed-title"
                        >
                            {decodeHtmlEntities(preview.title)}
                        </a>
                    )}
                    {preview.description && (
                        <div className="discord-embed-description">
                            {(() => {
                                const d = decodeHtmlEntities(preview.description);
                                return d.length > 300 ? d.slice(0, 300) + '...' : d;
                            })()}
                        </div>
                    )}
                </div>
                {preview.image && !hasLargeImage && !imageError && (
                    <img
                        src={preview.image}
                        alt=""
                        className="discord-embed-thumbnail"
                        loading="lazy"
                        onError={() => setImageError(true)}
                    />
                )}
            </div>
            {preview.image && hasLargeImage && !imageError && (
                <img
                    src={preview.image}
                    alt=""
                    className="discord-embed-image"
                    loading="lazy"
                    onError={() => setImageError(true)}
                />
            )}
        </div>
    );
});

// ============================================================================
// Lottie Sticker Component
// ============================================================================

const LottieSticker = memo(function LottieSticker({ data, name }: LottieStickerProps) {
    const containerRef = useRef<HTMLDivElement | null>(null);
    const animationRef = useRef<AnimationItem | null>(null);

    useEffect(() => {
        if (!containerRef.current || !data) return;

        // Dynamically import lottie-web only when needed
        import('lottie-web').then((lottie) => {
            // Clean up any existing animation
            if (animationRef.current) {
                animationRef.current.destroy();
            }

            animationRef.current = lottie.default.loadAnimation({
                container: containerRef.current!,
                renderer: 'svg',
                loop: true,
                autoplay: true,
                animationData: data,
            });
        }).catch((err) => {
            console.error('Failed to load lottie-web:', err);
        });

        return () => {
            if (animationRef.current) {
                animationRef.current.destroy();
                animationRef.current = null;
            }
        };
    }, [data]);

    if (!data) {
        return (
            <div className="discord-sticker-placeholder">
                <span>{name}</span>
            </div>
        );
    }

    return (
        <div
            ref={containerRef}
            className="discord-sticker-lottie"
            aria-label={name}
        />
    );
});

// ============================================================================
// Attachment Component
// ============================================================================

const Attachment = memo(function Attachment({ attachment }: AttachmentProps) {
    const { filename, url, size, content_type, width, height } = attachment;
    const openImageModal = useContext(ImageModalContext);

    const isImage = content_type?.startsWith('image/') || IMAGE_EXTENSIONS.test(filename || url);
    const isVideo = content_type?.startsWith('video/') || VIDEO_EXTENSIONS.test(filename || url) || (url && url.includes('/api/discord/cached-video'));
    const isAudio = content_type?.startsWith('audio/') || AUDIO_EXTENSIONS.test(filename || url);

    if (isImage) {
        return (
            <div className="discord-attachment-image-wrapper">
                <img
                    src={url}
                    alt={filename || 'Image'}
                    className="discord-attachment-image"
                    onClick={() => openImageModal?.({ url, alt: filename || 'Image' })}
                    style={{
                        cursor: 'pointer',
                        ...(width && height ? {
                            maxWidth: Math.min(width, 400),
                            aspectRatio: `${width} / ${height}`
                        } : {})
                    }}
                />
            </div>
        );
    }

    if (isVideo) {
        return (
            <div className="discord-attachment-video-wrapper">
                <video
                    src={url}
                    controls
                    className="discord-attachment-video"
                    preload="metadata"
                    style={width && height ? {
                        maxWidth: Math.min(width, 400),
                        aspectRatio: `${width} / ${height}`
                    } : undefined}
                >
                    <source src={url} type={content_type} />
                    Your browser does not support the video tag.
                </video>
            </div>
        );
    }

    if (isAudio) {
        return (
            <div className="discord-attachment-audio-wrapper">
                <div className="discord-attachment-file">
                    <svg className="discord-attachment-icon" viewBox="0 0 24 24" fill="currentColor">
                        <path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z" />
                    </svg>
                    <div className="discord-attachment-info">
                        <a href={url} target="_blank" rel="noopener noreferrer" className="discord-attachment-name">
                            {filename || 'Audio file'}
                        </a>
                        {size && <div className="discord-attachment-size">{formatFileSize(size)}</div>}
                    </div>
                </div>
                <audio src={url} controls className="discord-attachment-audio" preload="metadata">
                    Your browser does not support the audio tag.
                </audio>
            </div>
        );
    }

    // Generic file
    return (
        <div className="discord-attachment-file">
            <svg className="discord-attachment-icon" viewBox="0 0 24 24" fill="currentColor">
                <path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z" />
            </svg>
            <div className="discord-attachment-info">
                <a href={url} target="_blank" rel="noopener noreferrer" className="discord-attachment-name">
                    {filename || 'File'}
                </a>
                {size && <div className="discord-attachment-size">{formatFileSize(size)}</div>}
            </div>
        </div>
    );
});

// ============================================================================
// Message Component
// ============================================================================

const DiscordMessage = memo(function DiscordMessage({
    message,
    isFirstInGroup,
    showTimestamp,
    previewCache,
    onPreviewLoad,
    channelMap,
    usersMap,
    emojiCache,
    members,
    onChannelSelect,
    channelName,
    onReactionClick,
    onPollVoterClick,
    onContextMenu,
    isSearchResult,
    onJumpToMessage
}: MessageProps) {
    const {
        id,
        author,
        content,
        timestamp,
        attachments = [],
        embeds = [],
        stickers = [],
        reactions = [],
        poll = null,
        referenced_message,
        type
    } = message;

    // Check if this is an archived message from #dds-archive
    const isArchiveChannel = channelName === 'dds-archive';
    const archivedMessage = useMemo(() => {
        if (!isArchiveChannel) return null;
        return parseArchivedMessage(content);
    }, [isArchiveChannel, content]);

    // Use original data if this is a parsed archive message
    // Try to resolve the archived username to a real user
    const displayAuthor = useMemo(() => {
        if (!archivedMessage) return author;
        return resolveArchivedUser(archivedMessage.originalAuthor, usersMap, members);
    }, [archivedMessage, author, usersMap, members]);
    const displayContent = archivedMessage ? archivedMessage.originalContent : content;
    const displayTimestamp = archivedMessage ? (archivedMessage.originalTimestamp || timestamp) : timestamp;

    // Extract URLs from content for link previews
    const urls = useMemo(() => extractUrls(displayContent), [displayContent]);

    // Filter URLs that don't already have embeds
    // For YouTube, compare video IDs since URLs can vary (youtu.be vs youtube.com, with/without playlist)
    const urlsToPreview = useMemo(() => {
        const embedUrls = new Set(embeds?.map(e => e.url).filter(Boolean));

        // Extract YouTube video IDs from embeds
        const embedYouTubeIds = new Set();
        embeds?.forEach(e => {
            if (e.url) {
                const ytId = getYouTubeId(e.url);
                if (ytId) embedYouTubeIds.add(ytId);
            }
            // Also check video URL for YouTube embeds
            if (e.video?.url) {
                const videoYtId = getYouTubeId(e.video.url);
                if (videoYtId) embedYouTubeIds.add(videoYtId);
            }
        });

        // Exclude URLs that match any attachment's originalUrl
        const attachmentOriginalUrls = new Set(attachments?.map(a => a.originalUrl).filter(Boolean));

        return urls.filter(url => {
            // Skip if exact URL match
            if (embedUrls.has(url)) return false;

            // For YouTube URLs, skip if video ID already embedded
            const ytId = getYouTubeId(url);
            if (ytId && embedYouTubeIds.has(ytId)) return false;

            // Skip if URL matches any attachment originalUrl
            if (attachmentOriginalUrls.has(url)) return false;

            return true;
        });
    }, [urls, embeds, attachments]);

    // Build rolesMap from members data for role mention parsing (defensive)
    const rolesMap = useMemo(() => {
        const map = new Map();
        if (Array.isArray(members?.roles)) {
            members.roles.forEach(role => map.set(role?.id, role));
        }
        return map;
    }, [members?.roles]);

    const parsedContent = useMemo(() => parseDiscordMarkdown(displayContent, { channelMap, usersMap, rolesMap, emojiCache }), [displayContent, channelMap, usersMap, rolesMap, emojiCache]);

    // Handle channel link clicks
    const handleContentClick = useCallback((e) => {
        const channelLink = e.target.closest('.discord-channel-link');
        if (channelLink) {
            e.preventDefault();
            const channelId = channelLink.dataset.channelId;
            if (channelId && onChannelSelect) {
                const channel = channelMap?.get(channelId);
                if (channel) {
                    onChannelSelect(channel);
                }
            }
        }
    }, [channelMap, onChannelSelect]);

    // For archived messages, use the resolved user's avatar if available
    // Avatar might be a full URL or just a hash - handle both cases
    const avatarUrl = useMemo(() => {
        const avatarSource = displayAuthor?.avatar || displayAuthor?.avatarUrl;
        if (!avatarSource) return null;
        if (avatarSource.startsWith('http')) return avatarSource;
        return `https://cdn.discordapp.com/avatars/${displayAuthor.id}/${avatarSource}.png?size=80`;
    }, [displayAuthor]);

    const getInitial = (name) => (name || 'U')[0].toUpperCase();

    // System messages (join, boost, etc.)
    // Type 0 = default, 19 = reply, 20 = chat input command, 23 = context menu command
    // Types 20 and 23 are app/bot command messages and should render normally
    if (
        type &&
        type !== MESSAGE_TYPE_DEFAULT &&
        type !== MESSAGE_TYPE_REPLY &&
        type !== MESSAGE_TYPE_CHAT_INPUT_COMMAND &&
        type !== MESSAGE_TYPE_CONTEXT_MENU_COMMAND
    ) {
        // Special handling for poll result system messages (type 46)
        if (type === MESSAGE_TYPE_POLL_RESULT) {
            // Find the poll_result embed
            const pollResultEmbed = embeds?.find(e => e.type === 'poll_result');
            let pollFields = {};
            if (pollResultEmbed && pollResultEmbed.fields) {
                for (const field of pollResultEmbed.fields) {
                    pollFields[field.name] = field.value;
                }
            }
            // Get referenced poll message (should have poll info)
            const pollMessage = referenced_message;
            // Winner info
            const winnerText = pollFields['victor_answer_text'] || '';
            const winnerEmoji = pollFields['victor_answer_emoji_name'] || '';
            const winnerVotes = parseInt(pollFields['victor_answer_votes'] || '0', 10);
            const totalVotes = parseInt(pollFields['total_votes'] || '0', 10);
            const percent = totalVotes > 0 ? Math.round((winnerVotes / totalVotes) * 100) : 0;
            const pollTitle = pollFields['poll_question_text'] || (pollMessage?.poll?.question_text) || '';
            // Author of the poll (from referenced message)
            const pollAuthor = pollMessage?.author;
            // Handler for View Poll button
            const handleViewPoll = () => {
                if (onJumpToMessage && pollMessage?.id) {
                    onJumpToMessage(pollMessage.id);
                }
            };
            return (
                <div className="discord-message discord-system-message discord-poll-result-message">
                    <div className="discord-system-icon">
                        <svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
                            <path d="M18.5 12c0-1.77-.77-3.37-2-4.47V4.5h-2v1.98A5.96 5.96 0 0 0 12 6c-.97 0-1.89.23-2.71.63L7.11 4.45 5.64 5.92l2.18 2.18A5.96 5.96 0 0 0 6 12c0 3.31 2.69 6 6 6s6-2.69 6-6zm-6 4c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4z" />
                        </svg>
                    </div>
                    <div className="discord-poll-result-content">
                        <div className="discord-poll-result-header">
                            <span className="discord-username" style={pollAuthor?.color ? { color: toColorString(pollAuthor.color) } : undefined}>
                                {pollAuthor?.displayName || pollAuthor?.username || 'Unknown'}
                            </span>
                            <span className="discord-poll-title">’s poll <strong>{pollTitle}</strong> has closed</span>
                        </div>
                        <div className="discord-poll-result-winner">
                            <div className="discord-poll-result-bar" role="presentation">
                                <div
                                    className="discord-poll-result-fill"
                                    style={{ width: `${percent}%` }}
                                >
                                    <div className="discord-poll-result-fill-content">
                                        <span className="discord-poll-result-emoji">{winnerEmoji}</span>
                                        <span className="discord-poll-result-answer">{winnerText}</span>
                                        <span className="discord-poll-result-check" title="Winner">
                                            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
                                        </span>
                                    </div>
                                </div>
                            </div>
                            <div className="discord-poll-result-meta">
                                <span className="discord-poll-result-percent">{percent}%</span>
                                <span className="discord-poll-result-votes">{totalVotes} vote{totalVotes !== 1 ? 's' : ''}</span>
                            </div>
                        </div>
                        <button className="discord-poll-result-view-btn discord-poll-result-view-full" onClick={handleViewPoll}>
                            View Poll
                        </button>
                    </div>
                    <span className="discord-timestamp">{formatTimestamp(displayTimestamp)}</span>
                </div>
            );
        }
        // Default system message rendering
        // Generate system message text based on type
        const getSystemMessageText = () => {
            const username = displayAuthor?.displayName || displayAuthor?.username || 'Someone';
            switch (type) {
                case MESSAGE_TYPE_USER_JOIN:
                    // Discord has multiple join messages, pick randomly or use content if provided
                    const joinMessages = [
                        'joined the server.',
                        'just joined the server - glhf!',
                        'just joined. Everyone, look busy!',
                        'just arrived.',
                        'just landed.',
                        'just slid into the server.',
                        'hopped into the server.',
                        'appeared.',
                    ];
                    return content || joinMessages[0];
                case MESSAGE_TYPE_GUILD_BOOST:
                    return content || 'just boosted the server!';
                case MESSAGE_TYPE_GUILD_BOOST_TIER_1:
                    return content || 'just boosted the server! Server has achieved Level 1!';
                case MESSAGE_TYPE_GUILD_BOOST_TIER_2:
                    return content || 'just boosted the server! Server has achieved Level 2!';
                case MESSAGE_TYPE_GUILD_BOOST_TIER_3:
                    return content || 'just boosted the server! Server has achieved Level 3!';
                case MESSAGE_TYPE_CHANNEL_PINNED_MESSAGE:
                    return content || 'pinned a message to this channel.';
                case MESSAGE_TYPE_THREAD_CREATED:
                    return content || 'started a thread.';
                case MESSAGE_TYPE_RECIPIENT_ADD:
                    return content || 'added someone to the group.';
                case MESSAGE_TYPE_RECIPIENT_REMOVE:
                    return content || 'removed someone from the group.';
                case MESSAGE_TYPE_CALL:
                    return content || 'started a call.';
                case MESSAGE_TYPE_CHANNEL_NAME_CHANGE:
                    return content || 'changed the channel name.';
                case MESSAGE_TYPE_CHANNEL_ICON_CHANGE:
                    return content || 'changed the channel icon.';
                case MESSAGE_TYPE_CHANNEL_FOLLOW_ADD:
                    return content || 'added a channel follow.';
                case MESSAGE_TYPE_STAGE_START:
                    return content || 'started a Stage.';
                case MESSAGE_TYPE_STAGE_END:
                    return content || 'ended the Stage.';
                case MESSAGE_TYPE_STAGE_SPEAKER:
                    return content || 'is now a speaker.';
                case MESSAGE_TYPE_STAGE_TOPIC:
                    return content || 'changed the Stage topic.';
                default:
                    return content || '';
            }
        };

        const systemText = getSystemMessageText();
        // Choose icon based on message type
        const getSystemIcon = () => {
            switch (type) {
                case MESSAGE_TYPE_USER_JOIN:
                    return (
                        <svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
                            <path d="M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2m0 3l4.5 4.5-1.41 1.41L12 7.83l-3.09 3.08L7.5 9.5 12 5z" />
                        </svg>
                    );
                case MESSAGE_TYPE_GUILD_BOOST:
                case MESSAGE_TYPE_GUILD_BOOST_TIER_1:
                case MESSAGE_TYPE_GUILD_BOOST_TIER_2:
                case MESSAGE_TYPE_GUILD_BOOST_TIER_3:
                    return (
                        <svg viewBox="0 0 24 24" fill="#ff73fa" width="16" height="16">
                            <path d="M12.001 2.5c.512 0 .994.228 1.32.624l.088.114 8.5 12c.25.353.32.805.187 1.214l-.055.134-.061.114a1.5 1.5 0 01-1.191.79l-.119.01H3.331a1.5 1.5 0 01-1.398-2.048l.061-.114 8.5-12a1.5 1.5 0 011.408-.838l.099.01.1.014zM12 7.268L5.416 16.5h13.169L12 7.268z" />
                        </svg>
                    );
                case MESSAGE_TYPE_CHANNEL_PINNED_MESSAGE:
                    return (
                        <svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
                            <path d="M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z" />
                        </svg>
                    );
                default:
                    return (
                        <svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
                            <path d="M18.5 12c0-1.77-.77-3.37-2-4.47V4.5h-2v1.98A5.96 5.96 0 0 0 12 6c-.97 0-1.89.23-2.71.63L7.11 4.45 5.64 5.92l2.18 2.18A5.96 5.96 0 0 0 6 12c0 3.31 2.69 6 6 6s6-2.69 6-6zm-6 4c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4z" />
                        </svg>
                    );
            }
        };

        return (
            <div className="discord-message discord-system-message">
                <div className="discord-system-icon">
                    {getSystemIcon()}
                </div>
                <span className="discord-system-content">
                    <span className="discord-username" style={displayAuthor?.color ? { color: toColorString(displayAuthor.color) } : undefined}>
                        {displayAuthor?.displayName || displayAuthor?.username || 'Unknown'}
                    </span>
                    {' '}
                    {systemText ? (
                        <span dangerouslySetInnerHTML={{ __html: parseDiscordMarkdown(systemText, { usersMap, channelMap, emojiCache, rolesMap }) }} />
                    ) : (
                        <span className="discord-system-unknown">(system message type {type})</span>
                    )}
                </span>
                <span className="discord-timestamp">{formatTimestamp(displayTimestamp)}</span>
            </div>
        );
    }

    return (
        <>
            {/* Reply context */}
            {referenced_message && (
                <div
                    className="discord-reply-context"
                    onClick={() => {
                        if (referenced_message.id && onJumpToMessage) {
                            onJumpToMessage(referenced_message.id);
                        }
                    }}
                    style={{ cursor: referenced_message.id ? 'pointer' : 'default' }}
                >
                    <img
                        src={referenced_message.author?.avatar
                            ? (referenced_message.author.avatar.startsWith('http')
                                ? referenced_message.author.avatar
                                : `https://cdn.discordapp.com/avatars/${referenced_message.author.id}/${referenced_message.author.avatar}.png?size=32`)
                            : 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"/>'
                        }
                        alt=""
                        className="discord-reply-avatar"
                    />
                    <span className="discord-reply-username">
                        {referenced_message.author?.displayName || referenced_message.author?.username || 'Unknown'}
                    </span>
                    <span
                        className="discord-reply-content"
                        dangerouslySetInnerHTML={{
                            __html: referenced_message.content
                                ? parseDiscordMarkdown(
                                    (referenced_message.content.length > 100
                                        ? referenced_message.content.slice(0, 100) + '...'
                                        : referenced_message.content
                                    ).replace(/\n/g, ' '),
                                    { channelMap, usersMap, rolesMap, emojiCache }
                                ).replace(/<br\s*\/?>/gi, ' ')
                                : 'Click to see attachment'
                        }}
                    />
                </div>
            )}

            <div
                id={`message-${id}`}
                className={`discord-message ${isFirstInGroup ? 'first-in-group' : ''}`}
                onContextMenu={(e) => onContextMenu?.(e, id, content)}
            >
                {isFirstInGroup ? (
                    <div className="discord-avatar-wrapper">
                        {avatarUrl ? (
                            <img src={avatarUrl} alt="" className="discord-avatar" />
                        ) : (
                            <div className="discord-avatar-placeholder">
                                {getInitial(displayAuthor?.username)}
                            </div>
                        )}
                    </div>
                ) : (
                    <div className="discord-timestamp-gutter">
                        <span className="discord-hover-timestamp">
                            {formatTimestamp(displayTimestamp, 'time')}
                        </span>
                    </div>
                )}

                <div className="discord-message-content">
                    {isFirstInGroup && (
                        <div className="discord-message-header">
                            {/* Tags appear BEFORE the name */}
                            {displayAuthor?.isServerForwarded && (
                                <span className="discord-server-tag">SERVER</span>
                            )}
                            {(displayAuthor?.bot || displayAuthor?.isWebhook) && !displayAuthor?.isServerForwarded && (
                                <span className="discord-bot-tag">APP</span>
                            )}
                            {archivedMessage && (
                                <span className="discord-archive-tag" title={archivedMessage.topic}>
                                    ARCHIVED
                                </span>
                            )}
                            <span
                                className={`discord-username ${(displayAuthor?.bot || displayAuthor?.isWebhook) ? 'discord-bot-user' : ''}`}
                                style={displayAuthor?.color ? { color: toColorString(displayAuthor.color) } : undefined}
                            >
                                {displayAuthor?.displayName || displayAuthor?.username || 'Unknown User'}
                            </span>
                            <span className="discord-timestamp">{formatTimestamp(displayTimestamp)}</span>
                            {isSearchResult && onJumpToMessage && (
                                <button
                                    className="discord-jump-btn"
                                    onClick={(e) => {
                                        e.stopPropagation();
                                        onJumpToMessage(id);
                                    }}
                                    title="Jump to message"
                                >
                                    <svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14">
                                        <path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z" />
                                    </svg>
                                    Jump
                                </button>
                            )}
                        </div>
                    )}

                    {displayContent && (
                        <div
                            className="discord-text"
                            dangerouslySetInnerHTML={{ __html: parsedContent }}
                            onClick={handleContentClick}
                        />
                    )}

                    {/* Attachments */}
                    {attachments?.length > 0 && (
                        <div className="discord-attachments">
                            {attachments.map((att, idx) => (
                                <Attachment key={att.id || idx} attachment={att} />
                            ))}
                        </div>
                    )}

                    {/* Stickers */}
                    {stickers?.length > 0 && (
                        <div className="discord-stickers">
                            {stickers.map((sticker) => (
                                <div key={sticker.id} className="discord-sticker" title={sticker.name}>
                                    {sticker.formatType === 3 && sticker.lottieData ? (
                                        // Lottie stickers with cached data
                                        <LottieSticker data={sticker.lottieData} name={sticker.name} />
                                    ) : sticker.url ? (
                                        <img
                                            src={sticker.url}
                                            alt={sticker.name}
                                            className="discord-sticker-image"
                                            loading="lazy"
                                        />
                                    ) : (
                                        // Fallback for missing sticker data
                                        <div className="discord-sticker-placeholder">
                                            <span>{sticker.name}</span>
                                        </div>
                                    )}
                                </div>
                            ))}
                        </div>
                    )}

                    {/* Poll */}
                    {poll && (
                        <div className={`discord-poll ${poll.isFinalized ? 'finalized' : ''}`}>
                            <div className="discord-poll-header">
                                {poll.question.emoji && (
                                    poll.question.emoji.id ? (
                                        <img
                                            src={emojiCache[poll.question.emoji.id]?.url || `https://cdn.discordapp.com/emojis/${poll.question.emoji.id}.${poll.question.emoji.animated ? 'gif' : 'png'}`}
                                            alt={poll.question.emoji.name}
                                            className="discord-poll-emoji"
                                        />
                                    ) : (
                                        <span className="discord-poll-emoji">{poll.question.emoji.name}</span>
                                    )
                                )}
                                <span className="discord-poll-question">{poll.question.text}</span>
                                {poll.allowMultiselect && (
                                    <span className="discord-poll-multiselect">Select multiple</span>
                                )}
                            </div>
                            <div className="discord-poll-answers">
                                {poll.answers.map((answer) => {
                                    const percentage = (poll.totalVotes ?? 0) > 0
                                        ? Math.round(((answer.voteCount ?? 0) / (poll.totalVotes ?? 1)) * 100)
                                        : 0;
                                    return (
                                        <div key={answer.id ?? answer.answer_id} className="discord-poll-answer" onClick={(e) => onPollVoterClick?.(e, answer)} style={{ cursor: answer.voters?.length ? 'pointer' : 'default' }}>
                                            <div
                                                className="discord-poll-answer-bar"
                                                style={{ width: `${percentage}%` }}
                                            />
                                            <div className="discord-poll-answer-content">
                                                {(answer.emoji ?? answer.poll_media?.emoji) && (
                                                    (answer.emoji ?? answer.poll_media?.emoji)?.id ? (
                                                        <img
                                                            src={emojiCache[(answer.emoji ?? answer.poll_media?.emoji)!.id!]?.url || `https://cdn.discordapp.com/emojis/${(answer.emoji ?? answer.poll_media?.emoji)!.id}.${(answer.emoji ?? answer.poll_media?.emoji)!.animated ? 'gif' : 'png'}`}
                                                            alt={(answer.emoji ?? answer.poll_media?.emoji)?.name}
                                                            className="discord-poll-answer-emoji"
                                                        />
                                                    ) : (
                                                        <span className="discord-poll-answer-emoji">{(answer.emoji ?? answer.poll_media?.emoji)?.name}</span>
                                                    )
                                                )}
                                                <span className="discord-poll-answer-text">{answer.text ?? answer.poll_media?.text}</span>
                                                <span className="discord-poll-answer-votes">
                                                    {answer.voteCount ?? 0} ({percentage}%)
                                                </span>
                                            </div>
                                            {(answer.voters?.length ?? 0) > 0 && (
                                                <div className="discord-poll-voters">
                                                    {answer.voters!.slice(0, 10).map((voter) => (
                                                        <div
                                                            key={voter.id}
                                                            className="discord-poll-voter"
                                                            title={voter.displayName || voter.username}
                                                            aria-label={voter.displayName || voter.username}
                                                        >
                                                            {voter.avatar ? (
                                                                <img src={voter.avatar} alt="" />
                                                            ) : (
                                                                <span>{(voter.displayName || voter.username || 'U')[0].toUpperCase()}</span>
                                                            )}
                                                        </div>
                                                    ))}
                                                    {(answer.voters?.length ?? 0) > 10 && (
                                                        <span className="discord-poll-voters-more">
                                                            +{(answer.voters?.length ?? 0) - 10}
                                                        </span>
                                                    )}
                                                </div>
                                            )}
                                        </div>
                                    );
                                })}
                            </div>
                            <div className="discord-poll-footer">
                                <span className="discord-poll-total">
                                    {poll.totalVotes} {poll.totalVotes === 1 ? 'vote' : 'votes'}
                                </span>
                                {poll.isFinalized && (
                                    <span className="discord-poll-status">Poll ended</span>
                                )}
                                {!poll.isFinalized && poll.expiry && (
                                    <span className="discord-poll-expiry">
                                        Ends {new Date(poll.expiry).toLocaleDateString()}
                                    </span>
                                )}
                            </div>
                        </div>
                    )}

                    {/* Original embeds from Discord */}
                    {embeds?.map((embed, idx) => (
                        <div
                            key={idx}
                            className="discord-embed"
                            style={{ borderColor: embed.color ? `#${embed.color.toString(16).padStart(6, '0')}` : '#5865f2' }}
                        >
                            <div className="discord-embed-content-wrapper">
                                <div className="discord-embed-content">
                                    {embed.author && (
                                        <div className="discord-embed-author">
                                            {embed.author.icon_url && (
                                                <img src={embed.author.icon_url} alt="" className="discord-embed-author-icon" />
                                            )}
                                            <span className="discord-embed-author-name">{embed.author.name}</span>
                                        </div>
                                    )}
                                    {embed.title && (
                                        embed.url ? (
                                            <a
                                                href={embed.url}
                                                target="_blank"
                                                rel="noopener noreferrer"
                                                className="discord-embed-title"
                                            >
                                                {decodeHtmlEntities(embed.title)}
                                            </a>
                                        ) : (
                                            <div className="discord-embed-title">{decodeHtmlEntities(embed.title)}</div>
                                        )
                                    )}
                                    {embed.description && (
                                        <div
                                            className="discord-embed-description"
                                            dangerouslySetInnerHTML={{
                                                __html: parseDiscordMarkdown(embed.description, { channelMap, usersMap, rolesMap, emojiCache })
                                            }}
                                        />
                                    )}
                                    {/* Embed fields */}
                                    {(embed.fields?.length ?? 0) > 0 && (
                                        <div className="discord-embed-fields">
                                            {embed.fields!.map((field, fieldIdx) => (
                                                <div
                                                    key={fieldIdx}
                                                    className={`discord-embed-field ${field.inline ? 'inline' : ''}`}
                                                >
                                                    {field.name && (
                                                        <div className="discord-embed-field-name">{field.name}</div>
                                                    )}
                                                    {field.value && (
                                                        <div
                                                            className="discord-embed-field-value"
                                                            dangerouslySetInnerHTML={{
                                                                __html: parseDiscordMarkdown(field.value, { channelMap, usersMap, rolesMap, emojiCache })
                                                            }}
                                                        />
                                                    )}
                                                </div>
                                            ))}
                                        </div>
                                    )}
                                </div>
                                {embed.thumbnail?.url && embed.thumbnail.url.trim() && (
                                    <img
                                        src={embed.thumbnail.url}
                                        alt=""
                                        className="discord-embed-thumbnail"
                                        onError={(e) => (e.target as HTMLElement).style.display = 'none'}
                                    />
                                )}
                            </div>
                            {embed.image?.url && (
                                <img src={embed.image.url} alt="" className="discord-embed-image" />
                            )}
                            {embed.video?.url && (
                                // Check if it's a YouTube URL - use click-to-play thumbnail
                                embed.video.url.includes('youtube.com/embed/') || embed.video.url.includes('youtu.be') ? (
                                    (() => {
                                        // Extract video ID from embed URL
                                        const videoId = embed.video.url.includes('youtube.com/embed/')
                                            ? embed.video.url.split('/embed/')[1]?.split('?')[0]
                                            : embed.video.url.split('/').pop()?.split('?')[0];
                                        const thumbnailUrl = `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`;
                                        const watchUrl = `https://www.youtube.com/watch?v=${videoId}`;

                                        return (
                                            <a
                                                href={watchUrl}
                                                target="_blank"
                                                rel="noopener noreferrer"
                                                className="discord-embed-video-thumbnail-link"
                                                title="Watch on YouTube"
                                            >
                                                <div className="discord-embed-video-thumbnail">
                                                    <img
                                                        src={thumbnailUrl}
                                                        alt={embed.title || 'YouTube video'}
                                                        className="discord-embed-video-thumbnail-img"
                                                    />
                                                    <div className="discord-embed-video-play-overlay">
                                                        <svg viewBox="0 0 68 48" className="discord-embed-video-play-icon">
                                                            <path d="M66.52,7.74c-0.78-2.93-2.49-5.41-5.42-6.19C55.79,.13,34,0,34,0S12.21,.13,6.9,1.55 C3.97,2.33,2.27,4.81,1.48,7.74C0.06,13.05,0,24,0,24s0.06,10.95,1.48,16.26c0.78,2.93,2.49,5.41,5.42,6.19 C12.21,47.87,34,48,34,48s21.79-0.13,27.1-1.55c2.93-0.78,4.64-3.26,5.42-6.19C67.94,34.95,68,24,68,24S67.94,13.05,66.52,7.74z" fill="#f00"></path>
                                                            <path d="M 45,24 27,14 27,34" fill="#fff"></path>
                                                        </svg>
                                                    </div>
                                                </div>
                                            </a>
                                        );
                                    })()
                                ) : (
                                    <video src={embed.video.url} controls className="discord-embed-video-player" />
                                )
                            )}
                            {embed.footer && (
                                <div className="discord-embed-footer">
                                    {embed.footer.icon_url && (
                                        <img src={embed.footer.icon_url} alt="" className="discord-embed-footer-icon" />
                                    )}
                                    <span>{decodeHtmlEntities(embed.footer.text)}</span>
                                    {embed.timestamp && (
                                        <>
                                            <span className="discord-embed-footer-separator">•</span>
                                            <span>{formatTimestamp(embed.timestamp, 'short')}</span>
                                        </>
                                    )}
                                </div>
                            )}
                        </div>
                    ))}

                    {/* Link previews for URLs in content */}
                    {urlsToPreview.map((url) => (
                        <LinkPreview
                            key={url}
                            url={url}
                            cachedPreview={previewCache?.[url]}
                            onPreviewLoad={onPreviewLoad}
                        />
                    ))}

                    {/* Reactions */}
                    {reactions?.length > 0 && (
                        <div className="discord-reactions">
                            {reactions.map((reaction, idx) => (
                                <button
                                    key={idx}
                                    className={`discord-reaction ${reaction.me ? 'reacted' : ''}`}
                                    onClick={(e) => {
                                        e.stopPropagation();
                                        onReactionClick?.(e, id, reaction);
                                    }}
                                    title="Click to see who reacted"
                                >
                                    {reaction.emoji.id ? (
                                        <img
                                            src={reaction.emoji.url || `https://cdn.discordapp.com/emojis/${reaction.emoji.id}.${reaction.emoji.animated ? 'gif' : 'png'}`}
                                            alt={reaction.emoji.name}
                                            className="discord-reaction-emoji"
                                        />
                                    ) : (
                                        <span>{reaction.emoji.name}</span>
                                    )}
                                    <span>{reaction.count}</span>
                                </button>
                            ))}
                        </div>
                    )}
                </div>
            </div>
        </>
    );
});

// ============================================================================
// Main DiscordLogs Component
// ============================================================================

export default function DiscordLogs() {
    const messagesContainerRef = useRef<HTMLDivElement | null>(null);
    const pendingTargetMessageRef = useRef<string | null>(null); // For deep-linking - used in fetch, cleared after scroll
    const scrollToBottomRef = useRef(false); // Flag to trigger scroll to bottom after initial load
    const [channels, setChannels] = useState<DiscordChannel[]>([]);
    const [channelsByGuild, setChannelsByGuild] = useState<Record<string, DiscordChannel[]>>({});
    const [guilds, setGuilds] = useState<DiscordGuild[]>([]);
    const [selectedGuild, setSelectedGuild] = useState<DiscordGuild | null>(null);
    const [selectedChannel, setSelectedChannel] = useState<DiscordChannel | null>(null);
    const [messages, setMessages] = useState<DiscordMessage[]>([]);
    const [usersMap, setUsersMap] = useState<Record<string, DiscordUser>>({}); // Map of user ID -> { displayName, username, color }
    const [emojiCache, setEmojiCache] = useState<Record<string, DiscordEmoji>>({}); // Map of emoji ID -> { url, animated }
    const [members, setMembers] = useState<MembersData | null>(null); // { groups: [...], roles: [...] }
    const [loadingMembers, setLoadingMembers] = useState(false);
    const [memberListExpanded, setMemberListExpanded] = useState(false); // Default to collapsed
    const [linkCopied, setLinkCopied] = useState(false);
    const [loading, setLoading] = useState(true);
    const [loadingMessages, setLoadingMessages] = useState(false);
    const [loadingMore, setLoadingMore] = useState(false);
    const [hasMoreMessages, setHasMoreMessages] = useState(true);
    const [hasNewerMessages, setHasNewerMessages] = useState(false); // When viewing historical messages
    const [loadingNewer, setLoadingNewer] = useState(false);
    const [error, setError] = useState<string | null>(null);
    const [searchQuery, setSearchQuery] = useState('');
    const [searchResults, setSearchResults] = useState<SearchResult[] | null>(null); // Server-side search results
    const [searchLoading, setSearchLoading] = useState(false);
    const [previewCache, setPreviewCache] = useState<Record<string, LinkPreviewData>>({});
    const [imageModal, setImageModal] = useState<ImageModalState | null>(null); // { url, alt }
    const [reactionPopup, setReactionPopup] = useState<ReactionPopupState | null>(null); // { x, y, emoji, users, loading }
    const [pollVoterPopup, setPollVoterPopup] = useState<PollVoterPopupState | null>(null); // { x, y, answer, voters }
    const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null); // { x, y, messageId, content }
    const [channelContextMenu, setChannelContextMenu] = useState<ChannelContextMenuState | null>(null); // { x, y, channel }
    const [topicExpanded, setTopicExpanded] = useState(false); // Show full channel topic
    const [refetchCounter, setRefetchCounter] = useState(0); // Counter to force re-fetch messages
    const lastPollTimeRef = useRef(new Date().toISOString()); // Track last poll time for edit detection

    // Collapsible categories in sidebar
    const [collapsedCategories, setCollapsedCategories] = useState<Record<string, boolean>>({});
    const handleCategoryToggle = useCallback((catName: string) => {
        setCollapsedCategories(prev => ({ ...prev, [catName]: !prev[catName] }));
    }, []);

    // Debounced server-side search
    useEffect(() => {
        if (!searchQuery.trim() || searchQuery.length < 2 || !selectedChannel) {
            setSearchResults(null);
            return;
        }

        const timeoutId = setTimeout(async () => {
            setSearchLoading(true);
            try {
                const response = await authFetch(
                    `/api/discord/search?channelId=${selectedChannel.id}&q=${encodeURIComponent(searchQuery)}&limit=50`
                );
                if (response.ok) {
                    const data = await response.json();
                    // Normalize the search results
                    const normalizedMessages = (data.messages || []).map(msg => ({
                        ...msg,
                        referenced_message: msg.referencedMessage || msg.referenced_message,
                        attachments: (msg.attachments || []).map(att => ({
                            ...att,
                            content_type: att.contentType || att.content_type,
                        })),
                    }));
                    setSearchResults(normalizedMessages);
                }
            } catch (err) {
                console.error('Search failed:', err);
            } finally {
                setSearchLoading(false);
            }
        }, 300); // 300ms debounce

        return () => clearTimeout(timeoutId);
    }, [searchQuery, selectedChannel]);

    // Open image modal
    const openImageModal = useCallback((imageData) => {
        setImageModal(imageData);
    }, []);

    // Close image modal
    const closeImageModal = useCallback(() => {
        setImageModal(null);
    }, []);

    // Handle reaction click - fetch users who reacted
    const handleReactionClick = useCallback(async (e, messageId, reaction) => {
        const rect = e.target.getBoundingClientRect();
        const x = rect.left + rect.width / 2;
        const y = rect.top;

        // Show loading state
        setReactionPopup({
            x,
            y,
            emoji: reaction.emoji,
            users: [],
            loading: true,
        });

        try {
            const params = new URLSearchParams({
                messageId,
                emojiName: reaction.emoji.name,
            });
            if (reaction.emoji.id) {
                params.append('emojiId', reaction.emoji.id);
            }

            const response = await authFetch(`/api/discord/reaction-users?${params}`);
            if (!response.ok) throw new Error('Failed to fetch');
            const users = await response.json();

            setReactionPopup(prev => prev ? {
                ...prev,
                users,
                loading: false,
            } : null);
        } catch (err) {
            console.error('Failed to fetch reaction users:', err);
            setReactionPopup(null);
        }
    }, []);

    // Close reaction popup
    const closeReactionPopup = useCallback(() => {
        setReactionPopup(null);
    }, []);

    // Handle poll voter click - show voters popup
    const handlePollVoterClick = useCallback((e, answer) => {
        e.stopPropagation(); // Prevent click from bubbling up to handleClickOutside
        if (!answer?.voters?.length) return;
        const rect = e.currentTarget.getBoundingClientRect();
        const x = rect.left + rect.width / 2;
        const y = rect.top;
        setPollVoterPopup({
            x,
            y,
            answer,
            voters: answer.voters,
        });
    }, []);

    // Close poll voter popup
    const closePollVoterPopup = useCallback(() => {
        setPollVoterPopup(null);
    }, []);

    // Handle message context menu (right-click)
    const handleMessageContextMenu = useCallback((e, messageId, content) => {
        e.preventDefault();
        setContextMenu({
            x: e.clientX,
            y: e.clientY,
            messageId,
            content,
        });
    }, []);

    // Copy message link to clipboard
    const copyMessageLink = useCallback(() => {
        if (!contextMenu || !selectedGuild || !selectedChannel) return;
        const url = `${window.location.origin}${window.location.pathname}#${selectedGuild.id}/${selectedChannel.id}/${contextMenu.messageId}`;
        navigator.clipboard.writeText(url);
        setContextMenu(null);
    }, [contextMenu, selectedGuild, selectedChannel]);

    // Copy message content to clipboard
    const copyMessageContent = useCallback(() => {
        if (!contextMenu?.content) return;
        navigator.clipboard.writeText(contextMenu.content);
        setContextMenu(null);
    }, [contextMenu]);

    // Copy message ID to clipboard
    const copyMessageId = useCallback(() => {
        if (!contextMenu?.messageId) return;
        navigator.clipboard.writeText(contextMenu.messageId);
        setContextMenu(null);
    }, [contextMenu]);

    // Helper to scroll to a message element with correction for layout shifts
    const scrollToMessageElement = useCallback((element, highlightDuration = 2000) => {
        if (!element) return;

        // First scroll immediately to get close
        element.scrollIntoView({ behavior: 'instant', block: 'center' });

        // Add highlight effect
        element.classList.add('discord-message-highlight');
        setTimeout(() => element.classList.remove('discord-message-highlight'), highlightDuration);

        // After a delay for images to start loading, scroll again to correct for layout shifts
        setTimeout(() => {
            element.scrollIntoView({ behavior: 'smooth', block: 'center' });
        }, 300);
    }, []);

    // Helper to wait for an element to exist in DOM then scroll to it
    const waitForElementAndScroll = useCallback((messageId, highlightDuration = 2000, maxAttempts = 20) => {
        let attempts = 0;

        const tryScroll = () => {
            const element = document.getElementById(`message-${messageId}`);
            if (element) {
                scrollToMessageElement(element, highlightDuration);
                return true;
            }

            attempts++;
            if (attempts < maxAttempts) {
                requestAnimationFrame(tryScroll);
                return false;
            }
            console.warn(`Could not find message-${messageId} after ${maxAttempts} attempts`);
            return false;
        };

        requestAnimationFrame(tryScroll);
    }, [scrollToMessageElement]);

    // Jump to a specific message (used from search results and poll result view)
    const jumpToMessage = useCallback((messageId) => {
        if (!messageId) return;

        // First check if the message is already loaded
        const existingMessage = messages.find(m => m.id === messageId);
        if (existingMessage) {
            // Message is already loaded, just scroll to it
            const element = document.getElementById(`message-${messageId}`);
            if (element) {
                scrollToMessageElement(element);
                return;
            }
        }

        // Message not loaded, need to fetch around it
        // Clear search to exit search mode
        setSearchQuery('');
        setSearchResults(null);
        // Set the target message ID for the fetch effect
        pendingTargetMessageRef.current = messageId;
        // Trigger a re-fetch by incrementing the counter
        setRefetchCounter(c => c + 1);
        setLoadingMessages(true);
    }, [messages, scrollToMessageElement]);

    // Handle channel context menu (right-click)
    const handleChannelContextMenu = useCallback((e, channel) => {
        e.preventDefault();
        setChannelContextMenu({
            x: e.clientX,
            y: e.clientY,
            channel,
        });
    }, []);

    // Copy channel link to clipboard
    const copyChannelLink = useCallback(() => {
        if (!channelContextMenu?.channel || !selectedGuild) return;
        const url = `${window.location.origin}${window.location.pathname}#${selectedGuild.id}/${channelContextMenu.channel.id}`;
        navigator.clipboard.writeText(url);
        setChannelContextMenu(null);
    }, [channelContextMenu, selectedGuild]);

    // Copy channel ID to clipboard
    const copyChannelId = useCallback(() => {
        if (!channelContextMenu?.channel) return;
        navigator.clipboard.writeText(channelContextMenu.channel.id);
        setChannelContextMenu(null);
    }, [channelContextMenu]);

    // Copy channel name to clipboard
    const copyChannelName = useCallback(() => {
        if (!channelContextMenu?.channel) return;
        navigator.clipboard.writeText(channelContextMenu.channel.name);
        setChannelContextMenu(null);
    }, [channelContextMenu]);

    // Handle escape key to close modal
    useEffect(() => {
        const handleKeyDown = (e) => {
            if (e.key === 'Escape') {
                if (imageModal) closeImageModal();
                if (reactionPopup) closeReactionPopup();
                if (pollVoterPopup) closePollVoterPopup();
                if (contextMenu) setContextMenu(null);
                if (channelContextMenu) setChannelContextMenu(null);
            }
        };
        const handleClickOutside = (e) => {
            if (reactionPopup && !e.target.closest('.discord-reaction-popup')) {
                closeReactionPopup();
            }
            if (pollVoterPopup && !e.target.closest('.discord-reaction-popup')) {
                closePollVoterPopup();
            }
            if (contextMenu && !e.target.closest('.discord-context-menu')) {
                setContextMenu(null);
            }
            if (channelContextMenu && !e.target.closest('.discord-context-menu')) {
                setChannelContextMenu(null);
            }
        };
        window.addEventListener('keydown', handleKeyDown);
        window.addEventListener('click', handleClickOutside);
        return () => {
            window.removeEventListener('keydown', handleKeyDown);
            window.removeEventListener('click', handleClickOutside);
        };
    }, [imageModal, reactionPopup, pollVoterPopup, contextMenu, channelContextMenu, closeImageModal, closeReactionPopup, closePollVoterPopup]);

    // Update URL hash when guild/channel changes
    useEffect(() => {
        if (selectedGuild && selectedChannel) {
            const newHash = `#${selectedGuild.id}/${selectedChannel.id}`;
            if (window.location.hash !== newHash) {
                window.history.replaceState(null, '', newHash);
            }
        }
    }, [selectedGuild, selectedChannel]);

    // Copy shareable link to clipboard
    const copyShareLink = useCallback((messageId = null) => {
        let url = window.location.origin + window.location.pathname;
        if (selectedGuild && selectedChannel) {
            url += `#${selectedGuild.id}/${selectedChannel.id}`;
            if (messageId) {
                url += `/${messageId}`;
            }
        }
        navigator.clipboard.writeText(url).then(() => {
            setLinkCopied(true);
            setTimeout(() => setLinkCopied(false), 2000);
        });
    }, [selectedGuild, selectedChannel]);

    // Create channel lookup map for mentions
    const channelMap = useMemo(() => {
        const map = new Map();
        channels.forEach(channel => {
            map.set(channel.id, {
                id: channel.id,
                name: channel.name,
                guildId: channel.guildId,
                guildName: channel.guildName,
                guildIcon: channel.guildIcon,
            });
        });
        return map;
    }, [channels]);

    // Create roles lookup map for role mentions (defensive)
    const rolesMap = useMemo(() => {
        const map = new Map();
        if (Array.isArray(members?.roles)) {
            members.roles.forEach(role => map.set(role?.id, role));
        }
        return map;
    }, [members?.roles]);

    // Handle channel selection from mentions
    const handleChannelSelect = useCallback((channel) => {
        // Find the full channel object with guild info
        const guildId = channel.guildId || channelMap.get(channel.id)?.guildId;
        if (guildId) {
            const guild = guilds.find(g => g.id === guildId);
            if (guild) {
                setSelectedGuild(guild);
            }
            const channelList = channelsByGuild[guildId];
            const fullChannel = channelList?.find(c => c.id === channel.id);
            if (fullChannel) {
                setSelectedChannel(fullChannel);
            }
        }
    }, [guilds, channelsByGuild, channelMap]);

    // Load channels from API
    useEffect(() => {
        async function fetchChannels() {
            try {
                const response = await authFetch('/api/discord/channels');
                if (!response.ok) throw new Error('Failed to fetch channels');
                const data = await response.json() as DiscordChannel[];

                // Separate categories (type 4), text channels (type 0), and threads (types 10, 11, 12)
                const categories: Record<string, { id: string; name: string; position: number; guildId?: string }> = {};
                const textChannels: DiscordChannel[] = [];
                const threads: DiscordChannel[] = [];

                (data as DiscordChannel[]).forEach((channel: DiscordChannel) => {
                    if (channel.type === 4) {
                        categories[channel.id] = {
                            id: channel.id,
                            name: channel.name,
                            position: channel.position ?? 0,
                            guildId: channel.guildId,
                        };
                    } else if (channel.type === 10 || channel.type === 11 || channel.type === 12) {
                        threads.push(channel);
                    } else {
                        textChannels.push(channel);
                    }
                });

                // Create a map of parent channel ID to threads
                const threadsByParent: Record<string, DiscordChannel[]> = {};
                threads.forEach((thread: DiscordChannel) => {
                    if (thread.parentId) {
                        if (!threadsByParent[thread.parentId]) {
                            threadsByParent[thread.parentId] = [];
                        }
                        threadsByParent[thread.parentId].push(thread);
                    }
                });

                // Group channels by guild
                const byGuild: Record<string, DiscordChannel[]> = {};
                const guildMap: Record<string, DiscordGuild> = {};

                textChannels.forEach((channel: DiscordChannel) => {
                    const guildId = channel.guildId || 'unknown';
                    if (!byGuild[guildId]) {
                        byGuild[guildId] = [];
                        guildMap[guildId] = {
                            id: guildId,
                            name: channel.guildName || 'Discord Archive',
                            icon: channel.guildIcon || null,
                        } as DiscordGuild;
                    }
                    const categoryName = channel.parentId ? categories[channel.parentId]?.name ?? null : null;
                    const categoryPosition = channel.parentId ? categories[channel.parentId]?.position ?? -1 : -1;

                    // Get threads for this channel
                    const channelThreads = threadsByParent[channel.id] || [];

                    byGuild[guildId].push({
                        id: channel.id,
                        name: channel.name,
                        type: channel.type,
                        position: channel.position,
                        topic: channel.topic,
                        parentId: channel.parentId,
                        categoryName,
                        categoryPosition,
                        guild: guildMap[guildId],
                        messageCount: channel.messageCount ?? 0,
                        threads: channelThreads.map(t => ({
                            id: t.id,
                            name: t.name,
                            type: t.type,
                            messageCount: t.messageCount ?? 0,
                            parentId: t.parentId,
                            guildId: t.guildId,
                            guildName: t.guildName,
                            guildIcon: t.guildIcon,
                        })),
                    } as DiscordChannel);
                });

                // Sort guilds - put "no place like ::1" first
                const guildList = (Object.values(guildMap) as DiscordGuild[]).sort((a, b) => {
                    if (a.name === 'no place like ::1') return -1;
                    if (b.name === 'no place like ::1') return 1;
                    return a.name.localeCompare(b.name);
                });

                // Sort channels within each guild by category position, then channel position
                Object.keys(byGuild).forEach(guildId => {
                    byGuild[guildId].sort((a, b) => {
                        // First sort by category position (no category = -1, comes first)
                        if (a.categoryPosition !== b.categoryPosition) {
                            return (a.categoryPosition ?? -1) - (b.categoryPosition ?? -1);
                        }
                        // Then by channel position within category
                        return (a.position ?? 0) - (b.position ?? 0);
                    });
                });

                setGuilds(guildList);
                setChannelsByGuild(byGuild);
                setChannels(data as DiscordChannel[]);

                // Check URL hash for deep linking
                const hash = window.location.hash.slice(1); // Remove #
                const [hashGuildId, hashChannelId, hashMessageId] = hash.split('/');

                let initialGuild: DiscordGuild | null = null;
                let initialChannel: DiscordChannel | null = null;

                // Set target message ID for scrolling after messages load
                if (hashMessageId) {
                    pendingTargetMessageRef.current = hashMessageId;
                }

                // Try to find guild/channel from hash
                if (hashGuildId && guildMap[hashGuildId]) {
                    initialGuild = guildMap[hashGuildId];
                    if (hashChannelId && byGuild[hashGuildId]) {
                        initialChannel = byGuild[hashGuildId].find(c => c.id === hashChannelId && (c.messageCount ?? 0) > 0) ?? null;
                    }
                }

                // Fall back to first guild/channel if hash doesn't match
                if (!initialGuild && guildList.length > 0) {
                    initialGuild = guildList[0];
                }
                if (!initialChannel && initialGuild && byGuild[initialGuild.id]?.length > 0) {
                    // Pick first channel with messages
                    initialChannel = byGuild[initialGuild.id].find(c => c.type !== 4 && (c.messageCount ?? 0) > 0) ?? null;
                }

                if (initialGuild) {
                    setSelectedGuild(initialGuild);
                }
                if (initialChannel) {
                    setSelectedChannel(initialChannel);
                }
            } catch (err) {
                console.error('Failed to fetch channels:', err);
                setError('Failed to load channels');
            } finally {
                setLoading(false);
            }
        }

        // Wait for auth to be ready before fetching
        fetchChannels();
    }, []);

    // Load members when guild or channel changes
    useEffect(() => {
        const guildId = selectedGuild?.id;
        if (!guildId) return;

        async function fetchMembers() {
            setLoadingMembers(true);
            try {
                // Include channelId to filter members by channel visibility
                let url = `/api/discord/members?guildId=${guildId}`;
                const channelId = selectedChannel?.id;
                if (channelId) {
                    url += `&channelId=${channelId}`;
                }
                const response = await authFetch(url);
                if (!response.ok) throw new Error('Failed to fetch members');
                const data = await response.json();
                setMembers(data as MembersData);
            } catch (err) {
                console.error('Failed to fetch members:', err);
                setMembers(null);
            } finally {
                setLoadingMembers(false);
            }
        }

        fetchMembers();
    }, [selectedGuild, selectedChannel]);

    // Load messages from API when channel changes
    useEffect(() => {
        const channelId = selectedChannel?.id;
        if (!channelId) return;

        // Reset topic expanded state when channel changes
        setTopicExpanded(false);

        // Set loading immediately to prevent flash of "no messages" state
        setLoadingMessages(true);

        async function fetchMessages() {
            setMessages([]);
            setUsersMap({});
            setEmojiCache({});
            setHasMoreMessages(true);
            setHasNewerMessages(false); // Loading latest messages

            // Capture target message ID from ref (for deep-linking)
            const targetMessageId = pendingTargetMessageRef.current;

            try {
                // If we have a target message ID, use 'around' to fetch messages centered on it
                let url = `/api/discord/messages?channelId=${channelId}&limit=50`;
                if (targetMessageId) {
                    url += `&around=${targetMessageId}`;
                }

                const response = await authFetch(url);
                if (!response.ok) throw new Error('Failed to fetch messages');
                const data = await response.json();

                // Handle new response format { messages, users, emojiCache }
                const messagesData = data.messages || data;
                const usersData = data.users || {};
                const emojiCacheData = data.emojiCache || {};

                // If we were looking for a specific message but got no results,
                // fall back to loading the latest messages
                if (targetMessageId && messagesData.length === 0) {
                    console.warn(`Target message ${targetMessageId} not found, loading latest messages`);
                    pendingTargetMessageRef.current = null;
                    const fallbackResponse = await authFetch(`/api/discord/messages?channelId=${channelId}&limit=50`);
                    if (fallbackResponse.ok) {
                        const fallbackData = await fallbackResponse.json();
                        const fallbackMessages = fallbackData.messages || fallbackData;
                        const fallbackUsers = fallbackData.users || {};
                        const fallbackEmojis = fallbackData.emojiCache || {};
                        const normalizedFallback = fallbackMessages.map((msg: DiscordMessage) => ({
                            ...msg,
                            referenced_message: (msg as any).referencedMessage || msg.referenced_message,
                            attachments: (msg.attachments || []).map(att => ({
                                ...att,
                                content_type: (att as any).contentType || att.content_type,
                            })),
                        } as DiscordMessage));
                        setMessages(normalizedFallback.reverse());
                        setUsersMap(fallbackUsers);
                        setEmojiCache(fallbackEmojis);
                        setHasMoreMessages(fallbackMessages.length === 50);
                        scrollToBottomRef.current = true;
                        lastPollTimeRef.current = new Date().toISOString();
                        return;
                    }
                }

                // Normalize field names from API to component expectations
                const normalizedMessages = messagesData.map((msg: DiscordMessage) => ({
                    ...msg,
                    referenced_message: (msg as any).referencedMessage || msg.referenced_message,
                    attachments: (msg.attachments || []).map(att => ({
                        ...att,
                        content_type: (att as any).contentType || att.content_type,
                    })),
                } as DiscordMessage));

                // When using 'around', messages come back in ASC order already
                // When using default (no around), they come DESC so reverse them
                const orderedMessages = targetMessageId
                    ? normalizedMessages
                    : normalizedMessages.reverse();

                setMessages(orderedMessages);
                setUsersMap(usersData);
                setEmojiCache(emojiCacheData);
                setHasMoreMessages(messagesData.length === 50);

                // Reset poll time for edit detection
                lastPollTimeRef.current = new Date().toISOString();

                // Set flag to scroll after render (handled by useLayoutEffect)
                if (targetMessageId) {
                    // Keep the target in pendingTargetMessageRef for useLayoutEffect
                } else {
                    scrollToBottomRef.current = true;
                }
            } catch (err) {
                console.error('Failed to fetch messages:', err);
                setMessages([]);
            } finally {
                setLoadingMessages(false);
            }
        }

        fetchMessages();
        // Re-fetch when channel ID changes or refetchCounter is incremented (for jump-to-message)
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [selectedChannel?.id, refetchCounter]);

    // Handle scroll positioning after messages are rendered
    useLayoutEffect(() => {
        if (messages.length === 0) return;

        const container = messagesContainerRef.current;
        if (!container) return;

        // Handle target message (deep-linking)
        const targetMessageId = pendingTargetMessageRef.current;
        if (targetMessageId) {
            pendingTargetMessageRef.current = null;
            waitForElementAndScroll(targetMessageId, 5000); // 3 pulses (1.5s) + 3s fade
            return;
        }

        // Handle scroll to bottom on initial channel load
        if (scrollToBottomRef.current) {
            scrollToBottomRef.current = false;

            // Wait for content to render, then scroll to bottom
            const scrollToBottom = () => {
                container.scrollTop = container.scrollHeight;
            };

            // Scroll immediately
            scrollToBottom();

            // Then scroll again after a delay to correct for any layout shifts
            setTimeout(scrollToBottom, 300);
            setTimeout(scrollToBottom, 600);
        }
    }, [messages, waitForElementAndScroll]);

    // Load more messages (pagination)
    const loadMoreMessages = useCallback(async () => {
        const channelId = selectedChannel?.id;
        if (loadingMore || !hasMoreMessages || messages.length === 0 || !channelId) return;

        setLoadingMore(true);
        try {
            // Get the oldest message ID for pagination (messages are in ASC order, so first = oldest)
            const oldestMessage = messages[0];
            const response = await authFetch(
                `/api/discord/messages?channelId=${channelId}&limit=50&before=${oldestMessage.id}`
            );
            if (!response.ok) throw new Error('Failed to fetch more messages');
            const data = await response.json();

            // Handle new response format { messages, users, emojiCache }
            const messagesData = data.messages || data;
            const usersData = data.users || {};
            const emojiCacheData = data.emojiCache || {};

            const normalizedMessages = messagesData.map((msg: DiscordMessage) => ({
                ...msg,
                referenced_message: (msg as any).referencedMessage || msg.referenced_message,
                attachments: (msg.attachments || []).map(att => ({
                    ...att,
                    content_type: (att as any).contentType || att.content_type,
                })),
            } as DiscordMessage));

            // Prepend older messages (reversed to maintain ASC order)
            setMessages(prev => [...normalizedMessages.reverse(), ...prev]);
            // Merge new users into existing usersMap
            setUsersMap(prev => ({ ...prev, ...usersData }));
            // Merge new emojis into existing emojiCache
            setEmojiCache(prev => ({ ...prev, ...emojiCacheData }));
            setHasMoreMessages(messagesData.length === 50);
        } catch (err) {
            console.error('Failed to load more messages:', err);
        } finally {
            setLoadingMore(false);
        }
    }, [loadingMore, hasMoreMessages, messages, selectedChannel?.id]);

    // Jump to first message in channel
    const jumpToFirstMessage = useCallback(async () => {
        if (!selectedChannel || loadingMessages) return;

        setLoadingMessages(true);
        setMessages([]);

        try {
            // Fetch oldest messages using oldest=true parameter
            const response = await authFetch(
                `/api/discord/messages?channelId=${selectedChannel.id}&limit=50&oldest=true`
            );
            if (!response.ok) throw new Error('Failed to fetch first messages');
            const data = await response.json();

            const messagesData = data.messages || data;
            const usersData = data.users || {};
            const emojiCacheData = data.emojiCache || {};

            const normalizedMessages = messagesData.map(msg => ({
                ...msg,
                referenced_message: msg.referencedMessage || msg.referenced_message,
                attachments: (msg.attachments || []).map(att => ({
                    ...att,
                    content_type: att.contentType || att.content_type,
                })),
            }));

            setMessages(normalizedMessages);
            setUsersMap(usersData);
            setEmojiCache(emojiCacheData);
            setHasMoreMessages(messagesData.length === 50);
            setHasNewerMessages(true); // We're viewing oldest, so there are newer messages

            // Scroll to top after messages load
            requestAnimationFrame(() => {
                if (messagesContainerRef.current) {
                    messagesContainerRef.current.scrollTop = 0;
                }
            });
        } catch (err) {
            console.error('Failed to jump to first message:', err);
        } finally {
            setLoadingMessages(false);
        }
    }, [selectedChannel, loadingMessages]);

    // Load newer messages (when viewing historical/oldest messages)
    const loadNewerMessages = useCallback(async () => {
        const channelId = selectedChannel?.id;
        if (loadingNewer || !hasNewerMessages || messages.length === 0 || !channelId) return;

        setLoadingNewer(true);
        try {
            // Get the newest message ID currently loaded
            const newestMessage = messages[messages.length - 1];
            const response = await authFetch(
                `/api/discord/messages?channelId=${channelId}&limit=50&after=${newestMessage.id}`
            );
            if (!response.ok) throw new Error('Failed to fetch newer messages');
            const data = await response.json();

            const messagesData = data.messages || data;
            const usersData = data.users || {};
            const emojiCacheData = data.emojiCache || {};

            const normalizedMessages = messagesData.map((msg: DiscordMessage) => ({
                ...msg,
                referenced_message: (msg as any).referencedMessage || msg.referenced_message,
                attachments: (msg.attachments || []).map(att => ({
                    ...att,
                    content_type: (att as any).contentType || att.content_type,
                })),
            } as DiscordMessage));

            // Append newer messages
            setMessages(prev => [...prev, ...normalizedMessages]);
            setUsersMap(prev => ({ ...prev, ...usersData }));
            setEmojiCache(prev => ({ ...prev, ...emojiCacheData }));
            // If we got less than 50, we've reached the end (no more newer messages)
            if (messagesData.length < 50) {
                setHasNewerMessages(false);
            }
        } catch (err) {
            console.error('Failed to load newer messages:', err);
        } finally {
            setLoadingNewer(false);
        }
    }, [loadingNewer, hasNewerMessages, messages, selectedChannel?.id]);

    // Infinite scroll: load more when scrolling near the top or bottom
    useEffect(() => {
        const container = messagesContainerRef.current;
        if (!container) return;

        const handleScroll = () => {
            // Don't load more when viewing search results
            if (searchQuery.trim().length >= 2 && searchResults !== null) return;

            // Load older messages when within 200px of the top
            if (container.scrollTop < 200 && hasMoreMessages && !loadingMore) {
                // Save scroll position before loading
                const scrollHeightBefore = container.scrollHeight;
                loadMoreMessages().then(() => {
                    // Restore scroll position after new messages are prepended
                    requestAnimationFrame(() => {
                        const scrollHeightAfter = container.scrollHeight;
                        container.scrollTop = scrollHeightAfter - scrollHeightBefore;
                    });
                });
            }

            // Load newer messages when within 200px of the bottom (when viewing historical)
            const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
            if (distanceFromBottom < 200 && hasNewerMessages && !loadingNewer) {
                loadNewerMessages();
            }
        };

        container.addEventListener('scroll', handleScroll);
        return () => container.removeEventListener('scroll', handleScroll);
    }, [hasMoreMessages, loadingMore, loadMoreMessages, hasNewerMessages, loadingNewer, loadNewerMessages, searchQuery, searchResults]);

    // Poll for new messages and edits every 5 seconds
    useEffect(() => {
        if (!selectedChannel || loadingMessages || messages.length === 0) return;
        // Don't poll when viewing search results - it would add messages that aren't in search
        if (searchQuery.trim().length >= 2 && searchResults !== null) return;
        // Don't poll when viewing historical messages (jumped to first)
        if (hasNewerMessages) return;

        const pollInterval = setInterval(async () => {
            try {
                const pollStartTime = new Date().toISOString();

                // Get the newest message ID (messages are in ASC order, so last = newest)
                const newestMessage = messages[messages.length - 1];

                // Fetch new messages after newest
                const newMsgsResponse = await authFetch(
                    `/api/discord/messages?channelId=${selectedChannel.id}&limit=50&after=${newestMessage.id}`
                );

                let newMessages: DiscordMessage[] = [];
                let newUsersData: Record<string, DiscordUser> = {};
                let newEmojiCacheData: Record<string, DiscordEmoji> = {};

                if (newMsgsResponse.ok) {
                    const data = await newMsgsResponse.json();
                    const messagesData = data.messages || data;
                    newUsersData = data.users || {};
                    newEmojiCacheData = data.emojiCache || {};

                    if (messagesData.length > 0) {
                        newMessages = messagesData.map((msg: DiscordMessage) => ({
                            ...msg,
                            referenced_message: (msg as any).referencedMessage || msg.referenced_message,
                            attachments: (msg.attachments || []).map(att => ({
                                ...att,
                                content_type: (att as any).contentType || att.content_type,
                            })),
                        } as DiscordMessage));
                    }
                }

                // Fetch messages edited since last poll
                const editedResponse = await authFetch(
                    `/api/discord/messages?channelId=${selectedChannel.id}&limit=100&editedSince=${encodeURIComponent(lastPollTimeRef.current)}`
                );

                let editedMessages: DiscordMessage[] = [];
                if (editedResponse.ok) {
                    const editedData = await editedResponse.json();
                    const editedMessagesData = editedData.messages || editedData;
                    const editedUsersData = editedData.users || {};
                    const editedEmojiCacheData = editedData.emojiCache || {};
                    newUsersData = { ...newUsersData, ...editedUsersData };
                    newEmojiCacheData = { ...newEmojiCacheData, ...editedEmojiCacheData };

                    editedMessages = editedMessagesData.map((msg: DiscordMessage) => ({
                        ...msg,
                        referenced_message: (msg as any).referencedMessage || msg.referenced_message,
                        attachments: (msg.attachments || []).map(att => ({
                            ...att,
                            content_type: (att as any).contentType || att.content_type,
                        })),
                    } as DiscordMessage));
                }

                // Update last poll time for next iteration
                lastPollTimeRef.current = pollStartTime;

                // Check if user is scrolled near bottom before adding new messages
                const container = messagesContainerRef.current;
                const isNearBottom = container &&
                    (container.scrollHeight - container.scrollTop - container.clientHeight < 100);

                // Only update state if there are changes
                if (newMessages.length > 0 || editedMessages.length > 0) {
                    setMessages(prev => {
                        // Create a map of existing messages by ID
                        const msgMap = new Map<string, DiscordMessage>(prev.map(m => [m.id, m]));

                        // Update with edited messages (overwrite existing)
                        for (const msg of editedMessages) {
                            if (msgMap.has(msg.id)) {
                                msgMap.set(msg.id, msg);
                            }
                        }

                        // Append new messages
                        for (const msg of newMessages) {
                            if (!msgMap.has(msg.id)) {
                                msgMap.set(msg.id, msg);
                            }
                        }

                        // Convert back to array and sort by ID (snowflake = timestamp order)
                        return Array.from(msgMap.values()).sort((a, b) => {
                            if (a.id < b.id) return -1;
                            if (a.id > b.id) return 1;
                            return 0;
                        });
                    });

                    setUsersMap(prev => ({ ...prev, ...newUsersData }));
                    setEmojiCache(prev => ({ ...prev, ...newEmojiCacheData }));
                }

                // Auto-scroll to bottom if user was already near bottom and there are new messages
                if (isNearBottom && container && newMessages.length > 0) {
                    setTimeout(() => {
                        container.scrollTop = container.scrollHeight;
                    }, 50);
                }
            } catch (err) {
                console.error('Polling failed:', err);
            }
        }, 5000);

        return () => clearInterval(pollInterval);
    }, [selectedChannel, loadingMessages, messages, searchQuery, searchResults, hasNewerMessages]);    // Poll for channel/guild updates every 5 seconds
    useEffect(() => {
        if (loading) return; // Don't poll during initial load

        const pollInterval = setInterval(async () => {
            try {
                const response = await authFetch('/api/discord/channels');
                if (!response.ok) return;
                const data = await response.json() as DiscordChannel[];

                // Separate categories (type 4), text channels (type 0), and threads (types 10, 11, 12)
                const categories: Record<string, { id: string; name: string; position?: number; guildId?: string }> = {};
                const textChannels: DiscordChannel[] = [];
                const threads: DiscordChannel[] = [];

                data.forEach((channel: DiscordChannel) => {
                    if (channel.type === 4) {
                        categories[channel.id] = {
                            id: channel.id,
                            name: channel.name,
                            position: channel.position,
                            guildId: channel.guildId,
                        };
                    } else if (channel.type === 10 || channel.type === 11 || channel.type === 12) {
                        threads.push(channel);
                    } else {
                        textChannels.push(channel);
                    }
                });

                // Create a map of parent channel ID to threads
                const threadsByParent: Record<string, DiscordChannel[]> = {};
                threads.forEach((thread: DiscordChannel) => {
                    if (thread.parentId) {
                        if (!threadsByParent[thread.parentId]) {
                            threadsByParent[thread.parentId] = [];
                        }
                        threadsByParent[thread.parentId].push(thread);
                    }
                });

                // Rebuild guild/channel maps
                const byGuild: Record<string, DiscordChannel[]> = {};
                const guildMap: Record<string, DiscordGuild> = {};

                textChannels.forEach((channel: DiscordChannel) => {
                    const guildId = channel.guildId || 'unknown';
                    if (!byGuild[guildId]) {
                        byGuild[guildId] = [];
                        guildMap[guildId] = {
                            id: guildId,
                            name: channel.guildName || 'Discord Archive',
                            icon: channel.guildIcon || null,
                        };
                    }
                    const categoryName = channel.parentId ? categories[channel.parentId]?.name : null;
                    const categoryPosition = channel.parentId ? categories[channel.parentId]?.position : -1;

                    // Get threads for this channel
                    const channelThreads = threadsByParent[channel.id] || [];

                    byGuild[guildId].push({
                        id: channel.id,
                        name: channel.name,
                        type: channel.type,
                        position: channel.position,
                        topic: channel.topic,
                        parentId: channel.parentId,
                        categoryName,
                        categoryPosition,
                        guild: guildMap[guildId],
                        messageCount: channel.messageCount || 0,
                        threads: channelThreads.map(t => ({
                            id: t.id,
                            name: t.name,
                            type: t.type,
                            messageCount: t.messageCount || 0,
                            parentId: t.parentId,
                            guildId: t.guildId,
                            guildName: t.guildName,
                            guildIcon: t.guildIcon,
                        })),
                    });
                });

                // Sort guilds
                const guildList: DiscordGuild[] = Object.values(guildMap).sort((a: DiscordGuild, b: DiscordGuild) => {
                    if (a.name === 'no place like ::1') return -1;
                    if (b.name === 'no place like ::1') return 1;
                    return a.name.localeCompare(b.name);
                });

                // Sort channels within each guild
                Object.keys(byGuild).forEach(guildId => {
                    byGuild[guildId].sort((a: DiscordChannel, b: DiscordChannel) => {
                        if ((a.categoryPosition ?? -1) !== (b.categoryPosition ?? -1)) {
                            return (a.categoryPosition ?? -1) - (b.categoryPosition ?? -1);
                        }
                        return (a.position || 0) - (b.position || 0);
                    });
                });

                setGuilds(guildList);
                setChannelsByGuild(byGuild);
                setChannels(data);
                // Note: We don't update selectedChannel here to avoid triggering message reload
                // The channel list will show updated message counts from channelsByGuild
            } catch (err) {
                console.error('Channel polling failed:', err);
            }
        }, 5000);

        return () => clearInterval(pollInterval);
    }, [loading]);

    // Poll for member updates every 5 seconds
    useEffect(() => {
        if (!selectedGuild) return;

        const pollInterval = setInterval(async () => {
            try {
                // Include channelId to filter members by channel visibility
                let url = `/api/discord/members?guildId=${selectedGuild.id}`;
                if (selectedChannel?.id) {
                    url += `&channelId=${selectedChannel.id}`;
                }
                const response = await authFetch(url);
                if (!response.ok) return;
                const data = await response.json();
                setMembers(data);
            } catch (err) {
                console.error('Member polling failed:', err);
            }
        }, 5000);

        return () => clearInterval(pollInterval);
    }, [selectedGuild?.id, selectedChannel?.id]);

    // Handle preview cache updates
    const handlePreviewLoad = useCallback((url, preview) => {
        setPreviewCache(prev => ({ ...prev, [url]: preview }));
    }, []);

    // Filter messages by search - use server results if available, otherwise filter locally
    const filteredMessages = useMemo(() => {
        // If we have server-side search results, use those
        if (searchQuery.trim().length >= 2 && searchResults !== null) {
            return searchResults;
        }

        // No search query - return all messages
        if (!searchQuery.trim()) return messages;

        // Local filtering for short queries or while waiting for server results
        const query = searchQuery.toLowerCase();
        return messages.filter(msg => {
            // Check message content
            if (msg.content?.toLowerCase().includes(query)) return true;

            // Check author username/displayName
            if (msg.author?.username?.toLowerCase().includes(query)) return true;
            if (msg.author?.displayName?.toLowerCase().includes(query)) return true;

            // Check embed content
            const embeds = msg.embeds ?? [];
            if (embeds.length > 0) {
                for (const embed of embeds) {
                    if (embed.title?.toLowerCase().includes(query)) return true;
                    if (embed.description?.toLowerCase().includes(query)) return true;
                    if (embed.author?.name?.toLowerCase().includes(query)) return true;
                    if (embed.footer?.text?.toLowerCase().includes(query)) return true;
                    // Check embed fields
                    const fields = embed.fields ?? [];
                    if (fields.length > 0) {
                        for (const field of fields) {
                            if (field.name?.toLowerCase().includes(query)) return true;
                            if (field.value?.toLowerCase().includes(query)) return true;
                        }
                    }
                }
            }

            return false;
        });
    }, [messages, searchQuery, searchResults]);

    // Check if current channel is dds-archive
    const isArchiveChannel = selectedChannel?.name === 'dds-archive';

    // Check if current channel is a thread (types 10, 11, 12)
    const isThread = selectedChannel?.type === 10 || selectedChannel?.type === 11 || selectedChannel?.type === 12;

    // Check if Havoc bot is in the channel's member list
    const HAVOC_BOT_ID = '1219636064608583770';
    const isHavocInChannel = useMemo(() => {
        // Threads inherit access from parent channel - if we can see the thread, Havoc has access
        if (isThread) return true;
        if (!members?.groups) return true; // Assume present if members not loaded
        for (const group of members.groups) {
            if (group.members?.some(m => m.id === HAVOC_BOT_ID)) {
                return true;
            }
        }
        return false;
    }, [members, isThread]);

    // Group messages by author and time window (5 minutes)
    // Messages are in ASC order (oldest first, newest at bottom), so we group accordingly
    // For archive channel, parse messages to get real author/timestamp for grouping
interface MessageGroupDivider {
    type: 'divider';
    date: string | null;
}

interface MessageGroupMessages {
    type: 'messages';
    author: DiscordUser;
    effectiveAuthor: DiscordUser;
    messages: Array<DiscordMessage & { effectiveTimestamp: string | null }>;
}

type MessageGroup = MessageGroupDivider | MessageGroupMessages;

    const groupedMessages = useMemo(() => {
        const groups: MessageGroup[] = [];
        let currentGroup: MessageGroupMessages | null = null;
        let lastDate: string | null = null;

        filteredMessages.forEach((message) => {
            // For archive channel, parse to get real author and timestamp
            let effectiveAuthor = message.author;
            let effectiveTimestamp: string | null = message.timestamp ?? null;

            if (isArchiveChannel) {
                const parsed = parseArchivedMessage(message.content);
                if (parsed) {
                    // Use resolved user for proper grouping by real user ID
                    const resolvedUser = resolveArchivedUser(parsed.originalAuthor, usersMap, members);
                    effectiveAuthor = resolvedUser;
                    effectiveTimestamp = parsed.originalTimestamp ?? null;
                }
            }

            const effectiveTs = effectiveTimestamp ?? message.timestamp ?? null;
            const messageDate = effectiveTs ? new Date(effectiveTs).toDateString() : lastDate;

            // Check if we need a date divider (date changed from previous message)
            if (messageDate !== lastDate) {
                if (currentGroup) groups.push(currentGroup);
                groups.push({ type: 'divider', date: effectiveTs });
                currentGroup = null;
                lastDate = messageDate;
            }

            const lastMessage = currentGroup?.messages[currentGroup.messages.length - 1];
            const lastTimestamp = lastMessage?.effectiveTimestamp ?? lastMessage?.timestamp ?? null;
            const timeGapExceeded = lastTimestamp && effectiveTs
                ? Math.abs(new Date(lastTimestamp).getTime() - new Date(effectiveTs).getTime()) > 5 * 60 * 1000
                : true;

            // For ASC order: check time diff from the LAST message in current group
            // (which is the most recent since we're iterating oldest to newest)
            const shouldStartNewGroup = !currentGroup ||
                currentGroup.effectiveAuthor?.id !== effectiveAuthor?.id ||
                timeGapExceeded;

            const messageWithEffective = { ...message, effectiveTimestamp: effectiveTs } as DiscordMessage & { effectiveTimestamp: string | null };

            if (shouldStartNewGroup) {
                if (currentGroup) groups.push(currentGroup);
                currentGroup = {
                    type: 'messages',
                    author: effectiveAuthor,
                    effectiveAuthor,
                    messages: [messageWithEffective],
                };
            } else if (currentGroup) {
                currentGroup.messages.push(messageWithEffective);
            }
        });

        if (currentGroup) groups.push(currentGroup);
        return groups;
    }, [filteredMessages, isArchiveChannel, usersMap, members]);

    const memberGroups = members?.groups ?? [];

    if (loading) {
        return (
            <div className="discord-logs-container">
                <div className="discord-loading">
                    <ProgressSpinner style={{ width: '40px', height: '40px' }} strokeWidth="4" />
                    <span className="discord-loading-text">Loading Discord archive...</span>
                </div>
            </div>
        );
    }

    if (error) {
        return (
            <div className="discord-logs-container">
                <div className="discord-empty">
                    <svg viewBox="0 0 24 24" fill="currentColor">
                        <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" />
                    </svg>
                    <h3 className="discord-empty-title">Error</h3>
                    <p className="discord-empty-description">{error}</p>
                </div>
            </div>
        );
    }

    return (
        <ImageModalContext.Provider value={openImageModal}>
            <div className="discord-logs-container">
                {/* Header */}
                {selectedChannel && (
                    <div className="discord-header">
                        {selectedChannel.guild?.icon && (
                            <img
                                src={selectedChannel.guild.icon}
                                alt=""
                                className="discord-server-icon"
                            />
                        )}
                        <div className="discord-header-info">
                            <h2 className="discord-server-name">
                                {selectedChannel.guild?.name || 'Discord Archive'}
                            </h2>
                            <div className="discord-channel-name">
                                {/* Show thread icon for thread types (10, 11, 12), otherwise channel hash */}
                                {selectedChannel.type === 10 || selectedChannel.type === 11 || selectedChannel.type === 12 ? (
                                    <svg viewBox="0 0 24 24" fill="currentColor">
                                        <path d="M12 2.81a.75.75 0 0 0-1.5 0v1.5a.75.75 0 0 0 1.5 0v-1.5Zm3.78 2.16a.75.75 0 0 0-1.06-1.06l-1.06 1.06a.75.75 0 0 0 1.06 1.06l1.06-1.06Zm-7.56 0a.75.75 0 0 0-1.06 1.06l1.06 1.06a.75.75 0 0 0 1.06-1.06L8.22 4.97ZM12 7.75a4.25 4.25 0 1 0 0 8.5 4.25 4.25 0 0 0 0-8.5ZM6.25 12a5.75 5.75 0 1 1 11.5 0 5.75 5.75 0 0 1-11.5 0Zm-3.5-.75a.75.75 0 0 0 0 1.5h1.5a.75.75 0 0 0 0-1.5h-1.5Zm16 0a.75.75 0 0 0 0 1.5h1.5a.75.75 0 0 0 0-1.5h-1.5Zm-10.53 5.28a.75.75 0 0 0-1.06 1.06l1.06 1.06a.75.75 0 1 0 1.06-1.06l-1.06-1.06Zm8.56 1.06a.75.75 0 0 0-1.06-1.06l-1.06 1.06a.75.75 0 0 0 1.06 1.06l1.06-1.06ZM12 18.94a.75.75 0 0 0-1.5 0v1.5a.75.75 0 0 0 1.5 0v-1.5Z" />
                                    </svg>
                                ) : (
                                    <svg viewBox="0 0 24 24" fill="currentColor">
                                        <path d="M5.88657 21C5.57547 21 5.3399 20.7189 5.39427 20.4126L6.00001 17H2.59511C2.28449 17 2.04905 16.7198 2.10259 16.4138L2.27759 15.4138C2.31946 15.1746 2.52722 15 2.77011 15H6.35001L7.41001 9H4.00511C3.69449 9 3.45905 8.71977 3.51259 8.41381L3.68759 7.41381C3.72946 7.17456 3.93722 7 4.18011 7H7.76001L8.39677 3.41262C8.43914 3.17391 8.64664 3 8.88907 3H9.87344C10.1845 3 10.4201 3.28107 10.3657 3.58738L9.76001 7H15.76L16.3968 3.41262C16.4391 3.17391 16.6466 3 16.8891 3H17.8734C18.1845 3 18.4201 3.28107 18.3657 3.58738L17.76 7H21.1649C21.4755 7 21.711 7.28023 21.6574 7.58619L21.4824 8.58619C21.4406 8.82544 21.2328 9 20.9899 9H17.41L16.35 15H19.7549C20.0655 15 20.301 15.2802 20.2474 15.5862L20.0724 16.5862C20.0306 16.8254 19.8228 17 19.5799 17H16L15.3632 20.5874C15.3209 20.8261 15.1134 21 14.8709 21H13.8866C13.5755 21 13.3399 20.7189 13.3943 20.4126L14 17H8.00001L7.36325 20.5874C7.32088 20.8261 7.11337 21 6.87094 21H5.88657ZM9.41045 9L8.35045 15H14.3504L15.4104 9H9.41045Z" />
                                    </svg>
                                )}
                                {selectedChannel.name}
                                {selectedChannel.topic && (
                                    <>
                                        <span className="discord-topic-divider">|</span>
                                        <span
                                            className={`discord-channel-topic ${topicExpanded ? 'expanded' : ''}`}
                                            onClick={() => setTopicExpanded(!topicExpanded)}
                                            title={topicExpanded ? 'Click to collapse' : 'Click to expand'}
                                            dangerouslySetInnerHTML={{
                                                __html: parseDiscordMarkdown(selectedChannel.topic, { channelMap, usersMap, rolesMap, emojiCache })
                                            }}
                                        />
                                    </>
                                )}
                            </div>
                        </div>
                        <div className="discord-header-actions">
                            <span className="discord-message-count">
                                {selectedChannel.messageCount?.toLocaleString() || messages.length} messages
                            </span>
                            <button
                                className={`discord-copy-link-btn ${linkCopied ? 'copied' : ''}`}
                                onClick={() => copyShareLink()}
                                title={linkCopied ? 'Copied!' : 'Copy link to channel'}
                            >
                                {linkCopied ? (
                                    <svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
                                        <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
                                    </svg>
                                ) : (
                                    <svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
                                        <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z" />
                                    </svg>
                                )}
                            </button>
                            <button
                                className="discord-jump-first-btn"
                                onClick={jumpToFirstMessage}
                                title="Jump to first message"
                                disabled={loadingMessages}
                            >
                                <svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
                                    <path d="M18.41 16.59L13.82 12l4.59-4.59L17 6l-6 6 6 6zM6 6h2v12H6z" />
                                </svg>
                            </button>
                            <button
                                className={`discord-member-toggle-btn ${memberListExpanded ? 'active' : ''}`}
                                onClick={() => setMemberListExpanded(!memberListExpanded)}
                                title={memberListExpanded ? 'Hide member list' : 'Show member list'}
                            >
                                <svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
                                    <path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z" />
                                </svg>
                                {members?.totalMembers && (
                                    <span className="discord-member-count">{members.totalMembers}</span>
                                )}
                            </button>
                        </div>
                    </div>
                )}

                {/* Main layout: sidebar + content */}
                <div className="discord-main-layout">
                    {/* Sidebar with Guild and Channel selector */}
                    <div className="discord-sidebar">
                        {/* Guild tabs - compact vertical strip */}
                        {guilds.length > 1 && (
                            <div className="discord-guild-list">
                                {guilds.map((guild) => (
                                    <button
                                        key={guild.id}
                                        className={`discord-guild-btn ${selectedGuild?.id === guild.id ? 'active' : ''}`}
                                        onClick={() => {
                                            setSelectedGuild(guild);
                                            const guildChannels = channelsByGuild[guild.id];
                                            // Select first channel with messages
                                            const firstAccessible = guildChannels?.find(c => (c.messageCount ?? 0) > 0);
                                            if (firstAccessible) {
                                                setSelectedChannel(firstAccessible);
                                            }
                                        }}
                                        title={guild.name}
                                    >
                                        {guild.icon ? (
                                            <img
                                                src={guild.icon}
                                                alt={guild.name}
                                                className="discord-guild-icon"
                                            />
                                        ) : (
                                            <span className="discord-guild-initial">
                                                {guild.name.charAt(0).toUpperCase()}
                                            </span>
                                        )}
                                    </button>
                                ))}
                            </div>
                        )}

                        {/* Channel list for selected guild */}
                        <div className="discord-sidebar-channels">
                            {selectedGuild && channelsByGuild[selectedGuild.id]?.length > 0 && (
                                <div className="discord-channel-list" data-lenis-prevent>
                                    <div className="discord-channel-list-header">
                                        {selectedGuild.name}
                                    </div>
                                    {/* Collapsible categories state and handler at top level */}
                                    {/* ...existing code... */}
                                    {(() => {
                                        let lastCategory: string | null | undefined;
                                        // Filter out channels with no messages (no access), but include if they have threads with messages
                                        const accessibleChannels = channelsByGuild[selectedGuild.id].filter(c =>
                                            (c.messageCount ?? 0) > 0 || c.threads?.some(t => (t.messageCount ?? 0) > 0)
                                        );
                                        return accessibleChannels.map((channel) => {
                                            const catName = channel.categoryName ?? null;
                                            const showCategoryHeader = catName !== lastCategory;
                                            lastCategory = catName;
                                            // Filter threads that have messages
                                            const accessibleThreads = channel.threads?.filter(t => (t.messageCount ?? 0) > 0) || [];
                                            const isCollapsed = catName ? collapsedCategories[catName] : false;
                                            return (
                                                <React.Fragment key={channel.id}>
                                                    {showCategoryHeader && catName && (
                                                        <div className="discord-category-header" onClick={() => handleCategoryToggle(catName)} style={{ cursor: 'pointer', userSelect: 'none' }}>
                                                            <svg viewBox="0 0 24 24" fill="currentColor" width="12" height="12" style={{ transform: isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)', transition: 'transform 0.15s' }}>
                                                                <path d="M5.3 9.3a1 1 0 0 1 1.4 0l5.3 5.29 5.3-5.3a1 1 0 1 1 1.4 1.42l-6 6a1 1 0 0 1-1.4 0l-6-6a1 1 0 0 1 0-1.42Z" />
                                                            </svg>
                                                            {catName}
                                                        </div>
                                                    )}
                                                    {!isCollapsed && (channel.messageCount ?? 0) > 0 && (
                                                        <button
                                                            className={`discord-channel-btn ${channel.categoryName ? 'has-category' : ''} ${selectedChannel?.id === channel.id ? 'active' : ''}`}
                                                            onClick={() => setSelectedChannel(channel)}
                                                            onContextMenu={(e) => handleChannelContextMenu(e, channel)}
                                                            title={channel.name}
                                                        >
                                                            <svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
                                                                <path d="M5.88657 21C5.57547 21 5.3399 20.7189 5.39427 20.4126L6.00001 17H2.59511C2.28449 17 2.04905 16.7198 2.10259 16.4138L2.27759 15.4138C2.31946 15.1746 2.52722 15 2.77011 15H6.35001L7.41001 9H4.00511C3.69449 9 3.45905 8.71977 3.51259 8.41381L3.68759 7.41381C3.72946 7.17456 3.93722 7 4.18011 7H7.76001L8.39677 3.41262C8.43914 3.17391 8.64664 3 8.88907 3H9.87344C10.1845 3 10.4201 3.28107 10.3657 3.58738L9.76001 7H15.76L16.3968 3.41262C16.4391 3.17391 16.6466 3 16.8891 3H17.8734C18.1845 3 18.4201 3.28107 18.3657 3.58738L17.76 7H21.1649C21.4755 7 21.711 7.28023 21.6574 7.58619L21.4824 8.58619C21.4406 8.82544 21.2328 9 20.9899 9H17.41L16.35 15H19.7549C20.0655 15 20.301 15.2802 20.2474 15.5862L20.0724 16.5862C20.0306 16.8254 19.8228 17 19.5799 17H16L15.3632 20.5874C15.3209 20.8261 15.1134 21 14.8709 21H13.8866C13.5755 21 13.3399 20.7189 13.3943 20.4126L14 17H8.00001L7.36325 20.5874C7.32088 20.8261 7.11337 21 6.87094 21H5.88657ZM9.41045 9L8.35045 15H14.3504L15.4104 9H9.41045Z" />
                                                            </svg>
                                                            <span>{channel.name}</span>
                                                        </button>
                                                    )}
                                                    {/* Threads under this channel */}
                                                    {!isCollapsed && accessibleThreads.map((thread) => (
                                                        <button
                                                            key={thread.id}
                                                            className={`discord-channel-btn discord-thread-btn ${channel.categoryName ? 'has-category' : ''} ${selectedChannel?.id === thread.id ? 'active' : ''}`}
                                                            onClick={() => setSelectedChannel({
                                                                ...thread,
                                                                categoryName: channel.categoryName,
                                                                categoryPosition: channel.categoryPosition,
                                                                guild: channel.guild,
                                                            })}
                                                            onContextMenu={(e) => handleChannelContextMenu(e, thread)}
                                                            title={thread.name}
                                                        >
                                                            <svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
                                                                <path d="M12 2.81a.75.75 0 0 0-1.5 0v1.5a.75.75 0 0 0 1.5 0v-1.5Zm3.78 2.16a.75.75 0 0 0-1.06-1.06l-1.06 1.06a.75.75 0 0 0 1.06 1.06l1.06-1.06Zm-7.56 0a.75.75 0 0 0-1.06 1.06l1.06 1.06a.75.75 0 0 0 1.06-1.06L8.22 4.97ZM12 7.75a4.25 4.25 0 1 0 0 8.5 4.25 4.25 0 0 0 0-8.5ZM6.25 12a5.75 5.75 0 1 1 11.5 0 5.75 5.75 0 0 1-11.5 0Zm-3.5-.75a.75.75 0 0 0 0 1.5h1.5a.75.75 0 0 0 0-1.5h-1.5Zm16 0a.75.75 0 0 0 0 1.5h1.5a.75.75 0 0 0 0-1.5h-1.5Zm-10.53 5.28a.75.75 0 0 0-1.06 1.06l1.06 1.06a.75.75 0 1 0 1.06-1.06l-1.06-1.06Zm8.56 1.06a.75.75 0 0 0-1.06-1.06l-1.06 1.06a.75.75 0 0 0 1.06 1.06l1.06-1.06ZM12 18.94a.75.75 0 0 0-1.5 0v1.5a.75.75 0 0 0 1.5 0v-1.5Z" />
                                                            </svg>
                                                            <span>{thread.name}</span>
                                                        </button>
                                                    ))}
                                                </React.Fragment>
                                            );
                                        });
                                    })()}
                                </div>
                            )}
                        </div>
                    </div>

                    {/* Content area: search + messages */}
                    <div className="discord-content-area">
                        {/* Search bar */}
                        <div className="discord-search-bar">
                            <input
                                type="text"
                                className="discord-search-input"
                                placeholder="Search messages..."
                                value={searchQuery}
                                onChange={(e) => setSearchQuery(e.target.value)}
                            />
                            {searchLoading && (
                                <div className="discord-search-loading">
                                    <ProgressSpinner style={{ width: '16px', height: '16px' }} strokeWidth="4" />
                                </div>
                            )}
                        </div>

                        {/* Search info banner */}
                        {searchQuery.trim().length >= 2 && searchResults !== null && (
                            <div className="discord-search-info">
                                Found {searchResults.length} result{searchResults.length !== 1 ? 's' : ''} across all messages
                                <button
                                    className="discord-search-clear"
                                    onClick={() => setSearchQuery('')}
                                >
                                    Clear search
                                </button>
                            </div>
                        )}

                        {/* Messages */}
                        {loadingMessages ? (
                            <div className="discord-loading">
                                <ProgressSpinner style={{ width: '30px', height: '30px' }} strokeWidth="4" />
                                <span className="discord-loading-text">Loading messages...</span>
                            </div>
                        ) : filteredMessages.length === 0 ? (
                            <div className="discord-empty">
                                <svg viewBox="0 0 24 24" fill="currentColor">
                                    <path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H5.17L4 17.17V4h16v12z" />
                                </svg>
                                <h3 className="discord-empty-title">No messages found</h3>
                                <p className="discord-empty-description">
                                    {searchQuery ? 'No messages match your search. Try different keywords.' : 'This channel has no archived messages'}
                                </p>
                            </div>
                        ) : (
                            <div className="discord-messages" data-lenis-prevent ref={messagesContainerRef}>
                                {/* Loading indicator at top when fetching older messages */}
                                {loadingMore && (
                                    <div className="discord-loading-more">
                                        <ProgressSpinner style={{ width: '20px', height: '20px' }} strokeWidth="4" />
                                    </div>
                                )}
                                {groupedMessages.map((group, groupIdx) => {
                                    if (group.type === 'divider') {
                                        return (
                                            <div key={`divider-${groupIdx}`} className="discord-date-divider">
                                                <span>{formatDateDivider(group.date ?? '')}</span>
                                            </div>
                                        );
                                    }

                                    // Check if we're showing search results
                                    const isSearchMode = searchQuery.trim().length >= 2 && searchResults !== null;

                                    return (
                                        <div key={`group-${groupIdx}`} className="discord-message-group">
                                            {group.messages.map((message, msgIdx) => (
                                                <DiscordMessage
                                                    key={message.id}
                                                    message={message}
                                                    isFirstInGroup={msgIdx === 0}
                                                    showTimestamp={msgIdx === 0}
                                                    previewCache={previewCache}
                                                    onPreviewLoad={handlePreviewLoad}
                                                    channelMap={channelMap}
                                                    usersMap={usersMap}
                                                    emojiCache={emojiCache}
                                                    members={members ?? undefined}
                                                    onChannelSelect={handleChannelSelect}
                                                    channelName={selectedChannel?.name}
                                                    onReactionClick={handleReactionClick}
                                                    onPollVoterClick={handlePollVoterClick}
                                                    onContextMenu={handleMessageContextMenu}
                                                    isSearchResult={isSearchMode}
                                                    onJumpToMessage={jumpToMessage}
                                                />
                                            ))}
                                        </div>
                                    );
                                })}
                                {/* Disclaimer when Havoc bot is not in channel */}
                                {!isHavocInChannel && !loadingMembers && (
                                    <div className="discord-havoc-disclaimer">
                                        <svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
                                            <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" />
                                        </svg>
                                        <span>Havoc no longer has access to this channel. New activity may not be visible.</span>
                                    </div>
                                )}
                            </div>
                        )}
                    </div>

                    {/* Member list panel */}
                    {memberListExpanded && (
                        <div className="discord-member-list" data-lenis-prevent>
                            {loadingMembers ? (
                                <div className="discord-member-list-loading">
                                    <ProgressSpinner style={{ width: '24px', height: '24px' }} strokeWidth="4" />
                                </div>
                            ) : memberGroups.length > 0 ? (
                                <>
                                    {memberGroups.map((group, groupIdx) => {
                                        const roleColor = group.role?.color ? String(group.role.color) : undefined;
                                        const roleName = group.role?.name ?? 'Members';
                                        const groupMembers = group.members ?? [];
                                        const groupKey = group.role?.id ?? `group-${groupIdx}`;
                                        return (
                                            <div key={groupKey} className="discord-member-group">
                                                <div
                                                    className="discord-member-group-header"
                                                    style={roleColor ? { color: roleColor } : undefined}
                                                >
                                                    {roleName} — {groupMembers.length}
                                                </div>
                                                {groupMembers.map((member) => {
                                                    const memberColor = member.color ? String(member.color) : undefined;
                                                    const display = member.displayName || member.username || member.id;
                                                    return (
                                                        <div key={member.id} className="discord-member-item">
                                                            {member.avatar ? (
                                                                <img
                                                                    src={member.avatar}
                                                                    alt=""
                                                                    className="discord-member-avatar"
                                                                />
                                                            ) : (
                                                                <div className="discord-member-avatar-placeholder">
                                                                    {display[0]?.toUpperCase()}
                                                                </div>
                                                            )}
                                                            <span
                                                                className="discord-member-name"
                                                                style={memberColor ? { color: memberColor } : undefined}
                                                            >
                                                                {display}
                                                            </span>
                                                            {member.isBot && <span className="discord-bot-tag">APP</span>}
                                                        </div>
                                                    );
                                                })}
                                            </div>
                                        );
                                    })}
                                </>
                            ) : (
                                <div className="discord-member-list-empty">No members found</div>
                            )}
                        </div>
                    )}
                </div>
            </div>

            {/* Image Modal */}
            {imageModal && (
                <div className="discord-image-modal-overlay" onClick={closeImageModal}>
                    <div className="discord-image-modal" onClick={(e) => e.stopPropagation()}>
                        <button className="discord-image-modal-close" onClick={closeImageModal}>
                            <svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24">
                                <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
                            </svg>
                        </button>
                        <img
                            src={imageModal.url}
                            alt={imageModal.alt}
                            className="discord-image-modal-img"
                        />
                        <a
                            href={imageModal.url}
                            target="_blank"
                            rel="noopener noreferrer"
                            className="discord-image-modal-link"
                        >
                            Open original
                        </a>
                    </div>
                </div>
            )}

            {/* Reaction Users Popup */}
            {reactionPopup && (
                <div
                    className="discord-reaction-popup"
                    style={{
                        position: 'fixed',
                        left: reactionPopup.x,
                        top: reactionPopup.y,
                        transform: 'translate(-50%, -100%) translateY(-8px)',
                    }}
                    onClick={(e) => e.stopPropagation()}
                >
                    <div className="discord-reaction-popup-header">
                        {reactionPopup.emoji.id ? (
                            <img
                                src={reactionPopup.emoji.url || `https://cdn.discordapp.com/emojis/${reactionPopup.emoji.id}.${reactionPopup.emoji.animated ? 'gif' : 'png'}`}
                                alt={reactionPopup.emoji.name}
                                className="discord-reaction-popup-emoji"
                            />
                        ) : (
                            <span className="discord-reaction-popup-emoji">{reactionPopup.emoji.name}</span>
                        )}
                        <span className="discord-reaction-popup-count">{reactionPopup.users?.length || 0}</span>
                    </div>
                    <div className="discord-reaction-popup-users">
                        {reactionPopup.loading ? (
                            <div className="discord-reaction-popup-loading">
                                <ProgressSpinner style={{ width: '20px', height: '20px' }} strokeWidth="4" />
                            </div>
                        ) : reactionPopup.users?.length > 0 ? (
                            reactionPopup.users.map((user) => (
                                <div key={user.id} className="discord-reaction-popup-user">
                                    <img
                                        src={user.avatar || `https://cdn.discordapp.com/embed/avatars/0.png`}
                                        alt=""
                                        className="discord-reaction-popup-avatar"
                                    />
                                    <span className="discord-reaction-popup-name">{user.displayName}</span>
                                </div>
                            ))
                        ) : (
                            <div className="discord-reaction-popup-empty">No users found</div>
                        )}
                    </div>
                </div>
            )}

            {/* Poll Voter Popup - same style as reaction popup */}
            {pollVoterPopup && (
                <div
                    className="discord-reaction-popup"
                    style={{
                        position: 'fixed',
                        left: pollVoterPopup.x,
                        top: pollVoterPopup.y,
                        transform: 'translate(-50%, -100%) translateY(-8px)',
                    }}
                    onClick={(e) => e.stopPropagation()}
                >
                    <div className="discord-reaction-popup-header">
                        <svg className="discord-reaction-popup-emoji" viewBox="0 0 24 24" fill="currentColor" width="20" height="20" style={{ color: '#5865f2' }}>
                            <path d="M4 5h16v2H4V5zm0 4h10v2H4V9zm0 4h16v2H4v-2zm0 4h10v2H4v-2z" />
                        </svg>
                        <span className="discord-reaction-popup-count">{pollVoterPopup.voters?.length || 0}</span>
                    </div>
                    <div className="discord-reaction-popup-users">
                        {pollVoterPopup.voters?.length > 0 ? (
                            pollVoterPopup.voters.map((voter) => (
                                <div key={voter.id} className="discord-reaction-popup-user">
                                    <img
                                        src={voter.avatar || `https://cdn.discordapp.com/embed/avatars/0.png`}
                                        alt=""
                                        className="discord-reaction-popup-avatar"
                                    />
                                    <span className="discord-reaction-popup-name">{voter.displayName || voter.username}</span>
                                </div>
                            ))
                        ) : (
                            <div className="discord-reaction-popup-empty">No voters found</div>
                        )}
                    </div>
                </div>
            )}

            {/* Message Context Menu */}
            {contextMenu && (
                <div
                    className="discord-context-menu"
                    style={{
                        position: 'fixed',
                        left: contextMenu.x,
                        top: contextMenu.y,
                    }}
                    onClick={(e) => e.stopPropagation()}
                >
                    <button className="discord-context-menu-item" onClick={copyMessageLink}>
                        <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
                            <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z" />
                        </svg>
                        Copy Message Link
                    </button>
                    <button className="discord-context-menu-item" onClick={copyMessageContent}>
                        <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
                            <path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" />
                        </svg>
                        Copy Text
                    </button>
                    <button className="discord-context-menu-item" onClick={copyMessageId}>
                        <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
                            <path d="M20 9H4v2h16V9zM4 15h16v-2H4v2z" />
                        </svg>
                        Copy Message ID
                    </button>
                </div>
            )}

            {/* Channel Context Menu */}
            {channelContextMenu && (
                <div
                    className="discord-context-menu"
                    style={{
                        position: 'fixed',
                        left: channelContextMenu.x,
                        top: channelContextMenu.y,
                    }}
                    onClick={(e) => e.stopPropagation()}
                >
                    <button className="discord-context-menu-item" onClick={copyChannelLink}>
                        <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
                            <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z" />
                        </svg>
                        Copy Channel Link
                    </button>
                    <button className="discord-context-menu-item" onClick={copyChannelName}>
                        <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
                            <path d="M5.88657 21C5.57547 21 5.3399 20.7189 5.39427 20.4126L6.00001 17H2.59511C2.28449 17 2.04905 16.7198 2.10259 16.4138L2.27759 15.4138C2.31946 15.1746 2.52722 15 2.77011 15H6.35001L7.41001 9H4.00511C3.69449 9 3.45905 8.71977 3.51259 8.41381L3.68759 7.41381C3.72946 7.17456 3.93722 7 4.18011 7H7.76001L8.39677 3.41262C8.43914 3.17391 8.64664 3 8.88907 3H9.87344C10.1845 3 10.4201 3.28107 10.3657 3.58738L9.76001 7H15.76L16.3968 3.41262C16.4391 3.17391 16.6466 3 16.8891 3H17.8734C18.1845 3 18.4201 3.28107 18.3657 3.58738L17.76 7H21.1649C21.4755 7 21.711 7.28023 21.6574 7.58619L21.4824 8.58619C21.4406 8.82544 21.2328 9 20.9899 9H17.41L16.35 15H19.7549C20.0655 15 20.301 15.2802 20.2474 15.5862L20.0724 16.5862C20.0306 16.8254 19.8228 17 19.5799 17H16L15.3632 20.5874C15.3209 20.8261 15.1134 21 14.8709 21H13.8866C13.5755 21 13.3399 20.7189 13.3943 20.4126L14 17H8.00001L7.36325 20.5874C7.32088 20.8261 7.11337 21 6.87094 21H5.88657ZM9.41045 9L8.35045 15H14.3504L15.4104 9H9.41045Z" />
                        </svg>
                        Copy Channel Name
                    </button>
                    <button className="discord-context-menu-item" onClick={copyChannelId}>
                        <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
                            <path d="M20 9H4v2h16V9zM4 15h16v-2H4v2z" />
                        </svg>
                        Copy Channel ID
                    </button>
                </div>
            )}
        </ImageModalContext.Provider>
    );
}
