<script setup>
import { normalizeMarkdownContent } from '../../lib/markdown-content-normalizer.js'

const props = defineProps({
  modelValue: {
    type: [String, Array, Object],
    default: ''
  }
})

const emit = defineEmits(['update:modelValue'])

const editorRootRef = ref(null)
const textareaRef = ref(null)
const previewRef = ref(null)
const gutterRef = ref(null)
const activeMode = ref('write')
/** 커서가 있는 논리 줄(0-based, `\\n` 기준) */
const activeLogicalLineIndex = ref(0)
const mediaItems = ref([])
const isMediaPickerOpen = ref(false)
const isLoadingMedia = ref(false)
const isUploading = ref(false)
const mediaPickerTarget = ref('image')
const selectedMediaUrls = ref([])
const selectedImageWidth = ref('regular')

const imageWidthOptions = [
  { value: 'regular', label: '기본' },
  { value: 'wide', label: '와이드' },
  { value: 'full', label: '풀사이즈' }
]

const markdownValue = computed({
  get: () => normalizeMarkdownContent(props.modelValue),
  set: (value) => emit('update:modelValue', value)
})

/**
 * 이미지 마크다운 한 줄을 구조화한다.
 * @param {string} line - 이미지 마크다운 줄
 * @returns {{ alt: string, url: string, width: string }|null} 이미지 정보
 */
const parseImageMarkdownLine = (line) => {
  const match = line.trim().match(/^!\[(.*?)\]\((.*?)\)(?:\{width=(regular|wide|full)\})?$/)

  if (!match) {
    return null
  }

  return {
    alt: match[1] || '',
    url: match[2] || '',
    width: match[3] || 'regular'
  }
}

/**
 * 현재 커서가 속한 이미지 또는 갤러리 블록 정보를 찾는다.
 * @returns {{ type: 'image'|'gallery', startLine: number, endLine: number, images: Array<{ alt: string, url: string, width: string }> }|null}
 */
const activeMediaBlock = computed(() => {
  const lines = (markdownValue.value || '').split('\n')
  const currentLine = Math.min(activeLogicalLineIndex.value, lines.length - 1)
  const activeImage = parseImageMarkdownLine(lines[currentLine] || '')

  if (activeImage) {
    return {
      type: 'image',
      startLine: currentLine,
      endLine: currentLine,
      images: [activeImage]
    }
  }

  let galleryStart = -1

  for (let index = currentLine; index >= 0; index -= 1) {
    if ((lines[index] || '').trim() === ':::gallery') {
      galleryStart = index
      break
    }

    if ((lines[index] || '').trim() === ':::') {
      break
    }
  }

  if (galleryStart === -1) {
    return null
  }

  let galleryEnd = -1

  for (let index = galleryStart + 1; index < lines.length; index += 1) {
    if ((lines[index] || '').trim() === ':::') {
      galleryEnd = index
      break
    }
  }

  if (galleryEnd === -1 || currentLine > galleryEnd) {
    return null
  }

  return {
    type: 'gallery',
    startLine: galleryStart,
    endLine: galleryEnd,
    images: lines
      .slice(galleryStart + 1, galleryEnd)
      .map(parseImageMarkdownLine)
      .filter(Boolean)
  }
})

/**
 * 본문의 논리 줄(`\\n` 기준) 개수. 빈 본문은 1줄로 본다.
 * @returns {number}
 */
const gutterLineCount = computed(() => {
  const raw = markdownValue.value ?? ''
  const n = raw.split('\n').length

  return Math.max(1, n)
})

/**
 * textarea와 줄 번호 거터의 세로 스크롤을 맞춘다.
 * @returns {void}
 */
const syncGutterScroll = () => {
  const gutter = gutterRef.value
  const textarea = textareaRef.value

  if (gutter && textarea) {
    gutter.scrollTop = textarea.scrollTop
  }
}

/**
 * 커서 위치 기준으로 활성 논리 줄 인덱스를 갱신하고 거터 스크롤을 맞춘다.
 * @returns {void}
 */
const refreshCaretLogicalLine = () => {
  nextTick(() => {
    const textarea = textareaRef.value

    if (!textarea) {
      return
    }

    const value = markdownValue.value ?? ''
    const pos = Math.min(textarea.selectionStart, value.length)
    const lineIndex = value.slice(0, pos).split('\n').length - 1

    activeLogicalLineIndex.value = Math.max(0, lineIndex)
    syncGutterScroll()
  })
}

/**
 * textarea 스크롤 시 거터만 동기화한다.
 * @returns {void}
 */
const onTextareaScroll = () => {
  syncGutterScroll()
}

watch(() => props.modelValue, () => {
  refreshCaretLogicalLine()
})

watch(activeMode, (mode) => {
  if (mode === 'write') {
    refreshCaretLogicalLine()
    return
  }

  nextTick(() => {
    previewRef.value?.focus()
  })
})

