import { useSecondaryPage, useSmartHashtagNavigation, useSmartRelayNavigation } from '@/PageManager'
import Image from '@/components/Image'
import MediaPlayer from '@/components/MediaPlayer'
import WebPreview from '@/components/WebPreview'
import YoutubeEmbeddedPlayer from '@/components/YoutubeEmbeddedPlayer'
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
import { toNoteList } from '@/lib/link'
import { useMediaExtraction } from '@/hooks'
import { cleanUrl, isImage, isMedia, isVideo, isAudio, isWebsocketUrl } from '@/lib/url'
import { getImetaInfosFromEvent } from '@/lib/event'
import { Event, kinds } from 'nostr-tools'
import { useMemo, useState, useCallback, useEffect, useRef } from 'react'
import { createPortal } from 'react-dom'
import { createRoot, Root } from 'react-dom/client'
import Lightbox from 'yet-another-react-lightbox'
import Zoom from 'yet-another-react-lightbox/plugins/zoom'
import { EmbeddedNote, EmbeddedMention } from '@/components/Embedded'
import Wikilink from '@/components/UniversalContent/Wikilink'
import { preprocessAsciidocMediaLinks } from '../MarkdownArticle/preprocessMarkup'
import logger from '@/lib/logger'
import katex from 'katex'
import 'katex/dist/katex.min.css'
import { WS_URL_REGEX, YOUTUBE_URL_REGEX } from '@/constants'

/**
 * Truncate link display text to 200 characters, adding ellipsis if truncated
 */
function truncateLinkText(text: string, maxLength: number = 200): string {
  if (text.length <= maxLength) {
    return text
  }
  return text.substring(0, maxLength) + '...'
}

/**
 * Check if a URL is a YouTube URL
 */
function isYouTubeUrl(url: string): boolean {
  // Create a new regex instance to avoid state issues with global regex
  // Keep the 'i' flag for case-insensitivity but remove 'g' to avoid state issues
  const flags = YOUTUBE_URL_REGEX.flags.replace('g', '')
  const regex = new RegExp(YOUTUBE_URL_REGEX.source, flags)
  return regex.test(url)
}

/**
 * Convert markdown syntax to AsciiDoc syntax
 * This converts all markdown elements to their AsciiDoc equivalents before processing
 */
