<script lang="ts">
  import { marked } from 'marked';
  import { sanitizeMarkdown } from '../../services/security/sanitizer.js';
  import { findNIP21Links, parseNIP21 } from '../../services/nostr/nip21-parser.js';
  import { nip19 } from 'nostr-tools';
  import ProfileBadge from '../layout/ProfileBadge.svelte';
  import { fetchEmojiSet, resolveEmojiShortcode } from '../../services/nostr/nip30-emoji.js';
  import { renderAsciiDoc } from '../../services/content/asciidoctor-renderer.js';
  import HighlightOverlay from './HighlightOverlay.svelte';
  import { getHighlightsForEvent, findHighlightMatches, type Highlight } from '../../services/nostr/highlight-service.js';
  import { mountComponent } from './mount-component-action.js';
  import type { NostrEvent } from '../../types/nostr.js';
  import hljs from 'highlight.js';
  // Use VS Code theme for IDE-like appearance
  import 'highlight.js/styles/vs2015.css';
  
  import EmbeddedEvent from './EmbeddedEvent.svelte';
  let mountingEmbeddedEvents = $state(false); // Guard for mounting

  interface Props {
    content: string;
    event?: NostrEvent; // Optional event for emoji resolution
  }

  let { content, event }: Props = $props();
  let containerRef = $state<HTMLElement | null>(null);
  let emojiUrls = $state<Map<string, string>>(new Map());
  let highlights = $state<Highlight[]>([]);
  let highlightMatches = $state<Array<{ start: number; end: number; highlight: Highlight }>>([]);
  let highlightsLoaded = $state(false);
  
  // Cache for rendered markdown to avoid re-rendering same content
  const markdownCache = new Map<string, string>();
  const MAX_CACHE_SIZE = 100; // Limit cache size to prevent memory bloat

  // Validate if a string is a valid bech32 or hex string
  function isValidNostrId(str: string): boolean {
    if (!str || typeof str !== 'string') return false;
    // Check for HTML tags or other invalid characters
    if (/<[^>]+>/.test(str)) return false;
    // Check if it's hex (64 hex characters)
    if (/^[0-9a-f]{64}$/i.test(str)) return true;
    // Check if it's bech32 (npub1..., note1..., nevent1..., naddr1..., nprofile1...)
    if (/^(npub|note|nevent|naddr|nprofile)1[a-z0-9]+$/.test(str)) return true;
    return false;
  }

  // Extract pubkey from npub or nprofile
  function getPubkeyFromNIP21(parsed: ReturnType<typeof parseNIP21>): string | null {
    if (!parsed) return null;

    // Validate before decoding
    if (!isValidNostrId(parsed.data)) {
      console.warn('Invalid NIP-21 data format:', parsed.data);
      return null;
    }

    try {
      // parsed.data is the bech32 string (e.g., "npub1..." or "nprofile1...")
      // We need to decode it to get the actual pubkey
      const decoded = nip19.decode(parsed.data);
      
      if (parsed.type === 'npub') {
        // npub decodes directly to pubkey (hex string)
        if (decoded.type === 'npub') {
          return String(decoded.data);
        }
      } else if (parsed.type === 'nprofile') {
        // nprofile decodes to object with pubkey property
        if (decoded.type === 'nprofile' && decoded.data && typeof decoded.data === 'object' && 'pubkey' in decoded.data) {
          return String(decoded.data.pubkey);
        }
      }
    } catch (error) {
      console.error('Error decoding NIP-21 URI:', error, parsed);
      return null;
    }

    return null;
  }

  // Extract event identifier from note, nevent, or naddr
  // Returns the bech32 string (note1..., nevent1..., naddr1...) which EmbeddedEvent can decode
  function getEventIdFromNIP21(parsed: ReturnType<typeof parseNIP21>): string | null {
    if (!parsed) return null;

    if (parsed.type === 'note' || parsed.type === 'nevent' || parsed.type === 'naddr') {
      // Return the bech32 string - EmbeddedEvent will decode it
      return parsed.data;
    }

    return null;
  }

  // Escape HTML to prevent XSS
  function escapeHtml(text: string): string {
    return text
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');
  }

  // Convert plain media URLs (images, videos, audio) to HTML tags
  function convertMediaUrls(text: string): string {
    // Match media URLs (http/https URLs ending in media extensions)
    const imageExtensions = /\.(png|jpg|jpeg|gif|webp|svg|bmp|ico)(\?[^\s<>"']*)?$/i;
    const videoExtensions = /\.(mp4|webm|ogg|mov|avi|mkv)(\?[^\s<>"']*)?$/i;
    const audioExtensions = /\.(mp3|wav|ogg|flac|aac|m4a)(\?[^\s<>"']*)?$/i;
    const urlPattern = /https?:\/\/[^\s<>"']+/g;
    
    let result = text;
    const matches: Array<{ url: string; index: number; endIndex: number; type: 'image' | 'video' | 'audio' }> = [];
    
    // Find all URLs
    let match;
    while ((match = urlPattern.exec(text)) !== null) {
      const url = match[0];
      const index = match.index;
      const endIndex = index + url.length;
      
      // Check if this URL is already in markdown or HTML
      const before = text.substring(Math.max(0, index - 10), index);
      const after = text.substring(endIndex, Math.min(text.length, endIndex + 10));
      
      // Skip if it's already in markdown or HTML tags
      if (before.includes('![') || before.includes('<img') || before.includes('<video') || before.includes('<audio') || 
          after.startsWith('](') || after.startsWith('</img>') || after.startsWith('</video>') || after.startsWith('</audio>')) {
        continue;
      }
      
      // Determine media type
      if (imageExtensions.test(url)) {
        matches.push({ url, index, endIndex, type: 'image' });
      } else if (videoExtensions.test(url)) {
        matches.push({ url, index, endIndex, type: 'video' });
      } else if (audioExtensions.test(url)) {
        matches.push({ url, index, endIndex, type: 'audio' });
      }
    }
    
    // Replace from end to start to preserve indices
    for (let i = matches.length - 1; i >= 0; i--) {
      const { url, index, endIndex, type } = matches[i];
      const escapedUrl = escapeHtml(url);
      
      if (type === 'image') {
        result = result.substring(0, index) + `<img src="${escapedUrl}" alt="" style="max-width: 600px; width: 100%; height: auto;" />` + result.substring(endIndex);
      } else if (type === 'video') {
        result = result.substring(0, index) + `<video src="${escapedUrl}" controls preload="none" style="max-width: 600px; width: 100%; height: auto; max-height: 500px;"></video>` + result.substring(endIndex);
      } else if (type === 'audio') {
        result = result.substring(0, index) + `<audio src="${escapedUrl}" controls preload="none" style="max-width: 600px; width: 100%;"></audio>` + result.substring(endIndex);
      }
    }
    
    return result;
  }

  // Resolve custom emojis in content
  async function resolveContentEmojis(text: string): Promise<void> {
    if (!event) return; // Need event to resolve emojis
    
    // Find all :shortcode: patterns
    const emojiPattern = /:([a-zA-Z0-9_+-]+):/g;
    const matches: Array<{ shortcode: string; fullMatch: string }> = [];
    let match;
    while ((match = emojiPattern.exec(text)) !== null) {
      const shortcode = match[1];
      const fullMatch = match[0];
      if (!matches.find(m => m.fullMatch === fullMatch)) {
        matches.push({ shortcode, fullMatch });
      }
    }
    
    if (matches.length === 0) return;
    
    // Collect all pubkeys to check: event author and p tags
    const pubkeysToCheck = new Set<string>();
    pubkeysToCheck.add(event.pubkey);
    for (const tag of event.tags) {
      if (tag[0] === 'p' && tag[1]) {
        pubkeysToCheck.add(tag[1]);
      }
    }
    
    // Resolve emojis - only try specific pubkeys (don't search broadly to avoid background fetching)
    // Broad search should only happen when emoji picker is opened
    const resolvedUrls = new Map<string, string>();
    for (const { shortcode, fullMatch } of matches) {
      // Only try specific pubkeys (event author, p tags) - don't search broadly
      const url = await resolveEmojiShortcode(shortcode, Array.from(pubkeysToCheck), false);
      
      if (url) {
        resolvedUrls.set(fullMatch, url);
      }
    }
    
    emojiUrls = resolvedUrls;
  }

  // Replace emoji shortcodes with images in text, but skip code blocks
  function replaceEmojis(text: string): string {
    let processed = text;
    
    // Find all code blocks (Markdown and AsciiDoc syntax)
    const codeBlockRanges: Array<{ start: number; end: number }> = [];
    
    // First, match AsciiDoc source blocks (---- ... ----)
    // AsciiDoc source blocks can have attributes like [source, json] before the dashes
    // Match: 4+ dashes on a line, content, then 4+ dashes on a line
    // We'll match the dashes and then find the closing dashes
    const asciidocSourceBlockPattern = /^----+$/gm;
    const lines = text.split(/\r?\n/);
    let inAsciidocBlock = false;
    let blockStart = -1;
    let lineIndex = 0;
    let charIndex = 0;
    
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i];
      const lineStart = charIndex;
      const lineEnd = charIndex + line.length;
      
      // Check if this line is 4+ dashes
      if (/^----+$/.test(line.trim())) {
        if (!inAsciidocBlock) {
          // Starting a new block
          inAsciidocBlock = true;
          blockStart = lineStart;
        } else {
          // Ending a block
          codeBlockRanges.push({ start: blockStart, end: lineEnd });
          inAsciidocBlock = false;
          blockStart = -1;
        }
      }
      
      charIndex = lineEnd + 1; // +1 for newline
    }
    
    // If we're still in a block at the end, close it
    if (inAsciidocBlock && blockStart >= 0) {
      codeBlockRanges.push({ start: blockStart, end: text.length });
    }
    
    // Match Markdown/AsciiDoc fenced code blocks (```...```)
    // Match triple backticks with optional language identifier
    let match;
    const fencedCodeBlockPattern = /```[a-zA-Z]*\n?[\s\S]*?```/g;
    while ((match = fencedCodeBlockPattern.exec(text)) !== null) {
      const start = match.index;
      const end = start + match[0].length;
      // Only add if not already inside an AsciiDoc source block
      const isInsideAsciidoc = codeBlockRanges.some(range => start >= range.start && end <= range.end);
      if (!isInsideAsciidoc) {
        codeBlockRanges.push({ start, end });
      }
    }
    
    // Then match inline code (`code`) - but exclude those already inside other blocks
    // Match single backtick, but not if it's part of triple backticks
    const inlineCodePattern = /`[^`\n]+`/g;
    while ((match = inlineCodePattern.exec(text)) !== null) {
      const start = match.index;
      const end = start + match[0].length;
      
      // Check if this inline code is already inside another code block
      const isInsideBlock = codeBlockRanges.some(range => start >= range.start && end <= range.end);
      if (!isInsideBlock) {
        codeBlockRanges.push({ start, end });
      }
    }
    
    // Sort ranges by start position
    codeBlockRanges.sort((a, b) => a.start - b.start);
    
    // Helper function to check if a position is inside a code block
    function isInCodeBlock(index: number): boolean {
      return codeBlockRanges.some(range => index >= range.start && index < range.end);
    }
    
    // Replace from end to start to preserve indices
    const sortedEntries = Array.from(emojiUrls.entries()).sort((a, b) => {
      const indexA = processed.lastIndexOf(a[0]);
      const indexB = processed.lastIndexOf(b[0]);
      return indexB - indexA; // Sort by last index descending
    });
    
    for (const [shortcode, url] of sortedEntries) {
      const escapedUrl = escapeHtml(url);
      const escapedShortcode = escapeHtml(shortcode);
      // Replace with img tag, preserving the shortcode as alt text
      const imgTag = `<img src="${escapedUrl}" alt="${escapedShortcode}" class="emoji-inline" style="display: inline-block; width: 1.6em; height: 1.6em; vertical-align: middle; object-fit: contain;" />`;
      
      // Find all occurrences and only replace those outside code blocks
      let searchIndex = 0;
      while (true) {
        const index = processed.indexOf(shortcode, searchIndex);
        if (index === -1) break;
        
        if (!isInCodeBlock(index)) {
          // Replace this occurrence
          processed = processed.substring(0, index) + imgTag + processed.substring(index + shortcode.length);
          searchIndex = index + imgTag.length;
        } else {
          // Skip this occurrence (it's in a code block)
          searchIndex = index + shortcode.length;
        }
      }
    }
    
    return processed;
  }

  // Convert hashtags (#hashtag) to links
  function convertHashtags(text: string): string {
    // Match hashtags: # followed by alphanumeric characters, underscores, and hyphens
    // Don't match if it's already in a link or markdown
    const hashtagPattern = /(^|\s)(#[a-zA-Z0-9_-]+)/g;
    
    return text.replace(hashtagPattern, (match, prefix, hashtag) => {
      // Skip if it's already in a link or markdown
      const before = text.substring(Math.max(0, text.indexOf(match) - 20), text.indexOf(match));
      if (before.includes('<a') || before.includes('](') || before.includes('href=')) {
        return match;
      }
      
      const tagName = hashtag.substring(1); // Remove #
      const escapedTag = escapeHtml(tagName);
      const escapedHashtag = escapeHtml(hashtag);
      return `${prefix}<a href="/topics/${escapedTag}" class="hashtag-link">${escapedHashtag}</a>`;
    });
  }

  // Convert greentext (>text with no space) to styled spans
  // Groups consecutive greentext lines into a single block, preserving line breaks
  function convertGreentext(text: string): string {
    // Split by lines and process
    const lines = text.split('\n');
    const processedLines: string[] = [];
    let greentextBlock: string[] = [];
    
    const greentextPattern = /^(&gt;|>)([^\s>].*)$/;
    
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i];
      const match = line.match(greentextPattern);
      
      if (match) {
        // This is greentext - add to current block
        const greentextContent = escapeHtml(match[2]);
        greentextBlock.push(greentextContent);
      } else {
        // Not greentext - flush any accumulated greentext block
        if (greentextBlock.length > 0) {
          // Join with <br> to preserve line breaks, no extra spacing
          const blockContent = greentextBlock.map(content => `&gt;${content}`).join('<br>');
          processedLines.push(`<span class="greentext">${blockContent}</span>`);
          greentextBlock = [];
        }
        // Add the non-greentext line as-is
        processedLines.push(line);
      }
    }
    
    // Flush any remaining greentext block at the end
    if (greentextBlock.length > 0) {
      // Join with <br> to preserve line breaks, no extra spacing
      const blockContent = greentextBlock.map(content => `&gt;${content}`).join('<br>');
      processedLines.push(`<span class="greentext">${blockContent}</span>`);
    }
    
    return processedLines.join('\n');
  }

  // Process content: replace nostr URIs with HTML span elements and convert media URLs
  function processContent(text: string): string {
    // First, convert greentext (must be before markdown processing)
    let processed = convertGreentext(text);
    
    // Then, replace emoji shortcodes with images if resolved
    processed = replaceEmojis(processed);
    
    // Convert hashtags to links
    processed = convertHashtags(processed);
    
    // Then, convert plain media URLs (images, videos, audio) to HTML tags
    processed = convertMediaUrls(processed);
    
    // Find all NIP-21 links (nostr:npub, nostr:nprofile, nostr:nevent, etc.)
    const links = findNIP21Links(processed);
    
    // Separate into profile links and event links
    const profileLinks = links.filter(link => 
      link.parsed.type === 'npub' || link.parsed.type === 'nprofile'
    );

    const eventLinks = links.filter(link =>
      link.parsed.type === 'note' || link.parsed.type === 'nevent' || link.parsed.type === 'naddr'
    );

    // Replace event links with HTML div elements (for block-level display)
    // Process from end to start to preserve indices
    for (let i = eventLinks.length - 1; i >= 0; i--) {
      const link = eventLinks[i];
      const eventId = getEventIdFromNIP21(link.parsed);
      // Validate event ID before creating placeholder to prevent invalid fetches
      if (eventId && isValidNostrId(eventId)) {
        // Escape event ID to prevent XSS
        const escapedEventId = escapeHtml(eventId);
        // Create a div element for embedded event cards (block-level)
        const div = `<div data-nostr-event data-event-id="${escapedEventId}"></div>`;
        processed = 
          processed.slice(0, link.start) + 
          div + 
          processed.slice(link.end);
      }
    }

    // Replace profile links with HTML span elements that have data attributes
    // Process from end to start to preserve indices
    for (let i = profileLinks.length - 1; i >= 0; i--) {
      const link = profileLinks[i];
      const pubkey = getPubkeyFromNIP21(link.parsed);
      if (pubkey) {
        // Escape pubkey to prevent XSS (though pubkeys should be safe hex strings)
        const escapedPubkey = escapeHtml(pubkey);
        // Create a span element that will survive markdown rendering
        // Markdown won't process HTML tags, so this will remain as-is
        const span = `<span data-nostr-profile data-pubkey="${escapedPubkey}"></span>`;
        processed = 
          processed.slice(0, link.start) + 
          span + 
          processed.slice(link.end);
      }
    }

    return processed;
  }

  // Configure marked once - ensure images are rendered and HTML is preserved
  // Note: Code highlighting is done post-render in the effect below, not via marked options
  marked.setOptions({
    breaks: true, // Convert line breaks to <br>
    gfm: true, // GitHub Flavored Markdown
    silent: false // Don't suppress errors
    // HTML tags (like <img>) pass through by default in marked
  });

  // Resolve emojis when content or event changes
  $effect(() => {
    if (content && event) {
      // Run async resolution without blocking
      resolveContentEmojis(content).catch(err => {
        console.warn('Error resolving content emojis:', err);
      });
    } else {
      emojiUrls = new Map();
    }
  });

  // Check if event should use Asciidoctor (kinds 30818 and 30041)
  const useAsciidoctor = $derived(event?.kind === 30818 || event?.kind === 30041);
  const isKind30040 = $derived(event?.kind === 30040);

  // Load highlights for event
  async function loadHighlights(abortSignal: AbortSignal) {
    if (!event || !content) return;
    
    // Skip highlights for kind 30040 index events (they have no content)
    // Highlights should be loaded for the indexed sections instead
    if (isKind30040 && !content.trim()) {
      if (!abortSignal.aborted) {
        highlightsLoaded = true;
      }
      return;
    }
    
    // For kind 30040 sections, wait a bit for full publication load
    if (isKind30040) {
      await new Promise(resolve => setTimeout(resolve, 2000));
      
      // Check if operation was aborted after delay
      if (abortSignal.aborted) return;
    }
    
    try {
      // Load highlights for this specific event (which could be a section event)
      const dTag = event.tags.find(t => t[0] === 'd' && t[1])?.[1];
      const eventHighlights = await getHighlightsForEvent(
        event.id,
        event.kind,
        event.pubkey,
        dTag
      );
      
      // Check if operation was aborted before updating state
      if (abortSignal.aborted) return;
      
      highlights = eventHighlights;
      highlightMatches = findHighlightMatches(content, eventHighlights);
      highlightsLoaded = true;
    } catch (error) {
      // Only update state if not aborted
      if (abortSignal.aborted) return;
      
      console.error('Error loading highlights:', error);
      highlightsLoaded = true;
    }
  }

  // Load highlights when content or event changes
  $effect(() => {
    if (content && event) {
      // Create abort controller to track effect lifecycle
      const abortController = new AbortController();
      
      // Load highlights with abort signal
      loadHighlights(abortController.signal);
      
      // Cleanup: abort the async operation if effect re-runs or component unmounts
      return () => {
        abortController.abort();
      };
    }
  });

  // Post-process HTML to convert blockquotes that are actually greentext
  // Merges consecutive greentext blockquotes into a single block, preserving line breaks
  function postProcessGreentext(html: string): string {
    // Pattern to match one or more consecutive greentext blockquotes
    // Matches: <blockquote><p>&gt;text</p></blockquote> (with optional whitespace between)
    // where there's no space after &gt; (greentext pattern)
    // The (?:...) part matches zero or more additional consecutive blockquotes
    const consecutiveGreentextPattern = /(<blockquote[^>]*>\s*<p[^>]*>&gt;([^\s<].*?)<\/p>\s*<\/blockquote>)(\s*<blockquote[^>]*>\s*<p[^>]*>&gt;([^\s<].*?)<\/p>\s*<\/blockquote>)*/g;
    
    return html.replace(consecutiveGreentextPattern, (match) => {
      // Extract all greentext contents from the match
      const contentPattern = /<blockquote[^>]*>\s*<p[^>]*>&gt;([^\s<].*?)<\/p>\s*<\/blockquote>/g;
      const contents: string[] = [];
      let contentMatch;
      while ((contentMatch = contentPattern.exec(match)) !== null) {
        contents.push(contentMatch[1]);
      }
      
      if (contents.length === 0) {
        return match; // Shouldn't happen, but safety check
      }
      
      // Join all contents with <br> to preserve line breaks, no extra spacing
      const combinedContent = contents.map(c => escapeHtml(c)).map(content => `&gt;${content}`).join('<br>');
      return `<span class="greentext">${combinedContent}</span>`;
    });
  }

  // Render markdown or AsciiDoc to HTML
  function renderMarkdown(text: string): string {
    if (!content) return '';
    
    // Check cache first
    const cached = markdownCache.get(content);
    if (cached !== undefined) {
      return cached;
    }
    
    const processed = processContent(content);

    let html: string;
    
    if (useAsciidoctor) {
      // Use Asciidoctor for kinds 30818 and 30041
      try {
        html = renderAsciiDoc(processed);
      } catch (error) {
        console.error('Asciidoctor parsing error:', error);
        return processed; // Fallback to raw text if parsing fails
      }
    } else {
      // Use marked for all other kinds
      try {
        html = marked.parse(processed) as string;
      } catch (error) {
        console.error('Marked parsing error:', error);
        return processed; // Fallback to raw text if parsing fails
      }
    }
    
    // Post-process to fix any greentext that markdown converted to blockquotes
    html = postProcessGreentext(html);
    
    // Remove emoji images that are inside code blocks (they should be plain text)
    // This handles cases where emojis were replaced before markdown/AsciiDoc parsing
    // Handle <code> tags (inline code)
    html = html.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, (match, codeContent) => {
      // Remove any emoji img tags inside code blocks and restore the original shortcode
      return match.replace(/<img[^>]*class="emoji-inline"[^>]*alt="([^"]*)"[^>]*>/gi, '$1');
    });
    
    // Handle <pre> tags (code blocks and AsciiDoc source blocks)
    html = html.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, (match, preContent) => {
      // Remove any emoji img tags inside pre blocks and restore the original shortcode
      return match.replace(/<img[^>]*class="emoji-inline"[^>]*alt="([^"]*)"[^>]*>/gi, '$1');
    });
    
    // Handle AsciiDoc source blocks (they use <div class="listingblock"> with <pre> inside)
    html = html.replace(/<div[^>]*class="listingblock"[^>]*>([\s\S]*?)<\/div>/gi, (match, divContent) => {
      // Remove emoji images from AsciiDoc listing blocks
      return match.replace(/<img[^>]*class="emoji-inline"[^>]*alt="([^"]*)"[^>]*>/gi, '$1');
    });
    
    // Also handle any other code-related elements that might contain emojis
    html = html.replace(/<[^>]*class="[^"]*code[^"]*"[^>]*>([\s\S]*?)<\/[^>]+>/gi, (match, content) => {
      // Remove emoji images from any element with "code" in its class
      return match.replace(/<img[^>]*class="emoji-inline"[^>]*alt="([^"]*)"[^>]*>/gi, '$1');
    });
    
    // Fix malformed image tags - ensure all img src attributes are absolute URLs
    // This prevents the browser from trying to fetch markdown syntax or malformed tags as relative URLs
    html = html.replace(/<img\s+([^>]*?)>/gi, (match, attributes) => {
      // Extract src attribute
      const srcMatch = attributes.match(/src=["']([^"']+)["']/i);
      if (srcMatch) {
        const src = srcMatch[1];
        // If src doesn't start with http:// or https://, it might be malformed
        // Check if it looks like it should be a URL but isn't properly formatted
        if (!src.startsWith('http://') && !src.startsWith('https://') && !src.startsWith('data:') && !src.startsWith('/')) {
          // This might be a malformed URL - try to extract a valid URL from it
          const urlMatch = src.match(/(https?:\/\/[^\s<>"']+)/i);
          if (urlMatch) {
            // Replace with the valid URL
            return match.replace(srcMatch[0], `src="${urlMatch[1]}"`);
          }
          // If no valid URL found, remove the img tag to prevent 404s
          return '';
        }
      }
      // If no src attribute or src is empty, remove the img tag
      if (!srcMatch || !srcMatch[1]) {
        return '';
      }
      return match;
    });
    
    // Fix markdown image syntax that wasn't properly converted (e.g., ![image](url) as text)
    // This should be rare, but if marked didn't convert it, we need to handle it
    html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, url) => {
      // Only convert if it's a valid URL
      if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('data:')) {
        const escapedUrl = escapeHtml(url);
        const escapedAlt = escapeHtml(alt);
        return `<img src="${escapedUrl}" alt="${escapedAlt}" style="max-width: 600px; width: 100%; height: auto;" />`;
      }
      // If not a valid URL, remove the markdown syntax to prevent 404s
      return alt || '';
    });
    
    // Remove any escaped HTML that looks like img tags (e.g., &lt;img src="..."&gt;)
    // These might be causing the browser to try to fetch them as URLs
    html = html.replace(/&lt;img\s+[^&]*src=["']([^"']+)["'][^&]*&gt;/gi, (match, url) => {
      // If it's a valid image URL, convert to a proper img tag
      if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('data:')) {
        const escapedUrl = escapeHtml(url);
        return `<img src="${escapedUrl}" alt="" style="max-width: 600px; width: 100%; height: auto;" />`;
      }
      // Otherwise, remove to prevent 404s
      return '';
    });
    
    // Remove any plain text patterns that look like markdown image syntax or HTML img tags
    // These patterns in text nodes can cause the browser to try to fetch them as relative URLs
    // We match text between HTML tags (not inside tags) that contains these patterns
    html = html.replace(/>([^<]*?)(!\[[^\]]*\]\(https?:\/\/[^)]+\)|img\s+src=["']https?:\/\/[^"']+["'])([^<]*?)</gi, (match, before, pattern, after) => {
      // Extract URL from the pattern
      const urlMatch = pattern.match(/(https?:\/\/[^\s<>"')]+)/i);
      if (urlMatch) {
        const url = urlMatch[1];
        // If it's an image URL, convert to proper img tag
        if (/\.(png|jpg|jpeg|gif|webp|svg|bmp|ico)(\?[^\s<>"']*)?$/i.test(url)) {
          const escapedUrl = escapeHtml(url);
          return `>${before}<img src="${escapedUrl}" alt="" />${after}<`;
        }
      }
      // Otherwise, remove the problematic pattern to prevent 404s
      return `>${before}${after}<`;
    });
    
    // Filter out invalid relative links (like /a, /b, etc.) that cause 404s
    // These are likely malformed markdown links
    html = html.replace(/<a\s+href="\/([a-z]|\.)"[^>]*>.*?<\/a>/gi, (match, char) => {
      // Only filter single-character or dot links that aren't valid routes
      const validSingleCharRoutes = ['r', 'f', 'w', 't', 'c']; // /rss, /feed, /write, /topics, /cache
      if (validSingleCharRoutes.includes(char.toLowerCase())) {
        return match; // Keep valid routes
      }
      // Remove the link but keep the text content
      const textMatch = match.match(/>(.*?)<\/a>/);
      return textMatch ? textMatch[1] : '';
    });
    
    // Sanitize HTML (but preserve our data attributes and image src)
    const sanitized = sanitizeMarkdown(html);
    
    // Cache the result (with size limit to prevent memory bloat)
    if (markdownCache.size >= MAX_CACHE_SIZE) {
      // Remove oldest entry (simple FIFO)
      const firstKey = markdownCache.keys().next().value;
      if (firstKey !== undefined) {
        markdownCache.delete(firstKey);
      }
    }
    markdownCache.set(content, sanitized);
    
    return sanitized;
  }

  const renderedHtml = $derived(renderMarkdown(content));

  // Mount ProfileBadge components after rendering
  function mountProfileBadges() {
    if (!containerRef) return;
    
    // Find all profile placeholders and mount ProfileBadge components
    const placeholders = containerRef.querySelectorAll('[data-nostr-profile]:not([data-mounted])');
    
    if (placeholders.length > 0) {
      console.debug(`Mounting ${placeholders.length} ProfileBadge components`);
      
      placeholders.forEach((placeholder) => {
        const pubkey = placeholder.getAttribute('data-pubkey');
        if (pubkey && /^[0-9a-f]{64}$/i.test(pubkey)) {
          placeholder.setAttribute('data-mounted', 'true');
          
          try {
            // Clear and mount component
            placeholder.innerHTML = '';
            // Use inline mode for profile badges in markdown content
            const instance = mountComponent(placeholder as HTMLElement, ProfileBadge as any, { pubkey, inline: true });
            
            if (!instance) {
              console.warn('ProfileBadge mount returned null', { pubkey });
              // Fallback
              try {
                const npub = nip19.npubEncode(pubkey);
                placeholder.textContent = npub.slice(0, 12) + '...';
              } catch {
                placeholder.textContent = pubkey.slice(0, 12) + '...';
              }
            }
          } catch (error) {
            console.error('Error mounting ProfileBadge:', error, { pubkey });
            // Show fallback
            try {
              const npub = nip19.npubEncode(pubkey);
              placeholder.textContent = npub.slice(0, 12) + '...';
            } catch {
              placeholder.textContent = pubkey.slice(0, 12) + '...';
            }
          }
        } else if (pubkey) {
          console.warn('Invalid pubkey format:', pubkey);
          placeholder.textContent = pubkey.slice(0, 12) + '...';
        }
      });
    }
  }

  // Mount EmbeddedEvent components after rendering
  function mountEmbeddedEvents() {
    if (!containerRef || mountingEmbeddedEvents) return;
    
    // Find all event placeholders and mount EmbeddedEvent components
    const placeholders = containerRef.querySelectorAll('[data-nostr-event]:not([data-mounted])');
    
    if (placeholders.length > 0) {
      mountingEmbeddedEvents = true;
      try {
        // Validate event IDs before mounting to prevent invalid fetches
        const validPlaceholders: Element[] = [];
        placeholders.forEach((placeholder) => {
          const eventId = placeholder.getAttribute('data-event-id');
          // Use strict validation to prevent invalid fetches
          if (eventId && isValidNostrId(eventId)) {
            validPlaceholders.push(placeholder);
          } else if (eventId) {
            // Invalid event ID - mark as mounted to prevent retries
            placeholder.setAttribute('data-mounted', 'true');
            placeholder.textContent = ''; // Don't show invalid IDs
            console.debug('Skipping invalid event ID in MarkdownRenderer:', eventId);
          }
        });
        
        if (validPlaceholders.length > 0) {
          console.debug(`Mounting ${validPlaceholders.length} EmbeddedEvent components`);
          
          validPlaceholders.forEach((placeholder) => {
            const eventId = placeholder.getAttribute('data-event-id');
            if (eventId) {
              placeholder.setAttribute('data-mounted', 'true');
              
              try {
                // Clear and mount component
                placeholder.innerHTML = '';
                // Mount EmbeddedEvent component - it will decode and fetch the event
                const instance = mountComponent(placeholder as HTMLElement, EmbeddedEvent as any, { eventId });
                
                if (!instance) {
                  console.warn('EmbeddedEvent mount returned null', { eventId });
                  // Fallback: show the event ID
                  placeholder.textContent = eventId.slice(0, 20) + '...';
                }
              } catch (error) {
                console.error('Error mounting EmbeddedEvent:', error, { eventId });
                // Show fallback
                placeholder.textContent = eventId.slice(0, 20) + '...';
              }
            }
          });
        }
      } finally {
        mountingEmbeddedEvents = false;
      }
    }
  }


  $effect(() => {
    if (!containerRef || !renderedHtml) return;

    // Use requestAnimationFrame + setTimeout to ensure DOM is ready
    const frameId = requestAnimationFrame(() => {
      const timeoutId = setTimeout(() => {
        if (!containerRef) return;
        
        // Highlight code blocks (both Markdown and AsciiDoc)
        // Markdown: <pre><code>
        const codeBlocks = containerRef.querySelectorAll('pre code');
        codeBlocks.forEach((block) => {
          if (!block.classList.contains('hljs')) {
            const code = block.textContent || '';
            const className = block.className || '';
            const langMatch = className.match(/language-(\w+)/);
            const lang = langMatch ? langMatch[1] : '';
            
            if (lang && hljs.getLanguage(lang)) {
              try {
                block.innerHTML = hljs.highlight(code, { language: lang }).value;
                block.className = `hljs ${className}`;
              } catch (err) {
                console.warn('Highlight.js error:', err);
              }
            } else {
              try {
                block.innerHTML = hljs.highlightAuto(code).value;
                block.className = `hljs ${className}`;
              } catch (err) {
                console.warn('Highlight.js auto-detect error:', err);
              }
            }
          }
        });
        
        // AsciiDoc: <div class="listingblock"><pre><code> or <pre class="highlight"><code>
        if (!containerRef) return;
        const asciidocBlocks = containerRef.querySelectorAll('.listingblock pre code, pre.highlight code');
        asciidocBlocks.forEach((block) => {
          if (!block.classList.contains('hljs')) {
            const code = block.textContent || '';
            // AsciiDoc might have language in data-lang or class
            const preElement = block.closest('pre');
            const lang = preElement?.getAttribute('data-lang') || 
                        preElement?.className.match(/(?:^|\s)language-(\w+)/)?.[1] ||
                        block.className.match(/(?:^|\s)language-(\w+)/)?.[1] || '';
            
            if (lang && hljs.getLanguage(lang)) {
              try {
                block.innerHTML = hljs.highlight(code, { language: lang }).value;
                block.className = `hljs ${block.className}`;
              } catch (err) {
                console.warn('Highlight.js error:', err);
              }
            } else {
              try {
                block.innerHTML = hljs.highlightAuto(code).value;
                block.className = `hljs ${block.className}`;
              } catch (err) {
                console.warn('Highlight.js auto-detect error:', err);
              }
            }
          }
        });
        
        mountProfileBadges();
        mountEmbeddedEvents();
      }, 150);

      return () => clearTimeout(timeoutId);
    });

    return () => cancelAnimationFrame(frameId);
  });

  // Also use MutationObserver to catch any placeholders added later
  // Debounce to prevent excessive re-mounts
  let mutationDebounceTimeout: ReturnType<typeof setTimeout> | null = null;
  
  $effect(() => {
    if (!containerRef) return;

    const observer = new MutationObserver(() => {
      // Debounce mutations to prevent excessive re-mounts
      if (mutationDebounceTimeout) {
        clearTimeout(mutationDebounceTimeout);
      }
      mutationDebounceTimeout = setTimeout(() => {
        mountProfileBadges();
        mountEmbeddedEvents();
        mutationDebounceTimeout = null;
      }, 300); // 300ms debounce
    });

    observer.observe(containerRef, {
      childList: true,
      subtree: true
    });

    return () => {
      observer.disconnect();
      if (mutationDebounceTimeout) {
        clearTimeout(mutationDebounceTimeout);
        mutationDebounceTimeout = null;
      }
    };
  });
</script>

<HighlightOverlay highlights={highlightMatches} content={content} event={event!}>
  <div 
    bind:this={containerRef}
    class="markdown-content prose prose-sm dark:prose-invert max-w-none"
  >
    {@html renderedHtml}
  </div>
</HighlightOverlay>

<style>
  :global(.markdown-content) {
    word-wrap: break-word;
    overflow-wrap: break-word;
  }

  /* Increased paragraph spacing for better readability */
  :global(.markdown-content p) {
    margin-bottom: 1.25em;
    margin-top: 0;
  }

  /* Ensure consistent spacing between paragraphs and other elements */
  :global(.markdown-content p + p) {
    margin-top: 0;
  }

  :global(.markdown-content p + ul),
  :global(.markdown-content p + ol),
  :global(.markdown-content p + blockquote),
  :global(.markdown-content p + pre) {
    margin-top: 1em;
  }

  :global(.markdown-content img):not(.emoji-inline) {
    max-width: 600px;
    width: 100%;
    height: auto;
    border-radius: 0.25rem;
    margin: 0.5rem 0;
    display: block;
    visibility: visible !important;
    opacity: 1 !important;
    /* Content images should be prominent - no grayscale filters */
    filter: none !important;
  }

  :global(.markdown-content img.emoji-inline) {
    display: inline-block;
    margin: 0;
    vertical-align: middle;
    width: 1.6em;
    height: 1.6em;
    object-fit: contain;
    max-width: none; /* Emojis should keep their fixed size */
    /* Emojis should be in full color, no grayscale filter */
  }

  :global(.markdown-content video) {
    max-width: 600px;
    width: 100%;
    height: auto;
    border-radius: 0.25rem;
    margin: 0.5rem 0;
    display: block;
    /* Content videos should be prominent - no grayscale filters */
    filter: none !important;
  }

  :global(.markdown-content audio) {
    max-width: 600px;
    width: 100%;
    margin: 0.5rem 0;
    display: block;
    /* Content audio should be prominent - no grayscale filters */
    filter: none !important;
  }

  :global(.markdown-content a) {
    color: var(--fog-accent, #64748b);
    text-decoration: underline;
  }

  :global(.markdown-content a:hover) {
    text-decoration: none;
  }

  :global(.markdown-content a.hashtag-link) {
    color: var(--fog-accent, #64748b);
    text-decoration: none;
    font-weight: 500;
  }

  :global(.markdown-content a.hashtag-link:hover) {
    text-decoration: underline;
  }

  :global(.dark .markdown-content a.hashtag-link) {
    color: var(--fog-dark-accent, #94a3b8);
  }

  :global(.markdown-content code) {
    background: var(--fog-border, #e5e7eb);
    padding: 0.125rem 0.25rem;
    border-radius: 0.25rem;
    font-size: 0.875em;
  }

  :global(.dark .markdown-content code) {
    background: var(--fog-dark-border, #374151);
  }

  :global(.markdown-content pre) {
    background: var(--fog-border, #e5e7eb);
    padding: 1rem;
    border-radius: 0.5rem;
    overflow-x: auto;
    margin: 0.5rem 0;
  }

  :global(.dark .markdown-content pre) {
    background: var(--fog-dark-border, #374151);
  }

  :global(.markdown-content pre code) {
    background: transparent;
    padding: 0;
  }

  /* IDE-style code block styling - always dark/black background like VS Code/JetBrains */
  :global(.markdown-content pre) {
    background: #1e1e1e !important; /* VS Code dark background */
    border: 1px solid #3e3e3e;
    border-radius: 4px;
    padding: 1rem;
    margin: 1rem 0;
    overflow-x: auto;
    position: relative;
  }

  :global(.markdown-content pre code.hljs) {
    display: block;
    overflow-x: auto;
    padding: 0;
    background: transparent !important;
    color: #d4d4d4; /* VS Code text color */
    font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Droid Sans Mono', 'Source Code Pro', monospace;
    font-size: 0.9em;
    line-height: 1.5;
  }

  /* Inline code - keep light styling but make it subtle */
  :global(.markdown-content code.hljs:not(pre code)) {
    padding: 0.2em 0.4em;
    border-radius: 0.25rem;
    background: var(--fog-border, #e5e7eb);
    color: inherit;
  }

  :global(.dark .markdown-content code.hljs:not(pre code)) {
    background: var(--fog-dark-border, #374151);
  }

  /* Ensure pre blocks always have dark background regardless of theme */
  :global(.markdown-content pre) {
    background: #1e1e1e !important;
    border-color: #3e3e3e !important;
  }

  /* AsciiDoc code blocks - same styling */
  :global(.markdown-content .listingblock pre) {
    background: #1e1e1e !important;
    border: 1px solid #3e3e3e;
    border-radius: 4px;
    padding: 1rem;
    margin: 1rem 0;
    overflow-x: auto;
  }

  :global(.markdown-content .listingblock pre code.hljs) {
    background: transparent !important;
    color: #d4d4d4;
    font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Droid Sans Mono', 'Source Code Pro', monospace;
    font-size: 0.9em;
    line-height: 1.5;
  }

  :global(.markdown-content blockquote) {
    border-left: 4px solid var(--fog-border, #e5e7eb);
    padding-left: 1rem;
    margin: 0.5rem 0;
    color: var(--fog-text-light, #6b7280);
  }

  :global(.dark .markdown-content blockquote) {
    border-color: var(--fog-dark-border, #374151);
    color: var(--fog-dark-text-light, #9ca3af);
  }

  /* Greentext styling - 4chan style */
  :global(.markdown-content .greentext) {
    color: #4a7c3a; /* Deeper, darker green for better readability in light mode */
    display: block;
    margin: 0.25rem 0;
    font-family: inherit;
  }

  :global(.dark .markdown-content .greentext) {
    color: #8fbc8f; /* Lighter green for dark mode */
  }

  /* Ensure greentext lines appear on their own line even if markdown processes them */
  :global(.markdown-content p .greentext),
  :global(.markdown-content .greentext) {
    display: block;
    margin: 0.25rem 0;
  }

  /* Profile badges in markdown content should align with text baseline */
  :global(.markdown-content [data-nostr-profile]),
  :global(.markdown-content .profile-badge) {
    vertical-align: middle;
    display: inline-flex;
    align-items: center;
  }

  /* Ensure profile pictures in markdown content maintain circular shape and don't stretch */
  :global(.markdown-content .profile-badge .profile-picture),
  :global(.markdown-content .profile-badge .profile-placeholder) {
    width: 1.5rem !important;
    height: 1.5rem !important;
    max-width: 1.5rem !important;
    max-height: 1.5rem !important;
    min-width: 1.5rem !important;
    min-height: 1.5rem !important;
    aspect-ratio: 1 / 1 !important;
    border-radius: 50% !important;
    object-fit: cover !important;
    flex-shrink: 0 !important;
  }

  /* Embedded events should be block-level */
  :global(.markdown-content [data-nostr-event]) {
    display: block;
    margin: 1rem 0;
  }

  /* Ensure normal Unicode emojis are displayed correctly */
  :global(.markdown-content) {
    font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Droid Sans Mono', 'Source Code Pro', monospace;
    /* Ensure emojis are not filtered or hidden */
  }

  :global(.markdown-content *) {
    /* Normal emojis (Unicode characters) should not have filters applied */
    /* Only emoji images (custom emojis) should have the grayscale filter */
  }
</style>