<script setup lang="ts">
import { computed, onBeforeUnmount, ref, watch } from "vue";
import { EditorContent, useEditor } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";
import Collaboration from "@tiptap/extension-collaboration";
import CollaborationCursor from "@tiptap/extension-collaboration-cursor";
import Placeholder from "@tiptap/extension-placeholder";
import * as Y from "yjs";
import { WebrtcProvider } from "y-webrtc";

const props = defineProps<{
  modelValue: string;
  room: string;
  placeholder?: string;
  plain?: boolean;
  markdown?: boolean;
}>();

const emit = defineEmits<{
  (event: "update:modelValue", value: string): void;
}>();

const ydoc = new Y.Doc();
const provider = new WebrtcProvider(props.room, ydoc);
const isBootstrapped = ref(false);
const awarenessVersion = ref(0);

const userPalette = ["#2563eb", "#0ea5e9", "#14b8a6", "#16a34a", "#eab308", "#f97316", "#ef4444"];
const currentUser = {
  name: `You ${Math.floor(Math.random() * 900 + 100)}`,
  color: userPalette[Math.floor(Math.random() * userPalette.length)],
};

function escapeHtml(value: string) {
  return value
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
}

function parseInlineMarkdown(value: string) {
  let output = escapeHtml(value);

  output = output.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label: string, href: string) => {
    const rawHref = String(href ?? "").trim();
    const safeHref = /^(https?:\/\/|mailto:|tel:|\/)/i.test(rawHref) ? rawHref : "";
    if (!safeHref) return label;
    return `<a href="${escapeHtml(safeHref)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
  });

  output = output.replace(/`([^`]+)`/g, "<code>$1</code>");
  output = output.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
  output = output.replace(/__([^_]+)__/g, "<strong>$1</strong>");
  output = output.replace(/(^|[^*])\*([^*]+)\*(?!\*)/g, "$1<em>$2</em>");
  output = output.replace(/(^|[^_])_([^_]+)_(?!_)/g, "$1<em>$2</em>");
  output = output.replace(/~~([^~]+)~~/g, "<s>$1</s>");

  return output;
}

function markdownToHtml(value: string) {
  const lines = value.replaceAll("\r\n", "\n").split("\n");
  const chunks: string[] = [];
  const paragraphLines: string[] = [];
  const quoteLines: string[] = [];
  const listItems: string[] = [];
  let listType: "ul" | "ol" | null = null;
  let inCodeBlock = false;
  let codeLines: string[] = [];

  const flushParagraph = () => {
    if (!paragraphLines.length) return;
    chunks.push(`<p>${paragraphLines.map((line) => parseInlineMarkdown(line)).join("<br />")}</p>`);
    paragraphLines.length = 0;
  };

  const flushQuote = () => {
    if (!quoteLines.length) return;
    chunks.push(`<blockquote><p>${quoteLines.map((line) => parseInlineMarkdown(line)).join("<br />")}</p></blockquote>`);
    quoteLines.length = 0;
  };

  const flushList = () => {
    if (!listType || !listItems.length) {
      listType = null;
      listItems.length = 0;
      return;
    }
    const items = listItems.map((item) => `<li>${parseInlineMarkdown(item)}</li>`).join("");
    chunks.push(`<${listType}>${items}</${listType}>`);
    listType = null;
    listItems.length = 0;
  };

  const flushCode = () => {
    if (!inCodeBlock) return;
    chunks.push(`<pre><code>${escapeHtml(codeLines.join("\n"))}</code></pre>`);
    inCodeBlock = false;
    codeLines = [];
  };

  for (const sourceLine of lines) {
    const trimmed = sourceLine.trim();

    if (inCodeBlock) {
      if (trimmed.startsWith("```")) {
        flushCode();
      } else {
        codeLines.push(sourceLine);
      }
      continue;
    }

    if (trimmed.startsWith("```")) {
      flushParagraph();
      flushQuote();
      flushList();
      inCodeBlock = true;
      continue;
    }

    if (!trimmed) {
      flushParagraph();
      flushQuote();
      flushList();
      continue;
    }

    const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
    if (headingMatch) {
      flushParagraph();
      flushQuote();
      flushList();
      const headingHashes = headingMatch[1] ?? "";
      const headingText = headingMatch[2] ?? "";
      const level = Math.min(6, Math.max(1, headingHashes.length || 1));
      chunks.push(`<h${level}>${parseInlineMarkdown(headingText)}</h${level}>`);
      continue;
    }

    if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
      flushParagraph();
      flushQuote();
      flushList();
      chunks.push("<hr />");
      continue;
    }

    const quoteMatch = sourceLine.match(/^\s*>\s?(.*)$/);
    if (quoteMatch) {
      flushParagraph();
      flushList();
      quoteLines.push(quoteMatch[1] ?? "");
      continue;
    }
    flushQuote();

    const unorderedMatch = sourceLine.match(/^\s*[-*+]\s+(.+)$/);
    const orderedMatch = sourceLine.match(/^\s*\d+\.\s+(.+)$/);
    if (unorderedMatch || orderedMatch) {
      flushParagraph();
      const nextType: "ul" | "ol" = orderedMatch ? "ol" : "ul";
      if (listType && listType !== nextType) {
        flushList();
      }
      listType = nextType;
      listItems.push((orderedMatch?.[1] ?? unorderedMatch?.[1] ?? "").trim());
      continue;
    }

    flushList();
    paragraphLines.push(sourceLine.trimEnd());
  }

  flushParagraph();
  flushQuote();
  flushList();
  flushCode();

  if (!chunks.length) return "<p></p>";
  return chunks.join("");
}

