// MarkdownPreview.jsx — Phase 2 of the Contribute rewrite. The rendered
// preview pane that replaces Tiptap's read-only render for Discuss mode
// and sits next to the CM6 raw pane in Contribute mode.
//
// Two responsibilities:
//   • Markdown → HTML via marked, with ```mermaid fences extracted to
//     placeholder nodes that resolve once mermaid lazy-loads.
//   • Window-selection bridge per §8.12 — onMouseUp inside the preview
//     reports {text, coords} to the parent so the SelectionTooltip can
//     anchor against the rendered DOM (no PM/Tiptap surface here).
//
// Mermaid lazy-load: triggered on the first appearance of a ```mermaid
// fence in any rendered doc, not on mount. The module (~200 KB gzipped)
// stays out of the main bundle. `securityLevel: 'strict'` neutralizes
// hostile <script>/onclick payloads embedded in diagram source.
//
// The mermaid render path is deliberately decoupled from "blocks are
// immutable" — placeholder DOM nodes carry the source as a data
// attribute, and the renderer takes (source) → SVG. A future
// authoring layer can intercept before mount without restructuring.

import { useEffect, useRef, useState, useCallback } from 'react'
import { Marked } from 'marked'
import { sanitizeHtml } from '../lib/sanitizeHtml'
import { decorateAcceptedChanges } from './trackedOverlay.js'
import ChangeTooltip from './ChangeTooltip.jsx'

// Module-level mermaid loader. Holds the Promise across consumers so
// the chunk fetches exactly once across the app lifetime.
let mermaidPromise = null
function loadMermaid() {
  if (!mermaidPromise) {
    mermaidPromise = import('mermaid').then(m => {
      const mermaid = m.default
      mermaid.initialize({
        startOnLoad: false,
        securityLevel: 'strict',
        suppressErrorRendering: true,
      })
      return mermaid
    })
  }
  return mermaidPromise
}

function escapeHtml(s) {
  return String(s).replace(/[&<>"']/g, c => (
    { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]
  ))
}

// Scoped Marked instance so the mermaid-fence renderer doesn't leak
// to any other call site that might construct its own Marked.
const previewMarked = new Marked({
  renderer: {
    code({ text, lang }) {
      const tag = (lang || '').trim().split(/\s+/)[0]
      if (tag === 'mermaid') {
        const encoded = encodeURIComponent(text || '')
        // Placeholder shows the source as a <pre> until mermaid resolves;
        // keeps the preview useful even if mermaid never loads.
        return (
          `<div class="mermaid-block" data-mermaid-src="${encoded}">`
          + `<pre class="mermaid-placeholder">${escapeHtml(text || '')}</pre>`
          + `</div>`
        )
      }
      return false   // fall through to the default code renderer
    },
  },
})

