<template>
  <div
    class="vm-markdown"
    :class="{
      'is-shell': showShell,
      'is-disabled': disabled,
    }"
  >
    <div v-if="showTabs" class="vm-markdown__tabs" role="tablist">
      <button
        type="button"
        class="vm-markdown__tab"
        :class="{ 'is-on': tab === 'edit' }"
        role="tab"
        :aria-selected="tab === 'edit'"
        @click="tab = 'edit'"
      >
        {{ crudLabel('markdownEdit') }}
      </button>
      <button
        type="button"
        class="vm-markdown__tab"
        :class="{ 'is-on': tab === 'preview' }"
        role="tab"
        :aria-selected="tab === 'preview'"
        @click="tab = 'preview'"
      >
        {{ crudLabel('markdownPreview') }}
      </button>
    </div>

    <textarea
      v-if="editEnabled && (!previewEnabled || tab === 'edit')"
      ref="editorRef"
      class="vm-markdown__editor"
      :style="editorStyle"
      :value="text"
      :disabled="disabled || uploading"
      :placeholder="placeholder ?? crudLabel('markdownHint')"
      @input="onInput"
      @paste="onPaste"
    />

    <div
      v-if="previewEnabled && (!editEnabled || tab === 'preview')"
      class="vm-markdown__preview"
      :class="{ 'is-bare': isBarePreview }"
      :style="previewStyle"
      v-html="html"
    />

    <p v-if="uploading" class="vm-markdown__hint">{{ crudLabel('markdownUploading') }}</p>
  </div>
</template>

<script setup lang="ts">
import { crudLabel } from '../crud/style'
import { computed, nextTick, ref, watch } from 'vue'
import DOMPurify from 'dompurify'
import { marked } from 'marked'

defineOptions({ name: 'vm-markdown' })

const props = withDefaults(
  defineProps<{
    modelValue?: string | null
    /** 是否可编辑 Markdown 源码，默认 false */
    edit?: boolean
    /** 是否显示预览，默认 true */
    preview?: boolean
    disabled?: boolean
    placeholder?: string
    /** 编辑区 / 带壳预览区最小高度；只读预览传入后可滚动 */
    height?: number | string
    /** 粘贴图片时上传，返回可访问 url；未传则忽略图片粘贴 */
    uploadImage?: (file: File) => Promise<string>
  }>(),
  {
    edit: false,
    preview: true,
    disabled: false,
    placeholder: undefined,
  },
)

const emit = defineEmits<{
  'update:modelValue': [v: string]
}>()

marked.setOptions({
  gfm: true,
  breaks: false,
})

const editorRef = ref<HTMLTextAreaElement | null>(null)
const uploading = ref(false)
const tab = ref<'edit' | 'preview'>('edit')

const editEnabled = computed(() => Boolean(props.edit))
const previewEnabled = computed(() => {
  // 至少保留一种能力：两者都关时按只读预览
  if (!props.edit && !props.preview) return true
  return Boolean(props.preview)
})

const showTabs = computed(() => editEnabled.value && previewEnabled.value)
/** 编辑、双 Tab，或只读预览且指定了 height 时显示边框壳 */
const showShell = computed(
  () =>
    editEnabled.value ||
    showTabs.value ||
    (previewEnabled.value && !editEnabled.value && props.height != null),
)
/** 纯展示（如文档详情）无壳无固定高 */
const isBarePreview = computed(
  () => !editEnabled.value && props.height == null,
)

const text = computed(() => String(props.modelValue ?? ''))

const html = computed(() => {
  const raw = text.value
  if (!raw.trim()) return ''
  const parsed = marked.parse(raw, { async: false }) as string
  return DOMPurify.sanitize(parsed, {
    USE_PROFILES: { html: true },
  })
})

function toMinHeight(h: number | string) {
  return typeof h === 'number' ? `${h}px` : h
}

const editorStyle = computed(() => {
  const h = props.height ?? (editEnabled.value ? 280 : undefined)
  if (h == null) return undefined
  return { minHeight: toMinHeight(h) }
})

const previewStyle = computed(() => {
  if (isBarePreview.value) return undefined
  const h = props.height ?? (editEnabled.value ? 280 : undefined)
  if (h == null) return undefined
  return {
    minHeight: toMinHeight(h),
    maxHeight: toMinHeight(h),
    overflow: 'auto',
  }
})

watch(
  showTabs,
  (both) => {
    if (both) tab.value = 'edit'
  },
  { immediate: true },
)

function onInput(e: Event) {
  const v = (e.target as HTMLTextAreaElement).value
  emit('update:modelValue', v)
}

function insertAtCursor(snippet: string) {
  const el = editorRef.value
  const cur = text.value
  if (!el) {
    emit('update:modelValue', `${cur}${snippet}`)
    return
  }
  const start = el.selectionStart ?? cur.length
  const end = el.selectionEnd ?? cur.length
  const next = cur.slice(0, start) + snippet + cur.slice(end)
  emit('update:modelValue', next)
  void nextTick(() => {
    const pos = start + snippet.length
    el.focus()
    el.setSelectionRange(pos, pos)
  })
}

