<script setup>
const props = defineProps({
  modelValue: {
    type: String,
    default: ''
  }
})

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

const textareaRef = ref(null)
const activeMode = ref('write')
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: () => props.modelValue || '',
  set: (value) => emit('update:modelValue', value)
})

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

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)
  })
}

/**
 * 선택 영역을 지정 문자열로 교체한다.
 * @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 {{ 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'} 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 {
    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
  }
}

/**
 * 파일 입력 변경 처리
 * @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) {
    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('*', '*', '기울임')
  } else if (key === 'e') {
    event.preventDefault()
    wrapInline('`', '`', 'code')
  }
}
</script>

<template>
  <div 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">
      <textarea
        ref="textareaRef"
        v-model="markdownValue"
        class="admin-markdown-editor__textarea min-h-[620px] w-full resize-y rounded border border-[#e3e6e8] bg-white px-5 py-5 font-mono text-[15px] leading-7 text-[#15171a] outline-none transition-colors placeholder:text-[#8e9cac] focus:border-[#15171a]"
        placeholder="마크다운으로 글을 작성하세요."
        spellcheck="false"
        @keydown="handleKeydown"
        @paste="handlePaste"
        @drop="handleDrop"
        @dragover.prevent
      />
      <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 class="admin-markdown-editor__preview min-h-[620px] rounded border border-[#e3e6e8] bg-white px-6 py-5">
      <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 === 'gallery' ? '갤러리 미디어 선택' : '이미지 미디어 선택' }}
            </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>
