import { useSecondaryPage, useSmartHashtagNavigation, useSmartRelayNavigation } from '@/PageManager'
import Image from '@/components/Image'
import MediaPlayer from '@/components/MediaPlayer'
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 EmbeddedCitation from '@/components/EmbeddedCitation'
import { parsePaytoUri } from '@/lib/payto'
import PaytoLink from '@/components/PaytoLink'
import { DeletedEventProvider } from '@/providers/DeletedEventProvider'
import { ReplyProvider } from '@/providers/ReplyProvider'
import Wikilink from '@/components/UniversalContent/Wikilink'
import { BookstrContent } from '@/components/Bookstr'
import { preprocessAsciidocMediaLinks } from '../MarkdownArticle/preprocessMarkup'
import logger from '@/lib/logger'
import { extractBookMetadata } from '@/lib/bookstr-parser'
import { ExtendedKind } from '@/constants'
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
  // Also handle optional [] suffix (empty link text in AsciiDoc)
  // Note: Citations are already protected in passthrough (+++...+++), so nostr: links inside them won't be processed
  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, emptyBrackets) => {
    // Convert directly to AsciiDoc link format
    // This will be processed later in HTML post-processing to render as React components
    // If [] suffix is present, use empty link text, otherwise use the bech32Id
    const linkText = emptyBrackets ? '' : bech32Id
    return `link:nostr:${bech32Id}[${linkText}]`
  })
  
  // 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')
  
  // Protect existing AsciiDoc links (both url[text] and link:url[text] formats)
  // Do this FIRST before any other processing to avoid double-processing
  const asciidocLinkPlaceholders: string[] = []
  // Match AsciiDoc link format: url[text] or link:url[text]
  // Pattern matches: http(s)://url[text] or link:url[text]
  // URL can contain dots, slashes, hyphens, etc., but stops at whitespace or [
  // Then we match [text] where text can contain anything except ]
  // Use a more permissive pattern - match URL until [ then match [text]
  // The URL part can contain most characters except whitespace and [
  asciidoc = asciidoc.replace(/(https?:\/\/[^\s\[\]]+\[[^\]]+\])/g, (_match, link) => {
    // This is an AsciiDoc link format (url[text]), protect it
    const placeholder = `__ASCIIDOC_LINK_${asciidocLinkPlaceholders.length}__`
    asciidocLinkPlaceholders.push(link)
    return placeholder
  })
  // Also protect link:url[text] format
  asciidoc = asciidoc.replace(/(link:[^\s\[\]]+\[[^\]]+\])/g, (_match, link) => {
    const placeholder = `__ASCIIDOC_LINK_${asciidocLinkPlaceholders.length}__`
    asciidocLinkPlaceholders.push(link)
    return placeholder
  })
  
  // 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}]`
  })
  
  // Restore AsciiDoc links
  asciidocLinkPlaceholders.forEach((link, index) => {
    asciidoc = asciidoc.replace(`__ASCIIDOC_LINK_${index}__`, link)
  })
  
  // 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,
  parentImageUrl
}: {
  event: Event
  className?: string
  hideImagesAndInfo?: boolean
  parentImageUrl?: string
}) {
  const { push } = useSecondaryPage()
  const { navigateToHashtag } = useSmartHashtagNavigation()
  const { navigateToRelay } = useSmartRelayNavigation()
  const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
  const bookMetadata = useMemo(() => extractBookMetadata(event), [event])
  const isBookstrEvent = (event.kind === ExtendedKind.PUBLICATION || event.kind === ExtendedKind.PUBLICATION_CONTENT) && !!bookMetadata.book
  const contentRef = useRef<HTMLDivElement>(null)
  
  // Preprocess content: convert all markdown to AsciiDoc syntax
  const processedContent = useMemo(() => {
    let content = event.content
    
    // Normalize excessive newlines (reduce 3+ to 2)
    content = content.replace(/\n\s*\n\s*\n+/g, '\n\n')
    
    // PROTECT WIKILINKS FIRST before any other processing
    // This prevents AsciiDoc or other processors from converting them to regular links
    // First, protect bookstr wikilinks by converting them to passthrough format
    // Don't use [[...]] inside passthrough as AsciiDoc processes it - use a plain marker instead
    content = content.replace(/\[\[book::([^\]]+)\]\]/g, (_match, bookContent) => {
      const cleanContent = bookContent.trim()
      // Use AsciiDoc passthrough without brackets - AsciiDoc processes [[...]] even in passthrough
      // Use a unique marker format that won't conflict with other content
      return `+++BOOKSTR_MARKER:${cleanContent}:BOOKSTR_END+++`
    })
    
    // Protect citations by converting them to passthrough format
    // Don't use [[...]] inside passthrough as AsciiDoc processes it - use a plain marker instead
    content = content.replace(/\[\[citation::(end|foot|foot-end|inline|quote|prompt-end|prompt-inline)::([^\]]+)\]\]/g, (_match, citationType, citationId) => {
      // Strip all nostr: prefixes if present (handle cases like nostr:nostr:nevent1...)
      let cleanId = citationId.trim()
      while (cleanId.startsWith('nostr:')) {
        cleanId = cleanId.substring(6) // Remove 'nostr:' prefix
      }
      // Use a unique marker format that won't conflict with other content
      return `+++CITATION_MARKER:${citationType}::${cleanId}:CITATION_END+++`
    })
    
    // Then protect regular wikilinks by converting them to passthrough format
    // This prevents AsciiDoc from processing them and prevents URLs inside from being processed
    content = content.replace(/\[\[([^\]]+)\]\]/g, (_match, linkContent) => {
      // Skip if this was already processed as a bookstr wikilink (shouldn't happen, but safety check)
      if (linkContent.startsWith('book::')) {
        return _match
      }
      // Skip citations - they're already processed above
      if (linkContent.startsWith('citation::')) {
        return _match
      }
      // Convert to AsciiDoc passthrough format so it's preserved
      return `+++WIKILINK:${linkContent}+++`
    })
    
    // 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)])
  
  // Note: tagLinks removed - WebPreview is disabled for AsciiDoc articles
  
  // 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])
  
  // Note: contentLinks removed - WebPreview is disabled for AsciiDoc articles
  
  // 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
    const parentImageUrlCleaned = parentImageUrl ? cleanUrl(parentImageUrl) : 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
      // Skip if this matches the parent publication's image (to avoid duplicate cover images)
      if (parentImageUrlCleaned && cleaned === parentImageUrlCleaned) return false
      return true
    })
  }, [tagMedia, mediaUrlsInContent, metadata.image, hideImagesAndInfo, parentImageUrl])
  
  // 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])
  
  // Note: leftoverTagLinks removed - WebPreview is disabled for AsciiDoc articles
  
  // 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()
        
        // Debug: log HTML to check if passthrough markers are preserved
        if (process.env.NODE_ENV === 'development') {
          const hasBookstrMarker = htmlString.includes('BOOKSTR_START') || htmlString.includes('BOOKSTR')
          const hasWikilinkMarker = htmlString.includes('WIKILINK')
          logger.debug('AsciidocArticle: HTML contains markers', { 
            hasBookstrMarker, 
            hasWikilinkMarker,
            htmlPreview: htmlString.substring(0, 2000)
          })
        }
        
        // Note: Markdown is now converted to AsciiDoc in preprocessing,
        // so post-processing markdown should not be necessary
        
        // IMPORTANT: Process citations FIRST before any nostr: link processing
        // This prevents nostr: links inside citations from being processed incorrectly
        // Handle citation markers - convert passthrough markers to placeholders
        // AsciiDoc passthrough +++CITATION_MARKER:type::id:CITATION_END+++ outputs CITATION_MARKER:type::id:CITATION_END in HTML
        htmlString = htmlString.replace(/CITATION_MARKER:\s*(end|foot|foot-end|inline|quote|prompt-end|prompt-inline)::\s*(.+?)\s*:CITATION_END/g, (_match, citationType, citationId) => {
          // Strip all nostr: prefixes if present (handle cases like nostr:nostr:nevent1...)
          let cleanId = citationId.trim()
          while (cleanId.startsWith('nostr:')) {
            cleanId = cleanId.substring(6) // Remove 'nostr:' prefix
          }
          const escapedId = cleanId.replace(/"/g, '&quot;').replace(/'/g, '&#39;')
          // Use inline element for inline citations and footnotes/endnotes (they need to be inline)
          // Only block-level citations (quote) should use div
          const isInline = citationType === 'inline' || citationType === 'prompt-inline' || citationType === 'foot' || citationType === 'foot-end' || citationType === 'end' || citationType === 'prompt-end'
          const tag = isInline ? 'span' : 'div'
          return `<${tag} data-citation="${escapedId}" data-citation-type="${citationType}" class="citation-placeholder${isInline ? ' inline' : ''}"></${tag}>`
        })
        
        // 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 (naddr can be 200+ chars)
        // Match addresses with optional [] suffix
        htmlString = htmlString.replace(/>([^<]*nostr:((?:npub1|nprofile1|note1|nevent1|naddr1)[a-z0-9]{20,})(\[\])?[^<]*)</g, (_match, textContent) => {
          // Extract nostr addresses from the text content - use flexible pattern that handles long addresses
          // npub and note are typically 58 chars, but naddr can be 200+ chars
          const nostrRegex = /nostr:((?:npub1[a-z0-9]{58,}|nprofile1[a-z0-9]+|note1[a-z0-9]{58,}|nevent1[a-z0-9]+|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
            
            // Escape bech32Id for HTML attributes
            const escapedId = bech32Id.replace(/"/g, '&quot;').replace(/'/g, '&#39;')
            
            if (bech32Id.startsWith('npub') || bech32Id.startsWith('nprofile')) {
              replacements.push({
                start,
                end,
                replacement: `<span data-nostr-mention="${escapedId}" 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="${escapedId}" 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}<`
        })
        
        // Fallback: ensure any remaining nostr: addresses are shown as plain text
        // This catches any that weren't converted to placeholders
        htmlString = htmlString.replace(/([^>])nostr:((?:npub1|nprofile1|note1|nevent1|naddr1)[a-z0-9]{20,})(\[\])?/g, (_match, prefix, bech32Id, emptyBrackets) => {
          // Show as plain text if not already in a tag or placeholder
          return `${prefix}nostr:${bech32Id}${emptyBrackets || ''}`
        })

        // payto: URIs (RFC-8905 / NIP-A3) – replace <a href="payto://..."> and plain payto:// with placeholder
        htmlString = htmlString.replace(/<a[^>]*href=["'](payto:\/\/[^"']+)["'][^>]*>([^<]*)<\/a>/gi, (_match, paytoUri, _linkText) => {
          const parsed = parsePaytoUri(paytoUri)
          if (!parsed) return _match
          const escaped = paytoUri.replace(/"/g, '&quot;').replace(/&/g, '&amp;').replace(/'/g, '&#39;')
          return `<span data-payto-uri="${escaped}" class="payto-placeholder"></span>`
        })
        htmlString = htmlString.replace(/(^|[\s>])(payto:\/\/[a-z0-9-]+\/[^\s<\]\)\"']+)/gi, (_match, prefix, paytoUri) => {
          const parsed = parsePaytoUri(paytoUri.trim())
          if (!parsed) return _match
          const escaped = parsed.raw.replace(/"/g, '&quot;').replace(/&/g, '&amp;').replace(/'/g, '&#39;')
          return `${prefix}<span data-payto-uri="${escaped}" class="payto-placeholder"></span>`
        })
        
        // 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 bookstr markers - convert passthrough markers to placeholders
        // AsciiDoc passthrough +++BOOKSTR_MARKER:...:BOOKSTR_END+++ outputs BOOKSTR_MARKER:...:BOOKSTR_END in HTML
        // Match the delimited format to extract the exact content
        // IMPORTANT: Process this BEFORE any other pattern matching
        htmlString = htmlString.replace(/BOOKSTR_MARKER:\s*(.+?)\s*:BOOKSTR_END/g, (_match, bookContent) => {
          // Trim whitespace and escape special characters for HTML attributes
          const cleanContent = bookContent.trim()
          const escaped = cleanContent.replace(/"/g, '&quot;').replace(/'/g, '&#39;')
          logger.debug('BookstrContent: Found bookstr marker in HTML', { cleanContent, escaped })
          return `<span data-bookstr="${escaped}" class="bookstr-placeholder"></span>`
        })
        
        // Also handle if AsciiDoc converted it to WIKILINK: format (fallback)
        htmlString = htmlString.replace(/WIKILINK:bookstr::([^<>\s]+)/g, (_match, bookContent) => {
          const cleanContent = bookContent.trim()
          const escaped = cleanContent.replace(/"/g, '&quot;').replace(/'/g, '&#39;')
          logger.debug('BookstrContent: Found bookstr in WIKILINK format', { cleanContent, escaped })
          return `<span data-bookstr="${escaped}" class="bookstr-placeholder"></span>`
        })
        
        // 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
        // IMPORTANT: Skip any [[bookstr::...]] patterns that might have been missed
        htmlString = htmlString.replace(/WIKILINK:([^<>\s]+)/g, (_match, linkContent) => {
          // Skip if this is a bookstr wikilink
          if (linkContent.includes('bookstr::')) {
            return _match
          }
          // 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
        // Also check for bookstr content that might have been converted to links
        // Only replace links that need special handling - leave AsciiDoc-generated links alone
        const linkMatches: Array<{ match: string; href: string; linkText: string; index: number }> = []
        const bookstrLinkMatches: Array<{ match: string; bookContent: string; index: number }> = []
        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 this link contains bookstr content (might have been converted by AsciiDoc)
          if (linkText.includes('bookstr::') || href.includes('bookstr::')) {
            // Extract bookstr content from link text or href
            const bookstrMatch = linkText.match(/bookstr::([^\]]+)/) || href.match(/bookstr::([^\]]+)/)
            if (bookstrMatch) {
              const bookContent = bookstrMatch[1].trim()
              bookstrLinkMatches.push({ match, bookContent, index })
              continue
            }
          }
          
          // Only process links that need special handling (YouTube, relay URLs)
          // Leave regular HTTP/HTTPS links as-is since AsciiDoc already formatted them correctly
          if (isYouTubeUrl(href) || isWebsocketUrl(href)) {
            linkMatches.push({ match, href, linkText, index })
          }
        }
        
        // Replace bookstr links in reverse order to preserve indices
        for (let i = bookstrLinkMatches.length - 1; i >= 0; i--) {
          const { match, bookContent, index } = bookstrLinkMatches[i]
          const escaped = bookContent.replace(/"/g, '&quot;').replace(/'/g, '&#39;')
          logger.debug('BookstrContent: Found bookstr in converted link', { bookContent, escaped })
          htmlString = htmlString.substring(0, index) + 
            `<span data-bookstr="${escaped}" class="bookstr-placeholder"></span>` + 
            htmlString.substring(index + match.length)
        }
        
        // Replace only special links in reverse order to preserve indices
        for (let i = linkMatches.length - 1; i >= 0; i--) {
          const { match, href, linkText } = 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>`
          }
          
          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 regular links
        // NO WebPreview conversion for AsciiDoc articles
        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
            }
            // Convert to regular link - NO WebPreview
            const cleanedUrl = cleanUrl(match)
            return `<a href="${cleanedUrl}" 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">${match}</a>`
          }
          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())
  // Track which placeholders have been processed to avoid re-processing
  const processedPlaceholdersRef = useRef<Set<string>>(new Set())
  // Track citations for footnotes and endnotes sections
  const citationsRef = useRef<Array<{ id: string; type: string; citationId: string; index: number }>>([])
  const citationIndexRef = useRef(0)
  
  // Post-process rendered HTML to inject React components for nostr: links and handle hashtags
  useEffect(() => {
    if (!contentRef.current || !parsedHtml || isLoading) return
    
    // Only clean up roots that are no longer in the DOM
    const rootsToCleanup: Array<[Element, Root]> = []
    reactRootsRef.current.forEach((root, element) => {
      if (!element.isConnected) {
        rootsToCleanup.push([element, root])
        reactRootsRef.current.delete(element)
      }
    })
    
    // Unmount disconnected roots asynchronously to avoid race conditions
    if (rootsToCleanup.length > 0) {
      setTimeout(() => {
        rootsToCleanup.forEach(([, root]) => {
          try {
            root.unmount()
          } catch (err) {
            // Ignore errors during cleanup
          }
        })
      }, 0)
    }
    
    // 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')
        // Fallback: show as plain text
        const textNode = document.createTextNode(`nostr:${element.textContent || ''}`)
        element.parentNode?.replaceChild(textNode, element)
        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, with error handling
      try {
        const root = createRoot(container)
        root.render(<EmbeddedMention userId={bech32Id} />)
        reactRootsRef.current.set(container, root)
      } catch (error) {
        logger.error('Failed to render nostr mention', { bech32Id, error })
        // Fallback: show as plain text
        const textNode = document.createTextNode(`nostr:${bech32Id}`)
        parent.replaceChild(textNode, container)
      }
    })
    
    // 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')
        // Fallback: show as plain text
        const textNode = document.createTextNode(`nostr:${element.textContent || ''}`)
        element.parentNode?.replaceChild(textNode, element)
        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, with error handling
      try {
        const root = createRoot(container)
        root.render(<EmbeddedNote noteId={bech32Id} />)
        reactRootsRef.current.set(container, root)
      } catch (error) {
        logger.error('Failed to render nostr note', { bech32Id, error })
        // Fallback: show as plain text
        const textNode = document.createTextNode(`nostr:${bech32Id}`)
        parent.replaceChild(textNode, container)
      }
    })

    // Process payto: placeholders – replace with PaytoLink
    const paytoPlaceholders = contentRef.current.querySelectorAll('.payto-placeholder[data-payto-uri]')
    paytoPlaceholders.forEach((element) => {
      const paytoUri = element.getAttribute('data-payto-uri')
      if (!paytoUri) return
      const decoded = paytoUri.replace(/&quot;/g, '"').replace(/&amp;/g, '&').replace(/&#39;/g, "'")
      const container = document.createElement('span')
      container.className = 'inline'
      const parent = element.parentNode
      if (!parent) return
      parent.replaceChild(container, element)
      try {
        const root = createRoot(container)
        root.render(<PaytoLink paytoUri={decoded} className="text-primary hover:underline break-words" />)
        reactRootsRef.current.set(container, root)
      } catch (error) {
        logger.error('Failed to render payto link', { paytoUri: decoded, error })
        const textNode = document.createTextNode(decoded)
        parent.replaceChild(textNode, container)
      }
    })
    
    // Process citations - replace placeholders with React components
    // First pass: collect all citations and assign indices
    const citationPlaceholders = Array.from(contentRef.current.querySelectorAll('.citation-placeholder[data-citation]'))
    console.log('AsciidocArticle: Found citation placeholders', {
      count: citationPlaceholders.length,
      placeholders: citationPlaceholders.map(el => ({
        id: el.getAttribute('data-citation'),
        type: el.getAttribute('data-citation-type')
      }))
    })
    
    citationsRef.current = []
    citationIndexRef.current = 0
    
    citationPlaceholders.forEach((element) => {
      const citationId = element.getAttribute('data-citation')
      const citationType = element.getAttribute('data-citation-type') || 'end'
      if (!citationId) {
        console.warn('Citation placeholder found but no citation ID attribute')
        return
      }
      
      const citationIndex = citationIndexRef.current++
      citationsRef.current.push({
        id: `citation-${citationIndex}`,
        type: citationType,
        citationId,
        index: citationIndex
      })
    })
    
    console.log('AsciidocArticle: Collected citations', {
      count: citationsRef.current.length,
      citations: citationsRef.current
    })
    
    // Second pass: render citations based on type
    citationPlaceholders.forEach((element, idx) => {
      const citationId = element.getAttribute('data-citation')
      const citationType = element.getAttribute('data-citation-type') || 'end'
      if (!citationId) return
      
      const citation = citationsRef.current[idx]
      if (!citation) return
      
      const citationNumber = citation.index + 1
      const parent = element.parentNode
      if (!parent) {
        logger.warn('Citation placeholder has no parent node')
        return
      }
      
      // Handle different citation types
      if (citationType === 'inline' || citationType === 'prompt-inline') {
        // Inline citations render as clickable text
        const container = document.createElement('span')
        container.className = 'inline'
        container.style.display = 'inline'
        container.style.whiteSpace = 'nowrap'
        parent.replaceChild(container, element)
        
        const root = createRoot(container)
        root.render(
          <DeletedEventProvider>
            <ReplyProvider>
              <EmbeddedCitation
                citationId={citationId}
                displayType={citationType as 'inline' | 'prompt-inline'}
              />
            </ReplyProvider>
          </DeletedEventProvider>
        )
        reactRootsRef.current.set(container, root)
      } else if (citationType === 'foot' || citationType === 'foot-end') {
        // Footnotes render as superscript numbers
        const sup = document.createElement('sup')
        sup.className = 'citation-ref'
        sup.style.display = 'inline'
        sup.style.whiteSpace = 'nowrap'
        const link = document.createElement('a')
        link.href = `#citation-${citation.index}`
        link.id = `citation-ref-${citation.index}`
        link.className = 'text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline no-underline'
        link.textContent = `[${citationNumber}]`
        link.addEventListener('click', (e) => {
          e.preventDefault()
          const citationElement = document.getElementById(`citation-${citation.index}`)
          if (citationElement) {
            citationElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
          }
        })
        sup.appendChild(link)
        parent.replaceChild(sup, element)
      } else if (citationType === 'end' || citationType === 'prompt-end') {
        // Endnotes render as superscript numbers that link to references section
        const sup = document.createElement('sup')
        sup.className = 'citation-ref'
        sup.style.display = 'inline'
        sup.style.whiteSpace = 'nowrap'
        const link = document.createElement('a')
        link.href = '#references-section'
        link.id = `citation-ref-${citation.index}`
        link.className = 'text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline no-underline'
        link.textContent = `[${citationNumber}]`
        link.addEventListener('click', (e) => {
          e.preventDefault()
          const refSection = document.getElementById('references-section')
          if (refSection) {
            refSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
          }
        })
        sup.appendChild(link)
        parent.replaceChild(sup, element)
      } else if (citationType === 'quote') {
        // Quotes render as block-level citation cards
        const container = document.createElement('div')
        container.className = 'w-full my-2'
        parent.replaceChild(container, element)
        
        const root = createRoot(container)
        root.render(
          <DeletedEventProvider>
            <ReplyProvider>
              <EmbeddedCitation
                citationId={citationId}
                displayType="quote"
              />
            </ReplyProvider>
          </DeletedEventProvider>
        )
        reactRootsRef.current.set(container, root)
      }
    })
    
    // Render footnotes and references sections
    const footnotes = citationsRef.current.filter(c => c.type === 'foot' || c.type === 'foot-end')
    const endCitations = citationsRef.current.filter(c => c.type === 'end' || c.type === 'prompt-end')
    
    console.log('AsciidocArticle: Processing citations', {
      totalCitations: citationsRef.current.length,
      footnotesCount: footnotes.length,
      endCitationsCount: endCitations.length,
      allCitations: citationsRef.current
    })
    
    if (!contentRef.current?.parentElement) {
      console.warn('AsciidocArticle: contentRef parent not found, cannot render footnotes/references')
      return
    }
    
    const parentContainer = contentRef.current.parentElement
    
    // Check if sections already exist
    const existingFootnotes = parentContainer.querySelector('#footnotes-section')
    const existingReferences = parentContainer.querySelector('#references-section')
    
    // If sections already exist and we have no new citations, preserve existing sections
    // This handles the case where useEffect runs again after placeholders are replaced
    if ((existingFootnotes || existingReferences) && citationsRef.current.length === 0) {
      console.log('AsciidocArticle: Sections already exist, preserving them', {
        hasFootnotes: !!existingFootnotes,
        hasReferences: !!existingReferences
      })
      return
    }
    
    // Remove existing sections only if we're going to recreate them with new data
    if (existingFootnotes && footnotes.length > 0) {
      existingFootnotes.remove()
    }
    if (existingReferences && endCitations.length > 0) {
      existingReferences.remove()
    }
    
    console.log('AsciidocArticle: Rendering citation sections', {
      footnotesCount: footnotes.length,
      endCitationsCount: endCitations.length,
      totalCitations: citationsRef.current.length,
      parentContainer: parentContainer.tagName,
      hasContentRef: !!contentRef.current,
      hadExistingFootnotes: !!existingFootnotes,
      hadExistingReferences: !!existingReferences
    })
    
    // Render footnotes section
    if (footnotes.length > 0) {
      const footnotesSection = document.createElement('div')
      footnotesSection.id = 'footnotes-section'
      footnotesSection.className = 'mt-8 pt-4 border-t border-gray-300 dark:border-gray-700'
      
      const h3 = document.createElement('h3')
      h3.className = 'text-lg font-semibold mb-4'
      h3.textContent = 'Footnotes'
      footnotesSection.appendChild(h3)
      
      const ol = document.createElement('ol')
      // Academic style: proper list formatting with aligned numbers
      ol.className = 'list-decimal pl-6 space-y-3'
      ol.style.listStylePosition = 'outside'
      
      footnotes.forEach((citation) => {
        const li = document.createElement('li')
        li.id = citation.id
        li.className = 'text-sm pl-2'
        li.style.display = 'list-item'
        
        const citationContainer = document.createElement('span')
        citationContainer.className = 'inline'
        li.appendChild(citationContainer)
        
        const backLink = document.createElement('a')
        backLink.href = `#citation-ref-${citation.index}`
        backLink.className = 'text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline text-xs ml-2 inline-flex items-center'
        backLink.setAttribute('aria-label', 'Return to citation')
        // Use hyperlink icon instead of emoji
        backLink.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>'
        backLink.addEventListener('click', (e) => {
          e.preventDefault()
          const refElement = document.getElementById(`citation-ref-${citation.index}`)
          if (refElement) {
            refElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
          }
        })
        li.appendChild(backLink)
        
        ol.appendChild(li)
        
        // Render citation component - use a wrapper div to position backlink
        const citationWrapperDiv = document.createElement('div')
        citationWrapperDiv.className = 'inline'
        citationWrapperDiv.style.display = 'inline'
        citationContainer.appendChild(citationWrapperDiv)
        
        const citationRoot = createRoot(citationWrapperDiv)
        citationRoot.render(
          <DeletedEventProvider>
            <ReplyProvider>
              <EmbeddedCitation
                citationId={citation.citationId}
                displayType={citation.type === 'foot-end' ? 'foot-end' : 'foot'}
              />
            </ReplyProvider>
          </DeletedEventProvider>
        )
        reactRootsRef.current.set(citationWrapperDiv, citationRoot)
        
        // Insert backlink at end of first line after citation renders
        setTimeout(() => {
          const firstDiv = citationWrapperDiv.querySelector('div:first-child') as HTMLElement
          if (firstDiv) {
            firstDiv.style.display = 'inline'
            firstDiv.style.position = 'relative'
            backLink.style.position = 'absolute'
            backLink.style.right = '0'
            backLink.style.top = '0'
            firstDiv.appendChild(backLink)
          } else {
            citationWrapperDiv.appendChild(backLink)
          }
        }, 100)
      })
      
      footnotesSection.appendChild(ol)
      
      // Insert after contentRef div - use insertAdjacentElement for more reliable insertion
      contentRef.current.insertAdjacentElement('afterend', footnotesSection)
      
      // Verify insertion
      const insertedFootnotes = parentContainer.querySelector('#footnotes-section')
      console.log('AsciidocArticle: Footnotes section created and inserted', { 
        footnotesCount: footnotes.length,
        parentTagName: parentContainer.tagName,
        sectionId: footnotesSection.id,
        isInDOM: !!insertedFootnotes,
        sectionVisible: insertedFootnotes ? window.getComputedStyle(insertedFootnotes).display !== 'none' : false,
        sectionText: insertedFootnotes?.textContent?.substring(0, 100)
      })
    }
    
    // Render references section
    if (endCitations.length > 0) {
      const referencesSection = document.createElement('div')
      referencesSection.id = 'references-section'
      referencesSection.className = 'mt-8 pt-4 border-t border-gray-300 dark:border-gray-700'
      
      const h3 = document.createElement('h3')
      h3.className = 'text-lg font-semibold mb-4'
      h3.textContent = 'References'
      referencesSection.appendChild(h3)
      
      const ol = document.createElement('ol')
      // Academic style: proper list formatting with aligned numbers
      ol.className = 'list-decimal pl-6 space-y-3'
      ol.style.listStylePosition = 'outside'
      
      endCitations.forEach((citation) => {
        const li = document.createElement('li')
        li.id = `citation-end-${citation.index}`
        li.className = 'text-sm pl-2'
        li.style.display = 'list-item'
        
        const citationWrapper = document.createElement('div')
        citationWrapper.className = 'inline-block w-full relative'
        li.appendChild(citationWrapper)
        
        const citationContainer = document.createElement('span')
        citationContainer.className = 'inline'
        citationWrapper.appendChild(citationContainer)
        
        const backLink = document.createElement('a')
        backLink.href = `#citation-ref-${citation.index}`
        backLink.className = 'text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline text-xs ml-2 inline-flex items-center'
        backLink.setAttribute('aria-label', 'Return to citation')
        backLink.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>'
        backLink.addEventListener('click', (e) => {
          e.preventDefault()
          const refElement = document.getElementById(`citation-ref-${citation.index}`)
          if (refElement) {
            refElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
          }
        })
        
        ol.appendChild(li)
        
        // Render citation component
        const citationRoot = createRoot(citationContainer)
        citationRoot.render(
          <DeletedEventProvider>
            <ReplyProvider>
              <EmbeddedCitation
                citationId={citation.citationId}
                displayType={citation.type as 'end' | 'prompt-end'}
              />
            </ReplyProvider>
          </DeletedEventProvider>
        )
        reactRootsRef.current.set(citationContainer, citationRoot)
        
        // Insert backlink at end of first line after citation renders
        setTimeout(() => {
          const firstDiv = citationContainer.querySelector('div:first-child') as HTMLElement
          if (firstDiv) {
            firstDiv.style.display = 'inline'
            firstDiv.style.position = 'relative'
            backLink.style.position = 'absolute'
            backLink.style.right = '0'
            backLink.style.top = '0'
            firstDiv.appendChild(backLink)
          } else {
            citationWrapper.appendChild(backLink)
          }
        }, 100)
      })
      
      referencesSection.appendChild(ol)
      
      // Insert after footnotes section if it exists, otherwise after contentRef
      const footnotesSection = parentContainer.querySelector('#footnotes-section')
      if (footnotesSection) {
        // Insert after footnotes section
        footnotesSection.insertAdjacentElement('afterend', referencesSection)
      } else {
        // No footnotes section, insert after contentRef
        contentRef.current.insertAdjacentElement('afterend', referencesSection)
      }
      
      // Verify insertion
      const insertedReferences = parentContainer.querySelector('#references-section')
      console.log('AsciidocArticle: References section created and inserted', { 
        endCitationsCount: endCitations.length,
        hasFootnotesSection: !!footnotesSection,
        sectionId: referencesSection.id,
        isInDOM: !!insertedReferences,
        sectionHTML: insertedReferences?.outerHTML?.substring(0, 200)
      })
    }
    
    // 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 bookstr wikilinks - replace placeholders with React components
    // Only process elements that are still placeholders (not already converted to containers)
    const bookstrPlaceholders = contentRef.current.querySelectorAll('.bookstr-placeholder[data-bookstr]')
    bookstrPlaceholders.forEach((element) => {
      const bookstrContent = element.getAttribute('data-bookstr')
      if (!bookstrContent) return
      
      // Create a unique key for this placeholder
      const placeholderKey = `bookstr-${bookstrContent}`
      
      // Check if this placeholder has already been converted to a container
      // Look for a sibling or nearby container with the same key
      const parent = element.parentElement
      if (parent) {
        // Escape the attribute value for use in CSS selector
        // If the value contains double quotes, use single quotes for the selector
        // Otherwise escape double quotes and backslashes
        let selector: string
        if (placeholderKey.includes('"')) {
          // Use single quotes and escape any single quotes in the value
          const escapedValue = placeholderKey.replace(/'/g, "\\'")
          selector = `.bookstr-container[data-bookstr-key='${escapedValue}']`
        } else {
          // Use double quotes and escape any double quotes and backslashes
          const escapedValue = placeholderKey.replace(/["\\]/g, '\\$&')
          selector = `.bookstr-container[data-bookstr-key="${escapedValue}"]`
        }
        const existingContainer = parent.querySelector(selector)
        if (existingContainer) {
          // Container already exists - check if it has a React root
          if (reactRootsRef.current.has(existingContainer)) {
            // Already has a React root, just remove this duplicate placeholder
            element.remove()
            return
          } else {
            // Container exists but no root - this shouldn't happen, but clean it up
            existingContainer.remove()
          }
        }
      }
      
      // Skip if already processed (to avoid duplicate processing)
      if (processedPlaceholdersRef.current.has(placeholderKey)) {
        // If we've processed this but the element still exists, remove it
        element.remove()
        return
      }
      
      // Mark as processed
      processedPlaceholdersRef.current.add(placeholderKey)
      
      // Prepend book:: prefix since BookstrContent expects it
      const wikilink = `book::${bookstrContent}`
      
      logger.debug('BookstrContent: Rendering component', { bookstrContent, wikilink })
      
      // Create a container for React component
      const container = document.createElement('div')
      container.className = 'bookstr-container'
      container.setAttribute('data-bookstr-key', placeholderKey)
      element.parentNode?.replaceChild(container, element)
      
      // Use React to render the component - only render once per container
      // Check if this container already has a root to avoid re-rendering
      if (!reactRootsRef.current.has(container)) {
        const root = createRoot(container)
        root.render(<BookstrContent wikilink={wikilink} skipWebPreview={true} />)
        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
      
      // Skip if this is a bookstr wikilink (already processed)
      if (linkContent.startsWith('book::')) {
        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()
      
      // 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 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)
        })
      }
    })
    
    // No cleanup needed here - we only clean up disconnected roots above
    // Full cleanup happens on component unmount
  }, [parsedHtml, isLoading, navigateToHashtag, navigateToRelay])
  
  // Cleanup on component unmount
  useEffect(() => {
    return () => {
      const rootsToCleanup = Array.from(reactRootsRef.current.values())
      reactRootsRef.current.clear()
      processedPlaceholdersRef.current.clear()
      
      // Unmount asynchronously
      setTimeout(() => {
        rootsToCleanup.forEach((root) => {
          try {
            root.unmount()
          } catch (err) {
            // Ignore errors during cleanup
          }
        })
      }, 0)
    }
  }, [])
  
  // 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;
        }
        .asciidoc-content .citation-placeholder.inline,
        .asciidoc-content .citation-placeholder.inline > * {
          display: inline !important;
        }
        .asciidoc-content .citation-ref,
        .asciidoc-content .citation-ref > * {
          display: inline !important;
          white-space: nowrap;
        }
        .asciidoc-content sup.citation-ref {
          display: inline !important;
          vertical-align: super;
          font-size: 0.83em;
          line-height: 0;
        }
        /* Academic references section formatting */
        .asciidoc-content #references-section ol,
        .asciidoc-content #footnotes-section ol {
          list-style: decimal;
          padding-left: 1.5rem;
          list-style-position: outside;
        }
        .asciidoc-content #references-section li,
        .asciidoc-content #footnotes-section li {
          padding-left: 0.5rem;
          margin-bottom: 0.5rem;
          line-height: 1.6;
          display: list-item;
        }
        /* Position backlink at end of first line */
        .asciidoc-content #references-section li > div > span > div:first-child,
        .asciidoc-content #footnotes-section li > div > span > div:first-child {
          position: relative;
          display: inline-block;
          width: 100%;
        }
        .asciidoc-content #references-section h3,
        .asciidoc-content #footnotes-section h3 {
          font-size: 1.125rem;
          font-weight: 600;
          margin-bottom: 1rem;
        }
        /* Blockquote spacing in citations */
        .asciidoc-content #references-section blockquote,
        .asciidoc-content #footnotes-section blockquote {
          padding-left: 1.5rem !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.title && isBookstrEvent && (
          <h1 className="break-words">
            {bookMetadata.book
              ? bookMetadata.book
                  .split('-')
                  .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
                  .join(' ')
              : 'Bookstr Publication'}
          </h1>
        )}
        {!hideImagesAndInfo && isBookstrEvent && (
          <div className="text-xs text-muted-foreground space-x-2 mb-2">
            {bookMetadata.type && <span>Type: {bookMetadata.type}</span>}
            {bookMetadata.book && <span>Book: {bookMetadata.book
              .split('-')
              .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
              .join(' ')}</span>}
            {bookMetadata.chapter && <span>Chapter: {bookMetadata.chapter}</span>}
            {bookMetadata.verse && <span>Verse: {bookMetadata.verse}</span>}
            {bookMetadata.version && <span>Version: {bookMetadata.version.toUpperCase()}</span>}
          </div>
        )}
        {!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>
        )}
        {hideImagesAndInfo && !metadata.title && isBookstrEvent && (
          <h2 className="text-2xl font-bold mb-4 leading-tight break-words">
            {bookMetadata.book
              ? bookMetadata.book
                  .split('-')
                  .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
                  .join(' ')
              : 'Bookstr Publication'}
          </h2>
        )}
        {hideImagesAndInfo && isBookstrEvent && (
          <div className="text-xs text-muted-foreground space-x-2 mb-2">
            {bookMetadata.type && <span>Type: {bookMetadata.type}</span>}
            {bookMetadata.book && <span>Book: {bookMetadata.book
              .split('-')
              .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
              .join(' ')}</span>}
            {bookMetadata.chapter && <span>Chapter: {bookMetadata.chapter}</span>}
            {bookMetadata.verse && <span>Verse: {bookMetadata.verse}</span>}
            {bookMetadata.version && <span>Version: {bookMetadata.version.toUpperCase()}</span>}
          </div>
        )}
        
        {/* Metadata image */}
        {!hideImagesAndInfo && metadata.image && (() => {
          const cleanedMetadataImage = cleanUrl(metadata.image)
          const parentImageUrlCleaned = parentImageUrl ? cleanUrl(parentImageUrl) : null
          // Don't show if already in content
          if (cleanedMetadataImage && mediaUrlsInContent.has(cleanedMetadataImage)) {
            return null
          }
          // Don't show if it matches the parent publication's image (to avoid duplicate cover images)
          if (parentImageUrlCleaned && cleanedMetadataImage === parentImageUrlCleaned) {
            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>
        )}
        
        {/* Footnotes and References sections - rendered via useEffect after citations are processed */}
        <div id="footnotes-section-container"></div>
        <div id="references-section-container"></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: false,
              closeOnPullUp: true,
              closeOnPullDown: true
            }}
            render={{
              buttonPrev: allImages.length <= 1 ? () => null : undefined,
              buttonNext: allImages.length <= 1 ? () => null : undefined
            }}
            styles={{
              toolbar: { paddingTop: '2.25rem' }
            }}
            carousel={{
              finite: false
            }}
          />
        </div>,
        document.body
      )}
    </>
  )
}

