<script setup>
import {
  getImageAltAttribute,
  getImageDisplayCaption,
  parseImageMarkdownLine
} from '../../lib/markdown-image.js'
import { parseInlineSegments, readEditableTextFromElement, setEditableCaretOffset } from '../../lib/markdown-inline.js'
import {
  appendTextToMarkdownLine,
  getAppendTextForMerge,
  getMergeJunctionDisplayOffset,
  hasListMarker,
  hasQuoteMarker,
  isEmptyListMarkerLine,
  isEmptyQuoteMarkerLine,
  parseOrderedListMarker,
  stripListMarker,
  stripQuoteMarker
} from '../../lib/markdown-live-edit.js'
import { buildCodeBlockLines, parseCodeFenceLine } from '../../lib/markdown-code-block.js'
import { buildToggleBlockLines } from '../../lib/markdown-toggle.js'
import { parseCalloutOptions } from '../../lib/markdown-callout.js'
import ContentMarkdownCodeBlockEditor from './ContentMarkdownCodeBlockEditor.vue'
import ProseCodeBlock from './ProseCodeBlock.vue'
import ContentMarkdownCalloutEditor from './ContentMarkdownCalloutEditor.vue'
import ContentMarkdownToggleEditor from './ContentMarkdownToggleEditor.vue'

const props = defineProps({
  content: {
    type: String,
    default: ''
  },
  /** 관리자 미리보기: 갤러리 드래그 정렬 등 */
  interactive: {
    type: Boolean,
    default: false
  },
  /** 슬래시 명령 메뉴 키보드 탐색 중 */
  slashMenuActive: {
    type: Boolean,
    default: false
  },
  /** ESC로 슬래시 메뉴를 닫은 원본 줄 번호(0-based) */
  slashSuppressedLines: {
    type: Array,
    default: () => []
  }
})

const emit = defineEmits([
  'gallery-reorder',
  'block-content-change',
  'append-paragraph',
  'insert-after-line',
  'delete-line',
  'merge-with-previous-line',
  'slash-update',
  'slash-end',
  'slash-apply'
])

const BLANK_PARAGRAPH_MARKER = '<!--sori:blank-paragraph-->'

const activeLightboxImages = ref([])
const activeLightboxIndex = ref(0)
/** @type {import('vue').Ref<{ blockId: string, imageIndex: number }|null>} */
const galleryDragState = ref(null)
/** @type {import('vue').Ref<{ blockId: string, targetIndex: number }|null>} */
const galleryDropTarget = ref(null)
/** @type {import('vue').Ref<number|null>} */
const pendingFocusLine = ref(null)
/** @type {import('vue').Ref<'start'|'end'|'auto'>} 포커스 후 커서 위치 */
const pendingFocusPosition = ref('auto')
/** @type {import('vue').Ref<number|null>} 포커스 후 텍스트 오프셋 */
const pendingFocusOffset = ref(null)
const rendererRootRef = ref(null)
/** @type {import('vue').Ref<Set<number>>} 원문(raw) 편집 중인 목록 줄 */
const rawEditingSourceLines = ref(new Set())
/** @type {number} 문단 분리 연속 호출 방지 */
let lastParagraphSplitAt = 0

/**
 * 마크다운 블록을 생성
 * @param {string} type - 블록 타입
 * @param {string|Array<string>} text - 블록 텍스트
 * @param {number|null} level - 제목 레벨
 * @param {string} id - 블록 ID
 * @param {Object} options - 추가 블록 옵션
 * @returns {Object} 블록
 */
/**
 * 블록에 원본 마크다운 줄 범위를 붙인다.
 * @param {Object} block - 블록
 * @param {number} startLine - 시작 줄(0-based)
 * @param {number} endLine - 끝 줄(0-based)
 * @returns {Object} 블록
 */
const attachSourceRange = (block, startLine, endLine) => {
  block.meta = {
    ...(block.meta && typeof block.meta === 'object' ? block.meta : {}),
    startLine,
    endLine: endLine ?? startLine
  }

  return block
}

const createBlock = (type = 'paragraph', text = '', level = null, id = '', options = {}) => ({
  id,
  type,
  text,
  level,
  url: options.url || '',
  alt: options.alt || '',
  caption: options.caption || '',
  useAlt: options.useAlt === true,
  title: options.title || '',
  variant: options.variant || '',
  ordered: options.ordered || false,
  listNumbers: Array.isArray(options.listNumbers) ? options.listNumbers : [],
  width: options.width || 'regular',
  images: options.images || [],
  meta: options.meta && typeof options.meta === 'object' ? { ...options.meta } : {},
  calloutEmojiEnabled: options.calloutEmojiEnabled ?? true,
  calloutEmoji: options.calloutEmoji || '💡',
  calloutBackground: options.calloutBackground || 'blue',
  codeLanguage: options.codeLanguage || '',
  codeShowLineNumbers: options.codeShowLineNumbers !== false
})

/**
 * 이미지 마크다운 행을 이미지 데이터로 변환
 * @param {string} line - 마크다운 행
 * @returns {Object|null} 이미지 데이터
 */
const parseImageLine = (line) => parseImageMarkdownLine(line)

/**
 * 인용 마커 줄인지 확인한다.
 * @param {string} line - 마크다운 행
 * @returns {boolean}
 */
const isQuoteMarkerLine = (line) => {
  const trimmed = String(line ?? '').trim()
  return trimmed === '>' || /^>\s/.test(trimmed)
}

/**
 * 불릿 목록 마커 줄인지 확인한다.
 * @param {string} line - 마크다운 행
 * @returns {boolean}
 */
const isBulletListMarkerLine = (line) => {
  const trimmed = String(line ?? '').trim()
  return trimmed === '-' || /^-\s/.test(trimmed)
}

/**
 * 독립 블록으로 해석할 수 있는 마크다운 행인지 확인한다.
 * @param {string} line - 마크다운 행
 * @returns {boolean} 블록 시작 여부
 */