export default function MarkdownPreview({
  content,
  onSelectionChange,
  className,
  // Phase 3 — tracked-change overlay (§8.10). When `showTrackedChanges`
  // is true, accepted changes from the branch's changes list are
  // decorated inline as <span class="tracked-insert"> / <span
  // class="tracked-delete">. Hovering a decorated span surfaces a
  // ChangeTooltip with the source message + reason. `messages` is
  // optional; without it the tooltip falls back to badge + reason.
  changes,
  messages,
  showTrackedChanges = false,
}) {
  const hostRef = useRef(null)
  // Per-block source memo — lets us skip mermaid re-renders for blocks
  // whose source hasn't changed across doc updates.
  const lastMermaidSourcesRef = useRef([])
  // Monotonic counter the async mermaid pass uses to bail if a newer
  // render has already replaced the DOM beneath it.
  const renderTokenRef = useRef(0)
  // Tracked-change hover tooltip state. Shape: { change, position }.
  const [tooltip, setTooltip] = useState(null)

  // Render markdown → HTML on every content change. The tracked-change
  // overlay runs in the same effect so accepted-change spans appear
  // synchronously with the body itself — no flash of un-decorated text.
  useEffect(() => {
    if (!hostRef.current) return
    const html = sanitizeHtml(previewMarked.parse(content || ''))
    hostRef.current.innerHTML = html
    const token = ++renderTokenRef.current
    // Reset memo so the new block set re-renders from scratch.
    lastMermaidSourcesRef.current = []
    if (showTrackedChanges && changes && changes.length > 0) {
      decorateAcceptedChanges(hostRef.current, changes)
    }
    renderMermaidBlocks(hostRef.current, lastMermaidSourcesRef, token, renderTokenRef)
  }, [content, showTrackedChanges, changes])

  // Window-selection bridge for §8.12. Listen on document mouseup so
  // releases outside the preview still clear the prior selection.
  useEffect(() => {
    if (!onSelectionChange) return
    const handleMouseUp = () => {
      const sel = window.getSelection?.()
      if (!sel || sel.isCollapsed || sel.rangeCount === 0) {
        onSelectionChange(null)
        return
      }
      const range = sel.getRangeAt(0)
      const host = hostRef.current
      if (!host || !host.contains(range.commonAncestorContainer)) {
        // Selection is outside the preview — leave any active tooltip
        // selection alone (it belongs to another surface).
        return
      }
      const text = sel.toString()
      if (!text || !text.trim()) {
        onSelectionChange(null)
        return
      }
      const rect = range.getBoundingClientRect()
      onSelectionChange({
        text,
        coords: { top: rect.top, left: rect.left },
      })
    }
    document.addEventListener('mouseup', handleMouseUp)
    return () => document.removeEventListener('mouseup', handleMouseUp)
  }, [onSelectionChange])

  // Hover handler for tracked-change spans (§8.10). The decorated spans
  // carry data-change-id; we look the change row up out of `changes`
  // and surface a ChangeTooltip anchored to the cursor.
  const handleMouseMove = useCallback((e) => {
    if (!showTrackedChanges || !changes) return
    const span = e.target.closest?.('[data-change-id]')
    if (!span) {
      if (tooltip) setTooltip(null)
      return
    }
    const id = span.getAttribute('data-change-id')
    const change = changes.find(c => String(c.id) === String(id))
    if (!change) return
    setTooltip({ change, position: { x: e.clientX, y: e.clientY } })
  }, [showTrackedChanges, changes, tooltip])

  const handleMouseLeave = useCallback(() => {
    setTooltip(null)
  }, [])

  return (
    <>
      <div
        ref={hostRef}
        className={`markdown-preview${className ? ' ' + className : ''}`}
        onMouseMove={showTrackedChanges ? handleMouseMove : undefined}
        onMouseLeave={showTrackedChanges ? handleMouseLeave : undefined}
      />
      {tooltip && (
        <ChangeTooltip
          change={tooltip.change}
          messages={messages || []}
          position={tooltip.position}
        />
      )}
    </>
  )
}

async function renderMermaidBlocks(host, lastSourcesRef, token, tokenRef) {
  const blocks = host.querySelectorAll('.mermaid-block')
  if (blocks.length === 0) {
    lastSourcesRef.current = []
    return
  }
  let mermaid
  try {
    mermaid = await loadMermaid()
  } catch (err) {
    // Loading failed — leave the <pre> placeholders in place.
    console.error('mermaid load failed', err)
    return
  }
  if (tokenRef.current !== token) return   // stale pass
  const prev = lastSourcesRef.current
  const next = []
  for (let i = 0; i < blocks.length; i++) {
    const block = blocks[i]
    const src = decodeURIComponent(block.dataset.mermaidSrc || '')
    next.push(src)
    if (prev[i] === src && block.querySelector('svg')) continue
    if (tokenRef.current !== token) return
    try {
      const id = `mmd-${Math.random().toString(36).slice(2, 10)}`
      const { svg, bindFunctions } = await mermaid.render(id, src)
      if (tokenRef.current !== token) return
      if (!host.contains(block)) return
      block.innerHTML = svg
      bindFunctions?.(block)
    } catch (err) {
      block.innerHTML = (
        `<pre class="mermaid-error">Mermaid parse error: `
        + escapeHtml(err?.message || String(err))
        + `</pre>`
      )
    }
  }
  lastSourcesRef.current = next
}
