"use client";

import { type ReactNode, useEffect, useEffectEvent, useRef, useState } from "react";

type QuillRange = {
  index: number;
  length: number;
};

type QuillLikeInstance = {
  clipboard: {
    dangerouslyPasteHTML: (value: string) => void;
  };
  deleteText: (index: number, length: number, source?: string) => void;
  focus: () => void;
  format: (name: string, value: unknown, source?: string) => void;
  formatLine: (index: number, length: number, name: string, value: unknown, source?: string) => void;
  getBounds?: (index: number, length?: number) => { height: number; left: number; top: number; width: number };
  getFormat: (index?: number, length?: number) => Record<string, unknown>;
  getIndex: (blot: QuillBlot) => number;
  getSelection: (focus?: boolean) => QuillRange | null;
  getText: (index?: number, length?: number) => string;
  insertText: (index: number, text: string, source?: string) => void;
  off: (eventName: string, handler: (...args: unknown[]) => void) => void;
  on: (eventName: string, handler: (...args: unknown[]) => void) => void;
  root: HTMLDivElement;
  scroll: {
    find: (node: Node) => QuillBlot | null;
  };
  setSelection: (index: number, length: number, source?: string) => void;
};

type QuillBlot = {
  length?: () => number;
};

type QuillNamespace = {
  new (element: Element, options: unknown): QuillLikeInstance;
};

type BlockStyle = "bullet" | "code" | "heading" | "number" | "quote" | "text";

type InlineMenuPosition = {
  left: number;
  range: QuillRange;
  top: number;
};

type BlockTarget = {
  index: number;
  length: number;
  top: number;
};

type BlockMenuPosition = {
  left: number;
  style: BlockStyle;
  target: BlockTarget;
  top: number;
};

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

function safeLink(value: string) {
  const normalized = value.trim();

  return /^(https?:\/\/|mailto:|\/|#)/i.test(normalized) ? normalized : "";
}

function renderInlineMarkdown(value: string) {
  let result = escapeHtml(value);

  result = result.replace(/`([^`]+)`/g, "<code>$1</code>");
  result = result.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
  result = result.replace(/\*([^*]+)\*/g, "<em>$1</em>");
  result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label: string, href: string) => {
    const safeHref = safeLink(href.replaceAll("&amp;", "&"));
    return safeHref ? `<a href="${escapeHtml(safeHref)}">${label}</a>` : label;
  });

  return result;
}

function isSpecialMarkdownLine(line: string) {
  return (
    /^#{1,3}\s+/.test(line) ||
    /^>\s?/.test(line) ||
    /^[-*]\s+/.test(line) ||
    /^\d+\.\s+/.test(line) ||
    /^```/.test(line) ||
    /^\|/.test(line)
  );
}

