<script setup lang="ts">
/**
 * MarkdownEditor — markdown composer with toolbar, split-pane preview,
 * list auto-continuation, and an optional seamless mode.
 *
 * Standard mode: toolbar + side-by-side editor/preview (viewMode toggle).
 * Seamless mode (`seamless` prop): renders a live preview when unfocused;
 *   clicking the preview surface activates the editor. The toolbar gains a
 *   hold-to-peek button (and Ctrl/⌘+P keyboard shortcut) that temporarily
 *   shows the preview while the button is held.
 *
 * Keyboard shortcuts (when textarea is focused):
 *   Ctrl/⌘ + B    — bold
 *   Ctrl/⌘ + I    — italic
 *   Ctrl/⌘ + K    — link
 *   Ctrl/⌘ + `    — inline code
 *   Ctrl/⌘ + P    — hold-to-peek preview (seamless mode only)
 *   Enter          — continue the current list item (ul/ol)
 *   Enter (empty list item) — exit the list
 */
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue"
import {
  Bold,
  Braces,
  Code,
  Columns2,
  Eraser,
  Eye,
  Heading,
  Image,
  Italic,
  Link,
  List,
  ListOrdered,
  Minus,
  PanelLeft,
  Quote,
  Redo2,
  Strikethrough,
  Table,
  Undo2,
} from "@lucide/vue"
import { FieldFrame } from "@/components/ui/field-frame"
import { cn } from "@/lib/utils"

export interface MarkdownEditorProps {
  modelValue?: string | null
  id?: string
  name?: string
  label?: string
  hideLabel?: boolean
  placeholder?: string
  required?: boolean
  readonly?: boolean
  disabled?: boolean
  helpText?: string
  errors?: string[]
  /** Minimum visible row count (controls pane min-height). */
  rows?: number
  /**
   * Seamless mode — renders the markdown preview when the editor is not
   * focused. Clicking the preview surface activates the editor. The toolbar
   * gains a hold-to-peek button (and Ctrl/⌘+P keyboard shortcut) that shows
   * the preview while held, then returns to the editor on release.
   */
  seamless?: boolean
  /**
   * Mini preview — when combined with `seamless`, shows a small scaled
   * thumbnail of the rendered output in the top-left corner of the editor
   * while editing. Holding the peek button / Ctrl+⌘+P animates the thumbnail
   * expanding to fill the editor, then back to mini on release. Scroll
   * position is kept in sync with the textarea.
   */
  miniPreview?: boolean
}

const props = withDefaults(defineProps<MarkdownEditorProps>(), {
  errors: () => [],
  rows: 16,
})

const model = defineModel<string | null>("modelValue")

/* ------------------------------------------------------------------ */
/* View mode (standard mode only)                                     */
/* ------------------------------------------------------------------ */

type ViewMode = "edit" | "split" | "preview"
const viewMode = ref<ViewMode>("split")

/* ------------------------------------------------------------------ */
/* Seamless mode state                                                 */
/* ------------------------------------------------------------------ */

/** Whether the editor pane is active (seamless mode only). */
const isEditing = ref(false)
/** Whether the user is holding the peek button / Ctrl+P (seamless only). */
const isPeeking = ref(false)
/** Ref to the entire editor container; used for focusout-to-close logic. */
const containerRef = ref<HTMLElement | null>(null)

function enterEditMode(): void {
  if (props.disabled || props.readonly) return
  isEditing.value = true
  nextTick(() => textareaRef.value?.focus())
}

function onContainerFocusOut(_event: FocusEvent): void {
  if (!props.seamless) return
  // Defer the check: toolbar actions queue `nextTick(() => ta.focus())` which
  // are microtasks and run before this setTimeout(0) macrotask. By the time
  // this runs, activeElement has already settled on the textarea (or elsewhere).
  setTimeout(() => {
    if (!isEditing.value) return
    const active = document.activeElement
    const insideEditor = containerRef.value?.contains(active)
    const insideMenu = headingMenuRef.value?.contains(active)
    if (!insideEditor && !insideMenu) {
      isEditing.value = false
      isPeeking.value = false
    }
  }, 0)
}

/** Called on the toolbar div's mousedown in seamless mode.
 *  Prevents focus from leaving the textarea when toolbar buttons are clicked. */
function onToolbarMousedown(event: MouseEvent): void {
  if (props.seamless) {
    event.preventDefault()
  }
}

/** Hold-to-peek: pointer pressed on the Peek button. */
function startPeek(): void {
  if (!props.miniPreview) {
    // Without mini preview, the textarea unmounts when peeking — move focus to
    // the container first so onContainerFocusOut's activeElement check still
    // sees focus inside.
    containerRef.value?.focus()
  }
  isPeeking.value = true
}