function convertMarkdownToAsciidoc(content: string): string {
  let asciidoc = content
  
  // Note: We don't remove front matter here because the user's content uses --- as horizontal rules
  // If there's actual YAML front matter, it should be handled separately
  // For now, we'll convert --- to horizontal rules (except table separators)
  
  // Convert nostr addresses directly to AsciiDoc link format
  // Do this early so they're protected from other markdown conversions
  // naddr addresses can be 200+ characters, so we use + instead of specific length
  asciidoc = asciidoc.replace(/nostr:(npub1[a-z0-9]{58,}|nprofile1[a-z0-9]+|note1[a-z0-9]{58,}|nevent1[a-z0-9]+|naddr1[a-z0-9]+)/g, (_match, bech32Id) => {
    // Convert directly to AsciiDoc link format
    // This will be processed later in HTML post-processing to render as React components
    return `link:nostr:${bech32Id}[${bech32Id}]`
  })
  
  // Protect code blocks - we'll process them separately
  const codeBlockPlaceholders: string[] = []
  asciidoc = asciidoc.replace(/```(\w+)?\n([\s\S]*?)```/g, (_match, lang, code) => {
    const placeholder = `__CODE_BLOCK_${codeBlockPlaceholders.length}__`
    codeBlockPlaceholders.push(`[source${lang ? ',' + lang : ''}]\n----\n${code.trim()}\n----`)
    return placeholder
  })
  
  // Protect inline code - but handle LaTeX math separately
  const inlineCodePlaceholders: string[] = []
  
  // Handle LaTeX math in inline code blocks like `$...$`
  // The content may have escaped backslashes: `$\\frac{\\infty}{21,000,000} = \\infty$`
  // We need to detect LaTeX math and convert it to AsciiDoc stem: syntax
  asciidoc = asciidoc.replace(/`([^`\n]+)`/g, (_match, content) => {
    // Check if this is LaTeX math - pattern: $...$ where ... contains LaTeX syntax
    // Match the full pattern: $ followed by LaTeX expression and ending with $
    const latexMatch = content.match(/^\$([^$]+)\$$/)
    if (latexMatch) {
      // This is pure LaTeX math - convert to AsciiDoc stem syntax
      const latexExpr = latexMatch[1]
      // The latexExpr contains the LaTeX code (backslashes are already in the string)
      // AsciiDoc stem:[...] will process this with the stem processor
      return `stem:[${latexExpr}]`
    }
    
    // Check if content contains LaTeX math mixed with other text
    if (content.includes('$') && content.match(/\$[^$]+\$/)) {
      // Replace $...$ parts with stem:[...]
      const processed = content.replace(/\$([^$]+)\$/g, 'stem:[$1]')
      // If it's now just stem, return it directly, otherwise it needs to be in code
      if (processed.startsWith('stem:[') && processed.endsWith(']') && !processed.includes('`')) {
        return processed
      }
      // Mixed content - keep as code but with stem inside (won't work well, but preserve it)
      const placeholder = `__INLINE_CODE_${inlineCodePlaceholders.length}__`
      inlineCodePlaceholders.push(`\`${processed}\``)
      return placeholder
    }
    
    // Regular inline code - preserve it
    const placeholder = `__INLINE_CODE_${inlineCodePlaceholders.length}__`
    inlineCodePlaceholders.push(`\`${content}\``)
    return placeholder
  })
  
  // Convert headers (must be at start of line)
  asciidoc = asciidoc.replace(/^#{6}\s+(.+)$/gm, '====== $1 ======')
  asciidoc = asciidoc.replace(/^#{5}\s+(.+)$/gm, '===== $1 =====')
  asciidoc = asciidoc.replace(/^#{4}\s+(.+)$/gm, '==== $1 ====')
  asciidoc = asciidoc.replace(/^#{3}\s+(.+)$/gm, '=== $1 ===')
  asciidoc = asciidoc.replace(/^#{2}\s+(.+)$/gm, '== $1 ==')
  asciidoc = asciidoc.replace(/^#{1}\s+(.+)$/gm, '= $1 =')
  
  // Convert tables BEFORE horizontal rules (to avoid converting table separators)
  // Markdown tables: | col1 | col2 |\n|------|------|\n| data1 | data2 |
  // Use a simpler approach: match lines with pipes, separator row, and data rows
  asciidoc = asciidoc.replace(/(\|[^\n]+\|\s*\n\|[\s\-\|:]+\|\s*\n(?:\|[^\n]+\|\s*\n?)+)/gm, (match) => {
    const lines = match.trim().split('\n').map(line => line.trim()).filter(line => line)
    if (lines.length < 2) return match
    
    // First line is header, second is separator, rest are data
    const headerRow = lines[0]
    const separatorRow = lines[1]
    
    // Verify it's a table separator (has dashes)
    if (!separatorRow.match(/[\-:]/)) return match
    
    // Parse header cells - markdown format: | col1 | col2 | col3 |
    // When split by |, we get: ['', ' col1 ', ' col2 ', ' col3 ', '']
    // We need to extract all non-empty cells
    const headerParts = headerRow.split('|')
    const headerCells: string[] = []
    for (let i = 0; i < headerParts.length; i++) {
      const cell = headerParts[i].trim()
      // Skip empty cells only at the very start and end
      if (cell === '' && (i === 0 || i === headerParts.length - 1)) continue
      headerCells.push(cell)
    }
    
    if (headerCells.length < 2) return match
    
    const colCount = headerCells.length
    const dataRows = lines.slice(2)
    
    // Build AsciiDoc table - use equal width columns
    let tableAsciidoc = `[cols="${Array(colCount).fill('*').join(',')}"]\n|===\n`
    
    // Header row - prefix each cell with . to make it a header cell in AsciiDoc
    // Ensure cells are properly formatted (no leading/trailing spaces, escape special chars)
    const headerRowCells = headerCells.map(cell => {
      // Clean up the cell content
      let cleanCell = cell.trim()
      // Escape pipe characters if any
      cleanCell = cleanCell.replace(/\|/g, '\\|')
      // Return with . prefix for header
      return `.${cleanCell}`
    })
    tableAsciidoc += headerRowCells.join('|') + '\n\n'
    
    // Data rows
    dataRows.forEach(row => {
      if (!row.includes('|')) return
      const rowParts = row.split('|')
      const rowCells: string[] = []
      
      // Parse data row cells the same way as header
      for (let i = 0; i < rowParts.length; i++) {
        const cell = rowParts[i].trim()
        // Skip empty cells only at the very start and end
        if (cell === '' && (i === 0 || i === rowParts.length - 1)) continue
        rowCells.push(cell)
      }
      
      // Ensure we have the right number of cells
      while (rowCells.length < colCount) {
        rowCells.push('')
      }
      
      // Take only the number of columns we need
      const finalCells = rowCells.slice(0, colCount)
      tableAsciidoc += finalCells.map(cell => cell.replace(/\|/g, '\\|')).join('|') + '\n'
    })
    
    tableAsciidoc += '|==='
    return tableAsciidoc
  })
  
  // Convert horizontal rules (but not table separators, which are already processed)
  // Convert standalone --- lines to AsciiDoc horizontal rule
  // We do this after table processing to avoid interfering with table separators
  asciidoc = asciidoc.replace(/^---\s*$/gm, (match, offset, string) => {
    // Check if this is part of a table separator (would have been processed already)
    const lines = string.split('\n')
    const lineIndex = string.substring(0, offset).split('\n').length - 1
    const prevLine = lines[lineIndex - 1]?.trim() || ''
    const nextLine = lines[lineIndex + 1]?.trim() || ''
    
    // If it looks like a table separator (has pipes nearby), don't convert
    if (prevLine.includes('|') || nextLine.includes('|')) {
      return match
    }
    
    // Convert to AsciiDoc horizontal rule (three single quotes)
    return '\'\'\''
  })
  
  // Convert blockquotes - handle multi-line blockquotes
  // Match consecutive lines starting with >
  asciidoc = asciidoc.replace(/(^>\s+.+(?:\n>\s+.+)*)/gm, (match) => {
    const lines = match.split('\n').map((line: string) => line.replace(/^>\s*/, ''))
    const content = lines.join('\n').trim()
    return `____\n${content}\n____`
  })
  
  // Convert lists (must be at start of line)
  // Unordered lists: *, -, +
  asciidoc = asciidoc.replace(/^(\s*)[\*\-\+]\s+(.+)$/gm, '$1* $2')
  // Ordered lists: 1., 2., etc.
  asciidoc = asciidoc.replace(/^(\s*)\d+\.\s+(.+)$/gm, '$1. $2')
  
  // Convert images: ![alt](url) -> image:url[alt] (single colon for inline, but AsciiDoc will render as block)
  // For block images in AsciiDoc, we can use image:: or just ensure it's on its own line
  asciidoc = asciidoc.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_match, alt, url) => {
    // Escape brackets in alt text and URL if needed
    const escapedAlt = alt.replace(/\[/g, '\\[').replace(/\]/g, '\\]').replace(/"/g, '&quot;')
    // Use image:: for block-level images (double colon)
    // Add width attribute to make it responsive
    return `image::${url}[${escapedAlt},width=100%]`
  })
  
  // Convert links: [text](url) -> link:url[text]
  asciidoc = asciidoc.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => {
    // Skip if it was an image (shouldn't happen after image conversion, but safety check)
    if (match.startsWith('![')) return match
    // Escape brackets in link text
    const escapedText = text.replace(/\[/g, '\\[').replace(/\]/g, '\\]')
    return `link:${url}[${escapedText}]`
  })
  
  // Nostr addresses are already converted to link: format above, no need to restore
  
  // Convert strikethrough: ~~text~~ -> [line-through]#text#
  // Also handle single tilde strikethrough: ~text~ -> [line-through]#text#
  asciidoc = asciidoc.replace(/~~([^~\n]+?)~~/g, '[line-through]#$1#')
  // Single tilde strikethrough (common in some markdown flavors)
  asciidoc = asciidoc.replace(/(?<!~)~([^~\n]+?)~(?!~)/g, '[line-through]#$1#')
  
  // Note: Subscript ~text~ is now handled as strikethrough above
  // If you need subscript, use a different syntax or handle it differently
  
  // Convert superscript: ^text^
  asciidoc = asciidoc.replace(/\^([^\^\n]+?)\^/g, '[superscript]#$1#')
  
  // Convert bold: **text** or __text__
  asciidoc = asciidoc.replace(/\*\*([^*\n]+?)\*\*/g, '*$1*')
  asciidoc = asciidoc.replace(/__(?!_)([^_\n]+?)(?<!_)__/g, '*$1*')
  
  // Convert italic: *text* or _text_ (but not if already bold)
  // Process single asterisk for italic (but not if it's part of **bold**)
  asciidoc = asciidoc.replace(/(?<!\*)\*(?![\*\s])([^\*\n]+?)(?<!\*)\*(?!\*)/g, (match, text) => {
    // Skip if it looks like a list item
    if (/^\s*\*\s/.test(match)) return match
    // Skip if already processed as bold (shouldn't happen, but safety)
    if (match.includes('*$1*')) return match
    return `_${text}_`
  })
  // Process single underscore for italic
  asciidoc = asciidoc.replace(/(?<!_)_(?!_)([^_\n]+?)(?<!_)_(?!_)/g, (match, text) => {
    // Skip if already processed as bold
    if (match.includes('*$1*')) return match
    return `_${text}_`
  })
  
  // Restore inline code
  inlineCodePlaceholders.forEach((code, index) => {
    asciidoc = asciidoc.replace(`__INLINE_CODE_${index}__`, code)
  })
  
  // Restore code blocks
  codeBlockPlaceholders.forEach((block, index) => {
    asciidoc = asciidoc.replace(`__CODE_BLOCK_${index}__`, block)
  })
  
  return asciidoc
}

export default function AsciidocArticle({
  event,
  className,
  hideImagesAndInfo = false
}: {
  event: Event
  className?: string
  hideImagesAndInfo?: boolean
}) {
  const { push } = useSecondaryPage()
  const { navigateToHashtag } = useSmartHashtagNavigation()
  const { navigateToRelay } = useSmartRelayNavigation()
  const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
  const contentRef = useRef<HTMLDivElement>(null)
  
  // Preprocess content: convert all markdown to AsciiDoc syntax
  const processedContent = useMemo(() => {
    let content = event.content
    
    // Convert all markdown syntax to AsciiDoc syntax
    content = convertMarkdownToAsciidoc(content)
    
    // Now process raw URLs that aren't already in AsciiDoc syntax
    content = preprocessAsciidocMediaLinks(content)
    
    // Convert "Read naddr... instead." patterns to AsciiDoc links
    const redirectRegex = /Read (naddr1[a-z0-9]+) instead\./gi
    content = content.replace(redirectRegex, (_match, naddr) => {
      return `Read link:/notes/${naddr}[${naddr}] instead.`
    })
    
    return content
  }, [event.content])
  
  // Extract all media from event
  const extractedMedia = useMediaExtraction(event, event.content)
  
  // Extract media from tags only (for display at top)
  const tagMedia = useMemo(() => {
    const seenUrls = new Set<string>()
    const media: Array<{ url: string; type: 'image' | 'video' | 'audio'; poster?: string }> = []
    
    // Extract from imeta tags
    const imetaInfos = getImetaInfosFromEvent(event)
    imetaInfos.forEach((info) => {
      const cleaned = cleanUrl(info.url)
      if (!cleaned || seenUrls.has(cleaned)) return
      if (!isImage(cleaned) && !isMedia(cleaned)) return
      
      seenUrls.add(cleaned)
      if (info.m?.startsWith('image/') || isImage(cleaned)) {
        media.push({ url: info.url, type: 'image' })
      } else if (info.m?.startsWith('video/') || isVideo(cleaned)) {
        media.push({ url: info.url, type: 'video', poster: info.image })
      } else if (info.m?.startsWith('audio/') || isAudio(cleaned)) {
        media.push({ url: info.url, type: 'audio' })
      }
    })
    
    // Extract from r tags
    event.tags.filter(tag => tag[0] === 'r' && tag[1]).forEach(tag => {
      const url = tag[1]
      const cleaned = cleanUrl(url)
      if (!cleaned || seenUrls.has(cleaned)) return
      if (!isImage(cleaned) && !isMedia(cleaned)) return
      
      seenUrls.add(cleaned)
      if (isImage(cleaned)) {
        media.push({ url, type: 'image' })
      } else if (isVideo(cleaned)) {
        media.push({ url, type: 'video' })
      } else if (isAudio(cleaned)) {
        media.push({ url, type: 'audio' })
      }
    })
    
    // Extract from image tag
    const imageTag = event.tags.find(tag => tag[0] === 'image' && tag[1])
    if (imageTag?.[1]) {
      const cleaned = cleanUrl(imageTag[1])
      if (cleaned && !seenUrls.has(cleaned) && isImage(cleaned)) {
        seenUrls.add(cleaned)
        media.push({ url: imageTag[1], type: 'image' })
      }
    }
    
    return media
  }, [event.id, JSON.stringify(event.tags)])
  
  // Extract YouTube URLs from tags (for display at top)
  const tagYouTubeUrls = useMemo(() => {
    const youtubeUrls: string[] = []
    const seenUrls = new Set<string>()
    
    event.tags
      .filter(tag => tag[0] === 'r' && tag[1])
      .forEach(tag => {
        const url = tag[1]
        if (!url.startsWith('http://') && !url.startsWith('https://')) return
        if (!isYouTubeUrl(url)) return
        
        const cleaned = cleanUrl(url)
        if (cleaned && !seenUrls.has(cleaned)) {
          youtubeUrls.push(cleaned)
          seenUrls.add(cleaned)
        }
      })
    
    return youtubeUrls
  }, [event.id, JSON.stringify(event.tags)])
  
  // Extract non-media links from tags (excluding YouTube URLs)
  const tagLinks = useMemo(() => {
    const links: string[] = []
    const seenUrls = new Set<string>()
    
    event.tags
      .filter(tag => tag[0] === 'r' && tag[1])
      .forEach(tag => {
        const url = tag[1]
        if (!url.startsWith('http://') && !url.startsWith('https://')) return
        if (isImage(url) || isMedia(url)) return
        if (isYouTubeUrl(url)) return // Exclude YouTube URLs
        
        const cleaned = cleanUrl(url)
        if (cleaned && !seenUrls.has(cleaned)) {
          links.push(cleaned)
          seenUrls.add(cleaned)
        }
      })
    
    return links
  }, [event.id, JSON.stringify(event.tags)])
  
  // Get all images for gallery (deduplicated)
  const allImages = useMemo(() => {
    const seenUrls = new Set<string>()
    const images: Array<{ url: string; alt?: string }> = []
    
    // Add images from extractedMedia
    extractedMedia.images.forEach(img => {
      const cleaned = cleanUrl(img.url)
      if (cleaned && !seenUrls.has(cleaned)) {
        seenUrls.add(cleaned)
        images.push({ url: img.url, alt: img.alt })
      }
    })
    
    // Add metadata image if it exists
    if (metadata.image) {
      const cleaned = cleanUrl(metadata.image)
      if (cleaned && !seenUrls.has(cleaned) && isImage(cleaned)) {
        seenUrls.add(cleaned)
        images.push({ url: metadata.image })
      }
    }
    
    return images
  }, [extractedMedia.images, metadata.image])
  
  // Create image index map for lightbox
  const imageIndexMap = useMemo(() => {
    const map = new Map<string, number>()
    allImages.forEach((img, index) => {
      const cleaned = cleanUrl(img.url)
      if (cleaned) map.set(cleaned, index)
    })
    return map
  }, [allImages])
  
  // Parse content to find media URLs that are already rendered
  const mediaUrlsInContent = useMemo(() => {
    const urls = new Set<string>()
    const urlRegex = /https?:\/\/[^\s<>"']+/g
    let match
    while ((match = urlRegex.exec(event.content)) !== null) {
      const url = match[0]
      const cleaned = cleanUrl(url)
      if (cleaned && (isImage(cleaned) || isVideo(cleaned) || isAudio(cleaned))) {
        urls.add(cleaned)
      }
    }
    return urls
  }, [event.content])
  
  // Extract YouTube URLs from content
  const youtubeUrlsInContent = useMemo(() => {
    const urls = new Set<string>()
    const urlRegex = /https?:\/\/[^\s<>"']+/g
    let match
    while ((match = urlRegex.exec(event.content)) !== null) {
      const url = match[0]
      const cleaned = cleanUrl(url)
      if (cleaned && isYouTubeUrl(cleaned)) {
        urls.add(cleaned)
      }
    }
    return urls
  }, [event.content])
  
  // Extract non-media links from content (excluding YouTube URLs)
  const contentLinks = useMemo(() => {
    const links: string[] = []
    const seenUrls = new Set<string>()
    const urlRegex = /https?:\/\/[^\s<>"']+/g
    let match
    while ((match = urlRegex.exec(event.content)) !== null) {
      const url = match[0]
      if ((url.startsWith('http://') || url.startsWith('https://')) && !isImage(url) && !isMedia(url) && !isYouTubeUrl(url)) {
        const cleaned = cleanUrl(url)
        if (cleaned && !seenUrls.has(cleaned)) {
          links.push(cleaned)
          seenUrls.add(cleaned)
        }
      }
    }
    return links
  }, [event.content])
  
  // Image gallery state
  const [lightboxIndex, setLightboxIndex] = useState(-1)
  
  const openLightbox = useCallback((index: number) => {
    setLightboxIndex(index)
  }, [])
  
  // Filter tag media to only show what's not in content
  const leftoverTagMedia = useMemo(() => {
    const metadataImageUrl = metadata.image ? cleanUrl(metadata.image) : null
    return tagMedia.filter(media => {
      const cleaned = cleanUrl(media.url)
      if (!cleaned) return false
      // Skip if already in content
      if (mediaUrlsInContent.has(cleaned)) return false
      // Skip if this is the metadata image (shown separately)
      if (metadataImageUrl && cleaned === metadataImageUrl && !hideImagesAndInfo) return false
      return true
    })
  }, [tagMedia, mediaUrlsInContent, metadata.image, hideImagesAndInfo])
  
  // Filter tag YouTube URLs to only show what's not in content
  const leftoverTagYouTubeUrls = useMemo(() => {
    return tagYouTubeUrls.filter(url => {
      const cleaned = cleanUrl(url)
      return cleaned && !youtubeUrlsInContent.has(cleaned)
    })
  }, [tagYouTubeUrls, youtubeUrlsInContent])
  
  // Filter tag links to only show what's not in content (to avoid duplicate WebPreview cards)
  const leftoverTagLinks = useMemo(() => {
    const contentLinksSet = new Set(contentLinks.map(link => cleanUrl(link)).filter(Boolean))
    return tagLinks.filter(link => {
      const cleaned = cleanUrl(link)
      return cleaned && !contentLinksSet.has(cleaned)
    })
  }, [tagLinks, contentLinks])
  
  // Extract hashtags from content (for deduplication with metadata tags)
  const hashtagsInContent = useMemo(() => {
    const tags = new Set<string>()
    const hashtagRegex = /#([a-zA-Z0-9_]+)/g
    let match
    while ((match = hashtagRegex.exec(event.content)) !== null) {
      tags.add(match[1].toLowerCase())
    }
    return tags
  }, [event.content])
  
  // Filter metadata tags to only show what's not already in content
  const leftoverMetadataTags = useMemo(() => {
    return metadata.tags.filter(tag => !hashtagsInContent.has(tag.toLowerCase()))
  }, [metadata.tags, hashtagsInContent])
  
  // Parse AsciiDoc content and post-process for nostr: links and hashtags
  const [parsedHtml, setParsedHtml] = useState<string>('')
  const [isLoading, setIsLoading] = useState(true)
  
  useEffect(() => {
    let cancelled = false
    
    const parseAsciidoc = async () => {
      setIsLoading(true)
      try {
        const Asciidoctor = await import('@asciidoctor/core')
        const asciidoctor = Asciidoctor.default()
        
        if (cancelled) return
        
        const html = asciidoctor.convert(processedContent, {
          safe: 'safe',
          backend: 'html5',
          doctype: 'article',
          attributes: {
            'showtitle': true,
            'sectanchors': true,
            'sectlinks': true,
            'toc': 'left',
            'toclevels': 6,
            'toc-title': 'Table of Contents',
            'source-highlighter': 'highlight.js',
            'stem': 'latexmath',
            'data-uri': true,
            'imagesdir': '',
            'linkcss': false,
            'stylesheet': '',
            'stylesdir': '',
            'prewrap': true,
            'sectnums': false,
            'sectnumlevels': 6,
            'experimental': true,
            'compat-mode': false,
            'attribute-missing': 'warn',
            'attribute-undefined': 'warn',
            'skip-front-matter': true
          }
        })
        
        if (cancelled) return
        
        let htmlString = typeof html === 'string' ? html : html.toString()
        
        // Note: Markdown is now converted to AsciiDoc in preprocessing,
        // so post-processing markdown should not be necessary
        
        // Post-process HTML to handle nostr: links
        // Mentions (npub/nprofile) should be inline, events (note/nevent/naddr) should be block-level
        // First, handle nostr: links in <a> tags (from AsciiDoc link: syntax)
        // Match the full bech32 address format - addresses can vary in length
        // npub: 58 chars, nprofile: variable, note: 58 chars, nevent: variable, naddr: 200+ chars
        // Use a more flexible pattern that matches any valid bech32 address
        htmlString = htmlString.replace(/<a[^>]*href=["']nostr:((?:npub1|nprofile1|note1|nevent1|naddr1)[a-z0-9]{20,})["'][^>]*>([^<]*)<\/a>/gi, (_match, bech32Id, _linkText) => {
          // Validate bech32 ID and create appropriate placeholder
          if (!bech32Id) return _match
          
          // Escape the bech32 ID for HTML attributes
          const escapedId = bech32Id.replace(/"/g, '&quot;').replace(/'/g, '&#39;')
          
          if (bech32Id.startsWith('npub') || bech32Id.startsWith('nprofile')) {
            return `<span data-nostr-mention="${escapedId}" class="nostr-mention-placeholder"></span>`
          } else if (bech32Id.startsWith('note') || bech32Id.startsWith('nevent') || bech32Id.startsWith('naddr')) {
            return `<div data-nostr-note="${escapedId}" class="nostr-note-placeholder"></div>`
          }
          return _match
        })
        
        // Also handle nostr: addresses in plain text nodes (not already in <a> tags)
        // Process text nodes by replacing content between > and <
        // Use more flexible regex that matches any valid bech32 address
        htmlString = htmlString.replace(/>([^<]*nostr:((?:npub1|nprofile1|note1|nevent1|naddr1)[a-z0-9]+)[^<]*)</g, (_match, textContent) => {
          // Extract nostr addresses from the text content - use the same flexible pattern
          const nostrRegex = /nostr:((?:npub1|nprofile1|note1|nevent1|naddr1)[a-z0-9]+)/g
          let processedText = textContent
          const replacements: Array<{ start: number; end: number; replacement: string }> = []
          
          let m
          while ((m = nostrRegex.exec(textContent)) !== null) {
            const bech32Id = m[1]
            const start = m.index
            const end = m.index + m[0].length
            
            if (bech32Id.startsWith('npub') || bech32Id.startsWith('nprofile')) {
              replacements.push({
                start,
                end,
                replacement: `<span data-nostr-mention="${bech32Id}" class="nostr-mention-placeholder"></span>`
              })
            } else if (bech32Id.startsWith('note') || bech32Id.startsWith('nevent') || bech32Id.startsWith('naddr')) {
              replacements.push({
                start,
                end,
                replacement: `<div data-nostr-note="${bech32Id}" class="nostr-note-placeholder"></div>`
              })
            }
          }
          
          // Apply replacements in reverse order to preserve indices
          for (let i = replacements.length - 1; i >= 0; i--) {
            const r = replacements[i]
            processedText = processedText.substring(0, r.start) + r.replacement + processedText.substring(r.end)
          }
          
          return `>${processedText}<`
        })
        
        // Handle LaTeX math expressions from AsciiDoc stem processor
        // AsciiDoc with stem: latexmath outputs \(...\) for inline and \[...\] for block math
        // In HTML, these appear as literal \( and \) characters (backslash + parenthesis)
        // We need to match the literal backslash-paren sequence
        // In regex: \\ matches a literal backslash, \( matches a literal (
        htmlString = htmlString.replace(/\\\(([^)]+?)\\\)/g, (_match, latex) => {
          // Inline math - escape for HTML attribute
          // Unescape any HTML entities that might have been created
          const unescaped = latex.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, '&')
          const escaped = unescaped.replace(/"/g, '&quot;').replace(/'/g, '&#39;')
          return `<span data-latex-inline="${escaped}" class="latex-inline-placeholder"></span>`
        })
        htmlString = htmlString.replace(/\\\[([^\]]+?)\\\]/g, (_match, latex) => {
          // Block math - escape for HTML attribute
          const unescaped = latex.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, '&')
          const escaped = unescaped.replace(/"/g, '&quot;').replace(/'/g, '&#39;')
          return `<div data-latex-block="${escaped}" class="latex-block-placeholder my-4"></div>`
        })
        
        // Handle wikilinks - convert passthrough markers to placeholders
        // AsciiDoc passthrough +++WIKILINK:link|display+++ outputs just WIKILINK:link|display in HTML
        // Match WIKILINK: followed by any characters (including |) until end of text or HTML tag
        htmlString = htmlString.replace(/WIKILINK:([^<>\s]+)/g, (_match, linkContent) => {
          // Escape special characters for HTML attributes
          const escaped = linkContent.replace(/"/g, '&quot;').replace(/'/g, '&#39;')
          return `<span data-wikilink="${escaped}" class="wikilink-placeholder"></span>`
        })
        
        // Handle YouTube URLs and relay URLs in links
        // Process all link matches first to determine which are standalone
        const linkMatches: Array<{ match: string; href: string; linkText: string; index: number; isStandalone: boolean }> = []
        const linkRegex = /<a[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/g
        let linkMatch
        while ((linkMatch = linkRegex.exec(htmlString)) !== null) {
          const match = linkMatch[0]
          const href = linkMatch[1]
          const linkText = linkMatch[2]
          const index = linkMatch.index
          
          // Check if link is standalone (on its own line, not part of a sentence/list/quote)
          let isStandalone = false
          if (href.startsWith('http://') || href.startsWith('https://')) {
            // Get context around the link
            const beforeMatch = htmlString.substring(Math.max(0, index - 500), index)
            const afterMatch = htmlString.substring(index + match.length, Math.min(htmlString.length, index + match.length + 500))
            
            // Extract the parent paragraph/div content
            const paragraphMatch = beforeMatch.match(/<p[^>]*>([^<]*)$/)
            const divMatch = beforeMatch.match(/<div[^>]*>([^<]*)$/)
            
            // If link is in a paragraph, check if paragraph contains only the link
            if (paragraphMatch) {
              const paragraphEnd = afterMatch.match(/^([^<]*)<\/p>/)
              const paragraphContent = paragraphMatch[1] + linkText + (paragraphEnd?.[1] || '')
              const trimmedContent = paragraphContent.trim()
              // If paragraph contains only the link (possibly with whitespace), it's standalone
              if (trimmedContent === linkText.trim() || trimmedContent === '') {
                // Check if it's in a list or blockquote by looking further back
                const contextBefore = htmlString.substring(Math.max(0, index - 1000), index)
                if (!contextBefore.match(/<[uo]l[^>]*>/) && !contextBefore.match(/<blockquote[^>]*>/)) {
                  isStandalone = true
                }
              }
            }
            
            // If link is in a div and the div contains only the link, it's standalone
            if (!isStandalone && divMatch) {
              const divEnd = afterMatch.match(/^([^<]*)<\/div>/)
              const divContent = divMatch[1] + linkText + (divEnd?.[1] || '')
              const trimmedContent = divContent.trim()
              if (trimmedContent === linkText.trim() || trimmedContent === '') {
                const contextBefore = htmlString.substring(Math.max(0, index - 1000), index)
                if (!contextBefore.match(/<[uo]l[^>]*>/) && !contextBefore.match(/<blockquote[^>]*>/)) {
                  isStandalone = true
                }
              }
            }
            
            // If link appears to be on its own line (surrounded by block-level tags or whitespace)
            if (!isStandalone) {
              const beforeTrimmed = beforeMatch.replace(/\s*$/, '')
              const afterTrimmed = afterMatch.replace(/^\s*/, '')
              if (
                (beforeTrimmed.endsWith('</p>') || beforeTrimmed.endsWith('</div>') || beforeTrimmed.endsWith('<br') || beforeTrimmed === '') &&
                (afterTrimmed.startsWith('</p>') || afterTrimmed.startsWith('</div>') || afterTrimmed.startsWith('<p') || afterTrimmed.startsWith('<div') || afterTrimmed === '')
              ) {
                const contextBefore = htmlString.substring(Math.max(0, index - 1000), index)
                if (!contextBefore.match(/<[uo]l[^>]*>/) && !contextBefore.match(/<blockquote[^>]*>/)) {
                  isStandalone = true
                }
              }
            }
          }
          
          linkMatches.push({ match, href, linkText, index, isStandalone })
        }
        
        // Replace links in reverse order to preserve indices
        for (let i = linkMatches.length - 1; i >= 0; i--) {
          const { match, href, linkText, isStandalone } = linkMatches[i]
          let replacement = match
          
          // Check if the href is a YouTube URL
          if (isYouTubeUrl(href)) {
            const cleanedUrl = cleanUrl(href)
            replacement = `<div data-youtube-url="${cleanedUrl.replace(/"/g, '&quot;')}" class="youtube-placeholder my-2"></div>`
          }
          // Check if the href is a relay URL
          else if (isWebsocketUrl(href)) {
            const relayPath = `/relays/${encodeURIComponent(href)}`
            replacement = `<a href="${relayPath}" class="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words cursor-pointer" data-relay-url="${href}" data-original-text="${linkText.replace(/"/g, '&quot;')}">${linkText}</a>`
          }
          // For regular HTTP/HTTPS links, check if standalone
          else if (href.startsWith('http://') || href.startsWith('https://')) {
            if (isStandalone) {
              // Standalone link - render as WebPreview
              const cleanedUrl = cleanUrl(href)
              replacement = `<div data-webpreview-url="${cleanedUrl.replace(/"/g, '&quot;')}" class="webpreview-placeholder my-2"></div>`
            } else {
              // Inline link - keep as regular link
              const escapedLinkText = linkText.replace(/"/g, '&quot;')
              replacement = `<a href="${href}" class="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words" target="_blank" rel="noopener noreferrer" data-original-text="${escapedLinkText}">${linkText}</a>`
            }
          }
          // For other links (like relative links), keep as-is but add data attribute
          else {
            const escapedLinkText = linkText.replace(/"/g, '&quot;')
            replacement = match.replace(/<a/, `<a data-original-text="${escapedLinkText}"`)
          }
          
          htmlString = htmlString.substring(0, linkMatches[i].index) + replacement + htmlString.substring(linkMatches[i].index + match.length)
        }
        
        // Handle YouTube URLs in plain text (not in <a> tags)
        // Create a new regex instance to avoid state issues
        const youtubeRegex = new RegExp(YOUTUBE_URL_REGEX.source, YOUTUBE_URL_REGEX.flags)
        htmlString = htmlString.replace(youtubeRegex, (match) => {
          // Only replace if not already in a tag (basic check)
          if (!match.includes('<') && !match.includes('>') && isYouTubeUrl(match)) {
            const cleanedUrl = cleanUrl(match)
            return `<div data-youtube-url="${cleanedUrl.replace(/"/g, '&quot;')}" class="youtube-placeholder my-2"></div>`
          }
          return match
        })
        
        // Handle relay URLs in plain text (not in <a> tags) - convert to relay page links
        htmlString = htmlString.replace(WS_URL_REGEX, (match) => {
          // Only replace if not already in a tag (basic check)
          if (!match.includes('<') && !match.includes('>') && isWebsocketUrl(match)) {
            const relayPath = `/relays/${encodeURIComponent(match)}`
            return `<a href="${relayPath}" class="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words cursor-pointer" data-relay-url="${match}" data-original-text="${match.replace(/"/g, '&quot;')}">${match}</a>`
          }
          return match
        })
        
        // Handle plain HTTP/HTTPS URLs in text (not in <a> tags, not YouTube, not relay) - convert to WebPreview placeholders
        const httpUrlRegex = /https?:\/\/[^\s<>"']+/g
        htmlString = htmlString.replace(httpUrlRegex, (match) => {
          // Only replace if not already in a tag (basic check)
          if (!match.includes('<') && !match.includes('>')) {
            // Skip if it's a YouTube URL or relay URL (already handled)
            if (isYouTubeUrl(match) || isWebsocketUrl(match)) {
              return match
            }
            // Skip if it's an image or media URL (handled separately)
            if (isImage(match) || isVideo(match) || isAudio(match)) {
              return match
            }
            const cleanedUrl = cleanUrl(match)
            return `<div data-webpreview-url="${cleanedUrl.replace(/"/g, '&quot;')}" class="webpreview-placeholder my-2"></div>`
          }
          return match
        })
        
        setParsedHtml(htmlString)
      } catch (error) {
        logger.error('Failed to parse AsciiDoc', error as Error)
        setParsedHtml('<p>Error parsing AsciiDoc content</p>')
      } finally {
        if (!cancelled) {
          setIsLoading(false)
        }
      }
    }
    
    parseAsciidoc()
    
    return () => {
      cancelled = true
    }
  }, [processedContent])
  
  // Store React roots for cleanup
  const reactRootsRef = useRef<Map<Element, Root>>(new Map())
  
  // Post-process rendered HTML to inject React components for nostr: links and handle hashtags
  useEffect(() => {
    if (!contentRef.current || !parsedHtml || isLoading) return
    
    // Clean up previous roots
    reactRootsRef.current.forEach((root, element) => {
      root.unmount()
      reactRootsRef.current.delete(element)
    })
    
    // Process nostr: mentions - replace placeholders with React components (inline)
    const nostrMentions = contentRef.current.querySelectorAll('.nostr-mention-placeholder[data-nostr-mention]')
    nostrMentions.forEach((element) => {
      const bech32Id = element.getAttribute('data-nostr-mention')
      if (!bech32Id) {
        logger.warn('Nostr mention placeholder found but no bech32Id attribute')
        return
      }
      
      // Create an inline container for React component (mentions should be inline)
      const container = document.createElement('span')
      container.className = 'inline-block'
      const parent = element.parentNode
      if (!parent) {
        logger.warn('Nostr mention placeholder has no parent node')
        return
      }
      parent.replaceChild(container, element)
      
      // Use React to render the component
      const root = createRoot(container)
      root.render(<EmbeddedMention userId={bech32Id} />)
      reactRootsRef.current.set(container, root)
    })
    
    // Process nostr: notes - replace placeholders with React components
    const nostrNotes = contentRef.current.querySelectorAll('.nostr-note-placeholder[data-nostr-note]')
    nostrNotes.forEach((element) => {
      const bech32Id = element.getAttribute('data-nostr-note')
      if (!bech32Id) {
        logger.warn('Nostr note placeholder found but no bech32Id attribute')
        return
      }
      
      // Create a block-level container for React component that fills width
      const container = document.createElement('div')
      container.className = 'w-full my-2'
      const parent = element.parentNode
      if (!parent) {
        logger.warn('Nostr note placeholder has no parent node')
        return
      }
      parent.replaceChild(container, element)
      
      // Use React to render the component
      const root = createRoot(container)
      root.render(<EmbeddedNote noteId={bech32Id} />)
      reactRootsRef.current.set(container, root)
    })
    
    // Process LaTeX math expressions - render with KaTeX
    const latexInlinePlaceholders = contentRef.current.querySelectorAll('.latex-inline-placeholder[data-latex-inline]')
    latexInlinePlaceholders.forEach((element) => {
      const latex = element.getAttribute('data-latex-inline')
      if (!latex) return
      
      try {
        // Render LaTeX with KaTeX
        const rendered = katex.renderToString(latex, {
          throwOnError: false,
          displayMode: false
        })
        // Replace the placeholder with the rendered HTML
        element.outerHTML = rendered
      } catch (error) {
        logger.error('Error rendering LaTeX inline math:', error)
        // On error, show the raw LaTeX
        element.outerHTML = `<span>$${latex}$</span>`
      }
    })
    
    const latexBlockPlaceholders = contentRef.current.querySelectorAll('.latex-block-placeholder[data-latex-block]')
    latexBlockPlaceholders.forEach((element) => {
      const latex = element.getAttribute('data-latex-block')
      if (!latex) return
      
      try {
        // Render LaTeX with KaTeX in display mode
        const rendered = katex.renderToString(latex, {
          throwOnError: false,
          displayMode: true
        })
        // Replace the placeholder with the rendered HTML
        element.outerHTML = rendered
      } catch (error) {
        logger.error('Error rendering LaTeX block math:', error)
        // On error, show the raw LaTeX
        element.outerHTML = `<div>$$${latex}$$</div>`
      }
    })
    
    // Process YouTube URLs - replace placeholders with React components
    const youtubePlaceholders = contentRef.current.querySelectorAll('.youtube-placeholder[data-youtube-url]')
    youtubePlaceholders.forEach((element) => {
      const youtubeUrl = element.getAttribute('data-youtube-url')
      if (!youtubeUrl) return
      
      // Create a container for React component
      const container = document.createElement('div')
      container.className = 'my-2'
      element.parentNode?.replaceChild(container, element)
      
      // Use React to render the component
      const root = createRoot(container)
      root.render(<YoutubeEmbeddedPlayer url={youtubeUrl} className="max-w-[400px]" mustLoad={false} />)
      reactRootsRef.current.set(container, root)
    })
    
    // Process wikilinks - replace placeholders with React components
    const wikilinks = contentRef.current.querySelectorAll('.wikilink-placeholder[data-wikilink]')
    wikilinks.forEach((element) => {
      const linkContent = element.getAttribute('data-wikilink')
      if (!linkContent) return
      
      // Parse wikilink: extract target and display text
      let target = linkContent.includes('|') ? linkContent.split('|')[0].trim() : linkContent.trim()
      let displayText = linkContent.includes('|') ? linkContent.split('|')[1].trim() : linkContent.trim()
      
      // Handle book: prefix
      if (linkContent.startsWith('book:')) {
        target = linkContent.replace('book:', '').trim()
      }
      
      // Convert to d-tag format (same as MarkdownArticle)
      const dtag = target.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
      
      // Create a container for React component
      const container = document.createElement('span')
      container.className = 'inline-block'
      element.parentNode?.replaceChild(container, element)
      
      // Use React to render the component
      const root = createRoot(container)
      root.render(<Wikilink dTag={dtag} displayText={displayText} />)
      reactRootsRef.current.set(container, root)
    })
    
    // Process WebPreview placeholders - replace with React components
    const webpreviewPlaceholders = contentRef.current.querySelectorAll('.webpreview-placeholder[data-webpreview-url]')
    webpreviewPlaceholders.forEach((element) => {
      const url = element.getAttribute('data-webpreview-url')
      if (!url) return
      
      // Create a container for React component
      const container = document.createElement('div')
      container.className = 'my-2'
      element.parentNode?.replaceChild(container, element)
      
      // Use React to render the component
      const root = createRoot(container)
      root.render(<WebPreview url={url} className="w-full" />)
      reactRootsRef.current.set(container, root)
    })
    
    // Process hashtags in text nodes - convert #tag to links
    const walker = document.createTreeWalker(
      contentRef.current,
      NodeFilter.SHOW_TEXT,
      {
        acceptNode: (node) => {
          // Skip if parent is a link, code, or pre tag
          const parent = node.parentElement
          if (!parent) return NodeFilter.FILTER_ACCEPT
          if (parent.tagName === 'A' || parent.tagName === 'CODE' || parent.tagName === 'PRE') {
            return NodeFilter.FILTER_REJECT
          }
          return NodeFilter.FILTER_ACCEPT
        }
      }
    )
    
    const textNodes: Text[] = []
    let node
    while ((node = walker.nextNode())) {
      if (node.nodeType === Node.TEXT_NODE && node.textContent) {
        textNodes.push(node as Text)
      }
    }
    
    textNodes.forEach((textNode) => {
      const text = textNode.textContent || ''
      const hashtagRegex = /#([a-zA-Z0-9_]+)/g
      const matches = Array.from(text.matchAll(hashtagRegex))
      
      if (matches.length > 0) {
        const fragment = document.createDocumentFragment()
        let lastIndex = 0
        
        matches.forEach((match) => {
          if (match.index === undefined) return
          
          // Add text before hashtag
          if (match.index > lastIndex) {
            fragment.appendChild(document.createTextNode(text.slice(lastIndex, match.index)))
          }
          
          // Create hashtag link
          const link = document.createElement('a')
          link.href = `/notes?t=${match[1].toLowerCase()}`
          link.className = 'inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline cursor-pointer'
          link.textContent = `#${match[1]}`
          link.addEventListener('click', (e) => {
            e.stopPropagation()
            e.preventDefault()
            navigateToHashtag(`/notes?t=${match[1].toLowerCase()}`)
          })
          fragment.appendChild(link)
          
          lastIndex = match.index + match[0].length
        })
        
        // Add remaining text
        if (lastIndex < text.length) {
          fragment.appendChild(document.createTextNode(text.slice(lastIndex)))
        }
        
        textNode.parentNode?.replaceChild(fragment, textNode)
      }
    })
    
    // Handle all links - truncate display text and add click handlers for relay URLs
    const allLinks = contentRef.current.querySelectorAll('a[href]')
    allLinks.forEach((link) => {
      const href = link.getAttribute('href')
      if (!href) return
      
      // Get current link text (this might be the full URL or custom text)
      const linkText = link.textContent || ''
      
      // Truncate link text if it's longer than 200 characters
      if (linkText.length > 200) {
        const truncatedText = truncateLinkText(linkText)
        link.textContent = truncatedText
        // Store full text as title for tooltip
        if (!link.getAttribute('title')) {
          link.setAttribute('title', linkText)
        }
      }
      
      // Handle relay URL links - add click handlers to navigate to relay page
      const relayUrl = link.getAttribute('data-relay-url')
      if (relayUrl) {
        const relayPath = `/relays/${encodeURIComponent(relayUrl)}`
        link.setAttribute('href', relayPath)
        link.addEventListener('click', (e) => {
          e.stopPropagation()
          e.preventDefault()
          navigateToRelay(relayPath)
        })
      }
    })
    
    // Cleanup function
    return () => {
      reactRootsRef.current.forEach((root) => {
        root.unmount()
      })
      reactRootsRef.current.clear()
    }
  }, [parsedHtml, isLoading, navigateToHashtag, navigateToRelay])
  
  // Initialize syntax highlighting
  useEffect(() => {
    const initHighlight = async () => {
      if (typeof window !== 'undefined') {
        const hljs = await import('highlight.js')
        if (contentRef.current) {
          contentRef.current.querySelectorAll('pre code').forEach((block) => {
            const element = block as HTMLElement
            element.style.color = 'inherit'
            element.classList.add('text-gray-900', 'dark:text-gray-100')
            hljs.default.highlightElement(element)
            element.style.color = 'inherit'
          })
        }
      }
    }
    
    const timeoutId = setTimeout(initHighlight, 100)
    return () => clearTimeout(timeoutId)
  }, [parsedHtml])
  
  return (
    <>
      <style>{`
        .hljs {
          background: transparent !important;
        }
        .hljs-keyword,
        .hljs-selector-tag,
        .hljs-literal,
        .hljs-title,
        .hljs-section,
        .hljs-doctag,
        .hljs-type,
        .hljs-name,
        .hljs-strong {
          color: #dc2626 !important;
          font-weight: bold !important;
        }
        .hljs-string,
        .hljs-title.class_,
        .hljs-attr,
        .hljs-symbol,
        .hljs-bullet,
        .hljs-addition,
        .hljs-code,
        .hljs-regexp,
        .hljs-selector-pseudo,
        .hljs-selector-attr,
        .hljs-selector-class,
        .hljs-selector-id {
          color: #0284c7 !important;
        }
        .hljs-comment,
        .hljs-quote {
          color: #6b7280 !important;
        }
        .hljs-number,
        .hljs-deletion {
          color: #0d9488 !important;
        }
        .dark .hljs-keyword,
        .dark .hljs-selector-tag,
        .dark .hljs-literal,
        .dark .hljs-title,
        .dark .hljs-section,
        .dark .hljs-doctag,
        .dark .hljs-type,
        .dark .hljs-name,
        .dark .hljs-strong {
          color: #f87171 !important;
        }
        .dark .hljs-string,
        .dark .hljs-title.class_,
        .dark .hljs-attr,
        .dark .hljs-symbol,
        .dark .hljs-bullet,
        .dark .hljs-addition,
        .dark .hljs-code,
        .dark .hljs-regexp,
        .dark .hljs-selector-pseudo,
        .dark .hljs-selector-attr,
        .dark .hljs-selector-class,
        .dark .hljs-selector-id {
          color: #38bdf8 !important;
        }
        .dark .hljs-comment,
        .dark .hljs-quote {
          color: #9ca3af !important;
        }
        .dark .hljs-number,
        .dark .hljs-deletion {
          color: #5eead4 !important;
        }
        .asciidoc-content img {
          display: block;
          max-width: 400px;
          height: auto;
          border-radius: 0.5rem;
          cursor: zoom-in;
          margin: 0.5rem 0;
        }
        .asciidoc-content a[href^="/notes?t="] {
          color: #16a34a !important;
          text-decoration: none !important;
        }
        .asciidoc-content a[href^="/notes?t="]:hover {
          color: #15803d !important;
          text-decoration: underline !important;
        }
        .dark .asciidoc-content a[href^="/notes?t="] {
          color: #4ade80 !important;
        }
        .dark .asciidoc-content a[href^="/notes?t="]:hover {
          color: #86efac !important;
        }
      `}</style>
      <div className={`prose prose-zinc max-w-none dark:prose-invert break-words overflow-wrap-anywhere ${className || ''}`}>
        {/* Metadata */}
        {!hideImagesAndInfo && metadata.title && <h1 className="break-words">{metadata.title}</h1>}
        {!hideImagesAndInfo && metadata.summary && (
          <blockquote>
            <p className="break-words">{metadata.summary}</p>
          </blockquote>
        )}
        {hideImagesAndInfo && metadata.title && (
          <h2 className="text-2xl font-bold mb-4 leading-tight break-words">{metadata.title}</h2>
        )}
        
        {/* Metadata image */}
        {!hideImagesAndInfo && metadata.image && (() => {
          const cleanedMetadataImage = cleanUrl(metadata.image)
          // Don't show if already in content
          if (cleanedMetadataImage && mediaUrlsInContent.has(cleanedMetadataImage)) {
            return null
          }
          
          const metadataImageIndex = imageIndexMap.get(cleanedMetadataImage)
          
          return (
            <Image
              image={{ url: metadata.image, pubkey: event.pubkey }}
              className="max-w-[400px] w-full h-auto my-0 cursor-zoom-in"
              classNames={{
                wrapper: 'rounded-lg',
                errorPlaceholder: 'aspect-square h-[30vh]'
              }}
              onClick={(e) => {
                e.stopPropagation()
                if (metadataImageIndex !== undefined) {
                  openLightbox(metadataImageIndex)
                }
              }}
            />
          )
        })()}
        
        {/* Media from tags (only if not in content) */}
        {leftoverTagMedia.length > 0 && (
          <div className="space-y-4 mb-6">
            {leftoverTagMedia.map((media) => {
              const cleaned = cleanUrl(media.url)
              const mediaIndex = imageIndexMap.get(cleaned)
              
              if (media.type === 'image') {
                return (
                  <div key={`tag-media-${cleaned}`} className="my-2">
                    <Image
                      image={{ url: media.url, pubkey: event.pubkey }}
                      className="max-w-[400px] rounded-lg cursor-zoom-in"
                      classNames={{
                        wrapper: 'rounded-lg',
                        errorPlaceholder: 'aspect-square h-[30vh]'
                      }}
                      onClick={(e) => {
                        e.stopPropagation()
                        if (mediaIndex !== undefined) {
                          openLightbox(mediaIndex)
                        }
                      }}
                    />
                  </div>
                )
              } else if (media.type === 'video' || media.type === 'audio') {
                return (
                  <div key={`tag-media-${cleaned}`} className="my-2 w-full max-w-full overflow-hidden">
                    <MediaPlayer
                      src={media.url}
                      className="max-w-full sm:max-w-[400px] w-full"
                      mustLoad={true}
                      poster={media.poster}
                    />
                  </div>
                )
              }
              return null
            })}
          </div>
        )}
        
        {/* YouTube URLs from tags (only if not in content) */}
        {leftoverTagYouTubeUrls.length > 0 && (
          <div className="space-y-4 mb-6">
            {leftoverTagYouTubeUrls.map((url) => {
              const cleaned = cleanUrl(url)
              return (
                <div key={`tag-youtube-${cleaned}`} className="my-2">
                  <YoutubeEmbeddedPlayer
                    url={url}
                    className="max-w-[400px]"
                    mustLoad={false}
                  />
                </div>
              )
            })}
          </div>
        )}
        
        {/* Parsed AsciiDoc content */}
        {isLoading ? (
          <div>Loading content...</div>
        ) : (
          <div
            ref={contentRef}
            className="asciidoc-content break-words"
            dangerouslySetInnerHTML={{ __html: parsedHtml }}
          />
        )}
        
        {/* Hashtags from metadata (only if not already in content) */}
        {!hideImagesAndInfo && leftoverMetadataTags.length > 0 && (
          <div className="flex gap-2 flex-wrap pb-2 mt-4">
            {leftoverMetadataTags.map((tag) => (
              <div
                key={tag}
                title={tag}
                className="flex items-center rounded-full px-3 bg-muted text-muted-foreground max-w-44 cursor-pointer hover:bg-accent hover:text-accent-foreground"
                onClick={(e) => {
                  e.stopPropagation()
                  push(toNoteList({ hashtag: tag, kinds: [kinds.LongFormArticle] }))
                }}
              >
                #<span className="truncate">{tag}</span>
              </div>
            ))}
          </div>
        )}

                {/* WebPreview cards for links from tags (only if not already in content) */}
                {/* Note: Links in content are already rendered as links in the AsciiDoc HTML above, so we don't show WebPreview for them */}
                {leftoverTagLinks.length > 0 && (
                  <div className="space-y-3 mt-6">
                    {leftoverTagLinks.map((url, index) => (
                      <WebPreview key={`tag-${index}-${url}`} url={url} className="w-full" />
                    ))}
                  </div>
                )}
      </div>
      
      {/* Image gallery lightbox */}
      {allImages.length > 0 && lightboxIndex >= 0 && createPortal(
        <div onClick={(e) => e.stopPropagation()}>
          <Lightbox
            index={lightboxIndex}
            slides={allImages.map(({ url, alt }) => ({ 
              src: url, 
              alt: alt || url 
            }))}
            plugins={[Zoom]}
            open={lightboxIndex >= 0}
            close={() => setLightboxIndex(-1)}
            controller={{
              closeOnBackdropClick: true,
              closeOnPullUp: true,
              closeOnPullDown: true
            }}
            styles={{
              toolbar: { paddingTop: '2.25rem' }
            }}
            carousel={{
              finite: false
            }}
          />
        </div>,
        document.body
      )}
    </>
  )
}

