<script setup lang="ts">
import { watch, onBeforeUnmount } from "vue";
import { Editor, EditorContent } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";
import Link from "@tiptap/extension-link";
import { Placeholder } from "@tiptap/extensions";
import { marked } from "marked";
import DOMPurify from "dompurify";
import { serializeToMarkdown } from "@/utils/markdownSerializer";
import { TaskList, TaskItem } from "@tiptap/extension-list";
import { TagDecoration } from "@/extensions/TagDecoration";
import { WikilinkDecoration } from "@/extensions/WikilinkDecoration";
import { WikilinkSuggestion } from "@/extensions/WikilinkSuggestion";
import { SlashCommands } from "@/extensions/SlashCommands";

const props = withDefaults(
  defineProps<{
    modelValue: string;
    placeholder?: string;
  }>(),
  {
    placeholder: "",
  }
);

const emit = defineEmits<{
  "update:modelValue": [value: string];
  selectionChange: [payload: { text: string; start: number; end: number }];
  escape: [];
}>();

let updatingFromProp = false;
let lastEmittedMarkdown = props.modelValue;

function markdownToHtml(md: string): string {
  const html = marked(md) as string;
  return DOMPurify.sanitize(html);
}

let editor: Editor | null = null;

try {
  editor = new Editor({
    content: markdownToHtml(props.modelValue),
    editable: true,
    editorProps: {
      handleKeyDown: (_view, event) => {
        if (event.key === "Escape") {
          editor?.commands.blur();
          emit("escape");
          return true;
        }
        return false;
      },
      handlePaste: (_view, event) => {
        const text = event.clipboardData?.getData("text/plain");
        if (!text || !editor) return false;
        // If clipboard already has HTML (e.g. copying from a webpage), let Tiptap handle it
        const html = event.clipboardData?.getData("text/html");
        if (html) return false;
        // Check if the pasted text looks like it contains markdown formatting
        const hasMd = /(?:^#{1,6}\s|^\s*[-*+]\s|^\s*\d+\.\s|^\s*>|```|\*\*|__|~~|\[.+\]\(.+\))/m.test(text);
        if (!hasMd) return false;
        // Convert markdown to HTML and insert as formatted content
        const converted = markdownToHtml(text);
        editor.chain().focus().deleteSelection().insertContent(converted).run();
        return true;
      },
    },
    extensions: [
      StarterKit.configure({
        heading: { levels: [1, 2, 3, 4, 5, 6] },
        link: false,
      }),
      Link.configure({
        openOnClick: false,
        autolink: true,
      }),
      Placeholder.configure({
        placeholder: props.placeholder,
      }),
      TaskList,
      TaskItem.configure({ nested: true }),
      TagDecoration,
      WikilinkDecoration,
      WikilinkSuggestion,
      SlashCommands,
    ],
    onUpdate({ editor: ed }) {
      if (updatingFromProp) return;
      const md = serializeToMarkdown(ed.getJSON());
      lastEmittedMarkdown = md;
      emit("update:modelValue", md);
    },
    onSelectionUpdate({ editor: ed }) {
      const { from, to } = ed.state.selection;
      if (from === to) return;
      const selectedText = ed.state.doc.textBetween(from, to, "\n");
      if (!selectedText) return;

      const md = lastEmittedMarkdown;
      const fraction = from / (ed.state.doc.content.size || 1);
      const estIdx = Math.floor(fraction * md.length);

      let bestIdx = -1;
      let bestDist = Infinity;
      let searchFrom = 0;
      while (true) {
        const idx = md.indexOf(selectedText, searchFrom);
        if (idx === -1) break;
        const dist = Math.abs(idx - estIdx);
        if (dist < bestDist) { bestDist = dist; bestIdx = idx; }
        searchFrom = idx + 1;
      }
      if (bestIdx !== -1) {
        emit("selectionChange", {
          text: selectedText,
          start: bestIdx,
          end: bestIdx + selectedText.length,
        });
      }
    },
  });
} catch (e) {
  console.error("Tiptap editor creation failed:", e);
}

onBeforeUnmount(() => {
  editor?.destroy();
});

// Watch for external modelValue changes (e.g. AI assist accept)
watch(
  () => props.modelValue,
  (newVal) => {
    if (!editor) return;
    if (newVal === lastEmittedMarkdown) return;

    updatingFromProp = true;
    editor.commands.setContent(markdownToHtml(newVal));
    lastEmittedMarkdown = newVal;
    updatingFromProp = false;
  }
);

defineExpose({ editor });
</script>

<template>
  <div class="tiptap-wrapper tiptap-editor">
    <EditorContent v-if="editor" :editor="editor" class="prose" />
    <div v-else class="editor-error">Editor failed to load. Check console.</div>
  </div>
</template>

<style scoped>
.editor-error {
  padding: 1rem;
  color: var(--fs-error);
  font-size: 0.9rem;
}
</style>