/** Hold-to-peek: pointer released / left the Peek button. */
function endPeek(): void {
  isPeeking.value = false
  nextTick(() => textareaRef.value?.focus())
}

/* ------------------------------------------------------------------ */
/* Mini preview (seamless + miniPreview prop)                          */
/* ------------------------------------------------------------------ */

const MINI_W = 140
const MINI_H = 90
const CONTENT_W = 600
const MINI_SCALE = MINI_W / CONTENT_W  // ≈ 0.233 — content width → thumbnail width

/** Normalised scroll ratio [0–1] driven by the textarea's scrollTop. */
const miniScrollRatio = ref(0)
/** Ref to the unscaled inner content div inside the mini thumbnail. */
const miniInnerRef = ref<HTMLElement | null>(null)
/** Actual rendered height of the mini inner content (updated via ResizeObserver). */
const miniInnerHeight = ref(0)
/** Ref to the full-size expanded preview div shown while peeking. */
const fullPreviewRef = ref<HTMLElement | null>(null)

// Note: the watch that syncs miniInnerHeight with previewHtml is declared
// after previewHtml to avoid a temporal dead zone reference — see below.

// Keep the full-size peek preview scrolled to the same relative position as the textarea.
watch(miniScrollRatio, (ratio) => {
  const el = fullPreviewRef.value
  if (!el) return
  el.scrollTop = ratio * Math.max(0, el.scrollHeight - el.clientHeight)
})

function onTextareaScroll(event: Event): void {
  if (!props.miniPreview) return
  const ta = event.target as HTMLTextAreaElement
  miniScrollRatio.value = ta.scrollTop / Math.max(ta.scrollHeight - ta.clientHeight, 1)
}

const showMiniPreview = computed(() => props.seamless && props.miniPreview && isEditing.value)

/** CSS transform applied to the unscaled inner content to scale + scroll-sync it. */
const miniInnerTransform = computed(() => {
  // How many unscaled pixels are visible through the thumbnail window.
  const visibleH = MINI_H / MINI_SCALE
  const scrollable = Math.max(0, miniInnerHeight.value - visibleH)
  const offset = miniScrollRatio.value * scrollable
  return `translateY(-${offset}px) scale(${MINI_SCALE})`
})

// ── Thumbnail JS transition hooks ────────────────────────────────────

function onMiniEnter(el: Element, done: () => void): void {
  const div = el as HTMLDivElement
  div.style.transform = "scale(0.85) translate(4px, -4px)"
  div.style.transition = "none"
  requestAnimationFrame(() => {
    div.style.transition = "transform 0.2s ease"
    div.style.transform = "scale(1) translate(0, 0)"
    setTimeout(done, 200)
  })
}

function onMiniLeave(el: Element, done: () => void): void {
  const div = el as HTMLDivElement
  div.style.transition = "opacity 0.15s ease, transform 0.15s ease"
  div.style.opacity = "0"
  div.style.transform = "scale(1.08) translate(4px, -3px)"
  setTimeout(done, 150)
}

// ── Expanded peek JS transition hooks ────────────────────────────────
// Clip-path animates the full preview from the thumbnail's exact corner
// position (top-right), creating a "bloom from thumbnail" expansion effect.

function onPeekEnter(_el: Element, done: () => void): void {
  done()
}

function onPeekLeave(_el: Element, done: () => void): void {
  done()
}

/* ------------------------------------------------------------------ */
/* Heading picker — rendered via Teleport to escape overflow clipping  */
/* ------------------------------------------------------------------ */

const showHeadingMenu = ref(false)
const headingBtnRef = ref<HTMLButtonElement | null>(null)
const headingMenuRef = ref<HTMLElement | null>(null)
/** Fixed viewport position of the dropdown (updated on open). */
const headingMenuPos = ref({ top: 0, left: 0 })

function openHeadingMenu(): void {
  const rect = headingBtnRef.value?.getBoundingClientRect()
  if (rect) {
    headingMenuPos.value = { top: rect.bottom + 2, left: rect.left }
  }
  showHeadingMenu.value = true
}

function closeHeadingMenu(): void {
  showHeadingMenu.value = false
}

function toggleHeadingMenu(): void {
  if (showHeadingMenu.value) closeHeadingMenu()
  else openHeadingMenu()
}

/** Close when clicking outside both the trigger button and the teleported menu. */
function onHeadingMenuDocClick(event: MouseEvent): void {
  const target = event.target as Node
  if (
    headingBtnRef.value?.contains(target) ||
    headingMenuRef.value?.contains(target)
  ) {
    return
  }
  closeHeadingMenu()
}

watch(showHeadingMenu, (open) => {
  if (open) {
    document.addEventListener("click", onHeadingMenuDocClick, { capture: true })
  } else {
    document.removeEventListener("click", onHeadingMenuDocClick, { capture: true })
  }
})