function markdownToEditorHtml(markdown: string) {
  const lines = markdown.replace(/\r/g, "").split("\n");
  const blocks: string[] = [];
  let index = 0;

  while (index < lines.length) {
    const line = lines[index].trim();

    if (!line) {
      index += 1;
      continue;
    }

    if (/^```/.test(line)) {
      const codeLines: string[] = [];
      index += 1;

      while (index < lines.length && !/^```/.test(lines[index].trim())) {
        codeLines.push(lines[index]);
        index += 1;
      }

      if (index < lines.length) index += 1;
      blocks.push(`<pre class="ql-syntax" spellcheck="false">${escapeHtml(codeLines.join("\n"))}</pre>`);
      continue;
    }

    const heading = line.match(/^(#{1,3})\s+(.+)$/);
    if (heading) {
      const level = heading[1].length;
      blocks.push(`<h${level}>${renderInlineMarkdown(heading[2])}</h${level}>`);
      index += 1;
      continue;
    }

    if (/^>\s?/.test(line)) {
      const quoteLines: string[] = [];

      while (index < lines.length && /^>\s?/.test(lines[index].trim())) {
        quoteLines.push(lines[index].trim().replace(/^>\s?/, ""));
        index += 1;
      }

      blocks.push(`<blockquote>${quoteLines.map(renderInlineMarkdown).join("<br>")}</blockquote>`);
      continue;
    }

    if (/^[-*]\s+/.test(line)) {
      const items: string[] = [];

      while (index < lines.length && /^[-*]\s+/.test(lines[index].trim())) {
        items.push(lines[index].trim().replace(/^[-*]\s+/, ""));
        index += 1;
      }

      blocks.push(`<ul>${items.map((item) => `<li>${renderInlineMarkdown(item)}</li>`).join("")}</ul>`);
      continue;
    }

    if (/^\d+\.\s+/.test(line)) {
      const items: string[] = [];

      while (index < lines.length && /^\d+\.\s+/.test(lines[index].trim())) {
        items.push(lines[index].trim().replace(/^\d+\.\s+/, ""));
        index += 1;
      }

      blocks.push(`<ol>${items.map((item) => `<li>${renderInlineMarkdown(item)}</li>`).join("")}</ol>`);
      continue;
    }

    if (/^\|/.test(line)) {
      const tableLines: string[] = [];

      while (index < lines.length && /^\|/.test(lines[index].trim())) {
        const current = lines[index].trim();
        if (!/^\|?\s*:?-{3,}/.test(current)) tableLines.push(current);
        index += 1;
      }

      tableLines.forEach((tableLine, rowIndex) => {
        const cells = tableLine.split("|").map((cell) => cell.trim()).filter(Boolean);
        blocks.push(
          `<p>${cells.map((cell) => rowIndex === 0 ? `<strong>${renderInlineMarkdown(cell)}</strong>` : renderInlineMarkdown(cell)).join(" <span class=\"blog-editor-table-divider\">|</span> ")}</p>`,
        );
      });
      continue;
    }

    const paragraphLines = [line];
    index += 1;

    while (index < lines.length) {
      const nextLine = lines[index].trim();
      if (!nextLine || isSpecialMarkdownLine(nextLine)) break;
      paragraphLines.push(nextLine);
      index += 1;
    }

    blocks.push(`<p>${renderInlineMarkdown(paragraphLines.join(" "))}</p>`);
  }

  return blocks.join("") || "<p><br></p>";
}

function inlineMarkdownFromNode(node: Node): string {
  if (node.nodeType === Node.TEXT_NODE) {
    return node.nodeValue ?? "";
  }

  if (!(node instanceof HTMLElement)) {
    return "";
  }

  const content = Array.from(node.childNodes).map(inlineMarkdownFromNode).join("");

  if (node.tagName === "BR") return "\n";
  if (node.tagName === "STRONG" || node.tagName === "B") return `**${content}**`;
  if (node.tagName === "EM" || node.tagName === "I") return `*${content}*`;
  if (node.tagName === "CODE") return `\`${content}\``;

  if (node.tagName === "A") {
    const href = safeLink(node.getAttribute("href") ?? "");
    return href ? `[${content}](${href})` : content;
  }

  return content;
}

function htmlToMarkdown(html: string) {
  const documentBody = new DOMParser().parseFromString(html, "text/html").body;
  const blocks: string[] = [];
  const children = Array.from(documentBody.childNodes);

  for (let index = 0; index < children.length; index += 1) {
    const node = children[index];

    if (node.nodeType === Node.TEXT_NODE && !node.textContent?.trim()) continue;

    if (!(node instanceof HTMLElement)) {
      const text = inlineMarkdownFromNode(node).trim();
      if (text) blocks.push(text);
      continue;
    }

    const inline = inlineMarkdownFromNode(node).trim();

    if (/^H[1-3]$/.test(node.tagName)) {
      blocks.push(`${"#".repeat(Number(node.tagName.slice(1)))} ${inline}`);
      continue;
    }

    if (node.tagName === "BLOCKQUOTE") {
      blocks.push(inline.split("\n").map((line) => `> ${line}`).join("\n"));
      continue;
    }

    if (node.tagName === "PRE" || node.classList.contains("ql-code-block")) {
      const codeLines = [node.textContent ?? ""];

      while (
        children[index + 1] instanceof HTMLElement &&
        (children[index + 1] as HTMLElement).classList.contains("ql-code-block")
      ) {
        index += 1;
        codeLines.push(children[index].textContent ?? "");
      }

      blocks.push(`\`\`\`\n${codeLines.join("\n").trimEnd()}\n\`\`\``);
      continue;
    }

    if (node.tagName === "UL" || node.tagName === "OL") {
      const listItems = Array.from(node.children).filter((child) => child.tagName === "LI");
      const bulletList = node.tagName === "UL" || listItems.some((item) => item.getAttribute("data-list") === "bullet");
      blocks.push(
        listItems
          .map((item, itemIndex) => `${bulletList ? "-" : `${itemIndex + 1}.`} ${inlineMarkdownFromNode(item).trim()}`)
          .join("\n"),
      );
      continue;
    }

    if (inline) blocks.push(inline);
  }

  return blocks.join("\n\n").replace(/\n{3,}/g, "\n\n").trim();
}

function BlockIcon({ type }: { type: BlockStyle }) {
  if (type === "heading") return <span className="text-[12px] font-bold">H2</span>;
  if (type === "bullet") return <span className="text-[15px] font-semibold">•</span>;
  if (type === "number") return <span className="text-[11px] font-bold">1.</span>;
  if (type === "quote") return <span className="text-[16px] font-semibold">“</span>;
  if (type === "code") return <span className="font-mono text-[11px]">&lt;/&gt;</span>;
  return <span className="text-[15px] leading-none">T</span>;
}

const EDITOR_BLOCK_SELECTOR = "p,h1,h2,h3,blockquote,li,.ql-code-block,pre.ql-syntax";

function findEditorBlock(root: HTMLDivElement, target: EventTarget | null) {
  if (!(target instanceof Element)) return null;

  const block = target.closest(EDITOR_BLOCK_SELECTOR);
  return block instanceof HTMLElement && root.contains(block) ? block : null;
}

function fallbackBlockTarget(root: HTMLDivElement, block: HTMLElement) {
  let index = 0;

  for (const candidate of root.querySelectorAll<HTMLElement>(EDITOR_BLOCK_SELECTOR)) {
    const length = Math.max(1, (candidate.innerText || candidate.textContent || "").length + 1);

    if (candidate === block) {
      return { index, length };
    }

    index += length;
  }

  return { index: 0, length: 1 };
}

function resolveBlockTarget(quill: QuillLikeInstance, block: HTMLElement): BlockTarget {
  const fallback = fallbackBlockTarget(quill.root, block);
  let index = fallback.index;
  let length = fallback.length;

  try {
    const blot = quill.scroll.find(block);

    if (blot) {
      index = quill.getIndex(blot);
      const blotLength = blot.length?.();

      if (typeof blotLength === "number" && blotLength > 0) {
        length = blotLength;
      }
    }
  } catch {
    // The DOM can be replaced by Quill while the pointer is moving. The text-based fallback stays usable.
  }

  const rootRect = quill.root.getBoundingClientRect();
  const blockRect = block.getBoundingClientRect();

  return {
    index,
    length: Math.max(1, length),
    top: Math.max(0, blockRect.top - rootRect.top),
  };
}

function blockStyleFromFormat(format: Record<string, unknown>): BlockStyle {
  if (format.header) return "heading";
  if (format.list === "bullet") return "bullet";
  if (format.list === "ordered") return "number";
  if (format.blockquote) return "quote";
  if (format["code-block"]) return "code";
  return "text";
}

function FormatButton({
  children,
  label,
  onClick,
}: {
  children: ReactNode;
  label: string;
  onClick: () => void;
}) {
  return (
    <button
      type="button"
      title={label}
      aria-label={label}
      onMouseDown={(event) => {
        event.preventDefault();
        event.stopPropagation();
      }}
      onClick={onClick}
      className="inline-flex h-8 min-w-8 items-center justify-center rounded-[7px] px-1.5 text-[#526784] transition-colors hover:bg-[#edf4ff] hover:text-[#2f6df6]"
    >
      {children}
    </button>
  );
}

export function NotionBlogContentEditor({
  value,
  onChange,
}: {
  value: string;
  onChange: (value: string) => void;
}) {
  const editorRootRef = useRef<HTMLDivElement | null>(null);
  const quillRef = useRef<QuillLikeInstance | null>(null);
  const onChangeRef = useRef(onChange);
  const lastValueRef = useRef(value);
  const applyingValueRef = useRef(false);
  const activeBlockElementRef = useRef<HTMLElement | null>(null);
  const [editorReady, setEditorReady] = useState(false);
  const [activeBlock, setActiveBlock] = useState<BlockTarget | null>(null);
  const [inlineMenu, setInlineMenu] = useState<InlineMenuPosition | null>(null);
  const [blockMenu, setBlockMenu] = useState<BlockMenuPosition | null>(null);

  function clearActiveBlock() {
    activeBlockElementRef.current?.classList.remove("blog-editor-active-block");
    activeBlockElementRef.current = null;
    setActiveBlock(null);
  }

  function activateBlock(block: HTMLElement) {
    const quill = quillRef.current;

    if (!quill) return null;

    const target = resolveBlockTarget(quill, block);

    if (activeBlockElementRef.current !== block) {
      activeBlockElementRef.current?.classList.remove("blog-editor-active-block");
      block.classList.add("blog-editor-active-block");
      activeBlockElementRef.current = block;
      setActiveBlock(target);
    }

    return target;
  }

  function currentBlockTarget() {
    if (activeBlock) return activeBlock;

    const quill = quillRef.current;

    if (!quill) return null;

    const range = quill.getSelection(true);
    const blocks = Array.from(quill.root.querySelectorAll<HTMLElement>(EDITOR_BLOCK_SELECTOR));
    const matchedBlock = blocks.find((block) => {
      const target = resolveBlockTarget(quill, block);
      return range && range.index >= target.index && range.index < target.index + target.length;
    });

    if (matchedBlock) return activateBlock(matchedBlock);

    return {
      index: range?.index ?? Math.max(0, quill.getText().length - 1),
      length: Math.max(1, range?.length ?? 0),
      top: 0,
    };
  }

  function applyBlockFormat(quill: QuillLikeInstance, target: BlockTarget, style: BlockStyle) {
    quill.formatLine(target.index, target.length, "header", style === "heading" ? 2 : false, "user");
    quill.formatLine(
      target.index,
      target.length,
      "list",
      style === "bullet" ? "bullet" : style === "number" ? "ordered" : false,
      "user",
    );
    quill.formatLine(target.index, target.length, "blockquote", style === "quote", "user");
    quill.formatLine(target.index, target.length, "code-block", style === "code", "user");
  }

  function openBlockMenu(target = currentBlockTarget(), anchor?: { left: number; top: number }) {
    const quill = quillRef.current;

    if (!target || !quill) return;

    const rootBounds = quill.root.getBoundingClientRect();
    const left = anchor?.left ?? rootBounds.left + 46;
    const top = anchor?.top ?? rootBounds.top + target.top + 2;
    setInlineMenu(null);
    setBlockMenu({
      left: Math.max(12, Math.min(window.innerWidth - 242, left)),
      style: blockStyleFromFormat(quill.getFormat(target.index, target.length)),
      target,
      top: Math.max(8, Math.min(window.innerHeight - 404, top)),
    });
  }

  function applyBlock(style: BlockStyle, target = blockMenu?.target ?? currentBlockTarget()) {
    const quill = quillRef.current;

    if (!quill || !target) return;

    quill.setSelection(target.index, Math.max(0, target.length - 1), "silent");
    applyBlockFormat(quill, target, style);
    quill.focus();
    setBlockMenu(null);
    setInlineMenu(null);
    clearActiveBlock();
  }

  function addBlockAfter(style: BlockStyle, target = blockMenu?.target ?? currentBlockTarget()) {
    const quill = quillRef.current;

    if (!quill || !target) return;

    const insertIndex = target.index + target.length;
    const nextTarget = { index: insertIndex, length: 1, top: target.top };
    quill.insertText(insertIndex, "\n", "user");
    applyBlockFormat(quill, nextTarget, style);
    quill.setSelection(insertIndex, 0, "silent");
    quill.focus();
    setBlockMenu(null);
    clearActiveBlock();
  }

  function duplicateBlock(target = blockMenu?.target ?? currentBlockTarget()) {
    const quill = quillRef.current;

    if (!quill || !target) return;

    const copiedText = quill.getText(target.index, target.length);
    quill.insertText(target.index + target.length, copiedText, "user");
    quill.setSelection(target.index + target.length, 0, "silent");
    quill.focus();
    setBlockMenu(null);
    clearActiveBlock();
  }

  function deleteBlock(target = blockMenu?.target ?? currentBlockTarget()) {
    const quill = quillRef.current;

    if (!quill || !target) return;

    quill.deleteText(target.index, target.length, "user");
    quill.setSelection(Math.max(0, target.index - 1), 0, "silent");
    quill.focus();
    setBlockMenu(null);
    clearActiveBlock();
  }

  const handleContextBlockMenu = useEffectEvent((quill: QuillLikeInstance, event: MouseEvent) => {
    const block = findEditorBlock(quill.root, event.target);
    const target = block ? activateBlock(block) : currentBlockTarget();

    if (target) openBlockMenu(target, { left: event.clientX, top: event.clientY });
  });

  useEffect(() => {
    onChangeRef.current = onChange;
  }, [onChange]);

  useEffect(() => {
    const quill = quillRef.current;

    if (!quill || lastValueRef.current === value) return;

    applyingValueRef.current = true;
    quill.clipboard.dangerouslyPasteHTML(markdownToEditorHtml(value));
    lastValueRef.current = value;
    applyingValueRef.current = false;
  }, [value]);

  useEffect(() => {
    let disposed = false;
    let quill: QuillLikeInstance | null = null;
    let handleTextChange: (() => void) | null = null;
    let handleSelectionChange: ((...args: unknown[]) => void) | null = null;
    let handleNativeSelection: (() => void) | null = null;
    let handleContextMenu: ((event: MouseEvent) => void) | null = null;

    const bootEditor = async () => {
      if (!editorRootRef.current) return;

      const { default: ImportedQuill } = await import("quill");

      if (disposed || !editorRootRef.current) return;

      const Quill = ImportedQuill as unknown as QuillNamespace;
      quill = new Quill(editorRootRef.current, {
        formats: ["bold", "italic", "underline", "strike", "link", "header", "list", "blockquote", "code", "code-block"],
        modules: {
          history: { delay: 500, maxStack: 100, userOnly: true },
          toolbar: false,
        },
        placeholder: "여기에 내용을 작성하세요. 텍스트를 선택하면 바로 서식을 바꿀 수 있습니다.",
        theme: "snow",
      });
      quill.root.setAttribute("aria-label", "블로그 본문 편집기");
      quill.root.setAttribute("spellcheck", "true");
      applyingValueRef.current = true;
      quill.clipboard.dangerouslyPasteHTML(markdownToEditorHtml(lastValueRef.current));
      applyingValueRef.current = false;

      handleTextChange = () => {
        if (!quill || applyingValueRef.current) return;
        const nextValue = htmlToMarkdown(quill.root.innerHTML);
        lastValueRef.current = nextValue;
        onChangeRef.current(nextValue);
        clearActiveBlock();
      };
      const showInlineMenu = (range: QuillRange | null) => {
        if (!quill || !range || range.length === 0 || !quill.getBounds) {
          setInlineMenu(null);
          return;
        }

        const bounds = quill.getBounds(range.index, range.length);
        const rootBounds = quill.root.getBoundingClientRect();
        setInlineMenu({
          left: Math.max(12, Math.min(window.innerWidth - 238, rootBounds.left + bounds.left + bounds.width / 2 - 112)),
          range,
          top: Math.max(8, rootBounds.top + bounds.top - 44),
        });
      };
      handleSelectionChange = (...args) => {
        showInlineMenu(args[0] as QuillRange | null);
      };
      handleNativeSelection = () => {
        window.requestAnimationFrame(() => {
          showInlineMenu(quill?.getSelection() ?? null);
        });
      };
      handleContextMenu = (event) => {
        if (!quill) return;
        event.preventDefault();
        handleContextBlockMenu(quill, event);
      };

      quill.on("text-change", handleTextChange);
      quill.on("selection-change", handleSelectionChange);
      quill.root.addEventListener("contextmenu", handleContextMenu);
      quill.root.addEventListener("keyup", handleNativeSelection);
      quill.root.addEventListener("mouseup", handleNativeSelection);
      quillRef.current = quill;
      setEditorReady(true);
    };

    void bootEditor().catch(() => {
      if (!disposed) setEditorReady(false);
    });

    return () => {
      disposed = true;

      if (quill && handleTextChange) quill.off("text-change", handleTextChange);
      if (quill && handleSelectionChange) quill.off("selection-change", handleSelectionChange);
      if (quill && handleContextMenu) quill.root.removeEventListener("contextmenu", handleContextMenu);
      if (quill && handleNativeSelection) quill.root.removeEventListener("keyup", handleNativeSelection);
      if (quill && handleNativeSelection) quill.root.removeEventListener("mouseup", handleNativeSelection);
      activeBlockElementRef.current?.classList.remove("blog-editor-active-block");
      quillRef.current = null;
    };
  }, []);

  useEffect(() => {
    const dismiss = (event?: Event) => {
      const target = event?.target;

      if (target instanceof Element && target.closest(".blog-editor-inline-menu, .blog-editor-block-menu, .blog-editor-block-controls")) {
        return;
      }

      setInlineMenu(null);
      setBlockMenu(null);
    };

    window.addEventListener("scroll", dismiss, true);
    window.addEventListener("resize", dismiss);
    document.addEventListener("mousedown", dismiss);

    return () => {
      window.removeEventListener("scroll", dismiss, true);
      window.removeEventListener("resize", dismiss);
      document.removeEventListener("mousedown", dismiss);
    };
  }, []);

  function selectedRange() {
    return quillRef.current?.getSelection(true) ?? { index: 0, length: 0 };
  }

  function applyInline(name: "bold" | "italic" | "link") {
    const quill = quillRef.current;
    if (!quill) return;

    const range = inlineMenu?.range ?? selectedRange();
    quill.setSelection(range.index, range.length, "silent");

    if (name === "link") {
      const href = window.prompt("연결할 주소를 입력하세요.", "https://");
      const safeHref = href ? safeLink(href) : "";
      if (!safeHref) return;
      quill.format("link", safeHref, "user");
    } else {
      const format = quill.getFormat(range.index, Math.max(1, range.length));
      quill.format(name, !format[name], "user");
    }

    quill.focus();
    setInlineMenu(null);
  }

  function handleEditorMouseMove(event: React.MouseEvent<HTMLDivElement>) {
    const quill = quillRef.current;

    if (!quill) return;
    if (event.target instanceof Element && event.target.closest(".blog-editor-block-controls, .blog-editor-block-menu, .blog-editor-inline-menu")) return;

    const block = findEditorBlock(quill.root, event.target);
    if (block) activateBlock(block);
  }

  function handleEditorPointerDown(event: React.PointerEvent<HTMLDivElement>) {
    const quill = quillRef.current;

    if (!quill) return;
    const block = findEditorBlock(quill.root, event.target);
    if (block) activateBlock(block);
  }

  return (
    <div
      className="official-blog-editor relative min-h-[560px]"
      onMouseLeave={clearActiveBlock}
      onMouseMove={handleEditorMouseMove}
      onPointerDown={handleEditorPointerDown}
    >
      <div className="flex items-center justify-between border-b border-[#edf1f6] px-1 py-2 sm:px-2">
        <div className="flex items-center gap-2 text-[12px] text-[#7a8ba5]">
          <span className="font-semibold text-[#526784]">본문</span>
          <span className="hidden sm:inline">행에 마우스를 올려 블록을 다루거나 텍스트를 선택해 서식을 바꾸세요.</span>
        </div>
        <button type="button" onClick={() => addBlockAfter("text")} className="inline-flex h-8 items-center gap-1.5 rounded-[8px] px-2 text-[12px] font-semibold text-[#61738f] transition-colors hover:bg-[#f2f6fc] hover:text-[#2f6df6]" title="현재 위치 아래에 텍스트 블록 추가">
          <span className="text-[17px] leading-none">+</span> 블록
        </button>
      </div>
      <div ref={editorRootRef} className={editorReady ? "" : "opacity-0"} />

      {activeBlock ? (
        <div className="blog-editor-block-controls blog-editor-block-controls-visible" style={{ top: activeBlock.top + 55 }} onMouseDown={(event) => { event.preventDefault(); event.stopPropagation(); }}>
          <button type="button" onClick={() => addBlockAfter("text", activeBlock)} title="이 블록 아래에 추가" aria-label="이 블록 아래에 추가">
            <span className="text-[18px] leading-none">+</span>
          </button>
          <button type="button" onClick={() => openBlockMenu(activeBlock)} title="블록 메뉴" aria-label="블록 메뉴">
            <span className="blog-editor-grip" aria-hidden="true"><i /><i /><i /><i /><i /><i /></span>
          </button>
        </div>
      ) : null}

      {inlineMenu ? (
        <div className="blog-editor-inline-menu" style={{ left: inlineMenu.left, top: inlineMenu.top }} onMouseDown={(event) => { event.preventDefault(); event.stopPropagation(); }}>
          <FormatButton label="굵게" onClick={() => applyInline("bold")}><strong className="text-[14px]">B</strong></FormatButton>
          <FormatButton label="기울임" onClick={() => applyInline("italic")}><em className="text-[14px]">I</em></FormatButton>
          <FormatButton label="밑줄" onClick={() => { const quill = quillRef.current; if (!quill) return; const range = inlineMenu.range; const format = quill.getFormat(range.index, Math.max(1, range.length)); quill.format("underline", !format.underline, "user"); quill.focus(); }}><span className="text-[14px] underline">U</span></FormatButton>
          <FormatButton label="취소선" onClick={() => { const quill = quillRef.current; if (!quill) return; const range = inlineMenu.range; const format = quill.getFormat(range.index, Math.max(1, range.length)); quill.format("strike", !format.strike, "user"); quill.focus(); }}><span className="text-[14px] line-through">S</span></FormatButton>
          <FormatButton label="링크" onClick={() => applyInline("link")}><span className="text-[14px]">↗</span></FormatButton>
        </div>
      ) : null}

      {blockMenu ? (
        <div className="blog-editor-block-menu" style={{ left: blockMenu.left, top: blockMenu.top }} onMouseDown={(event) => { event.preventDefault(); event.stopPropagation(); }}>
          <p className="px-2 pb-1 pt-1.5 text-[11px] font-semibold text-[#8290a6]">현재 블록</p>
          {([
            ["text", "텍스트", "일반 문단"],
            ["heading", "소제목", "H2 제목"],
            ["bullet", "글머리 목록", "순서 없는 목록"],
            ["number", "번호 목록", "순서 있는 목록"],
            ["quote", "인용", "핵심 안내"],
            ["code", "코드", "설정값 또는 예시"],
          ] as const).map(([style, label, description]) => (
            <button key={style} type="button" onClick={() => applyBlock(style, blockMenu.target)} className={`group flex w-full items-center gap-2.5 rounded-[8px] px-2 py-2 text-left hover:bg-[#f3f7ff] ${blockMenu.style === style ? "bg-[#edf4ff]" : ""}`}>
              <span className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-[7px] bg-[#f4f7fb] text-[#526784] group-hover:bg-white group-hover:text-[#2f6df6]"><BlockIcon type={style} /></span>
              <span className="min-w-0"><span className="block text-[12px] font-semibold text-[#30425f]">{label}</span><span className="block text-[11px] text-[#8491a5]">{description}</span></span>
            </button>
          ))}
          <div className="my-1 border-t border-[#edf1f6]" />
          <button type="button" onClick={() => addBlockAfter("text", blockMenu.target)} className="flex w-full items-center gap-2 rounded-[8px] px-2 py-2 text-left text-[12px] font-semibold text-[#526784] hover:bg-[#f3f7ff] hover:text-[#2f6df6]"><span className="text-[16px] leading-none">+</span> 아래에 블록 추가</button>
          <button type="button" onClick={() => duplicateBlock(blockMenu.target)} className="flex w-full items-center gap-2 rounded-[8px] px-2 py-2 text-left text-[12px] font-semibold text-[#526784] hover:bg-[#f3f7ff] hover:text-[#2f6df6]"><span className="text-[14px] leading-none">□</span> 블록 복제</button>
          <button type="button" onClick={() => deleteBlock(blockMenu.target)} className="flex w-full items-center gap-2 rounded-[8px] px-2 py-2 text-left text-[12px] font-semibold text-[#b24c54] hover:bg-[#fff4f4]"><span className="text-[15px] leading-none">×</span> 블록 삭제</button>
        </div>
      ) : null}
    </div>
  );
}