async function onPaste(e: ClipboardEvent) {
  if (!props.uploadImage || props.disabled || uploading.value) return
  const items = e.clipboardData?.items
  if (!items?.length) return

  const files: File[] = []
  for (const item of Array.from(items)) {
    if (item.kind === 'file' && item.type.startsWith('image/')) {
      const file = item.getAsFile()
      if (file) files.push(file)
    }
  }
  if (!files.length) return

  e.preventDefault()
  uploading.value = true
  try {
    for (const file of files) {
      const url = await props.uploadImage(file)
      const alt =
        (file.name || 'image').replace(/\.[^.]+$/, '').trim() || 'image'
      insertAtCursor(`\n![${alt}](${url})\n`)
    }
  } finally {
    uploading.value = false
  }
}
</script>

<style lang="scss" scoped>
.vm-markdown {
  color: var(--foreground);
  font-size: 14px;
  line-height: 1.7;
  word-break: break-word;
}

.vm-markdown.is-shell {
  border: 1px solid var(--border);
  border-radius: var(--radius, 12px);
  overflow: hidden;
  background: var(--card, #fff);
}

.vm-markdown.is-disabled {
  opacity: 0.65;
  pointer-events: none;
}

.vm-markdown__tabs {
  display: flex;
  border-bottom: 1px solid var(--border);
  background: var(--muted, #f7f8fc);
}

.vm-markdown__tab {
  appearance: none;
  border: 0;
  background: transparent;
  padding: 10px 16px;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  color: var(--muted-foreground);
  cursor: pointer;
}

.vm-markdown__tab.is-on {
  color: var(--brand);
  background: var(--card, #fff);
  box-shadow: inset 0 -2px 0 var(--brand);
}

.vm-markdown__editor {
  display: block;
  width: 100%;
  margin: 0;
  padding: 14px 16px;
  border: 0;
  resize: vertical;
  box-sizing: border-box;
  background: color-mix(in srgb, var(--brand) 3%, var(--background, #fff));
  color: var(--foreground);
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  font-size: 13.5px;
  line-height: 1.6;
}

.vm-markdown__editor:focus {
  outline: none;
  background: var(--card, #fff);
}

.vm-markdown__preview {
  padding: 14px 16px;
  box-sizing: border-box;
}

.vm-markdown__preview.is-bare {
  padding: 0;
  min-height: 0;
  max-height: none;
  overflow: visible;
}

.vm-markdown__hint {
  margin: 0;
  padding: 8px 16px 12px;
  font-size: 13px;
  color: var(--muted-foreground);
}

.vm-markdown__preview {
  :deep(h1),
  :deep(h2),
  :deep(h3),
  :deep(h4) {
    margin: 1.25em 0 0.55em;
    font-weight: 700;
    letter-spacing: -0.01em;
    line-height: 1.35;
    color: var(--foreground);
  }

  :deep(h1) {
    margin-top: 0;
    font-size: 22px;
  }

  :deep(h2) {
    font-size: 18px;
  }

  :deep(h3) {
    font-size: 16px;
  }

  :deep(p) {
    margin: 0.65em 0;
  }

  :deep(ul),
  :deep(ol) {
    margin: 0.65em 0;
    padding-left: 1.35em;
  }

  :deep(li) {
    margin: 0.28em 0;
  }

  :deep(a) {
    color: var(--brand);
    text-decoration: underline;
    text-underline-offset: 2px;
  }

  :deep(strong) {
    font-weight: 700;
  }

  :deep(code) {
    padding: 0.12em 0.4em;
    border-radius: 6px;
    background: color-mix(in srgb, var(--foreground) 8%, var(--background));
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
    font-size: 0.88em;
  }

  :deep(pre) {
    margin: 0.85em 0;
    overflow: auto;
    padding: 14px 16px;
    border-radius: 12px;
    background: color-mix(in srgb, var(--foreground) 5%, var(--muted));
    line-height: 1.55;
  }

  :deep(pre code) {
    padding: 0;
    border-radius: 0;
    background: transparent;
    font-size: 12.5px;
  }

  :deep(blockquote) {
    margin: 0.85em 0;
    padding: 0.2em 0 0.2em 12px;
    border-left: 3px solid var(--brand);
    color: var(--muted-foreground);
  }

  :deep(hr) {
    margin: 1.25em 0;
    border: none;
    border-top: 1px solid var(--border);
  }

  :deep(table) {
    width: 100%;
    margin: 0.85em 0;
    border-collapse: collapse;
    font-size: 13px;
  }

  :deep(th),
  :deep(td) {
    padding: 8px 10px;
    border: 1px solid var(--border);
    text-align: left;
  }

  :deep(th) {
    background: color-mix(in srgb, var(--foreground) 4%, var(--background));
    font-weight: 650;
  }

  :deep(img) {
    max-width: 100%;
    height: auto;
    border-radius: 8px;
  }
}
</style>