onBeforeUnmount(() => {
  document.removeEventListener("click", onHeadingMenuDocClick, { capture: true })
})

/* ------------------------------------------------------------------ */
/* Textarea ref + cursor manipulation                                  */
/* ------------------------------------------------------------------ */

const textareaRef = ref<HTMLTextAreaElement | null>(null)

const editorMinHeight = computed(() => `${props.rows * 1.5}rem`)

/** Wrap the current selection (or a placeholder) with before/after markers. */
function wrapSelection(before: string, after = "", placeholder = "text"): void {
  const ta = textareaRef.value
  if (!ta || props.disabled || props.readonly) return
  const start = ta.selectionStart
  const end = ta.selectionEnd
  const selected = ta.value.slice(start, end) || placeholder
  const newValue = ta.value.slice(0, start) + before + selected + after + ta.value.slice(end)
  model.value = newValue
  nextTick(() => {
    ta.focus()
    ta.setSelectionRange(start + before.length, start + before.length + selected.length)
  })
}

/** Prefix the line at the current cursor position. */
function insertAtLineStart(prefix: string): void {
  const ta = textareaRef.value
  if (!ta || props.disabled || props.readonly) return
  const start = ta.selectionStart
  const lineStart = ta.value.lastIndexOf("\n", start - 1) + 1
  const newValue = ta.value.slice(0, lineStart) + prefix + ta.value.slice(lineStart)
  model.value = newValue
  nextTick(() => {
    ta.focus()
    ta.setSelectionRange(start + prefix.length, start + prefix.length)
  })
}

/** Insert a block at the cursor with surrounding newline guards. */
function insertBlock(block: string): void {
  const ta = textareaRef.value
  if (!ta || props.disabled || props.readonly) return
  const start = ta.selectionStart
  const before = start > 0 && ta.value[start - 1] !== "\n" ? "\n" : ""
  const newValue = ta.value.slice(0, start) + before + block + "\n" + ta.value.slice(start)
  model.value = newValue
  nextTick(() => {
    ta.focus()
    const pos = start + before.length + block.length + 1
    ta.setSelectionRange(pos, pos)
  })
}

/* ------------------------------------------------------------------ */
/* Toolbar actions                                                     */
/* ------------------------------------------------------------------ */

function onUndo(): void {
  textareaRef.value?.focus()
  document.execCommand("undo")
}
function onRedo(): void {
  textareaRef.value?.focus()
  document.execCommand("redo")
}
function onClear(): void {
  if (!props.disabled && !props.readonly) {
    model.value = ""
    nextTick(() => textareaRef.value?.focus())
  }
}
function onHeading(level: number): void {
  insertAtLineStart("#".repeat(level) + " ")
  showHeadingMenu.value = false
}
function onBold(): void { wrapSelection("**", "**", "bold text") }
function onItalic(): void { wrapSelection("*", "*", "italic text") }
function onStrikethrough(): void { wrapSelection("~~", "~~", "strikethrough") }
function onBlockquote(): void { insertAtLineStart("> ") }
function onOrderedList(): void { insertAtLineStart("1. ") }
function onUnorderedList(): void { insertAtLineStart("- ") }
function onTable(): void {
  insertBlock(
    "| Column 1 | Column 2 | Column 3 |\n| --- | --- | --- |\n| Cell 1 | Cell 2 | Cell 3 |",
  )
}
function onHR(): void { insertBlock("---") }
function onLink(): void { wrapSelection("[", "](url)", "link text") }
function onImage(): void { insertBlock("![alt text](image-url)") }
function onCode(): void { wrapSelection("`", "`", "code") }
function onCodeFence(): void { insertBlock("```\ncode here\n```") }

/* ------------------------------------------------------------------ */
/* Keyboard handling                                                   */
/* ------------------------------------------------------------------ */