const isMarkdownBlockStart = (line) => {
  const trimmedLine = line.trim()

  return trimmedLine === BLANK_PARAGRAPH_MARKER ||
    trimmedLine === '>>>' ||
    trimmedLine === ':::bookmark' ||
    trimmedLine === ':::signup' ||
    trimmedLine === ':::gallery' ||
    trimmedLine === ':::embed' ||
    trimmedLine.startsWith(':::callout') ||
    trimmedLine.startsWith(':::toggle') ||
    trimmedLine.startsWith('```') ||
    trimmedLine === '---' ||
    /^(#{1,6})\s+(.+)$/.test(trimmedLine) ||
    trimmedLine.startsWith('> ') ||
    /^- /.test(trimmedLine) ||
    /^\d+\.\s+/.test(trimmedLine) ||
    Boolean(parseImageLine(trimmedLine))
}

/**
 * 문단 행에서 hard break 표식(레거시)을 제거한다.
 * @param {string} line - 마크다운 행
 * @returns {string} 정리된 문단 행
 */
const cleanParagraphLine = (line) => line.replace(/( {2,}|\\)$/, '').trim()

/**
 * 빈 줄 공백 블록 높이를 반환한다.
 * @param {Object} block - 렌더링 블록
 * @returns {string} Tailwind 높이 클래스
 */
const getSpacerHeightClass = (block) => block.meta?.legacy ? 'h-6' : 'h-8'

/**
 * 닫힘 표식까지의 행 목록을 반환
 * @param {Array<string>} lines - 전체 마크다운 행
 * @param {number} startIndex - 본문 시작 인덱스
 * @returns {{contentLines: Array<string>, nextIndex: number}} 블록 본문과 다음 인덱스
 */
const collectFencedLines = (lines, startIndex) => {
  const contentLines = []
  let index = startIndex

  while (index < lines.length && lines[index].trim() !== ':::') {
    contentLines.push(lines[index])
    index += 1
  }

  return {
    contentLines,
    nextIndex: index + 1
  }
}

/**
 * 북마크 fenced 블록 본문에서 URL·제목·설명·썸네일을 파싱한다.
 * @param {string} raw - fenced 내부 텍스트
 * @returns {{url: string, title: string, description: string, thumbnail: string}} 북마크 메타
 */
const parseBookmarkMeta = (raw) => {
  const lines = raw.split('\n').map((l) => l.trim()).filter(Boolean)
  const meta = {
    url: '',
    title: '',
    description: '',
    thumbnail: ''
  }

  for (const line of lines) {
    const kv = line.match(/^(\w+)=(.*)$/)

    if (kv) {
      const key = kv[1].toLowerCase()
      const val = kv[2].trim()

      if (key === 'url') {
        meta.url = val
      } else if (key === 'title') {
        meta.title = val
      } else if (key === 'description' || key === 'desc') {
        meta.description = val
      } else if (key === 'thumbnail' || key === 'image') {
        meta.thumbnail = val
      }

      continue
    }

    if (!meta.url && /^https?:\/\//i.test(line)) {
      meta.url = line
      continue
    }

    if (meta.url && !meta.title) {
      meta.title = line
      continue
    }

    if (meta.url && meta.title && !meta.description) {
      meta.description = line
    }
  }

  return meta
}

/**
 * 회원가입(CTA) fenced 블록 본문에서 표시 문구를 파싱한다.
 * @param {string} raw - fenced 내부 텍스트
 * @returns {{title: string, description: string, button: string, placeholder: string}} CTA 메타
 */
const parseSignupMeta = (raw) => {
  const meta = {
    title: '뉴스레터에 가입하세요',
    description: '새 글이 올라오면 받아보실 수 있어요.',
    button: '구독하기',
    placeholder: 'you@example.com'
  }
  const lines = raw.split('\n').map((l) => l.trim()).filter(Boolean)

  for (const line of lines) {
    const kv = line.match(/^(\w+)=(.*)$/)

    if (!kv) {
      continue
    }

    const key = kv[1].toLowerCase()
    const val = kv[2].trim()

    if (key === 'title') {
      meta.title = val
    } else if (key === 'description' || key === 'desc') {
      meta.description = val
    } else if (key === 'button') {
      meta.button = val
    } else if (key === 'placeholder') {
      meta.placeholder = val
    }
  }

  return meta
}

/**
 * 마크다운 문자열을 렌더링용 블록 목록으로 변환
 * @param {string} markdown - 마크다운 문자열
 * @returns {Array<Object>} 블록 목록
 */
const parseMarkdownBlocks = (markdown) => {
  const lines = markdown.split('\n')
  const blocks = []
  let index = 0

  while (index < lines.length) {
    const line = lines[index]
    const trimmedLine = line.trim()

    if (trimmedLine === BLANK_PARAGRAPH_MARKER) {
      const startLine = index
      blocks.push(attachSourceRange(createBlock('spacer', '', null, `block-${blocks.length}`, { meta: { legacy: true } }), startLine, startLine))
      index += 1
      continue
    }

    if (!trimmedLine) {
      const startLine = index
      blocks.push(attachSourceRange(createBlock('spacer', '', null, `block-${blocks.length}`), startLine, startLine))
      index += 1
      continue
    }

    if (trimmedLine === '>>>') {
      const startLine = index
      const contentLines = []
      index += 1

      while (index < lines.length && lines[index].trim() !== '<<<') {
        contentLines.push(lines[index])
        index += 1
      }

      blocks.push(attachSourceRange(
        createBlock('quote', contentLines.join('\n'), null, `block-${blocks.length}`, { variant: 'alt' }),
        startLine,
        index - 1
      ))
      index += 1
      continue
    }

    if (trimmedLine === ':::bookmark') {
      const startLine = index
      const { contentLines, nextIndex } = collectFencedLines(lines, index + 1)
      const bookmarkMeta = parseBookmarkMeta(contentLines.join('\n'))

      if (bookmarkMeta.url) {
        blocks.push(attachSourceRange(
          createBlock('bookmark', '', null, `block-${blocks.length}`, { meta: bookmarkMeta }),
          startLine,
          nextIndex
        ))
      }

      index = nextIndex
      continue
    }

    if (trimmedLine === ':::signup') {
      const startLine = index
      const { contentLines, nextIndex } = collectFencedLines(lines, index + 1)
      const signupMeta = parseSignupMeta(contentLines.join('\n'))
      blocks.push(attachSourceRange(
        createBlock('signup', '', null, `block-${blocks.length}`, { meta: signupMeta }),
        startLine,
        nextIndex
      ))
      index = nextIndex
      continue
    }

    if (trimmedLine === ':::gallery') {
      const startLine = index
      const { contentLines, nextIndex } = collectFencedLines(lines, index + 1)
      const images = []

      contentLines.forEach((contentLine) => {
        const image = parseImageLine(contentLine)
        if (image) {
          images.push(image)
        }
      })

      blocks.push(attachSourceRange(createBlock('gallery', '', null, `block-${blocks.length}`, {
        images
      }), startLine, nextIndex))
      index = nextIndex
      continue
    }

    if (trimmedLine.startsWith(':::callout')) {
      const startLine = index
      const { contentLines, nextIndex } = collectFencedLines(lines, index + 1)
      blocks.push(attachSourceRange(
        createBlock('callout', contentLines.join('\n'), null, `block-${blocks.length}`, parseCalloutOptions(trimmedLine)),
        startLine,
        nextIndex
      ))
      index = nextIndex
      continue
    }

    if (trimmedLine.startsWith(':::toggle')) {
      const startLine = index
      const { contentLines, nextIndex } = collectFencedLines(lines, index + 1)
      const title = trimmedLine.replace(/^:::toggle\s*/, '').trim()
      blocks.push(attachSourceRange(
        createBlock('toggle', contentLines.join('\n'), null, `block-${blocks.length}`, { title }),
        startLine,
        nextIndex
      ))
      index = nextIndex
      continue
    }

    if (trimmedLine === ':::embed') {
      const startLine = index
      const { contentLines, nextIndex } = collectFencedLines(lines, index + 1)
      blocks.push(attachSourceRange(
        createBlock('embed', '', null, `block-${blocks.length}`, { url: contentLines.join('\n').trim() }),
        startLine,
        nextIndex
      ))
      index = nextIndex
      continue
    }

    const image = parseImageLine(trimmedLine)

    if (image) {
      const startLine = index
      blocks.push(attachSourceRange(createBlock('image', '', null, `block-${blocks.length}`, image), startLine, startLine))
      index += 1
      continue
    }

    if (trimmedLine.startsWith('```')) {
      const startLine = index
      const fenceOptions = parseCodeFenceLine(trimmedLine) || { language: '', showLineNumbers: true }
      const codeLines = []
      index += 1

      while (index < lines.length && !lines[index].trim().startsWith('```')) {
        codeLines.push(lines[index])
        index += 1
      }

      blocks.push(attachSourceRange(
        createBlock('code', codeLines.join('\n'), null, `block-${blocks.length}`, {
          codeLanguage: fenceOptions.language,
          codeShowLineNumbers: fenceOptions.showLineNumbers
        }),
        startLine,
        index
      ))
      index += 1
      continue
    }

    if (trimmedLine === '---') {
      const startLine = index
      blocks.push(attachSourceRange(createBlock('divider', '', null, `block-${blocks.length}`), startLine, startLine))
      index += 1
      continue
    }

    const headingMatch = trimmedLine.match(/^(#{1,6})\s+(.+)$/)

    if (headingMatch) {
      const startLine = index
      blocks.push(attachSourceRange(
        createBlock('heading', headingMatch[2], headingMatch[1].length, `block-${blocks.length}`),
        startLine,
        startLine
      ))
      index += 1
      continue
    }

    if (isQuoteMarkerLine(line)) {
      const startLine = index
      const quoteLines = []

      while (index < lines.length && isQuoteMarkerLine(lines[index])) {
        quoteLines.push(lines[index].trim().replace(/^>\s?/, ''))
        index += 1
      }

      blocks.push(attachSourceRange(
        createBlock('quote', quoteLines.join('\n'), null, `block-${blocks.length}`),
        startLine,
        index - 1
      ))
      continue
    }

    if (isBulletListMarkerLine(line)) {
      const startLine = index
      const items = []

      while (index < lines.length && isBulletListMarkerLine(lines[index])) {
        items.push(lines[index].trim().replace(/^-\s?/, ''))
        index += 1
      }

      blocks.push(attachSourceRange(createBlock('list', items, null, `block-${blocks.length}`), startLine, index - 1))
      continue
    }

    if (/^\d+\.\s*/.test(trimmedLine)) {
      const startLine = index
      const items = []
      const listNumbers = []

      while (index < lines.length && /^\d+\.\s*/.test(lines[index].trim())) {
        const match = lines[index].trim().match(/^(\d+)\.\s*(.*)$/)
        listNumbers.push(Number(match[1]))
        items.push(String(match[2] ?? '').trim())
        index += 1
      }

      blocks.push(attachSourceRange(
        createBlock('list', items, null, `block-${blocks.length}`, { ordered: true, listNumbers }),
        startLine,
        index - 1
      ))
      continue
    }

    blocks.push(attachSourceRange(
      createBlock('paragraph', cleanParagraphLine(line), null, `block-${blocks.length}`),
      index,
      index
    ))
    index += 1
  }

  return blocks
}

const blocks = computed(() => parseMarkdownBlocks(props.content))
const activeLightboxImage = computed(() => activeLightboxImages.value[activeLightboxIndex.value])

watch(() => props.content, () => {
  if (pendingFocusLine.value === null) {
    return
  }

  const line = pendingFocusLine.value
  const position = pendingFocusPosition.value
  const offset = pendingFocusOffset.value
  pendingFocusLine.value = null
  pendingFocusPosition.value = 'auto'
  pendingFocusOffset.value = null

  nextTick(() => {
    nextTick(() => {
      focusEditableAtLine(line, 0, position, offset)
    })
  })
})

/**
 * 지정한 원본 줄의 편집 영역에 포커스를 둔다.
 * @param {number} lineIndex - 줄 번호(0-based)
 * @param {number} [attempt=0] - DOM 탐색 재시도 횟수
 * @param {'start'|'end'|'auto'} [cursorPosition='auto'] - 커서 위치
 * @param {number|null} [caretOffset=null] - 텍스트 오프셋
 * @returns {void}
 */
const focusEditableAtLine = (lineIndex, attempt = 0, cursorPosition = 'auto', caretOffset = null) => {
  if (!import.meta.client) {
    return
  }

  const element = rendererRootRef.value?.querySelector(`[data-source-line="${lineIndex}"]`)

  if (!element) {
    if (attempt < 8) {
      requestAnimationFrame(() => {
        focusEditableAtLine(lineIndex, attempt + 1, cursorPosition, caretOffset)
      })
    }

    return
  }

  const line = getMarkdownLine(lineIndex)
  const isBlankMarker = /^>\s*$/.test(line) || /^[-*+]\s*$/.test(line) || /^\d+\.\s*$/.test(line.trim())

  if (isBlankMarker || !line.trim()) {
    element.textContent = ''
    element.innerHTML = ''
  }

  element.focus({ preventScroll: true })

  if (typeof caretOffset === 'number' && caretOffset >= 0) {
    setEditableCaretOffset(/** @type {HTMLElement} */ (element), caretOffset)
    return
  }

  if (cursorPosition === 'start' || (cursorPosition === 'auto' && (isBlankMarker || !line.trim()))) {
    setEditableCaretOffset(/** @type {HTMLElement} */ (element), 0)
    return
  }

  if (cursorPosition === 'end') {
    const text = readEditableTextFromElement(/** @type {HTMLElement} */ (element))
    setEditableCaretOffset(/** @type {HTMLElement} */ (element), text.length)
    return
  }

  setEditableCaretOffset(/** @type {HTMLElement} */ (element), 0)
}

defineExpose({
  focusEditableAtLine
})

/**
 * 인라인 편집 원문 모드 표시 상태를 갱신한다.
 * @param {{ sourceLine: number, active: boolean }} payload - 줄 번호·활성 여부
 * @returns {void}
 */
const onInlineRawMode = ({ sourceLine, active }) => {
  if (typeof sourceLine !== 'number') {
    return
  }

  const next = new Set(rawEditingSourceLines.value)

  if (active) {
    next.add(sourceLine)
  } else {
    next.delete(sourceLine)
  }

  rawEditingSourceLines.value = next
}

/**
 * 제목 레벨별 편집 영역 클래스
 * @param {number} level - 제목 레벨
 * @returns {string} 클래스 문자열
 */
const getHeadingEditableClass = (level) => {
  const base = 'prose-heading mb-2.5 font-semibold leading-[1.25] tracking-normal first:mt-0'

  if (level === 1) {
    return `${base} text-[clamp(1.35rem,1.25rem+0.35vw,1.6rem)] leading-[1.15]`
  }

  if (level === 2) {
    return `${base} text-[clamp(1.2rem,1.15rem+0.3vw,1.4rem)]`
  }

  if (level === 3) {
    return `${base} text-[clamp(1.1rem,1.05rem+0.25vw,1.25rem)]`
  }

  if (level === 4) {
    return `${base} text-[clamp(1.025rem,1rem+0.2vw,1.15rem)]`
  }

  if (level === 5) {
    return `${base} text-[clamp(0.95rem,0.925rem+0.15vw,1.05rem)]`
  }

  return `${base} text-[clamp(0.9rem,0.875rem+0.1vw,1rem)]`
}

/**
 * commit 이벤트 페이로드를 정규화한다.
 * @param {string|{ value: string, raw?: boolean }} payload - 페이로드
 * @returns {{ value: string, raw: boolean }}
 */
const normalizeCommitPayload = (payload) => {
  if (typeof payload === 'string') {
    return { value: payload, raw: false }
  }

  return {
    value: String(payload?.value ?? ''),
    raw: payload?.raw === true
  }
}

/**
 * insert-below 이벤트 페이로드를 정규화한다.
 * @param {string|Object} payload - 페이로드
 * @returns {{ value: string, raw: boolean, before: string, after: string, caretAtStart: boolean, caretAtEnd: boolean }}
 */
const normalizeInsertBelowPayload = (payload) => {
  if (typeof payload === 'string') {
    return {
      value: payload,
      raw: false,
      before: '',
      after: '',
      caretAtStart: false,
      caretAtEnd: true
    }
  }

  return {
    value: String(payload?.value ?? ''),
    raw: payload?.raw === true,
    before: String(payload?.before ?? ''),
    after: String(payload?.after ?? ''),
    caretAtStart: payload?.caretAtStart === true,
    caretAtEnd: payload?.caretAtEnd === true
  }
}

/**
 * 원본 마크다운 줄을 반환한다.
 * @param {number} lineIndex - 줄 번호
 * @returns {string} 줄 텍스트
 */
const getMarkdownLine = (lineIndex) => String(props.content || '').split('\n')[lineIndex] ?? ''

/**
 * 블록에 해당하는 원본 마크다운 줄 목록을 반환한다.
 * @param {Object} block - 블록
 * @returns {string[]} 줄 목록
 */
const getBlockSourceLines = (block) => String(props.content || '').split('\n').slice(
  block.meta.startLine,
  (block.meta.endLine ?? block.meta.startLine) + 1
)

/**
 * 인용 줄 마크다운을 만든다.
 * @param {string} value - 편집 값
 * @param {boolean} raw - 원문 모드 여부
 * @returns {string} 마크다운 줄
 */
const formatQuoteLine = (value, raw) => {
  if (raw) {
    if (!hasQuoteMarker(value)) {
      return String(value ?? '').trim()
    }

    return value
  }

  const clean = stripQuoteMarker(value)
  return clean ? `> ${clean}` : '> '
}

/**
 * 목록 항목 마크다운 줄을 만든다.
 * @param {string} value - 편집 값
 * @param {boolean} raw - 원문 모드 여부
 * @param {Object} block - 목록 블록
 * @param {number} itemIndex - 항목 인덱스
 * @returns {string} 마크다운 줄
 */
/**
 * 순서 목록 항목에 표시할 번호를 반환한다.
 * @param {Object} block - 목록 블록
 * @param {number} itemIndex - 항목 인덱스
 * @returns {number} 목록 번호
 */
const getListMarkerNumber = (block, itemIndex) => {
  if (block.listNumbers?.[itemIndex] != null) {
    return block.listNumbers[itemIndex]
  }

  const line = getMarkdownLine(block.meta.startLine + itemIndex)
  const parsed = parseOrderedListMarker(line)

  return parsed?.number ?? itemIndex + 1
}

const formatListLine = (value, raw, block, itemIndex) => {
  if (raw) {
    if (!hasListMarker(value, block.ordered)) {
      return String(value ?? '').trim()
    }

    return value
  }

  const clean = stripListMarker(value, block.ordered)

  if (block.ordered) {
    const number = getListMarkerNumber(block, itemIndex)
    return clean ? `${number}. ${clean}` : `${number}. `
  }

  return clean ? `- ${clean}` : '- '
}

/**
 * 빈 목록 마커 줄을 만든다.
 * @param {Object} block - 목록 블록
 * @param {number} itemIndex - 항목 인덱스
 * @returns {string} 마크다운 줄
 */
const createEmptyListMarkerLine = (block, itemIndex) => {
  if (block.ordered) {
    return `${getListMarkerNumber(block, itemIndex)}. `
  }

  return '- '
}

/**
 * 인용 블록을 줄 단위로 분리한다.
 * @param {Object} block - 인용 블록
 * @returns {string[]} 줄 목록
 */
const getQuoteLines = (block) => {
  const lineCount = (block.meta.endLine ?? block.meta.startLine) - block.meta.startLine + 1
  const fromText = String(block.text ?? '').split('\n')

  while (fromText.length < lineCount) {
    fromText.push('')
  }

  if (!fromText.length) {
    return ['']
  }

  return fromText.slice(0, lineCount)
}

/**
 * 코드 블록 줄 번호 목록을 만든다.
 * @param {Object} block - 코드 블록
 * @returns {number[]} 줄 번호(1부터)
 */
const getCodeLineNumbers = (block) => {
  const text = String(block.text ?? '')
  const lineCount = text.length ? text.split('\n').length : 1

  return Array.from({ length: lineCount }, (_, index) => index + 1)
}

/**
 * 인라인 편집 결과를 마크다운 줄로 반영한다.
 * @param {Object} block - 블록
 * @param {string[]} replacementLines - 대체 줄
 * @returns {void}
 */
const commitInlineBlockLines = (block, replacementLines) => {
  if (!props.interactive || typeof block.meta?.startLine !== 'number') {
    return
  }

  emit('block-content-change', {
    startLine: block.meta.startLine,
    endLine: block.meta.endLine ?? block.meta.startLine,
    replacementLines
  })
}

/**
 * 문단 인라인 편집 반영
 * @param {Object} block - 블록
 * @param {string} text - 편집된 텍스트
 * @returns {void}
 */
/**
 * 코드 블록 편집 반영
 * @param {Object} block - 코드 블록
 * @param {string[]} replacementLines - 마크다운 줄
 * @returns {void}
 */
const onCodeBlockCommit = (block, replacementLines) => {
  commitInlineBlockLines(block, replacementLines)
}

/**
 * 코드 블록 마지막 줄에서 아래로 이탈 — 본문 저장 후 다음 문단 삽입
 * @param {Object} block - 코드 블록
 * @param {string|Object} payload - insert-below 페이로드
 * @returns {void}
 */
const onCodeBlockInsertBelow = (block, payload) => {
  const { value } = normalizeInsertBelowPayload(payload)

  onCodeBlockCommit(block, buildCodeBlockLines({
    language: block.codeLanguage,
    showLineNumbers: block.codeShowLineNumbers,
    body: value
  }))
  onInsertBelowBlock(block, { lines: [''] })
}

/**
 * 콜아웃 편집 반영
 * @param {Object} block - 콜아웃 블록
 * @param {string[]} replacementLines - 마크다운 줄
 * @returns {void}
 */
const onCalloutBlockCommit = (block, replacementLines) => {
  commitInlineBlockLines(block, replacementLines)
}

/**
 * 토글 편집 반영
 * @param {Object} block - 토글 블록
 * @param {string[]} replacementLines - 마크다운 줄
 * @returns {void}
 */
const onToggleBlockCommit = (block, replacementLines) => {
  commitInlineBlockLines(block, replacementLines)
}

/**
 * 토글 본문 마지막 줄에서 아래로 이탈
 * @param {Object} block - 토글 블록
 * @param {string|Object} payload - insert-below 페이로드
 * @returns {void}
 */
const onToggleBlockInsertBelow = (block, payload) => {
  const { value } = normalizeInsertBelowPayload(payload)

  onToggleBlockCommit(block, buildToggleBlockLines({
    title: block.title,
    body: value
  }))
  pendingFocusPosition.value = 'start'
  pendingFocusOffset.value = 0
  onInsertBelowBlock(block, { lines: [''] })
}

const onParagraphInlineCommit = (block, text) => {
  const value = String(text ?? '').replace(/\r/g, '')
  const lines = value.split('\n').map((line) => line.trimEnd())

  if (lines.length <= 1) {
    commitInlineBlockLines(block, [lines[0] ?? ''])
    return
  }

  commitInlineBlockLines(block, lines.filter((line) => line.length > 0))
}

/**
 * 빈 줄(스페이서) 편집 반영
 * @param {Object} block - 블록
 * @param {string} text - 편집된 텍스트
 * @returns {void}
 */
const onSpacerInlineCommit = (block, text) => {
  if (!String(text ?? '').trim()) {
    return
  }

  commitInlineBlockLines(block, [text])
}

/**
 * 문단 Enter 분리 결과 줄 배열을 만든다.
 * @param {string} head - 커서 앞 텍스트
 * @param {string} tail - 커서 뒤 텍스트
 * @returns {string[]} 분리된 줄
 */
const buildParagraphSplitLines = (head, tail) => {
  const h = String(head ?? '').replace(/\n/g, ' ')
  const t = String(tail ?? '').replace(/\n/g, ' ')

  if (!t.length) {
    return [h, '']
  }

  if (!h.length) {
    return ['', t]
  }

  return [h, t]
}

/**
 * 문단 Enter 분리 — 마크다운에 빈 줄을 넣어 다음 문단을 만든다.
 * @param {Object} block - 블록
 * @param {{ before: string, after: string }} payload - 커서 앞·뒤 텍스트
 * @returns {void}
 */
const onParagraphSplit = (block, { before, after }) => {
  const now = Date.now()

  if (now - lastParagraphSplitAt < 120) {
    return
  }

  lastParagraphSplitAt = now

  const replacementLines = buildParagraphSplitLines(before, after)
  const focusLine = block.meta.startLine + Math.max(replacementLines.length - 1, 1)

  pendingFocusLine.value = focusLine
  pendingFocusPosition.value = 'start'
  commitInlineBlockLines(block, replacementLines)
}

/**
 * 블록 아래에 줄을 삽입한다.
 * @param {Object} block - 블록
 * @param {{ lines?: string[] }} options - 삽입 옵션
 * @returns {void}
 */
const onInsertBelowBlock = (block, options = {}) => {
  const endLine = block.meta.endLine ?? block.meta.startLine
  const lines = options.lines ?? ['']

  pendingFocusLine.value = endLine + 1
  emit('insert-after-line', {
    afterLine: endLine,
    lines,
    focusLine: endLine + 1
  })
}

/**
 * 목록 항목 Enter — 빈 마커 줄이면 문단으로 탈출, 내용이 있으면 아래에 빈 줄만 삽입한다.
 * @param {Object} block - 목록 블록
 * @param {number} itemIndex - 항목 인덱스
 * @param {string|{ value: string, raw?: boolean }} payload - 편집 내용
 * @returns {void}
 */
const onListItemInsertBelow = (block, itemIndex, payload) => {
  const { value, raw, before, after, caretAtStart, caretAtEnd } = normalizeInsertBelowPayload(payload)
  const nextLines = getBlockSourceLines(block)

  if (caretAtStart && after.length) {
    nextLines[itemIndex] = formatListLine(after, raw, block, itemIndex)
    nextLines.splice(itemIndex, 0, createEmptyListMarkerLine(block, itemIndex))
    pendingFocusLine.value = block.meta.startLine + itemIndex
    pendingFocusPosition.value = 'start'
    commitInlineBlockLines(block, nextLines)
    return
  }

  if (before.length && after.length) {
    nextLines[itemIndex] = formatListLine(before, raw, block, itemIndex)
    nextLines.splice(itemIndex + 1, 0, formatListLine(after, raw, block, itemIndex + 1))
    pendingFocusLine.value = block.meta.startLine + itemIndex + 1
    pendingFocusPosition.value = 'start'
    commitInlineBlockLines(block, nextLines)
    return
  }

  if (itemIndex < nextLines.length) {
    nextLines[itemIndex] = formatListLine(value, raw, block, itemIndex)
  }

  const committedLine = nextLines[itemIndex] ?? ''

  if (isEmptyListMarkerLine(committedLine, block.ordered)) {
    nextLines[itemIndex] = ''
    pendingFocusLine.value = block.meta.startLine + itemIndex
    pendingFocusPosition.value = 'start'
    commitInlineBlockLines(block, nextLines)
    return
  }

  commitInlineBlockLines(block, nextLines)

  if (caretAtEnd || !before.length) {
    pendingFocusLine.value = (block.meta.endLine ?? block.meta.startLine) + 1
    pendingFocusPosition.value = 'start'
    onInsertBelowBlock(block, { lines: [''] })
  }
}

/**
 * 라이브 모드 하단 클릭 — 새 문단 추가
 * @returns {void}
 */
const onLiveTailClick = () => {
  if (!props.interactive) {
    return
  }

  const lines = String(props.content || '').split('\n')

  if (!lines.length || lines[lines.length - 1] !== '') {
    emit('append-paragraph')
    pendingFocusLine.value = lines.length ? lines.length + 1 : 0
    return
  }

  pendingFocusLine.value = lines.length - 1
  nextTick(() => {
    focusEditableAtLine(lines.length - 1)
  })
}

/**
 * 제목 인라인 편집 반영
 * @param {Object} block - 블록
 * @param {string} text - 편집된 텍스트
 * @returns {void}
 */
const onHeadingInlineCommit = (block, payload) => {
  const { value, raw } = normalizeCommitPayload(payload)

  if (raw) {
    commitInlineBlockLines(block, [value])
    return
  }

  const headingPrefix = `${'#'.repeat(Math.min(Math.max(block.level, 1), 6))} `
  const cleanText = String(value ?? '').replace(/\s+/g, ' ').trim()
  commitInlineBlockLines(block, [`${headingPrefix}${cleanText}`.trimEnd() || headingPrefix.trim()])
}

/**
 * 인용 줄 인라인 편집 반영
 * @param {Object} block - 인용 블록
 * @param {number} lineIndex - 줄 인덱스
 * @param {string|{ value: string, raw?: boolean }} payload - 편집 내용
 * @returns {void}
 */
const onQuoteLineInlineCommit = (block, lineIndex, payload) => {
  const { value, raw } = normalizeCommitPayload(payload)
  const sourceLines = String(props.content || '').split('\n').slice(
    block.meta.startLine,
    (block.meta.endLine ?? block.meta.startLine) + 1
  )
  const nextLines = [...sourceLines]

  nextLines[lineIndex] = formatQuoteLine(value, raw)
  commitInlineBlockLines(block, nextLines)
}

/**
 * 인용 줄 아래에 새 인용 줄을 삽입한다.
 * @param {Object} block - 인용 블록
 * @param {number} lineIndex - 줄 인덱스
 * @returns {void}
 */
const onQuoteLineInsertBelow = (block, lineIndex, payload) => {
  const { value, raw, before, after, caretAtStart } = normalizeInsertBelowPayload(payload)
  const nextLines = getBlockSourceLines(block)

  if (caretAtStart && after.length) {
    nextLines[lineIndex] = formatQuoteLine(after, raw)
    nextLines.splice(lineIndex, 0, '> ')
    pendingFocusLine.value = block.meta.startLine + lineIndex
    pendingFocusPosition.value = 'start'
    commitInlineBlockLines(block, nextLines)
    return
  }

  if (before.length && after.length) {
    nextLines[lineIndex] = formatQuoteLine(before, raw)
    nextLines.splice(lineIndex + 1, 0, formatQuoteLine(after, raw))
    pendingFocusLine.value = block.meta.startLine + lineIndex + 1
    pendingFocusPosition.value = 'start'
    commitInlineBlockLines(block, nextLines)
    return
  }

  if (lineIndex < nextLines.length) {
    nextLines[lineIndex] = formatQuoteLine(value, raw)
  }

  const committedLine = nextLines[lineIndex] ?? ''

  if (isEmptyQuoteMarkerLine(committedLine)) {
    nextLines[lineIndex] = ''
    pendingFocusLine.value = block.meta.startLine + lineIndex
    pendingFocusPosition.value = 'start'
    commitInlineBlockLines(block, nextLines)
    return
  }

  nextLines.splice(lineIndex + 1, 0, '> ')
  pendingFocusLine.value = block.meta.startLine + lineIndex + 1
  pendingFocusPosition.value = 'start'
  commitInlineBlockLines(block, nextLines)
}

/**
 * 목록 항목 인라인 편집 반영
 * @param {Object} block - 블록
 * @param {number} itemIndex - 항목 인덱스
 * @param {string|{ value: string, raw?: boolean }} payload - 편집 내용
 * @returns {void}
 */
const onListItemInlineCommit = (block, itemIndex, payload) => {
  const { value, raw } = normalizeCommitPayload(payload)
  const sourceLines = String(props.content || '').split('\n').slice(
    block.meta.startLine,
    (block.meta.endLine ?? block.meta.startLine) + 1
  )

  const nextLines = sourceLines.map((line, index) => {
    if (index !== itemIndex) {
      return line
    }

    return formatListLine(value, raw, block, index)
  })

  commitInlineBlockLines(block, nextLines)
}

/**
 * 라이브 모드에서 현재 줄을 삭제한다.
 * @param {number} lineIndex - 줄 번호
 * @returns {void}
 */
const onDeleteLine = (lineIndex) => {
  if (typeof lineIndex !== 'number' || lineIndex < 0) {
    return
  }

  const focusLine = lineIndex > 0 ? lineIndex - 1 : 0

  pendingFocusLine.value = focusLine
  pendingFocusPosition.value = lineIndex > 0 ? 'end' : 'start'
  emit('delete-line', lineIndex)
}

/**
 * 현재 줄을 이전 줄 끝에 병합한다.
 * @param {number} lineIndex - 줄 번호
 * @param {string|Object} payload - 편집 내용
 * @returns {void}
 */
const onMergeWithPreviousLine = (lineIndex, payload) => {
  if (typeof lineIndex !== 'number' || lineIndex <= 0) {
    return
  }

  const { value, raw } = normalizeCommitPayload(payload)
  const lines = String(props.content || '').split('\n')
  const prevLine = lines[lineIndex - 1] ?? ''
  const appendText = getAppendTextForMerge(value, prevLine, raw)

  if (!appendText) {
    onDeleteLine(lineIndex)
    return
  }

  const mergedLine = appendTextToMarkdownLine(prevLine, appendText)

  pendingFocusLine.value = lineIndex - 1
  pendingFocusPosition.value = 'auto'
  pendingFocusOffset.value = getMergeJunctionDisplayOffset(prevLine, raw)
  emit('merge-with-previous-line', { lineIndex, mergedLine })
}

/**
 * 줄바꿈이 포함된 인라인 마크다운을 줄 단위 세그먼트로 변환한다.
 * @param {string} value - 원본 문자열
 * @returns {Array<Array<{ type: string, text: string, href?: string }>>} 줄별 인라인 세그먼트
 */
const parseInlineSegmentLines = (value) => {
  return String(value || '').split('\n').map(parseInlineSegments)
}

/**
 * 라이트박스를 연다
 * @param {Array<Object>} images - 이미지 목록
 * @param {number} index - 시작 인덱스
 * @returns {void}
 */
const openLightbox = (images, index) => {
  activeLightboxImages.value = images
  activeLightboxIndex.value = index
}

/**
 * 라이트박스를 닫는다
 * @returns {void}
 */
const closeLightbox = () => {
  activeLightboxImages.value = []
  activeLightboxIndex.value = 0
}

/**
 * 라이트박스 이전 이미지로 이동
 * @returns {void}
 */
const showPreviousImage = () => {
  activeLightboxIndex.value = activeLightboxIndex.value === 0
    ? activeLightboxImages.value.length - 1
    : activeLightboxIndex.value - 1
}

/**
 * 라이트박스 다음 이미지로 이동
 * @returns {void}
 */
const showNextImage = () => {
  activeLightboxIndex.value = (activeLightboxIndex.value + 1) % activeLightboxImages.value.length
}

/**
 * 갤러리 드래그 시작
 * @param {DragEvent} event - 드래그 이벤트
 * @param {string} blockId - 블록 ID
 * @param {number} imageIndex - 이미지 인덱스
 * @returns {void}
 */
const onGalleryDragStart = (event, blockId, imageIndex) => {
  if (!props.interactive) {
    return
  }

  galleryDragState.value = { blockId, imageIndex }
  event.dataTransfer.effectAllowed = 'move'
  event.dataTransfer.setData('text/plain', String(imageIndex))
}

/**
 * 갤러리 드래그 종료
 * @returns {void}
 */
const onGalleryDragEnd = () => {
  galleryDragState.value = null
  galleryDropTarget.value = null
}

/**
 * 갤러리 드래그 중 드롭 대상 셀 표시
 * @param {DragEvent} event - 드래그 이벤트
 * @param {Object} block - 갤러리 블록
 * @param {number} imageIndex - 호버 중인 이미지 인덱스
 * @returns {void}
 */
const onGalleryDragOverItem = (event, block, imageIndex) => {
  if (!props.interactive || !galleryDragState.value) {
    return
  }

  event.preventDefault()

  if (galleryDragState.value.blockId !== block.id) {
    return
  }

  if (galleryDragState.value.imageIndex === imageIndex) {
    galleryDropTarget.value = null
    return
  }

  galleryDropTarget.value = { blockId: block.id, targetIndex: imageIndex }
}

/**
 * 갤러리 영역을 벗어나면 드롭 대상 표시를 해제한다.
 * @param {DragEvent} event - 드래그 이벤트
 * @param {Object} block - 갤러리 블록
 * @returns {void}
 */
const onGalleryDragLeaveGallery = (event, block) => {
  const related = event.relatedTarget

  if (related && event.currentTarget.contains(related)) {
    return
  }

  if (galleryDropTarget.value?.blockId === block.id) {
    galleryDropTarget.value = null
  }
}

/**
 * 갤러리 드롭으로 순서 변경
 * @param {DragEvent} event - 드롭 이벤트
 * @param {Object} block - 갤러리 블록
 * @param {number} targetIndex - 대상 인덱스
 * @returns {void}
 */
const onGalleryDrop = (event, block, targetIndex) => {
  if (!props.interactive || !galleryDragState.value) {
    return
  }

  event.preventDefault()

  const { blockId, imageIndex: fromIndex } = galleryDragState.value

  if (blockId !== block.id || fromIndex === targetIndex) {
    galleryDragState.value = null
    return
  }

  const images = [...block.images]
  const [moved] = images.splice(fromIndex, 1)
  images.splice(targetIndex, 0, moved)

  emit('gallery-reorder', {
    startLine: block.meta?.startLine,
    endLine: block.meta?.endLine,
    images
  })

  galleryDragState.value = null
  galleryDropTarget.value = null
}

/**
 * 라이트박스 키보드 조작(Esc 닫기, 좌우 이전·다음)
 * @param {KeyboardEvent} event - 키보드 이벤트
 * @returns {void}
 */
const handleLightboxKeydown = (event) => {
  if (!activeLightboxImage.value) {
    return
  }

  if (event.key === 'Escape') {
    event.preventDefault()
    closeLightbox()
    return
  }

  if (activeLightboxImages.value.length <= 1) {
    return
  }

  if (event.key === 'ArrowLeft') {
    event.preventDefault()
    showPreviousImage()
    return
  }

  if (event.key === 'ArrowRight') {
    event.preventDefault()
    showNextImage()
  }
}

watch(activeLightboxImage, (image) => {
  if (!import.meta.client) {
    return
  }

  if (image) {
    window.addEventListener('keydown', handleLightboxKeydown)
  } else {
    window.removeEventListener('keydown', handleLightboxKeydown)
  }
})

onBeforeUnmount(() => {
  if (import.meta.client) {
    window.removeEventListener('keydown', handleLightboxKeydown)
  }
})
</script>

<template>
  <div ref="rendererRootRef" class="content-markdown-renderer">
    <template v-for="block in blocks" :key="block.id">
      <ContentMarkdownEditableInline
        v-if="block.type === 'spacer' && interactive"
        tag="p"
        block-class="content-markdown-renderer__paragraph content-markdown-renderer__spacer-line text-base text-[var(--site-text)]"
        enter-mode="split-paragraph"
        :slash-menu-active="slashMenuActive"
        :slash-command-suppressed="slashSuppressedLines.includes(block.meta.startLine)"
        :source-line="block.meta.startLine"
        :model-value="''"
        @commit="onSpacerInlineCommit(block, $event)"
        @split="onParagraphSplit(block, $event)"
        @delete-line="onDeleteLine"
        @merge-with-previous="onMergeWithPreviousLine(block.meta.startLine, $event)"
        @slash-update="emit('slash-update', $event)"
        @slash-end="emit('slash-end', $event)"
        @slash-apply="emit('slash-apply', $event)"
      />
      <div v-else-if="block.type === 'spacer'" class="content-markdown-renderer__spacer" :class="getSpacerHeightClass(block)" aria-hidden="true" />
      <ContentMarkdownEditableInline
        v-else-if="block.type === 'heading' && interactive"
        :tag="`h${Math.min(Math.max(block.level, 1), 6)}`"
        :block-class="getHeadingEditableClass(block.level)"
        enter-mode="insert-below"
        allow-raw-toggle
        :raw-line="getMarkdownLine(block.meta.startLine)"
        :source-line="block.meta.startLine"
        :model-value="block.text"
        @commit="onHeadingInlineCommit(block, $event)"
        @insert-below="onInsertBelowBlock(block)"
        @delete-line="onDeleteLine"
        @merge-with-previous="onMergeWithPreviousLine(block.meta.startLine, $event)"
      />
      <ProseHeading v-else-if="block.type === 'heading'" :level="block.level">
        <template v-for="(segment, segmentIndex) in parseInlineSegments(block.text)" :key="`${block.id}-heading-${segmentIndex}`">
          <strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
          <em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
          <code v-else-if="segment.type === 'code'" class="content-markdown-renderer__inline-code rounded bg-[#252525] px-1.5 py-0.5 text-[0.9em] text-white">{{ segment.text }}</code>
          <a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
          <template v-else>{{ segment.text }}</template>
        </template>
      </ProseHeading>
      <ProseBlockquote
        v-else-if="block.type === 'quote' && interactive && block.variant !== 'alt'"
        :variant="block.variant || 'default'"
      >
        <ContentMarkdownEditableInline
          v-for="(quoteLine, quoteLineIndex) in getQuoteLines(block)"
          :key="`quote-line-${block.meta.startLine + quoteLineIndex}`"
          block-class="content-markdown-renderer__quote-line"
          :model-value="quoteLine"
          enter-mode="insert-below"
          allow-raw-toggle
          :raw-line="getMarkdownLine(block.meta.startLine + quoteLineIndex)"
          :source-line="block.meta.startLine + quoteLineIndex"
          @commit="onQuoteLineInlineCommit(block, quoteLineIndex, $event)"
          @insert-below="onQuoteLineInsertBelow(block, quoteLineIndex, $event)"
          @delete-line="onDeleteLine"
          @merge-with-previous="onMergeWithPreviousLine(block.meta.startLine + quoteLineIndex, $event)"
          @raw-mode="onInlineRawMode"
        />
      </ProseBlockquote>
      <ProseBlockquote v-else-if="block.type === 'quote'" :variant="block.variant || 'default'">
        <template v-for="(segment, segmentIndex) in parseInlineSegments(block.text)" :key="`${block.id}-quote-${segmentIndex}`">
          <strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
          <em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
          <code v-else-if="segment.type === 'code'" class="content-markdown-renderer__inline-code rounded bg-[#252525] px-1.5 py-0.5 text-[0.9em] text-white">{{ segment.text }}</code>
          <a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
          <template v-else>{{ segment.text }}</template>
        </template>
      </ProseBlockquote>
      <ProseList v-else-if="block.type === 'list' && interactive" :ordered="block.ordered || false">
        <li
          v-for="(item, itemIndex) in block.text"
          :key="`list-line-${block.meta.startLine + itemIndex}`"
          class="content-markdown-renderer__list-item flex items-center gap-2"
          :class="rawEditingSourceLines.has(block.meta.startLine + itemIndex) ? 'content-markdown-renderer__list-item--raw' : ''"
        >
          <span
            v-if="block.ordered"
            class="content-markdown-renderer__list-marker content-markdown-renderer__list-marker--ordered"
            aria-hidden="true"
          >{{ getListMarkerNumber(block, itemIndex) }}.</span>
          <span
            v-else
            class="content-markdown-renderer__list-marker content-markdown-renderer__list-marker--bullet"
            aria-hidden="true"
          />
          <ContentMarkdownEditableInline
            class="min-w-0 flex-1"
            :model-value="item"
            enter-mode="insert-below"
            allow-raw-toggle
            :raw-line="getMarkdownLine(block.meta.startLine + itemIndex)"
            :source-line="block.meta.startLine + itemIndex"
            @commit="onListItemInlineCommit(block, itemIndex, $event)"
            @insert-below="onListItemInsertBelow(block, itemIndex, $event)"
            @delete-line="onDeleteLine"
            @merge-with-previous="onMergeWithPreviousLine(block.meta.startLine + itemIndex, $event)"
            @raw-mode="onInlineRawMode"
          />
        </li>
      </ProseList>
      <ProseList v-else-if="block.type === 'list'" :ordered="block.ordered || false">
        <li
          v-for="(item, itemIndex) in block.text"
          :key="`${block.id}-${itemIndex}`"
          class="content-markdown-renderer__list-item flex items-center gap-2"
        >
          <span
            v-if="block.ordered"
            class="content-markdown-renderer__list-marker content-markdown-renderer__list-marker--ordered"
            aria-hidden="true"
          >{{ getListMarkerNumber(block, itemIndex) }}.</span>
          <span
            v-else
            class="content-markdown-renderer__list-marker content-markdown-renderer__list-marker--bullet"
            aria-hidden="true"
          />
          <span class="min-w-0 flex-1">
          <template v-for="(segment, segmentIndex) in parseInlineSegments(item)" :key="`${block.id}-${itemIndex}-${segmentIndex}`">
            <strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
            <em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
            <code v-else-if="segment.type === 'code'" class="content-markdown-renderer__inline-code rounded bg-[#252525] px-1.5 py-0.5 text-[0.9em] text-white">{{ segment.text }}</code>
            <a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
            <template v-else>{{ segment.text }}</template>
          </template>
          </span>
        </li>
      </ProseList>
      <ProseImage
        v-else-if="block.type === 'image'"
        :src="block.url"
        :alt="getImageAltAttribute(block)"
        :caption="getImageDisplayCaption(block)"
        :variant="block.width"
      />
      <ContentMarkdownCalloutEditor
        v-else-if="block.type === 'callout' && interactive"
        :callout-emoji-enabled="block.calloutEmojiEnabled"
        :callout-emoji="block.calloutEmoji"
        :callout-background="block.calloutBackground"
        :body-source-line="block.meta.startLine + 1"
        :model-value="block.text"
        @commit="onCalloutBlockCommit(block, $event)"
      />
      <ProseCallout
        v-else-if="block.type === 'callout'"
        :emoji-enabled="block.calloutEmojiEnabled"
        :emoji="block.calloutEmoji"
        :background="block.calloutBackground"
      >
        <template v-for="(segment, segmentIndex) in parseInlineSegments(block.text)" :key="`${block.id}-callout-${segmentIndex}`">
          <strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
          <em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
          <code v-else-if="segment.type === 'code'" class="content-markdown-renderer__inline-code rounded bg-[#252525] px-1.5 py-0.5 text-[0.9em] text-white">{{ segment.text }}</code>
          <a v-else-if="segment.type === 'link'" class="underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
          <template v-else>{{ segment.text }}</template>
        </template>
      </ProseCallout>
      <ContentMarkdownToggleEditor
        v-else-if="block.type === 'toggle' && interactive"
        :title="block.title"
        :title-source-line="block.meta.startLine"
        :body-source-line="block.meta.startLine + 1"
        :model-value="block.text"
        @commit="onToggleBlockCommit(block, $event)"
        @insert-below="onToggleBlockInsertBelow(block, $event)"
      />
      <ProseToggle v-else-if="block.type === 'toggle'" :title="block.title || '더 보기'" animated>
        <template v-for="(segment, segmentIndex) in parseInlineSegments(block.text)" :key="`${block.id}-toggle-${segmentIndex}`">
          <strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
          <em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
          <code v-else-if="segment.type === 'code'" class="content-markdown-renderer__inline-code rounded bg-[#252525] px-1.5 py-0.5 text-[0.9em] text-white">{{ segment.text }}</code>
          <a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
          <template v-else>{{ segment.text }}</template>
        </template>
      </ProseToggle>
      <ProseBookmark
        v-else-if="block.type === 'bookmark' && block.meta.url"
        :url="block.meta.url"
        :title="block.meta.title"
        :description="block.meta.description"
        :thumbnail="block.meta.thumbnail"
      />
      <ProseSignup
        v-else-if="block.type === 'signup'"
        :title="block.meta.title"
        :description="block.meta.description"
        :button-label="block.meta.button"
        :placeholder="block.meta.placeholder"
      />
      <ProseEmbed v-else-if="block.type === 'embed'" :url="block.url" />
      <div
        v-else-if="block.type === 'gallery'"
        class="content-markdown-renderer__gallery my-8 grid grid-cols-2 gap-2 md:grid-cols-3"
        @dragleave="onGalleryDragLeaveGallery($event, block)"
      >
        <figure
          v-for="(image, imageIndex) in block.images"
          :key="`${block.id}-${imageIndex}-${image.url}`"
          class="content-markdown-renderer__gallery-item relative min-w-0"
          :class="{
            'content-markdown-renderer__gallery-item--interactive': interactive,
            'content-markdown-renderer__gallery-item--dragging': interactive && galleryDragState?.blockId === block.id && galleryDragState?.imageIndex === imageIndex,
            'content-markdown-renderer__gallery-item--drop-target': interactive && galleryDropTarget?.blockId === block.id && galleryDropTarget?.targetIndex === imageIndex
          }"
          :draggable="interactive"
          @dragstart="onGalleryDragStart($event, block.id, imageIndex)"
          @dragend="onGalleryDragEnd"
          @dragover="onGalleryDragOverItem($event, block, imageIndex)"
          @drop="onGalleryDrop($event, block, imageIndex)"
        >
          <button
            class="content-markdown-renderer__gallery-button w-full overflow-hidden rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)]"
            type="button"
            @click="openLightbox(block.images, imageIndex)"
          >
            <img
              class="content-markdown-renderer__gallery-image aspect-[4/3] w-full object-cover transition-transform hover:scale-[1.02]"
              :src="image.url"
              :alt="getImageAltAttribute(image)"
            >
          </button>
          <figcaption
            v-if="getImageDisplayCaption(image)"
            class="content-markdown-renderer__gallery-caption mt-1.5 text-center text-xs text-[var(--site-muted)]"
          >
            {{ getImageDisplayCaption(image) }}
          </figcaption>
        </figure>
      </div>
      <ContentMarkdownCodeBlockEditor
        v-else-if="block.type === 'code' && interactive"
        :language="block.codeLanguage"
        :show-line-numbers="block.codeShowLineNumbers"
        :body-source-line="block.meta.startLine + 1"
        :model-value="block.text"
        @commit="onCodeBlockCommit(block, $event)"
        @insert-below="onCodeBlockInsertBelow(block, $event)"
      />
      <ProseCodeBlock
        v-else-if="block.type === 'code'"
        class="content-markdown-renderer__code"
        :language="block.codeLanguage"
        :show-line-numbers="block.codeShowLineNumbers"
        :line-numbers="getCodeLineNumbers(block)"
        show-copy
        :copy-text="block.text"
      >
        <code>{{ block.text }}</code>
      </ProseCodeBlock>
      <hr v-else-if="block.type === 'divider'" class="content-markdown-renderer__divider my-5 border-line">
      <ContentMarkdownEditableInline
        v-else-if="block.type === 'paragraph' && interactive"
        tag="p"
        block-class="content-markdown-renderer__paragraph content-markdown-renderer__paragraph--editable mb-2.5 min-h-[1.75rem] text-base text-[var(--site-text)] last:mb-0"
        enter-mode="split-paragraph"
        :slash-menu-active="slashMenuActive"
        :slash-command-suppressed="slashSuppressedLines.includes(block.meta.startLine)"
        :source-line="block.meta.startLine"
        :model-value="block.text"
        @commit="onParagraphInlineCommit(block, $event)"
        @split="onParagraphSplit(block, $event)"
        @delete-line="onDeleteLine"
        @merge-with-previous="onMergeWithPreviousLine(block.meta.startLine, $event)"
        @slash-update="emit('slash-update', $event)"
        @slash-end="emit('slash-end', $event)"
        @slash-apply="emit('slash-apply', $event)"
      />
      <p v-else-if="block.type === 'paragraph'" class="content-markdown-renderer__paragraph mb-2.5 text-base text-[var(--site-text)] last:mb-0">
        <template v-for="(lineSegments, lineIndex) in parseInlineSegmentLines(block.text)" :key="`${block.id}-paragraph-line-${lineIndex}`">
          <br v-if="lineIndex > 0">
          <template v-for="(segment, segmentIndex) in lineSegments" :key="`${block.id}-paragraph-${lineIndex}-${segmentIndex}`">
            <strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
            <em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
            <code v-else-if="segment.type === 'code'" class="content-markdown-renderer__inline-code rounded bg-[#252525] px-1.5 py-0.5 text-[0.9em] text-white">{{ segment.text }}</code>
            <a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
            <template v-else>{{ segment.text }}</template>
          </template>
        </template>
      </p>
    </template>

    <div
      v-if="interactive"
      class="content-markdown-renderer__live-tail mt-1 min-h-[120px] flex-1 cursor-text"
      role="button"
      tabindex="-1"
      aria-label="새 문단 추가"
      @mousedown.prevent="onLiveTailClick"
    />

    <div
      v-if="activeLightboxImage"
      class="content-markdown-renderer__lightbox fixed inset-0 z-50 grid place-items-center bg-black/90 px-5 py-8"
      role="dialog"
      aria-modal="true"
      aria-label="갤러리 이미지 보기"
      tabindex="-1"
      @click.self="closeLightbox"
    >
      <button class="content-markdown-renderer__lightbox-close absolute right-5 top-5 rounded bg-white px-3 py-2 text-sm font-semibold text-ink" type="button" @click="closeLightbox">
        닫기
      </button>
      <button
        v-if="activeLightboxImages.length > 1"
        class="content-markdown-renderer__lightbox-prev absolute left-5 top-1/2 rounded bg-white px-3 py-2 text-sm font-semibold text-ink"
        type="button"
        @click="showPreviousImage"
      >
        이전
      </button>
      <img
        class="content-markdown-renderer__lightbox-image max-h-[84vh] max-w-[92vw] object-contain"
        :src="activeLightboxImage.url"
        :alt="getImageAltAttribute(activeLightboxImage)"
      >
      <button
        v-if="activeLightboxImages.length > 1"
        class="content-markdown-renderer__lightbox-next absolute right-5 top-1/2 rounded bg-white px-3 py-2 text-sm font-semibold text-ink"
        type="button"
        @click="showNextImage"
      >
        다음
      </button>
    </div>
  </div>
</template>

<style scoped>
.content-markdown-renderer__spacer-line {
  margin-bottom: 0;
  min-height: 1.75rem;
  padding: 0;
}

.content-markdown-renderer__quote-line {
  margin-bottom: 0.25rem;
}

.content-markdown-renderer__quote-line:last-child {
  margin-bottom: 0;
}

.content-markdown-renderer__list-marker {
  width: 21px;
  flex-shrink: 0;
}

.content-markdown-renderer__list-marker--ordered {
  text-align: right;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--site-accent);
}

.content-markdown-renderer__list-marker--bullet {
  display: flex;
  align-items: center;
  justify-content: flex-end;
}

.content-markdown-renderer__list-marker--bullet::after {
  width: 6px;
  height: 6px;
  border-radius: 9999px;
  background-color: var(--site-accent);
  content: '';
}

.content-markdown-renderer__list-item--raw .content-markdown-renderer__list-marker {
  opacity: 0.35;
}

.content-markdown-renderer__paragraph--editable:empty {
  min-height: 1.75rem;
}

.content-markdown-renderer {
  user-select: text;
}

.content-markdown-renderer__gallery-item--interactive {
  cursor: grab;
}

.content-markdown-renderer__gallery-item--interactive:active {
  cursor: grabbing;
}

.content-markdown-renderer__gallery-item--dragging {
  opacity: 0.45;
}

.content-markdown-renderer__gallery-item--drop-target {
  outline: 3px solid #ff7a00;
  outline-offset: 3px;
  border-radius: 12px;
}

.content-markdown-renderer__gallery-item--drop-target::after {
  content: '여기로 이동';
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  place-items: center;
  border-radius: 10px;
  background: rgba(255, 122, 0, 0.22);
  color: #fff;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: -0.02em;
  pointer-events: none;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
}
</style>