/**
 * 작성 모드와 미리보기 모드를 전환한다.
 * @returns {void}
 */
const toggleEditorMode = () => {
  activeMode.value = activeMode.value === 'write' ? 'preview' : 'write'
}

onMounted(() => {
  /**
   * document selectionchange에서 작성 textarea가 포커스일 때만 활성 줄을 갱신한다.
   * @returns {void}
   */
  const onSelectionChange = () => {
    if (activeMode.value !== 'write') {
      return
    }

    const textarea = textareaRef.value

    if (!textarea || document.activeElement !== textarea) {
      return
    }

    refreshCaretLogicalLine()
  }

  /**
   * 에디터 안에서 Cmd/Ctrl+E를 누르면 작성/미리보기 모드를 전환한다.
   * @param {KeyboardEvent} event - 키보드 이벤트
   * @returns {void}
   */
  const onDocumentKeydown = (event) => {
    const root = editorRootRef.value

    if (!root || !root.contains(document.activeElement)) {
      return
    }

    if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== 'e') {
      return
    }

    event.preventDefault()
    toggleEditorMode()
  }

  document.addEventListener('selectionchange', onSelectionChange)
  document.addEventListener('keydown', onDocumentKeydown)

  onBeforeUnmount(() => {
    document.removeEventListener('selectionchange', onSelectionChange)
    document.removeEventListener('keydown', onDocumentKeydown)
  })

  refreshCaretLogicalLine()
})

/**
 * 본문 에디터에 포커스한다.
 * @returns {void}
 */
const focusFirstBlock = () => {
  nextTick(() => {
    textareaRef.value?.focus()
    refreshCaretLogicalLine()
  })
}

defineExpose({
  focusFirstBlock
})

/**
 * textarea 선택 영역 정보를 반환한다.
 * @returns {{ start: number, end: number, value: string }} 선택 정보
 */
const getSelectionState = () => {
  const textarea = textareaRef.value
  const value = markdownValue.value

  if (!textarea) {
    return {
      start: value.length,
      end: value.length,
      value
    }
  }

  return {
    start: textarea.selectionStart,
    end: textarea.selectionEnd,
    value
  }
}

/**
 * textarea 커서 위치를 갱신한다.
 * @param {number} start - 시작 위치
 * @param {number} end - 끝 위치
 * @returns {void}
 */
const setTextareaSelection = (start, end = start) => {
  nextTick(() => {
    const textarea = textareaRef.value
    if (!textarea) {
      return
    }

    textarea.focus()
    textarea.setSelectionRange(start, end)
    refreshCaretLogicalLine()
  })
}

/**
 * 선택 영역을 지정 문자열로 교체한다.
 * @param {string} replacement - 대체 문자열
 * @param {number} cursorOffset - 교체 문자열 안 커서 위치
 * @param {number|null} selectionLength - 선택 길이
 * @returns {void}
 */
const replaceSelection = (replacement, cursorOffset = replacement.length, selectionLength = null) => {
  const { start, end, value } = getSelectionState()
  markdownValue.value = `${value.slice(0, start)}${replacement}${value.slice(end)}`
  const nextStart = start + cursorOffset
  setTextareaSelection(nextStart, nextStart + (selectionLength ?? 0))
}

/**
 * 블록형 마크다운 조각을 커서 위치에 삽입한다.
 * @param {string} snippet - 삽입할 마크다운
 * @returns {void}
 */
const insertBlockSnippet = (snippet) => {
  const { start, end, value } = getSelectionState()
  const needsBeforeLine = start > 0 && value[start - 1] !== '\n'
  const needsAfterLine = end < value.length && value[end] !== '\n'
  const replacement = `${needsBeforeLine ? '\n\n' : ''}${snippet}${needsAfterLine ? '\n\n' : ''}`
  replaceSelection(replacement)
}

/**
 * 선택 텍스트를 인라인 마크다운으로 감싼다.
 * @param {string} prefix - 앞 표식
 * @param {string} suffix - 뒤 표식
 * @param {string} placeholder - 선택이 없을 때 들어갈 텍스트
 * @returns {void}
 */
const wrapInline = (prefix, suffix, placeholder) => {
  const { start, end, value } = getSelectionState()
  const selected = value.slice(start, end)
  const inner = selected || placeholder
  const replacement = `${prefix}${inner}${suffix}`
  markdownValue.value = `${value.slice(0, start)}${replacement}${value.slice(end)}`
  const selectionStart = start + prefix.length
  setTextareaSelection(selectionStart, selectionStart + inner.length)
}

/**
 * 선택된 줄을 변환한다.
 * @param {(line: string) => string} transformLine - 줄 변환 함수
 * @returns {void}
 */