function onKeydown(event: KeyboardEvent): void {
  const ta = textareaRef.value
  if (!ta) return

  // ── Ctrl/⌘ shortcuts ──────────────────────────────────────────────
  if (event.ctrlKey || event.metaKey) {
    switch (event.key.toLowerCase()) {
      case "b":
        event.preventDefault()
        onBold()
        return
      case "i":
        event.preventDefault()
        onItalic()
        return
      case "k":
        event.preventDefault()
        onLink()
        return
      case "`":
        event.preventDefault()
        onCode()
        return
      case "p":
        // Hold-to-peek: show preview while Ctrl+P is held (seamless mode only).
        if (props.seamless && !event.repeat) {
          event.preventDefault()
          if (!props.miniPreview) {
            // Without mini preview the textarea unmounts — move focus first.
            containerRef.value?.focus()
          }
          isPeeking.value = true
          // Release peek on ANY keyup (covers releasing P or Ctrl first).
          const cleanup = () => {
            isPeeking.value = false
            nextTick(() => textareaRef.value?.focus())
            window.removeEventListener("keyup", cleanup)
          }
          window.addEventListener("keyup", cleanup)
        }
        return
    }
    return
  }

  // ── Enter: list auto-continuation ─────────────────────────────────
  if (event.key === "Enter" && !event.shiftKey) {
    const cursor = ta.selectionStart
    const lineStart = ta.value.lastIndexOf("\n", cursor - 1) + 1
    const line = ta.value.slice(lineStart, cursor)

    // Unordered list
    const ulMatch = line.match(/^([-*+]) (.*)$/)
    if (ulMatch) {
      event.preventDefault()
      const prefix = ulMatch[1]! + " "
      if (!ulMatch[2]!.trim()) {
        // Empty item → exit list, remove the marker
        const newValue = ta.value.slice(0, lineStart) + "\n" + ta.value.slice(cursor)
        model.value = newValue
        nextTick(() => ta.setSelectionRange(lineStart + 1, lineStart + 1))
      } else {
        const insertion = "\n" + prefix
        model.value = ta.value.slice(0, cursor) + insertion + ta.value.slice(cursor)
        nextTick(() => ta.setSelectionRange(cursor + insertion.length, cursor + insertion.length))
      }
      return
    }

    // Ordered list
    const olMatch = line.match(/^(\d+)\. (.*)$/)
    if (olMatch) {
      event.preventDefault()
      const num = Number.parseInt(olMatch[1]!, 10)
      if (!olMatch[2]!.trim()) {
        // Empty item → exit list
        const newValue = ta.value.slice(0, lineStart) + "\n" + ta.value.slice(cursor)
        model.value = newValue
        nextTick(() => ta.setSelectionRange(lineStart + 1, lineStart + 1))
      } else {
        const prefix = `${num + 1}. `
        const insertion = "\n" + prefix
        model.value = ta.value.slice(0, cursor) + insertion + ta.value.slice(cursor)
        nextTick(() => ta.setSelectionRange(cursor + insertion.length, cursor + insertion.length))
      }
      return
    }
  }
}

function onInput(event: Event): void {
  const target = event.target as HTMLTextAreaElement | null
  model.value = target?.value ?? ""
}

/* ------------------------------------------------------------------ */
/* Markdown renderer                                                   */
/* ------------------------------------------------------------------ */

const previewHtml = computed<string>(() => renderMarkdown(model.value ?? ""))

function escapeHtml(s: string): string {
  return s
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
}

function inlineMarkdown(escaped: string): string {
  return escaped
    .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" class="max-w-full rounded" />')
    .replace(
      /\[([^\]]+)\]\(([^)]+)\)/g,
      '<a href="$2" rel="noopener noreferrer" class="text-primary underline underline-offset-2">$1</a>',
    )
    .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
    .replace(/\*(.+?)\*/g, "<em>$1</em>")
    .replace(/~~(.+?)~~/g, "<del>$1</del>")
    .replace(
      /`([^`]+)`/g,
      '<code class="rounded bg-muted px-1 py-0.5 font-mono text-[0.85em]">$1</code>',
    )
}

const HEADING_SIZES: Record<number, string> = {
  1: "text-3xl font-bold mt-6 mb-3",
  2: "text-2xl font-bold mt-5 mb-2",
  3: "text-xl font-semibold mt-4 mb-2",
  4: "text-lg font-semibold mt-3 mb-1",
  5: "text-base font-medium mt-2 mb-1",
  6: "text-sm font-medium mt-2 mb-1",
}

function isBlockStart(line: string): boolean {
  return /^(#{1,6}\s|>\s|[-*+]\s|\d+\.\s|```|(-{3,}|\*{3,}|_{3,})\s*$|\|)/.test(line)
}