function normalizeInitialContent(value: string) {
  const input = value.trim();
  if (!input) return "<p></p>";
  if (input.includes("<") && input.includes(">")) return value;

  if (props.markdown) {
    return markdownToHtml(value);
  }

  const blocks = value
    .replaceAll("\r\n", "\n")
    .split(/\n\n+/)
    .map((block) => `<p>${escapeHtml(block).replaceAll("\n", "<br />")}</p>`);

  return blocks.join("");
}

const editor = useEditor({
  extensions: [
    StarterKit.configure({
      history: false,
    }),
    Placeholder.configure({
      placeholder: props.placeholder ?? "Type here...",
      includeChildren: true,
    }),
    Collaboration.configure({
      document: ydoc,
      field: "contact",
    }),
    CollaborationCursor.configure({
      provider,
      user: currentUser,
    }),
  ],
  autofocus: true,
  editorProps: {
    attributes: {
      class: "contact-editor-content",
      spellcheck: "true",
    },
  },
  onCreate: ({ editor: instance }) => {
    if (instance.isEmpty) {
      instance.commands.setContent(normalizeInitialContent(props.modelValue), false);
    }
    isBootstrapped.value = true;
  },
  onUpdate: ({ editor: instance }) => {
    emit("update:modelValue", instance.getHTML());
  },
});

watch(
  () => props.modelValue,
  (incoming) => {
    const instance = editor.value;
    if (!instance || !isBootstrapped.value) return;

    const current = instance.getHTML();
    if (incoming === current || !incoming.trim()) return;

    if (instance.isEmpty) {
      instance.commands.setContent(normalizeInitialContent(incoming), false);
    }
  },
);

const peerCount = computed(() => {
  awarenessVersion.value;
  const states = Array.from(provider.awareness.getStates().values());
  return states.length;
});

const onAwarenessChange = () => {
  awarenessVersion.value += 1;
};

provider.awareness.on("change", onAwarenessChange);

function runCommand(action: () => void) {
  const instance = editor.value;
  if (!instance) return;
  action();
  instance.commands.focus();
}

onBeforeUnmount(() => {
  provider.awareness.off("change", onAwarenessChange);
  editor.value?.destroy();
  provider.destroy();
  ydoc.destroy();
});
</script>

<template>
  <div :class="props.plain ? 'space-y-2' : 'space-y-3'">
    <div :class="props.plain ? 'flex flex-wrap items-center justify-between gap-2 bg-transparent p-0' : 'flex flex-wrap items-center justify-between gap-2 rounded-xl border border-base-300 bg-base-100 p-2'">
      <div class="flex flex-wrap items-center gap-1">
        <button
          class="btn btn-xs"
          :class="editor?.isActive('bold') ? 'btn-primary' : 'btn-ghost'"
          @click="runCommand(() => editor?.chain().focus().toggleBold().run())"
        >
          B
        </button>
        <button
          class="btn btn-xs"
          :class="editor?.isActive('italic') ? 'btn-primary' : 'btn-ghost'"
          @click="runCommand(() => editor?.chain().focus().toggleItalic().run())"
        >
          I
        </button>
        <button
          class="btn btn-xs"
          :class="editor?.isActive('bulletList') ? 'btn-primary' : 'btn-ghost'"
          @click="runCommand(() => editor?.chain().focus().toggleBulletList().run())"
        >
          List
        </button>
        <button
          class="btn btn-xs"
          :class="editor?.isActive('heading', { level: 2 }) ? 'btn-primary' : 'btn-ghost'"
          @click="runCommand(() => editor?.chain().focus().toggleHeading({ level: 2 }).run())"
        >
          H2
        </button>
        <button
          class="btn btn-xs"
          :class="editor?.isActive('blockquote') ? 'btn-primary' : 'btn-ghost'"
          @click="runCommand(() => editor?.chain().focus().toggleBlockquote().run())"
        >
          Quote
        </button>
      </div>

      <p class="px-1 text-xs text-base-content/60">Live: {{ peerCount }}</p>
    </div>

    <div :class="props.plain ? 'bg-transparent p-0' : 'rounded-xl border border-base-300 bg-base-100 p-2'">
      <EditorContent :editor="editor" class="contact-editor min-h-[420px]" />
    </div>
  </div>
</template>

<style scoped>
.contact-editor :deep(.ProseMirror) {
  min-height: 390px;
  padding: 0.75rem;
  outline: none;
  line-height: 1.65;
  color: rgba(17, 24, 39, 0.95);
}

.contact-editor :deep(.ProseMirror p) {
  margin: 0.45rem 0;
}

.contact-editor :deep(.ProseMirror h1),
.contact-editor :deep(.ProseMirror h2),
.contact-editor :deep(.ProseMirror h3) {
  margin: 0.75rem 0 0.45rem;
  font-weight: 700;
  line-height: 1.3;
}

.contact-editor :deep(.ProseMirror ul),
.contact-editor :deep(.ProseMirror ol) {
  margin: 0.45rem 0;
  padding-left: 1.25rem;
}

.contact-editor :deep(.ProseMirror blockquote) {
  margin: 0.6rem 0;
  border-left: 3px solid rgba(30, 107, 255, 0.5);
  padding-left: 0.75rem;
  color: rgba(55, 65, 81, 0.95);
}

.contact-editor :deep(.ProseMirror .collaboration-cursor__caret) {
  margin-left: -1px;
  margin-right: -1px;
  border-left: 1px solid currentColor;
  border-right: 1px solid currentColor;
  pointer-events: none;
  position: relative;
}

.contact-editor :deep(.ProseMirror .collaboration-cursor__label) {
  position: absolute;
  top: -1.35em;
  left: -1px;
  border-radius: 4px;
  padding: 0.1rem 0.4rem;
  color: #fff;
  font-size: 10px;
  font-weight: 600;
  white-space: nowrap;
  line-height: 1.2;
}
</style>