const transformSelectedLines = (transformLine) => {
  const { start, end, value } = getSelectionState()
  const lineStart = value.lastIndexOf('\n', Math.max(0, start - 1)) + 1
  const nextLineBreak = value.indexOf('\n', end)
  const lineEnd = nextLineBreak === -1 ? value.length : nextLineBreak
  const selectedBlock = value.slice(lineStart, lineEnd)
  const replacement = selectedBlock.split('\n').map(transformLine).join('\n')

  markdownValue.value = `${value.slice(0, lineStart)}${replacement}${value.slice(lineEnd)}`
  setTextareaSelection(lineStart, lineStart + replacement.length)
}

/**
 * 제목 마크다운을 적용한다.
 * @param {number} level - 제목 레벨
 * @returns {void}
 */
const applyHeading = (level) => {
  transformSelectedLines((line) => {
    const text = line.replace(/^#{1,6}\s+/, '').trimStart()
    return `${'#'.repeat(level)} ${text || '제목'}`
  })
}

/**
 * 인용 마크다운을 적용한다.
 * @returns {void}
 */
const applyQuote = () => {
  transformSelectedLines((line) => line.startsWith('> ') ? line : `> ${line}`)
}

/**
 * 목록 마크다운을 적용한다.
 * @returns {void}
 */
const applyList = () => {
  transformSelectedLines((line) => line.startsWith('- ') ? line : `- ${line}`)
}

/**
 * 코드 블록을 삽입한다.
 * @returns {void}
 */
const insertCodeBlock = () => {
  const { start, end, value } = getSelectionState()
  const selected = value.slice(start, end) || 'code'
  const snippet = `\`\`\`\n${selected}\n\`\`\``
  replaceSelection(snippet, 4, selected.length)
}

/**
 * 이미지 마크다운 문자열을 생성한다.
 * @param {{ url: string, alt?: string, width?: string }} image - 이미지 정보
 * @returns {string} 이미지 마크다운
 */
const createImageMarkdown = (image) => {
  const width = image.width && image.width !== 'regular'
    ? `{width=${image.width}}`
    : ''
  return `![${image.alt || ''}](${image.url})${width}`
}

/**
 * 지정 줄 범위를 새 줄 목록으로 교체한다.
 * @param {number} startLine - 시작 줄
 * @param {number} endLine - 끝 줄
 * @param {string[]} replacementLines - 대체 줄 목록
 * @param {boolean} focusEditor - 교체 후 textarea에 포커스를 돌릴지 여부
 * @returns {void}
 */
const replaceLineRange = (startLine, endLine, replacementLines, focusEditor = true) => {
  const lines = (markdownValue.value || '').split('\n')
  const nextLines = [
    ...lines.slice(0, startLine),
    ...replacementLines,
    ...lines.slice(endLine + 1)
  ]

  markdownValue.value = nextLines.join('\n')

  if (focusEditor) {
    setTextareaSelection(nextLines.slice(0, startLine + replacementLines.length).join('\n').length)
  }
}

/**
 * 현재 미디어 블록을 이미지 목록 기준으로 다시 작성한다.
 * @param {Array<{ alt: string, url: string, width?: string }>} images - 이미지 목록
 * @returns {void}
 */
const replaceActiveMediaImages = (images) => {
  const block = activeMediaBlock.value

  if (!block) {
    return
  }

  if (block.type === 'image') {
    if (!images[0]) {
      replaceLineRange(block.startLine, block.endLine, [], false)
      return
    }

    replaceLineRange(block.startLine, block.endLine, [createImageMarkdown(images[0])], false)
    return
  }

  if (!images.length) {
    replaceLineRange(block.startLine, block.endLine, [], false)
    return
  }

  replaceLineRange(block.startLine, block.endLine, [
    ':::gallery',
    ...images.map(createImageMarkdown),
    ':::'
  ], false)
}

/**
 * 현재 미디어 블록의 특정 이미지를 수정한다.
 * @param {number} imageIndex - 이미지 인덱스
 * @param {Partial<{ alt: string, url: string, width: string }>} patch - 변경 값
 * @returns {void}
 */
const updateActiveMediaImage = (imageIndex, patch) => {
  const block = activeMediaBlock.value

  if (!block) {
    return
  }

  const images = block.images.map((image, index) => index === imageIndex ? { ...image, ...patch } : image)
  replaceActiveMediaImages(images)
}

/**
 * 현재 갤러리 이미지 순서를 바꾼다.
 * @param {number} imageIndex - 이동할 이미지 인덱스
 * @param {-1|1} direction - 이동 방향
 * @returns {void}
 */
const moveActiveGalleryImage = (imageIndex, direction) => {
  const block = activeMediaBlock.value

  if (!block || block.type !== 'gallery') {
    return
  }

  const nextIndex = imageIndex + direction

  if (nextIndex < 0 || nextIndex >= block.images.length) {
    return
  }

  const images = [...block.images]
  const [target] = images.splice(imageIndex, 1)
  images.splice(nextIndex, 0, target)
  replaceActiveMediaImages(images)
}

/**
 * 현재 미디어 블록에서 이미지를 삭제한다.
 * @param {number} imageIndex - 삭제할 이미지 인덱스
 * @returns {void}
 */
const removeActiveMediaImage = (imageIndex) => {
  const block = activeMediaBlock.value

  if (!block) {
    return
  }

  replaceActiveMediaImages(block.images.filter((_, index) => index !== imageIndex))
}

/**
 * 현재 갤러리에 이미지를 추가한다.
 * @param {Array<{ url: string, alt?: string, width?: string }>} images - 추가 이미지 목록
 * @returns {void}
 */
const appendImagesToActiveGallery = (images) => {
  const block = activeMediaBlock.value

  if (!block || block.type !== 'gallery') {
    insertGallery(images)
    return
  }

  replaceActiveMediaImages([...block.images, ...images])
}

/**
 * 이미지 마크다운을 삽입한다.
 * @param {{ url: string, alt?: string, width?: string }} image - 이미지 정보
 * @returns {void}
 */
const insertImage = (image) => {
  insertBlockSnippet(createImageMarkdown(image))
}

/**
 * 갤러리 마크다운을 삽입한다.
 * @param {Array<{ url: string, alt?: string, width?: string }>} images - 이미지 목록
 * @returns {void}
 */
const insertGallery = (images) => {
  if (!images.length) {
    return
  }

  insertBlockSnippet([':::gallery', ...images.map(createImageMarkdown), ':::'].join('\n'))
}

/**
 * 미디어 목록을 불러온다.
 * @returns {Promise<void>}
 */
const fetchMediaItems = async () => {
  isLoadingMedia.value = true

  try {
    mediaItems.value = await $fetch('/admin/api/media')
  } finally {
    isLoadingMedia.value = false
  }
}

/**
 * 미디어 선택 창을 연다.
 * @param {'image'|'gallery'|'active-gallery'} target - 삽입 대상
 * @returns {Promise<void>}
 */
const openMediaPicker = async (target) => {
  mediaPickerTarget.value = target
  selectedMediaUrls.value = []
  isMediaPickerOpen.value = true
  await fetchMediaItems()
}

/**
 * 미디어 선택 창을 닫는다.
 * @returns {void}
 */
const closeMediaPicker = () => {
  isMediaPickerOpen.value = false
  selectedMediaUrls.value = []
}

/**
 * 미디어 선택 상태를 토글한다.
 * @param {Object} mediaItem - 미디어 항목
 * @returns {void}
 */
const toggleMediaSelection = (mediaItem) => {
  if (mediaPickerTarget.value === 'image') {
    selectedMediaUrls.value = [mediaItem.url]
    return
  }

  selectedMediaUrls.value = selectedMediaUrls.value.includes(mediaItem.url)
    ? selectedMediaUrls.value.filter((url) => url !== mediaItem.url)
    : [...selectedMediaUrls.value, mediaItem.url]
}

/**
 * 선택한 미디어를 마크다운에 삽입한다.
 * @returns {void}
 */
const applyMediaSelection = () => {
  const selectedItems = selectedMediaUrls.value
    .map((url) => mediaItems.value.find((item) => item.url === url))
    .filter(Boolean)

  if (mediaPickerTarget.value === 'image') {
    const [item] = selectedItems
    if (item) {
      insertImage({
        url: item.url,
        alt: item.name || '',
        width: selectedImageWidth.value
      })
    }
  } else if (mediaPickerTarget.value === 'active-gallery') {
    appendImagesToActiveGallery(selectedItems.map((item) => ({
      url: item.url,
      alt: item.name || ''
    })))
  } else {
    insertGallery(selectedItems.map((item) => ({
      url: item.url,
      alt: item.name || ''
    })))
  }

  closeMediaPicker()
}

/**
 * 이미지 파일을 업로드한다.
 * @param {FileList|Array<File>} files - 업로드 파일 목록
 * @returns {Promise<Array<Object>>} 업로드된 파일 목록
 */
const uploadImages = async (files) => {
  const formData = new FormData()
  Array.from(files).forEach((file) => {
    formData.append('files', file)
  })

  const result = await $fetch('/admin/api/uploads', {
    method: 'POST',
    body: formData
  })

  return result.files || []
}

/**
 * 업로드 파일을 현재 커서 위치에 삽입한다.
 * @param {FileList|Array<File>} files - 파일 목록
 * @param {'image'|'gallery'} target - 삽입 대상
 * @returns {Promise<void>}
 */
const uploadAndInsert = async (files, target = 'image') => {
  if (!files?.length) {
    return
  }

  isUploading.value = true

  try {
    const uploadedFiles = await uploadImages(files)
    const images = uploadedFiles.map((file) => ({
      url: file.url,
      alt: file.name || '',
      width: target === 'image' ? selectedImageWidth.value : 'regular'
    }))

    if (target === 'gallery') {
      insertGallery(images)
    } else if (images[0]) {
      insertImage(images[0])
    }

    mediaItems.value = await $fetch('/admin/api/media').catch(() => mediaItems.value)
  } finally {
    isUploading.value = false
  }
}

/**
 * 인라인 HTML 노드를 마크다운 문자열로 변환한다.
 * @param {Node} node - HTML 노드
 * @returns {string} 마크다운 문자열
 */
const convertHtmlInlineNodeToMarkdown = (node) => {
  if (node.nodeType === Node.TEXT_NODE) {
    return node.textContent || ''
  }

  if (node.nodeType !== Node.ELEMENT_NODE) {
    return ''
  }

  const element = /** @type {HTMLElement} */ (node)
  const tagName = element.tagName.toLowerCase()
  const childText = Array.from(element.childNodes).map(convertHtmlInlineNodeToMarkdown).join('')

  if (tagName === 'strong' || tagName === 'b') {
    return `**${childText}**`
  }

  if (tagName === 'em' || tagName === 'i') {
    return `*${childText}*`
  }

  if (tagName === 'code') {
    return `\`${childText}\``
  }

  if (tagName === 'a') {
    const href = element.getAttribute('href')
    return href ? `[${childText || href}](${href})` : childText
  }

  if (tagName === 'img') {
    const src = element.getAttribute('src')
    const alt = element.getAttribute('alt') || ''
    return src ? `![${alt}](${src})` : ''
  }

  if (tagName === 'br') {
    return '\n'
  }

  return childText
}

/**
 * HTML 블록 노드를 마크다운 문자열로 변환한다.
 * @param {Node} node - HTML 노드
 * @param {number} listIndex - 순서 목록 번호
 * @returns {string} 마크다운 문자열
 */
const convertHtmlBlockNodeToMarkdown = (node, listIndex = 1) => {
  if (node.nodeType === Node.TEXT_NODE) {
    return (node.textContent || '').trim()
  }

  if (node.nodeType !== Node.ELEMENT_NODE) {
    return ''
  }

  const element = /** @type {HTMLElement} */ (node)
  const tagName = element.tagName.toLowerCase()
  const inlineText = Array.from(element.childNodes).map(convertHtmlInlineNodeToMarkdown).join('').trim()

  if (/^h[1-6]$/.test(tagName)) {
    return `${'#'.repeat(Number(tagName.slice(1)))} ${inlineText}`
  }

  if (tagName === 'p') {
    return inlineText
  }

  if (tagName === 'blockquote') {
    return inlineText.split('\n').map((line) => `> ${line}`).join('\n')
  }

  if (tagName === 'pre') {
    return `\`\`\`\n${element.textContent?.trim() || ''}\n\`\`\``
  }

  if (tagName === 'li') {
    return `${listIndex}. ${inlineText}`
  }

  if (tagName === 'ul' || tagName === 'ol') {
    return Array.from(element.children)
      .filter((child) => child.tagName.toLowerCase() === 'li')
      .map((child, index) => {
        const marker = tagName === 'ol' ? `${index + 1}.` : '-'
        const text = Array.from(child.childNodes).map(convertHtmlInlineNodeToMarkdown).join('').trim()
        return `${marker} ${text}`
      })
      .join('\n')
  }

  if (tagName === 'div' || tagName === 'section' || tagName === 'article') {
    const childBlocks = Array.from(element.childNodes)
      .map(convertHtmlBlockNodeToMarkdown)
      .filter(Boolean)

    return childBlocks.length ? childBlocks.join('\n\n') : inlineText
  }

  return inlineText
}

/**
 * 클립보드 HTML을 마크다운 문서 조각으로 변환한다.
 * @param {string} html - HTML 문자열
 * @returns {string} 마크다운 문자열
 */
const convertHtmlToMarkdown = (html) => {
  const document = new DOMParser().parseFromString(html, 'text/html')

  return Array.from(document.body.childNodes)
    .map(convertHtmlBlockNodeToMarkdown)
    .filter(Boolean)
    .join('\n\n')
    .replace(/\n{3,}/g, '\n\n')
    .trim()
}

/**
 * 파일 입력 변경 처리
 * @param {Event} event - 파일 입력 이벤트
 * @param {'image'|'gallery'} target - 삽입 대상
 * @returns {Promise<void>}
 */
const handleFileInput = async (event, target) => {
  await uploadAndInsert(event.target.files, target)
  event.target.value = ''
}

/**
 * 붙여넣은 이미지 파일을 업로드한다.
 * @param {ClipboardEvent} event - 붙여넣기 이벤트
 * @returns {Promise<void>}
 */
const handlePaste = async (event) => {
  const imageFiles = Array.from(event.clipboardData?.files || []).filter((file) => file.type.startsWith('image/'))

  if (!imageFiles.length) {
    const html = event.clipboardData?.getData('text/html')

    if (html) {
      const markdown = convertHtmlToMarkdown(html)

      if (markdown) {
        event.preventDefault()
        replaceSelection(markdown)
      }
    }

    return
  }

  event.preventDefault()
  await uploadAndInsert(imageFiles, imageFiles.length > 1 ? 'gallery' : 'image')
}

/**
 * 드롭한 이미지 파일을 업로드한다.
 * @param {DragEvent} event - 드롭 이벤트
 * @returns {Promise<void>}
 */
const handleDrop = async (event) => {
  const imageFiles = Array.from(event.dataTransfer?.files || []).filter((file) => file.type.startsWith('image/'))

  if (!imageFiles.length) {
    return
  }

  event.preventDefault()
  await uploadAndInsert(imageFiles, imageFiles.length > 1 ? 'gallery' : 'image')
}

/**
 * 키보드 단축키를 처리한다.
 * @param {KeyboardEvent} event - 키보드 이벤트
 * @returns {void}
 */
const handleKeydown = (event) => {
  if (!(event.metaKey || event.ctrlKey)) {
    return
  }

  const key = event.key.toLowerCase()

  if (key === 'b') {
    event.preventDefault()
    wrapInline('**', '**', '굵은 글씨')
  } else if (key === 'i') {
    event.preventDefault()
    wrapInline('*', '*', '기울임')
  }
}
</script>

<template>
  <div ref="editorRootRef" class="admin-markdown-editor grid gap-3">
    <div class="admin-markdown-editor__toolbar flex flex-wrap items-center gap-1.5 rounded border border-[#e3e6e8] bg-white p-2">
      <button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="applyHeading(1)">
        H1
      </button>
      <button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="applyHeading(2)">
        H2
      </button>
      <button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="applyHeading(3)">
        H3
      </button>
      <button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm font-bold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="wrapInline('**', '**', '굵은 글씨')">
        B
      </button>
      <button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm italic text-[#394047] hover:bg-[#eff1f2]" type="button" @click="wrapInline('*', '*', '기울임')">
        I
      </button>
      <button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 font-mono text-sm text-[#394047] hover:bg-[#eff1f2]" type="button" @click="wrapInline('`', '`', 'code')">
        Code
      </button>
      <button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="applyQuote">
        인용
      </button>
      <button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="applyList">
        목록
      </button>
      <button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="insertCodeBlock">
        코드블록
      </button>
      <button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="insertBlockSnippet('---')">
        구분선
      </button>
      <span class="admin-markdown-editor__divider mx-1 h-6 w-px bg-[#e3e6e8]" aria-hidden="true" />
      <select v-model="selectedImageWidth" class="admin-markdown-editor__image-width rounded border border-[#d7dde2] bg-white px-2 py-1.5 text-sm text-[#394047]">
        <option v-for="option in imageWidthOptions" :key="option.value" :value="option.value">
          {{ option.label }}
        </option>
      </select>
      <button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="openMediaPicker('image')">
        미디어 이미지
      </button>
      <button class="admin-markdown-editor__tool rounded px-2.5 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="openMediaPicker('gallery')">
        미디어 갤러리
      </button>
      <label class="admin-markdown-editor__upload cursor-pointer rounded bg-[#15171a] px-2.5 py-1.5 text-sm font-semibold text-white">
        이미지 업로드
        <input class="sr-only" type="file" accept="image/*" @change="handleFileInput($event, 'image')">
      </label>
      <label class="admin-markdown-editor__upload cursor-pointer rounded bg-[#15171a] px-2.5 py-1.5 text-sm font-semibold text-white">
        갤러리 업로드
        <input class="sr-only" type="file" accept="image/*" multiple @change="handleFileInput($event, 'gallery')">
      </label>
      <div class="admin-markdown-editor__mode ml-auto inline-flex rounded border border-[#d7dde2] bg-[#f6f7f8] p-0.5">
        <button
          class="admin-markdown-editor__mode-button rounded px-3 py-1.5 text-sm font-semibold"
          :class="activeMode === 'write' ? 'bg-white text-black shadow-sm' : 'text-[#6b7280]'"
          type="button"
          @click="activeMode = 'write'"
        >
          작성
        </button>
        <button
          class="admin-markdown-editor__mode-button rounded px-3 py-1.5 text-sm font-semibold"
          :class="activeMode === 'preview' ? 'bg-white text-black shadow-sm' : 'text-[#6b7280]'"
          type="button"
          @click="activeMode = 'preview'"
        >
          미리보기
        </button>
      </div>
    </div>

    <div v-if="activeMode === 'write'" class="admin-markdown-editor__write relative">
      <div class="admin-markdown-editor__editor-surface flex min-h-[620px] overflow-hidden rounded border border-[#e3e6e8] bg-white">
        <div
          ref="gutterRef"
          class="admin-markdown-editor__gutter min-w-[2.75rem] shrink-0 select-none overflow-y-auto overflow-x-hidden border-r border-[#e3e6e8] bg-[#f6f7f8] py-5 font-mono text-[13px] leading-7 text-[#8e9cac]"
          aria-hidden="true"
        >
          <div
            v-for="ln in gutterLineCount"
            :key="`gutter-line-${ln}`"
            class="admin-markdown-editor__gutter-line min-h-[28px] pr-2 text-right tabular-nums"
            :class="{ 'admin-markdown-editor__gutter-line--active': ln - 1 === activeLogicalLineIndex }"
          >
            {{ ln }}
          </div>
        </div>
        <textarea
          ref="textareaRef"
          v-model="markdownValue"
          class="admin-markdown-editor__textarea min-h-[620px] flex-1 resize-y border-0 bg-transparent py-5 pl-2 pr-5 font-mono text-[15px] leading-7 text-[#15171a] outline-none transition-colors placeholder:text-[#8e9cac] focus:ring-0"
          placeholder="마크다운으로 글을 작성하세요."
          spellcheck="false"
          @keydown="handleKeydown"
          @paste="handlePaste"
          @drop="handleDrop"
          @dragover.prevent
          @scroll="onTextareaScroll"
          @input="refreshCaretLogicalLine"
          @click="refreshCaretLogicalLine"
          @keyup="refreshCaretLogicalLine"
          @select="refreshCaretLogicalLine"
          @focus="refreshCaretLogicalLine"
        />
      </div>
      <section
        v-if="activeMediaBlock"
        class="admin-markdown-editor__media-editor mt-3 rounded border border-[#e3e6e8] bg-white p-4"
        aria-label="현재 미디어 블록 편집"
      >
        <div class="admin-markdown-editor__media-editor-header flex flex-wrap items-center justify-between gap-3">
          <div>
            <h3 class="admin-markdown-editor__media-editor-title text-sm font-bold text-[#15171a]">
              {{ activeMediaBlock.type === 'gallery' ? '현재 갤러리 편집' : '현재 이미지 편집' }}
            </h3>
            <p class="admin-markdown-editor__media-editor-meta mt-1 text-xs text-[#6b7280]">
              {{ activeMediaBlock.type === 'gallery' ? `${activeMediaBlock.images.length}개 이미지` : '커서가 위치한 이미지 줄' }}
            </p>
          </div>
          <button
            v-if="activeMediaBlock.type === 'gallery'"
            class="admin-markdown-editor__media-editor-add rounded bg-[#15171a] px-3 py-1.5 text-xs font-semibold text-white"
            type="button"
            @click="openMediaPicker('active-gallery')"
          >
            이미지 추가
          </button>
        </div>

        <div class="admin-markdown-editor__media-editor-list mt-4 grid gap-3">
          <div
            v-for="(image, imageIndex) in activeMediaBlock.images"
            :key="`media-editor-image-${imageIndex}`"
            class="admin-markdown-editor__media-editor-row grid gap-3 rounded border border-[#edf0f2] bg-[#fafafa] p-3 md:grid-cols-[120px_minmax(0,1fr)]"
          >
            <img
              class="admin-markdown-editor__media-editor-thumb aspect-[4/3] w-full rounded bg-[#eff1f2] object-cover"
              :src="image.url"
              :alt="image.alt || ''"
            >
            <div class="admin-markdown-editor__media-editor-fields grid gap-2">
              <label class="admin-markdown-editor__media-editor-field grid gap-1 text-xs font-semibold text-[#394047]">
                대체 텍스트
                <input
                  class="admin-markdown-editor__media-editor-input rounded border border-[#d7dde2] bg-white px-3 py-2 text-sm font-normal text-[#15171a]"
                  :value="image.alt"
                  type="text"
                  @input="updateActiveMediaImage(imageIndex, { alt: $event.target.value })"
                >
              </label>
              <label class="admin-markdown-editor__media-editor-field grid gap-1 text-xs font-semibold text-[#394047]">
                이미지 URL
                <input
                  class="admin-markdown-editor__media-editor-input rounded border border-[#d7dde2] bg-white px-3 py-2 text-sm font-normal text-[#15171a]"
                  :value="image.url"
                  type="text"
                  @input="updateActiveMediaImage(imageIndex, { url: $event.target.value })"
                >
              </label>
              <div class="admin-markdown-editor__media-editor-actions flex flex-wrap items-center gap-2">
                <select
                  class="admin-markdown-editor__media-editor-select rounded border border-[#d7dde2] bg-white px-2 py-1.5 text-sm text-[#394047]"
                  :value="image.width"
                  @change="updateActiveMediaImage(imageIndex, { width: $event.target.value })"
                >
                  <option v-for="option in imageWidthOptions" :key="option.value" :value="option.value">
                    {{ option.label }}
                  </option>
                </select>
                <button
                  v-if="activeMediaBlock.type === 'gallery'"
                  class="admin-markdown-editor__media-editor-action rounded px-2.5 py-1.5 text-xs font-semibold text-[#394047] hover:bg-[#eff1f2] disabled:cursor-not-allowed disabled:opacity-40"
                  type="button"
                  :disabled="imageIndex === 0"
                  @click="moveActiveGalleryImage(imageIndex, -1)"
                >
                  위로
                </button>
                <button
                  v-if="activeMediaBlock.type === 'gallery'"
                  class="admin-markdown-editor__media-editor-action rounded px-2.5 py-1.5 text-xs font-semibold text-[#394047] hover:bg-[#eff1f2] disabled:cursor-not-allowed disabled:opacity-40"
                  type="button"
                  :disabled="imageIndex === activeMediaBlock.images.length - 1"
                  @click="moveActiveGalleryImage(imageIndex, 1)"
                >
                  아래로
                </button>
                <button
                  class="admin-markdown-editor__media-editor-remove rounded px-2.5 py-1.5 text-xs font-semibold text-red-600 hover:bg-red-50"
                  type="button"
                  @click="removeActiveMediaImage(imageIndex)"
                >
                  삭제
                </button>
              </div>
            </div>
          </div>
        </div>
      </section>
      <div v-if="isUploading" class="admin-markdown-editor__uploading absolute right-3 top-3 rounded bg-[#15171a] px-3 py-1.5 text-xs font-semibold text-white">
        업로드 중
      </div>
    </div>

    <div
      v-else
      ref="previewRef"
      class="admin-markdown-editor__preview min-h-[620px] rounded border border-[#e3e6e8] bg-white px-6 py-5 text-[#15171a]"
      style="--site-text: #15171a; --site-muted: #6b7280; --site-panel: #f6f7f8; --site-line: #e3e6e8; --site-accent: #2eb6ea;"
      tabindex="0"
    >
      <ContentMarkdownRenderer v-if="markdownValue.trim()" :content="markdownValue" />
      <p v-else class="admin-markdown-editor__preview-empty text-sm text-[#8e9cac]">
        미리보기할 본문이 없습니다.
      </p>
    </div>

    <div v-if="isMediaPickerOpen" class="admin-markdown-editor__media-modal fixed inset-0 z-50 grid place-items-center bg-black/40 px-4 py-6" @click.self="closeMediaPicker">
      <div class="admin-markdown-editor__media-panel flex max-h-[86vh] w-full max-w-5xl flex-col overflow-hidden rounded bg-white shadow-2xl">
        <header class="admin-markdown-editor__media-header flex items-center justify-between border-b border-[#e3e6e8] px-5 py-4">
          <div>
            <h2 class="admin-markdown-editor__media-title text-lg font-bold text-black">
              {{ mediaPickerTarget === 'image' ? '이미지 미디어 선택' : '갤러리 미디어 선택' }}
            </h2>
            <p class="admin-markdown-editor__media-count mt-1 text-sm text-[#6b7280]">
              {{ selectedMediaUrls.length }}개 선택됨
            </p>
          </div>
          <button class="admin-markdown-editor__media-close rounded px-3 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="closeMediaPicker">
            닫기
          </button>
        </header>

        <div class="admin-markdown-editor__media-body overflow-y-auto p-5">
          <div v-if="isLoadingMedia" class="admin-markdown-editor__media-loading py-12 text-center text-sm text-[#8e9cac]">
            불러오는 중
          </div>
          <div v-else-if="mediaItems.length" class="admin-markdown-editor__media-grid grid grid-cols-2 gap-3 md:grid-cols-4">
            <button
              v-for="item in mediaItems"
              :key="item.url"
              class="admin-markdown-editor__media-item group overflow-hidden rounded border bg-white text-left transition"
              :class="selectedMediaUrls.includes(item.url) ? 'border-[#15171a] ring-2 ring-[#15171a]/20' : 'border-[#e3e6e8] hover:border-[#8e9cac]'"
              type="button"
              @click="toggleMediaSelection(item)"
            >
              <img class="admin-markdown-editor__media-thumb aspect-[4/3] w-full bg-[#f6f7f8] object-cover" :src="item.url" :alt="item.name || ''">
              <span class="admin-markdown-editor__media-name block truncate px-3 py-2 text-xs font-semibold text-[#394047]">
                {{ item.name || item.url }}
              </span>
            </button>
          </div>
          <div v-else class="admin-markdown-editor__media-empty py-12 text-center text-sm text-[#8e9cac]">
            선택할 미디어가 없습니다.
          </div>
        </div>

        <footer class="admin-markdown-editor__media-footer flex items-center justify-end gap-2 border-t border-[#e3e6e8] px-5 py-4">
          <button class="admin-markdown-editor__media-cancel rounded px-4 py-2 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="closeMediaPicker">
            취소
          </button>
          <button
            class="admin-markdown-editor__media-apply rounded bg-[#15171a] px-4 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-40"
            type="button"
            :disabled="selectedMediaUrls.length === 0"
            @click="applyMediaSelection"
          >
            삽입
          </button>
        </footer>
      </div>
    </div>
  </div>
</template>

<style scoped>
.admin-markdown-editor__gutter-line--active {
  background-color: rgba(46, 182, 234, 0.16);
  color: #15171a;
  font-weight: 600;
}
</style>