function renderMarkdown(input: string): string {
  if (!input.trim()) return ""

  const lines = input.split("\n")
  const out: string[] = []
  let i = 0

  while (i < lines.length) {
    const line = lines[i] ?? ""

    // ── Code fence ────────────────────────────────────────────────────
    if (line.startsWith("```")) {
      const lang = line.slice(3).trim()
      const codeLines: string[] = []
      i++
      while (i < lines.length && !(lines[i] ?? "").startsWith("```")) {
        codeLines.push(lines[i] ?? "")
        i++
      }
      out.push(
        `<pre class="my-4 overflow-x-auto rounded-md bg-muted px-4 py-3 text-sm font-mono">${
          lang ? `<code class="language-${escapeHtml(lang)}">` : "<code>"
        }${escapeHtml(codeLines.join("\n"))}</code></pre>`,
      )
      i++
      continue
    }

    // ── Horizontal rule ───────────────────────────────────────────────
    if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
      out.push('<hr class="my-6 border-border" />')
      i++
      continue
    }

    // ── Heading ───────────────────────────────────────────────────────
    // Use `(.*)` (not `.+`) so a bare "# " with no trailing content still
    // matches and advances i — otherwise the paragraph fallthrough sees it
    // as a block-start, refuses to consume it, and loops forever.
    const headingMatch = line.match(/^(#{1,6})\s+(.*)$/)
    if (headingMatch) {
      const lvl = headingMatch[1]!.length
      const text = headingMatch[2]!.trim()
      if (text) {
        out.push(
          `<h${lvl} class="${HEADING_SIZES[lvl] ?? ""} leading-tight">${inlineMarkdown(escapeHtml(text))}</h${lvl}>`,
        )
      }
      i++
      continue
    }

    // ── Blockquote ────────────────────────────────────────────────────
    if (line.startsWith("> ")) {
      const bqLines: string[] = []
      while (i < lines.length && (lines[i] ?? "").startsWith("> ")) {
        bqLines.push((lines[i] ?? "").slice(2))
        i++
      }
      out.push(
        `<blockquote class="my-3 border-l-4 border-muted-foreground/30 pl-4 italic text-muted-foreground">${inlineMarkdown(escapeHtml(bqLines.join("\n")))}</blockquote>`,
      )
      continue
    }

    // ── Unordered list ────────────────────────────────────────────────
    if (/^[-*+]\s/.test(line)) {
      const items: string[] = []
      while (i < lines.length && /^[-*+]\s/.test(lines[i] ?? "")) {
        items.push(
          `<li class="leading-relaxed">${inlineMarkdown(escapeHtml((lines[i] ?? "").replace(/^[-*+]\s/, "")))}</li>`,
        )
        i++
      }
      out.push(`<ul class="my-3 list-disc space-y-1 pl-6">${items.join("")}</ul>`)
      continue
    }

    // ── Ordered list ──────────────────────────────────────────────────
    if (/^\d+\.\s/.test(line)) {
      const items: string[] = []
      while (i < lines.length && /^\d+\.\s/.test(lines[i] ?? "")) {
        items.push(
          `<li class="leading-relaxed">${inlineMarkdown(escapeHtml((lines[i] ?? "").replace(/^\d+\.\s/, "")))}</li>`,
        )
        i++
      }
      out.push(`<ol class="my-3 list-decimal space-y-1 pl-6">${items.join("")}</ol>`)
      continue
    }

    // ── Table ─────────────────────────────────────────────────────────
    if (line.includes("|") && /^\|[\s:|-]+\|/.test(lines[i + 1] ?? "")) {
      const headers = line
        .split("|")
        .slice(1, -1)
        .map(
          (h) =>
            `<th class="border border-border bg-muted/50 px-3 py-2 text-left font-semibold">${inlineMarkdown(escapeHtml(h.trim()))}</th>`,
        )
      i += 2
      const rows: string[] = []
      while (
        i < lines.length &&
        (lines[i] ?? "").includes("|") &&
        !/^\|[\s:|-]+\|/.test(lines[i] ?? "")
      ) {
        const cells = (lines[i] ?? "")
          .split("|")
          .slice(1, -1)
          .map(
            (c) =>
              `<td class="border border-border px-3 py-2">${inlineMarkdown(escapeHtml(c.trim()))}</td>`,
          )
        rows.push(`<tr class="even:bg-muted/20">${cells.join("")}</tr>`)
        i++
      }
      out.push(
        `<div class="my-4 overflow-x-auto rounded-md border"><table class="w-full border-collapse text-sm"><thead><tr>${headers.join("")}</tr></thead><tbody>${rows.join("")}</tbody></table></div>`,
      )
      continue
    }

    // ── Empty line ────────────────────────────────────────────────────
    if (!line.trim()) {
      i++
      continue
    }

    // ── Paragraph ─────────────────────────────────────────────────────
    const paraLines: string[] = []
    while (i < lines.length && (lines[i] ?? "").trim() && !isBlockStart(lines[i] ?? "")) {
      paraLines.push(lines[i] ?? "")
      i++
    }
    if (paraLines.length) {
      out.push(
        `<p class="my-2 leading-relaxed">${inlineMarkdown(escapeHtml(paraLines.join(" ")))}</p>`,
      )
    } else {
      // Defensive guard: if we reach here with no lines consumed the current
      // line is a block-start that no handler above matched (e.g. a bare "> "
      // with future syntax). Skip it so the outer while can't loop forever.
      i++
    }
  }

  return out.join("\n")
}

// Re-measure the mini thumbnail's inner content height whenever the rendered
// HTML changes or the element ref mounts. Declared here (after renderMarkdown
// and HEADING_SIZES) so watch's eager source evaluation doesn't hit a TDZ.
// Post-flush ensures we read scrollHeight after Vue has committed the new DOM.
watch([miniInnerRef, previewHtml], () => {
  const el = miniInnerRef.value
  if (el) miniInnerHeight.value = el.scrollHeight
}, { flush: "post" })

/* ------------------------------------------------------------------ */
/* Shared computed for pane visibility / widths                       */
/* ------------------------------------------------------------------ */

/** Whether the source textarea should be rendered. */
const showSource = computed<boolean>(() => {
  if (props.seamless) {
    // In mini-preview mode the textarea stays mounted during peek; the overlay
    // covers it instead of unmounting the textarea.
    if (props.miniPreview) return isEditing.value
    return isEditing.value && !isPeeking.value
  }
  return viewMode.value !== "preview"
})

/** Whether the standalone preview pane should be rendered. */
const showPreview = computed<boolean>(() => {
  if (props.seamless) {
    // In mini-preview mode all preview rendering is handled by the overlay.
    if (props.miniPreview) return false
    return !isEditing.value || isPeeking.value
  }
  return viewMode.value !== "edit"
})

const sourceWidthClass = computed<string>(() => {
  if (props.seamless) return "w-full"
  return viewMode.value === "split" ? "w-1/2 border-r" : "w-full"
})

const previewWidthClass = computed<string>(() => {
  if (props.seamless) return "w-full"
  return viewMode.value === "split" ? "w-1/2 border-l" : "w-full"
})
</script>

<template>
  <FieldFrame
    :label="label"
    :required="required"
    :hide-label="hideLabel"
    :for="id"
    :help-text="helpText"
    :errors="errors"
    data-testid="markdown-editor"
  >
    <!-- ─── SEAMLESS MODE: unfocused preview surface ──────────────── -->
    <div
      v-if="props.seamless && !isEditing"
      role="button"
      tabindex="0"
      :style="{ minHeight: editorMinHeight }"
      :class="cn(
        'group relative cursor-text rounded-md px-4 py-3 transition-all',
        'ring-1 ring-transparent hover:ring-ring/30',
        'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
        props.disabled ? 'pointer-events-none opacity-50' : '',
      )"
      data-testid="markdown-editor-preview-surface"
      @click="enterEditMode"
      @keydown.enter.prevent="enterEditMode"
      @keydown.space.prevent="enterEditMode"
    >
      <div
        v-if="previewHtml"
        class="prose prose-sm max-w-none text-sm dark:prose-invert"
        data-testid="markdown-editor-preview"
        v-html="previewHtml"
      />
      <p
        v-else
        class="text-sm text-muted-foreground"
      >
        {{ placeholder ?? "Write markdown here…" }}
      </p>
      <span
        class="absolute bottom-2 right-2 rounded-sm bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100"
      >
        Click to edit
      </span>
    </div>

    <!-- ─── MAIN EDITOR CONTAINER ─────────────────────────────────── -->
    <div
      v-if="!props.seamless || isEditing"
      ref="containerRef"
      tabindex="-1"
      class="flex flex-col rounded-md border focus-visible:outline-none"
      @focusout="onContainerFocusOut"
    >
      <!-- Toolbar -->
      <div
        class="flex shrink-0 items-center gap-px overflow-x-auto border-b bg-muted/20 px-1.5 py-1"
        data-testid="markdown-editor-toolbar"
        @mousedown="onToolbarMousedown"
      >
        <!-- Undo / Redo / Clear -->
        <button
          type="button"
          class="flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40"
          title="Undo"
          data-testid="markdown-editor-undo"
          @click="onUndo"
        >
          <Undo2 class="h-3.5 w-3.5" />
        </button>
        <button
          type="button"
          class="flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40"
          title="Redo"
          data-testid="markdown-editor-redo"
          @click="onRedo"
        >
          <Redo2 class="h-3.5 w-3.5" />
        </button>
        <button
          type="button"
          class="flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40"
          title="Clear"
          :disabled="disabled || readonly"
          data-testid="markdown-editor-clear"
          @click="onClear"
        >
          <Eraser class="h-3.5 w-3.5" />
        </button>

        <div class="mx-1 h-5 w-px shrink-0 bg-border" />

        <!-- Heading picker trigger -->
        <button
          ref="headingBtnRef"
          type="button"
          class="flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40"
          :class="showHeadingMenu ? 'bg-accent text-accent-foreground' : ''"
          title="Heading"
          :disabled="disabled || readonly"
          data-testid="markdown-editor-heading"
          @click="toggleHeadingMenu"
        >
          <Heading class="h-3.5 w-3.5" />
        </button>

        <!-- Text formatting -->
        <button
          type="button"
          class="flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40"
          title="Bold (Ctrl+B)"
          :disabled="disabled || readonly"
          data-testid="markdown-editor-bold"
          @click="onBold"
        >
          <Bold class="h-3.5 w-3.5" />
        </button>
        <button
          type="button"
          class="flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40"
          title="Italic (Ctrl+I)"
          :disabled="disabled || readonly"
          data-testid="markdown-editor-italic"
          @click="onItalic"
        >
          <Italic class="h-3.5 w-3.5" />
        </button>
        <button
          type="button"
          class="flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40"
          title="Strikethrough"
          :disabled="disabled || readonly"
          data-testid="markdown-editor-strikethrough"
          @click="onStrikethrough"
        >
          <Strikethrough class="h-3.5 w-3.5" />
        </button>
        <button
          type="button"
          class="flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40"
          title="Blockquote"
          :disabled="disabled || readonly"
          data-testid="markdown-editor-blockquote"
          @click="onBlockquote"
        >
          <Quote class="h-3.5 w-3.5" />
        </button>

        <div class="mx-1 h-5 w-px shrink-0 bg-border" />

        <!-- Lists + blocks -->
        <button
          type="button"
          class="flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40"
          title="Ordered list"
          :disabled="disabled || readonly"
          data-testid="markdown-editor-ordered-list"
          @click="onOrderedList"
        >
          <ListOrdered class="h-3.5 w-3.5" />
        </button>
        <button
          type="button"
          class="flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40"
          title="Unordered list"
          :disabled="disabled || readonly"
          data-testid="markdown-editor-unordered-list"
          @click="onUnorderedList"
        >
          <List class="h-3.5 w-3.5" />
        </button>
        <button
          type="button"
          class="flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40"
          title="Table"
          :disabled="disabled || readonly"
          data-testid="markdown-editor-table"
          @click="onTable"
        >
          <Table class="h-3.5 w-3.5" />
        </button>
        <button
          type="button"
          class="flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40"
          title="Horizontal rule"
          :disabled="disabled || readonly"
          data-testid="markdown-editor-hr"
          @click="onHR"
        >
          <Minus class="h-3.5 w-3.5" />
        </button>

        <div class="mx-1 h-5 w-px shrink-0 bg-border" />

        <!-- Insert elements -->
        <button
          type="button"
          class="flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40"
          title="Link (Ctrl+K)"
          :disabled="disabled || readonly"
          data-testid="markdown-editor-link"
          @click="onLink"
        >
          <Link class="h-3.5 w-3.5" />
        </button>
        <button
          type="button"
          class="flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40"
          title="Image"
          :disabled="disabled || readonly"
          data-testid="markdown-editor-image"
          @click="onImage"
        >
          <Image class="h-3.5 w-3.5" />
        </button>
        <button
          type="button"
          class="flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40"
          title="Inline code (Ctrl+`)"
          :disabled="disabled || readonly"
          data-testid="markdown-editor-code"
          @click="onCode"
        >
          <Code class="h-3.5 w-3.5" />
        </button>
        <button
          type="button"
          class="flex h-7 w-7 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40"
          title="Code block"
          :disabled="disabled || readonly"
          data-testid="markdown-editor-code-fence"
          @click="onCodeFence"
        >
          <Braces class="h-3.5 w-3.5" />
        </button>

        <!-- Right side -->
        <div class="ml-auto flex items-center gap-px pl-2">
          <!-- Peek button (seamless mode only): hold to preview, release to edit -->
          <button
            v-if="props.seamless"
            type="button"
            class="flex h-7 w-7 items-center justify-center rounded transition-colors"
            :class="isPeeking
              ? 'bg-accent text-accent-foreground'
              : 'text-muted-foreground hover:bg-accent/60 hover:text-accent-foreground'"
            title="Hold to preview (Ctrl+P)"
            data-testid="markdown-editor-peek"
            @pointerdown.stop="startPeek"
            @pointerup.stop="endPeek"
            @pointerleave.stop="endPeek"
          >
            <Eye class="h-3.5 w-3.5" />
          </button>

          <!-- View mode toggle (standard mode only) -->
          <template v-else>
            <button
              type="button"
              class="flex h-7 w-7 items-center justify-center rounded transition-colors"
              :class="viewMode === 'edit'
                ? 'bg-accent text-accent-foreground'
                : 'text-muted-foreground hover:bg-accent/60 hover:text-accent-foreground'"
              title="Edit only"
              data-testid="markdown-editor-view-edit"
              @click="viewMode = 'edit'"
            >
              <PanelLeft class="h-3.5 w-3.5" />
            </button>
            <button
              type="button"
              class="flex h-7 w-7 items-center justify-center rounded transition-colors"
              :class="viewMode === 'split'
                ? 'bg-accent text-accent-foreground'
                : 'text-muted-foreground hover:bg-accent/60 hover:text-accent-foreground'"
              title="Split view"
              data-testid="markdown-editor-view-split"
              @click="viewMode = 'split'"
            >
              <Columns2 class="h-3.5 w-3.5" />
            </button>
            <button
              type="button"
              class="flex h-7 w-7 items-center justify-center rounded transition-colors"
              :class="viewMode === 'preview'
                ? 'bg-accent text-accent-foreground'
                : 'text-muted-foreground hover:bg-accent/60 hover:text-accent-foreground'"
              title="Preview only"
              data-testid="markdown-editor-view-preview"
              @click="viewMode = 'preview'"
            >
              <Eye class="h-3.5 w-3.5" />
            </button>
          </template>
        </div>
      </div>

      <!-- ─── Editor panes ─────────────────────────────────────────── -->
      <div
        class="relative flex overflow-hidden rounded-b-md"
        :style="{ minHeight: editorMinHeight }"
      >
        <!-- Source textarea -->
        <textarea
          v-if="showSource"
          :id="id"
          ref="textareaRef"
          :name="name ?? undefined"
          :placeholder="placeholder ?? 'Write markdown here...'"
          :readonly="readonly"
          :disabled="disabled"
          :required="required"
          :value="model ?? ''"
          :data-element-name="name ?? undefined"
          data-testid="markdown-editor-source"
          :class="cn(
            'flex-1 resize-none bg-background px-4 py-3 font-mono text-sm leading-relaxed focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
            sourceWidthClass,
            errors.length ? 'border-destructive ring-destructive' : '',
          )"
          @input="onInput"
          @keydown="onKeydown"
          @scroll="onTextareaScroll"
        />

        <!-- Preview -->
        <div
          v-if="showPreview"
          data-testid="markdown-editor-preview"
          :class="cn(
            'flex-1 overflow-y-auto px-6 py-4 text-sm prose prose-sm max-w-none dark:prose-invert',
            previewWidthClass,
          )"
          v-html="previewHtml"
        />

        <!-- ─── Mini preview overlays (seamless + miniPreview prop) ─ -->

        <!--
          Thumbnail: a CONTENT_W-px div scaled down to MINI_W×MINI_H,
          scroll-synced with the textarea via translateY on the inner content.
        -->
        <Transition :css="false" @enter="onMiniEnter" @leave="onMiniLeave">
          <div
            v-if="showMiniPreview && !isPeeking"
            class="pointer-events-none absolute overflow-hidden rounded-md border bg-background shadow-md"
            :style="{
              top: '8px',
              right: '8px',
              width: `${MINI_W}px`,
              height: `${MINI_H}px`,
              zIndex: 10,
              opacity: '0.85',
            }"
            data-testid="markdown-editor-mini-preview"
          >
            <div
              ref="miniInnerRef"
              class="prose prose-sm pointer-events-none absolute left-0 top-0 max-w-none dark:prose-invert"
              :style="{
                transform: miniInnerTransform,
                transformOrigin: '0 0',
                width: `${CONTENT_W}px`,
                padding: '0.75rem',
                fontSize: '0.625rem',
                lineHeight: '1.4',
              }"
              v-html="previewHtml"
            />
          </div>
        </Transition>

        <!--
          Expanded peek: clip-path animates from the thumbnail's corner,
          "blooming" to fill the full panes area. The textarea stays mounted
          underneath (focus is never disturbed in miniPreview mode).
        -->
        <Transition :css="false" @enter="onPeekEnter" @leave="onPeekLeave">
          <div
            v-if="showMiniPreview && isPeeking"
            ref="fullPreviewRef"
            class="prose prose-sm absolute inset-0 max-w-none overflow-y-auto bg-background px-6 py-4 text-sm dark:prose-invert"
            style="z-index: 20; pointer-events: none;"
            data-testid="markdown-editor-peek-preview"
            v-html="previewHtml"
          />
        </Transition>
      </div>
    </div>
  </FieldFrame>

  <!-- Heading dropdown — teleported to body to escape all overflow contexts -->
  <Teleport to="body">
    <div
      v-if="showHeadingMenu"
      ref="headingMenuRef"
      :style="{
        position: 'fixed',
        top: `${headingMenuPos.top}px`,
        left: `${headingMenuPos.left}px`,
        zIndex: 9999,
      }"
      class="flex min-w-[5rem] flex-col gap-px rounded-md border bg-popover p-1 shadow-md"
      data-testid="markdown-editor-heading-menu"
      @mousedown.prevent
    >
      <button
        v-for="lvl in [1, 2, 3, 4, 5, 6]"
        :key="lvl"
        type="button"
        class="flex h-7 w-full items-center rounded px-2.5 text-sm hover:bg-accent"
        :class="lvl <= 2 ? 'font-bold' : lvl <= 4 ? 'font-semibold' : 'font-medium text-muted-foreground'"
        :data-testid="`markdown-editor-heading-${lvl}`"
        @click="onHeading(lvl)"
      >
        H{{ lvl }}
      </button>
    </div>
  </Teleport>
</template>
