<script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { ElMessage } from 'element-plus';
import { useAdminLocale } from '../composables/useAdminLocale';
import api from '../api';
import { fetchMediaLibraryImages } from '../utils/media-library';
import { resolveApiError } from '../i18n/form-validation';

const props = withDefaults(
  defineProps<{
    modelValue: string;
    placeholder?: string;
    uploadCategory?: string;
    disabled?: boolean;
    fill?: boolean;
  }>(),
  { uploadCategory: 'contents', fill: false },
);

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

const { t } = useAdminLocale();

const IMAGE_ACCEPT = 'image/png,image/jpeg,image/webp,image/gif,image/svg+xml';
const MAX_UPLOAD_SIZE = 5 * 1024 * 1024;
const EDITOR_IMAGE_MAX_HEIGHT = '200px';

const editorRef = ref<HTMLDivElement | null>(null);
const mediaPickerVisible = ref(false);
/** blob/object URL -> File，保存时由父组件调用 uploadPendingImages 上传 */
const pendingFiles = new Map<string, File>();
const mediaFiles = ref<Array<{ id: string; filename: string; url: string; mimeType: string; category?: string }>>([]);
const mediaLoading = ref(false);
const syncing = ref(false);
let savedRange: Range | null = null;

function isNodeInEditor(node: Node | null): boolean {
  const el = editorRef.value;
  if (!el || !node) return false;
  return el.contains(node.nodeType === Node.TEXT_NODE ? node.parentNode : node);
}

function saveSelection() {
  const sel = window.getSelection();
  const el = editorRef.value;
  if (!sel || sel.rangeCount === 0 || !el) return;
  const range = sel.getRangeAt(0);
  if (isNodeInEditor(range.commonAncestorContainer)) {
    savedRange = range.cloneRange();
  }
}

function restoreSelection(): boolean {
  const el = editorRef.value;
  if (!savedRange || !el) return false;
  try {
    if (!isNodeInEditor(savedRange.commonAncestorContainer)) return false;
    const sel = window.getSelection();
    if (!sel) return false;
    sel.removeAllRanges();
    sel.addRange(savedRange);
    return true;
  } catch {
    savedRange = null;
    return false;
  }
}

function onDocumentSelectionChange() {
  if (isNodeInEditor(window.getSelection()?.anchorNode ?? null)) {
    saveSelection();
  }
}

function normalizeHtml(html: string) {
  const trimmed = html.trim();
  if (!trimmed) return '';
  return trimmed;
}

function syncFromModel() {
  const el = editorRef.value;
  if (!el) return;
  syncing.value = true;
  el.innerHTML = props.modelValue || '';
  if (el.innerHTML) normalizeEditorImages(el);
  syncing.value = false;
}

function emitChange() {
  if (syncing.value || !editorRef.value) return;
  const html = normalizeHtml(editorRef.value.innerHTML);
  cleanupOrphanedBlobs(html);
  emit('update:modelValue', html);
}

function cleanupOrphanedBlobs(html: string) {
  const used = new Set<string>();
  for (const match of html.matchAll(/blob:[^\s"'<>]+/g)) {
    used.add(match[0]);
  }
  for (const blobUrl of pendingFiles.keys()) {
    if (!used.has(blobUrl)) {
      URL.revokeObjectURL(blobUrl);
      pendingFiles.delete(blobUrl);
    }
  }
}

function revokeAllPendingBlobs() {
  for (const blobUrl of pendingFiles.keys()) {
    URL.revokeObjectURL(blobUrl);
  }
  pendingFiles.clear();
}

function exec(cmd: string, value?: string) {
  editorRef.value?.focus();
  document.execCommand(cmd, false, value);
  emitChange();
}

function applyInlineImageStyles(img: HTMLImageElement) {
  img.removeAttribute('width');
  img.removeAttribute('height');
  img.style.maxWidth = '100%';
  img.style.width = 'auto';
  img.style.height = 'auto';
  img.style.maxHeight = EDITOR_IMAGE_MAX_HEIGHT;
  img.style.objectFit = 'contain';
  img.style.display = 'block';
  img.style.margin = '10px 0';
  img.style.borderRadius = '6px';
}

function createEditorImage(url: string, pending = false) {
  const img = document.createElement('img');
  img.src = url;
  if (pending || url.startsWith('blob:')) {
    img.setAttribute('data-local-blob', '1');
  }
  applyInlineImageStyles(img);
  return img;
}

function normalizeEditorImages(root: HTMLElement) {
  root.querySelectorAll('img').forEach((node) => applyInlineImageStyles(node as HTMLImageElement));
}

function insertImageUrl(url: string) {
  const el = editorRef.value;
  if (!el) return;

  el.focus();
  restoreSelection();

  const img = createEditorImage(url, url.startsWith('blob:'));
  const sel = window.getSelection();

  if (sel && sel.rangeCount > 0) {
    const range = sel.getRangeAt(0);
    if (isNodeInEditor(range.commonAncestorContainer)) {
      range.deleteContents();
      range.insertNode(img);
      const after = document.createRange();
      after.setStartAfter(img);
      after.collapse(true);
      sel.removeAllRanges();
      sel.addRange(after);
      savedRange = after.cloneRange();
      normalizeEditorImages(el);
      emitChange();
      return;
    }
  }

  el.appendChild(img);
  normalizeEditorImages(el);
  emitChange();
}

function insertPendingImage(file: File) {
  if (file.size > MAX_UPLOAD_SIZE) {
    ElMessage.error(t('content.upload.size_error'));
    return;
  }
  saveSelection();
  const blobUrl = URL.createObjectURL(file);
  pendingFiles.set(blobUrl, file);
  insertImageUrl(blobUrl);
}

async function uploadSingleFile(file: File): Promise<string> {
  const fd = new FormData();
  fd.append('file', file);
  const { data } = await api.post(`/admin/uploads?category=${props.uploadCategory}`, fd, {
    headers: { 'Content-Type': 'multipart/form-data' },
  });
  const url = data.data?.url as string;
  if (!url) {
    throw new Error(t('content.upload.failed'));
  }
  return url;
}

/** 扫描 HTML 中的 blob: 图片，上传后替换为 /uploads/... URL */
async function uploadPendingImages(html: string): Promise<string> {
  if (!html.includes('blob:')) return html;

  let result = html;
  const blobUrls = [...new Set([...html.matchAll(/blob:[^\s"'<>]+/g)].map((m) => m[0]))];

  for (const blobUrl of blobUrls) {
    const file = pendingFiles.get(blobUrl);
    if (!file) continue;

    const serverUrl = await uploadSingleFile(file);
    result = result.replaceAll(blobUrl, serverUrl);
    URL.revokeObjectURL(blobUrl);
    pendingFiles.delete(blobUrl);
  }

  return result;
}

function getHtml(): string {
  if (!editorRef.value) return normalizeHtml(props.modelValue);
  return normalizeHtml(editorRef.value.innerHTML);
}

defineExpose({ uploadPendingImages, getHtml });

function onImageFileChange(e: Event) {
  const input = e.target as HTMLInputElement;
  if (input.files?.[0]) {
    insertPendingImage(input.files[0]);
    input.value = '';
  }
}

async function openMediaPicker() {
  saveSelection();
  mediaPickerVisible.value = true;
  mediaLoading.value = true;
  try {
    mediaFiles.value = await fetchMediaLibraryImages({ pageSize: 200 });
  } catch (err: unknown) {
    mediaFiles.value = [];
    ElMessage.error(resolveApiError(err, t, 'content.upload.load_media_failed'));
  } finally {
    mediaLoading.value = false;
  }
}

function pickMediaFile(url: string) {
  insertImageUrl(url);
  mediaPickerVisible.value = false;
}

function onPaste(e: ClipboardEvent) {
  const items = e.clipboardData?.items;
  if (!items) return;
  for (const item of items) {
    if (item.type.startsWith('image/')) {
      e.preventDefault();
      saveSelection();
      const file = item.getAsFile();
      if (file) insertPendingImage(file);
      return;
    }
  }
  void nextTick(() => {
    const el = editorRef.value;
    if (!el) return;
    normalizeEditorImages(el);
    emitChange();
  });
}

watch(
  () => props.modelValue,
  (val, prev) => {
    if (val === prev) return;
    const el = editorRef.value;
    if (!el) return;
    if (normalizeHtml(el.innerHTML) !== normalizeHtml(val)) {
      syncFromModel();
    }
  },
);

onMounted(async () => {
  await nextTick();
  syncFromModel();
  document.addEventListener('selectionchange', onDocumentSelectionChange);
});

onBeforeUnmount(() => {
  document.removeEventListener('selectionchange', onDocumentSelectionChange);
  revokeAllPendingBlobs();
});
</script>

<template>
  <div class="rich-editor" :class="{ 'is-disabled': disabled, 'rich-editor--fill': fill }">
    <div class="toolbar">
      <button type="button" title="Bold" :disabled="disabled" @mousedown.prevent @click="exec('bold')">
        <strong>B</strong>
      </button>
      <button type="button" title="Italic" :disabled="disabled" @mousedown.prevent @click="exec('italic')">
        <em>I</em>
      </button>
      <button type="button" :disabled="disabled" @mousedown.prevent @click="exec('insertUnorderedList')">
        •
      </button>
      <button type="button" :disabled="disabled" @mousedown.prevent @click="exec('insertOrderedList')">
        1.
      </button>
      <label class="toolbar-upload" @mousedown.prevent="saveSelection">
        <input
          type="file"
          :accept="IMAGE_ACCEPT"
          style="display: none"
          :disabled="disabled"
          @change="onImageFileChange"
        />
        {{ t('content.editor.insert_image') }}
      </label>
      <button
        type="button"
        :disabled="disabled"
        @mousedown.prevent="saveSelection"
        @click="openMediaPicker"
      >
        {{ t('content.upload.pick_media') }}
      </button>
    </div>
    <div
      ref="editorRef"
      class="editor-body"
      :data-placeholder="placeholder || t('content.editor.placeholder')"
      :contenteditable="disabled ? 'false' : 'true'"
      @input="emitChange"
      @blur="emitChange"
      @paste="onPaste"
    />
  </div>

  <el-dialog
    v-model="mediaPickerVisible"
    :title="t('content.upload.pick_media_title')"
    width="680px"
    destroy-on-close
    append-to-body
    :z-index="4000"
  >
    <div v-if="mediaLoading" class="media-state">{{ t('common.loading') }}</div>
    <div v-else-if="mediaFiles.length === 0" class="media-state">{{ t('content.upload.no_media') }}</div>
    <div v-else class="media-grid">
      <div
        v-for="file in mediaFiles"
        :key="file.id"
        class="media-card"
        @click="pickMediaFile(file.url)"
      >
        <div class="media-thumb">
          <img v-if="file.mimeType !== 'image/svg+xml'" :src="file.url" :alt="file.filename" loading="lazy" />
          <div v-else class="media-svg">SVG</div>
        </div>
        <div class="media-name" :title="file.filename">{{ file.filename }}</div>
        <div v-if="file.category" class="media-category">{{ file.category }}</div>
      </div>
    </div>
  </el-dialog>
</template>

<style scoped>
.rich-editor {
  width: 100%;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  background: #fff;
}

.rich-editor--fill {
  display: flex;
  flex-direction: column;
  min-height: 420px;
  height: 100%;
}

.rich-editor.is-disabled {
  opacity: 0.7;
}

.toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  padding: 5px 6px;
  border-bottom: 1px solid var(--border);
  background: #fbfaf7;
}

.toolbar button,
.toolbar-upload {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 28px;
  height: 26px;
  padding: 0 8px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: #fff;
  color: var(--text);
  font-size: 11px;
  font-family: inherit;
  cursor: pointer;
}

.toolbar button:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.toolbar-upload {
  font-weight: 600;
}

.editor-body {
  min-height: 140px;
  max-height: 280px;
  overflow-x: hidden;
  overflow-y: auto;
  padding: 8px 10px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--text);
  outline: none;
  word-break: break-word;
}

.rich-editor--fill .editor-body {
  flex: 1;
  min-height: 400px;
  max-height: none;
}

.editor-body:empty::before {
  content: attr(data-placeholder);
  color: var(--text-muted);
  pointer-events: none;
}

.editor-body :deep(img) {
  box-sizing: border-box;
  max-width: 100%;
  width: auto;
  height: auto;
  max-height: 200px;
  object-fit: contain;
  display: block;
  margin: 10px 0;
  border-radius: 6px;
}

.rich-editor--fill .editor-body :deep(img) {
  max-height: 200px;
}

.editor-body :deep(ul),
.editor-body :deep(ol) {
  margin: 8px 0;
  padding-left: 22px;
}

.media-state {
  text-align: center;
  padding: 40px 0;
  color: var(--text-muted);
}

.media-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
  gap: 12px;
  max-height: 420px;
  overflow-y: auto;
}

.media-card {
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  cursor: pointer;
  background: #fff;
}

.media-card:hover {
  border-color: #d5cfc3;
}

.media-thumb {
  height: 80px;
  background: #f4f0e8;
  display: flex;
  align-items: center;
  justify-content: center;
}

.media-thumb img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

.media-svg {
  font-size: 14px;
  font-weight: 700;
  color: var(--text-muted);
}

.media-name {
  padding: 6px 8px 2px;
  font-size: 11px;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.media-category {
  padding: 0 8px 6px;
  font-size: 10px;
  color: var(--text-muted);
  opacity: 0.85;
}
</style>
