'use client';

import { useCallback, useEffect, useMemo, useRef, useState, type MouseEvent as ReactMouseEvent } from "react";
import type { Block, PartialBlock } from "@blocknote/core";
import {
  BlockColorsItem,
  SideMenu,
  TableColumnHeaderItem,
  TableRowHeaderItem,
  useBlockNoteEditor,
  useComponentsContext,
  type DragHandleMenuProps,
  type SideMenuProps,
} from "@blocknote/react";
import { Plus } from "lucide-react";
import { useRouter } from "next/navigation";
import type { CustomBlockSchema } from "../schema";
import { deleteOnlineTable } from "@/lib/online-table";
import { emitAssetsChanged, emitDocumentsChanged } from "@/lib/events";
import { useMoveEmbedPickerStore } from "@/store/move-embed-picker";
import { useCommentsUiStore } from "@/store/comments-ui";
import { createChildDocumentCommand, deleteDocumentCommand } from "@/lib/documents/tree-command-client";

type InlineNode = { text?: unknown };
type TableMenuBlock = Parameters<
  typeof TableRowHeaderItem
>[0]["block"];
type DraftBlock = PartialBlock<CustomBlockSchema> & { id?: string };
type ConvertOption = {
  label: string;
  type?: Block<CustomBlockSchema>["type"];
  props?: Record<string, unknown>;
  shortcut?: string;
  action?: () => void;
};

type CustomDragProps = DragHandleMenuProps<CustomBlockSchema> & {
  currentDocumentId: string;
  workspaceId: string | null;
};

const FourDotHandleIcon = (props: React.SVGProps<SVGSVGElement>) => (
  <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" {...props}>
    <circle cx="6" cy="6" r="1.2" />
    <circle cx="10" cy="6" r="1.2" />
    <circle cx="6" cy="10" r="1.2" />
    <circle cx="10" cy="10" r="1.2" />
  </svg>
);

const extractText = (block: Block<CustomBlockSchema>) => {
  const inlineNodes = block.content as InlineNode[] | undefined;
  const maybeText = inlineNodes?.[0]?.text;
  if (typeof maybeText === "string" && maybeText.trim().length > 0) {
    return maybeText.trim();
  }
  return "未命名页面";
};

const clearMindmapAutosaveCache = (targetDocumentId: string, mindmapId?: string) => {
  if (typeof window === "undefined") return;
  try {
    const prefix = "wolai-mindmap-autosave-";
    const targetPrefix = `${prefix}${targetDocumentId}`;
    const directKey = mindmapId ? `${targetPrefix}:${mindmapId}` : targetPrefix;
    const keys: string[] = [];
    for (let i = 0; i < window.localStorage.length; i += 1) {
      const k = window.localStorage.key(i);
      if (!k) continue;
      if (mindmapId) {
        if (k === directKey) keys.push(k);
      } else if (k === targetPrefix || k.startsWith(`${targetPrefix}:`)) {
        keys.push(k);
      }
    }
    keys.forEach((k) => window.localStorage.removeItem(k));
  } catch {
    // ignore
  }
};

const markMindmapDeleting = (targetDocumentId: string, mindmapId?: string) => {
  if (typeof window === "undefined") return;
  try {
    const w = window as unknown as {
      __wolaiMindmapDeletingKeys?: Set<string>;
    };
    if (!w.__wolaiMindmapDeletingKeys) {
      w.__wolaiMindmapDeletingKeys = new Set<string>();
    }
    const key = mindmapId ? `${targetDocumentId}:${mindmapId}` : targetDocumentId;
    w.__wolaiMindmapDeletingKeys.add(key);
    window.setTimeout(() => {
      try {
        w.__wolaiMindmapDeletingKeys?.delete(key);
      } catch {
        // ignore
      }
    }, 8000);
  } catch {
    // ignore
  }
};

const CustomDragHandleMenu = ({ block, currentDocumentId, workspaceId }: CustomDragProps) => {
  const Components = useComponentsContext()!;
  const editor = useBlockNoteEditor<CustomBlockSchema>();
  const router = useRouter();
  const openPicker = useMoveEmbedPickerStore((s) => s.openPicker);
  const openCommentsForBlock = useCommentsUiStore((s) => s.openForBlock);

  const duplicateBlock = useCallback(() => {
    const blockWithoutId: DraftBlock = { ...block };
    delete blockWithoutId.id;
    editor.insertBlocks([blockWithoutId], block, "after");
  }, [block, editor]);

  const removePageReference = useCallback(async () => {
    if (block.type === "pageReference") {
      const pageId = block.props.pageId;
      if (pageId) {
        await deleteDocumentCommand({
          documentId: pageId,
          workspaceId,
        });
        if (typeof window !== "undefined") {
          emitDocumentsChanged(pageId);
        }
      }
    }
    editor.removeBlocks([block.id]);
    router.refresh();
  }, [block, editor, router, workspaceId]);

  const handleDeleteBlock = useCallback(async () => {
    if (block.type === "pageReference") {
      void removePageReference();
      return;
    }
    if (block.type === "onlineTable") {
      const tableId = block.props.tableId as string | undefined;
      if (tableId) {
        try {
          await deleteOnlineTable(tableId);
        } catch (error) {
          console.error("删除在线表格失败", error);
          window.alert("删除在线表格失败，请稍后重试");
          return;
        }
        if (typeof window !== "undefined") {
          window.dispatchEvent(new CustomEvent("online-table-deleted", { detail: { tableId } }));
          emitAssetsChanged(currentDocumentId);
        }
      }
      editor.removeBlocks([block.id]);
      return;
    }
    if (block.type === "media") {
      const assetId = block.props.assetId as string | undefined;
      if (assetId) {
        const resp = await fetch("/api/media/batch", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ action: "delete", assetIds: [assetId] }),
        });
        if (!resp.ok) {
          const payload = await resp.json().catch(() => ({}));
          window.alert(payload?.error ?? "删除附件失败");
          return;
        }
        emitAssetsChanged(currentDocumentId);
      }
      editor.removeBlocks([block.id]);
      return;
    }
    if (block.type === "mindmap") {
      // 关键：必须先标记“删除中”，避免 MindmapBlock 卸载清理把数据 POST 回去导致“删除后复活”。
      markMindmapDeleting(currentDocumentId, block.id);
      clearMindmapAutosaveCache(currentDocumentId, block.id);
      const resp = await fetch(`/api/mindmap/${currentDocumentId}/${block.id}`, { method: "DELETE" });
      if (!resp.ok) {
        const payload = await resp.json().catch(() => ({}));
        window.alert(payload?.error ?? "删除思维导图失败");
        return;
      }
      emitAssetsChanged(currentDocumentId, undefined, undefined, false, [block.id]);
      // 侧边栏/全局删除监听也会尝试移除对应块，这里做 try/catch 避免重复删除导致报错
      try {
        editor.removeBlocks([block.id]);
      } catch {
        // ignore
      }
      return;
    }
    editor.removeBlocks([block.id]);
  }, [block, currentDocumentId, editor, removePageReference]);

  const turnToPage = useCallback(async () => {
    try {
      const payload = await createChildDocumentCommand({
        parentId: currentDocumentId,
        title: extractText(block),
        blocks: [block],
      });

      editor.replaceBlocks(
        [block.id],
        [
          {
            type: "pageReference",
            props: { pageId: payload.pageId, title: payload.title, asChildPage: true },
          } as PartialBlock<CustomBlockSchema>,
        ],
      );
      router.refresh();
    } catch (error) {
      console.error("块转页面失败", error);
    }
  }, [block, currentDocumentId, editor, router]);

  const handleMoveEmbedPick = useCallback(
    async (mode: "move" | "embed", targetDocumentId: string | null) => {
      if (!targetDocumentId) {
        return;
      }

      if (mode === "embed" && targetDocumentId === currentDocumentId) {
        if (typeof window !== "undefined") {
          window.alert("禁止嵌入到当前页面");
        }
        return;
      }

      const endpoint = mode === "embed" ? "/api/blocks/embed" : "/api/blocks/move";
      const response = await fetch(endpoint, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          sourceDocumentId: currentDocumentId,
          blockId: block.id,
          targetDocumentId,
          position: "end",
        }),
      });

      if (!response.ok) {
        const payload = await response.json().catch(() => ({}));
        const message =
          payload?.error ?? (mode === "embed" ? "嵌入失败，请检查目标页面" : "移动失败，请检查目标页面");
        if (typeof window !== "undefined") {
          window.alert(message);
        }
        return;
      }

      if (mode === "move") {
        // 说明：移动块本体：本地编辑器也要移除该块，避免等待刷新造成错觉。
        try {
          editor.removeBlocks([block.id]);
        } catch {
          // ignore
        }
        router.refresh();
        return;
      }

      if (typeof window !== "undefined") {
        window.alert("已在目标页面末尾插入嵌入引用块");
      }
    },
    [block.id, currentDocumentId, editor, router],
  );

  const moveOrEmbedBlock = useCallback(async () => {
    // 说明：拖拽菜单点击后会立即卸载，必须使用全局 Host 承载弹窗。
    openPicker({
      workspaceId,
      defaultMode: "move",
      modes: ["move", "embed"],
      allowRoot: false,
      excludeIds: [currentDocumentId],
      onPick: handleMoveEmbedPick,
    });
    return;
  }, [currentDocumentId, handleMoveEmbedPick, openPicker, workspaceId]);

  const convertOptions = useMemo<ConvertOption[]>(
    () => [
      { label: "文本", type: "paragraph", shortcut: "Ctrl+Alt+0" },
      { label: "待办列表", type: "checkListItem", shortcut: "Ctrl+Shift+5" },
      { label: "高级待办列表", type: "advancedTodo" },
      { label: "主标题", type: "heading", props: { level: 1 }, shortcut: "Ctrl+Shift+1" },
      { label: "大标题", type: "heading", props: { level: 2 }, shortcut: "Ctrl+Shift+2" },
      { label: "中标题", type: "heading", props: { level: 3 }, shortcut: "Ctrl+Shift+3" },
      { label: "小标题", type: "heading", props: { level: 4 }, shortcut: "Ctrl+Shift+4" },
      { label: "页面", action: turnToPage },
      { label: "列表", type: "bulletListItem", shortcut: "Ctrl+Shift+6" },
      { label: "数字列表", type: "numberedListItem", shortcut: "Ctrl+Shift+7" },
      { label: "折叠列表", type: "toggleListItem", shortcut: "Ctrl+Shift+8" },
      { label: "折叠标题", type: "heading", props: { level: 2, isToggleable: true } },
      { label: "引述文字", type: "quote" },
      { label: "代码片段", type: "codeBlock" },
    ],
    [turnToPage],
  );

  const convertBlock = useCallback(
    (option: ConvertOption) => {
      if (option.action) {
        option.action();
        return;
      }
      if (!option.type) return;
      editor.updateBlock(block as any, {
        type: option.type as any,
        props: (option.props ?? {}) as any,
      } as any);
    },
    [block, editor],
  );

  const copyBlockLink = useCallback(async () => {
    if (typeof window === "undefined") {
      return;
    }
    const url = `${window.location.origin}/documents/${currentDocumentId}#block-${block.id}`;
    try {
      await navigator.clipboard.writeText(url);
      window.alert("块链接已复制");
    } catch {
      window.prompt("复制失败，请手动复制", url);
    }
  }, [block.id, currentDocumentId]);

  const openOnRight = useCallback(() => {
    if (typeof window === "undefined") {
      return;
    }
    const url = `${window.location.origin}/documents/${currentDocumentId}?preview=sidebar&focus=${block.id}`;
    window.open(url, "_blank", "noopener,noreferrer");
  }, [block.id, currentDocumentId]);

  const setAdvancedTodoStatus = useCallback(
    (status: "todo" | "doing" | "done" | "cancelled") => {
      if (block.type !== "advancedTodo") return;
      editor.updateBlock(block, {
        props: { status },
      });
    },
    [block, editor],
  );

  const toggleProgressMode = useCallback(() => {
    if (block.type !== "progressMeter") return;
    editor.updateBlock(block, {
      props: { auto: !block.props.auto },
    });
  }, [block, editor]);

  const setManualProgress = useCallback(() => {
    if (block.type !== "progressMeter") return;
    const value = Number.parseInt(window.prompt("手动设置进度（0-100）", String(block.props.percent ?? 0)) ?? "", 10);
    if (Number.isNaN(value)) return;
    const clamped = Math.min(100, Math.max(0, value));
    editor.updateBlock(block, {
      props: { percent: clamped },
    });
  }, [block, editor]);

  return (
    <Components.Generic.Menu.Dropdown className="bn-menu-dropdown bn-drag-handle-menu">
      <Components.Generic.Menu.Item className="bn-menu-item" onClick={openOnRight}>
        在右侧边栏打开
      </Components.Generic.Menu.Item>

      <Components.Generic.Menu.Root sub>
        <Components.Generic.Menu.Trigger sub>
          <Components.Generic.Menu.Item className="bn-menu-item" subTrigger>
            转换为
          </Components.Generic.Menu.Item>
        </Components.Generic.Menu.Trigger>
        <Components.Generic.Menu.Dropdown sub className="bn-menu-dropdown">
          {convertOptions.map((option) => (
            <Components.Generic.Menu.Item
              key={option.label}
              className="bn-menu-item flex items-center justify-between gap-4"
              onClick={() => convertBlock(option)}
            >
              <span>{option.label}</span>
              {option.shortcut && <span className="text-[10px] text-gray-400">{option.shortcut}</span>}
            </Components.Generic.Menu.Item>
          ))}
        </Components.Generic.Menu.Dropdown>
      </Components.Generic.Menu.Root>

      <Components.Generic.Menu.Item className="bn-menu-item" onClick={duplicateBlock}>
        拷贝副本
      </Components.Generic.Menu.Item>

      <Components.Generic.Menu.Item className="bn-menu-item" onClick={copyBlockLink}>
        复制链接
      </Components.Generic.Menu.Item>

      <Components.Generic.Menu.Item className="bn-menu-item" onClick={moveOrEmbedBlock}>
        移动/嵌入到...
      </Components.Generic.Menu.Item>

      <Components.Generic.Menu.Item
        className="bn-menu-item"
        onClick={() => window.alert("块历史功能开发中，敬请期待")}
      >
        块历史...
      </Components.Generic.Menu.Item>

      <Components.Generic.Menu.Item
        className="bn-menu-item"
        onClick={() => {
          if (!workspaceId) {
            window.alert("缺少 workspaceId，无法打开评论");
            return;
          }
          openCommentsForBlock({ workspaceId, documentId: currentDocumentId, blockId: block.id });
        }}
      >
        评论
      </Components.Generic.Menu.Item>

      <Components.Generic.Menu.Item className="bn-menu-item" onClick={handleDeleteBlock}>
        删除
      </Components.Generic.Menu.Item>

      <BlockColorsItem block={block}>颜色</BlockColorsItem>
      <TableRowHeaderItem block={block as TableMenuBlock}>表头（行）</TableRowHeaderItem>
      <TableColumnHeaderItem block={block as TableMenuBlock}>表头（列）</TableColumnHeaderItem>

      {block.type === "advancedTodo" && (
        <>
          <Components.Generic.Menu.Divider className="bn-menu-divider" />
          {[
            { label: "设为未开始", status: "todo" as const },
            { label: "设为进行中", status: "doing" as const },
            { label: "设为已完成", status: "done" as const },
            { label: "设为取消", status: "cancelled" as const },
          ].map((item) => (
            <Components.Generic.Menu.Item
              key={item.status}
              className="bn-menu-item"
              onClick={() => setAdvancedTodoStatus(item.status)}
            >
              {item.label}
            </Components.Generic.Menu.Item>
          ))}
        </>
      )}

      {block.type === "progressMeter" && (
        <>
          <Components.Generic.Menu.Divider className="bn-menu-divider" />
          <Components.Generic.Menu.Item className="bn-menu-item" onClick={toggleProgressMode}>
            {block.props.auto ? "切换为手动进度" : "切换为自动进度"}
          </Components.Generic.Menu.Item>
          {!block.props.auto && (
            <Components.Generic.Menu.Item className="bn-menu-item" onClick={setManualProgress}>
              手动设置百分比
            </Components.Generic.Menu.Item>
          )}
        </>
      )}
    </Components.Generic.Menu.Dropdown>
  );
};

type CustomSideMenuProps = SideMenuProps<CustomBlockSchema> & {
  currentDocumentId: string;
  workspaceId: string | null;
  unresolvedCommentCountByBlockId?: Record<string, number>;
};

const WolaiDragHandleWithInsert = (props: CustomSideMenuProps) => {
  const Components = useComponentsContext()!;
  const {
    editor,
    block,
    blockDragStart,
    blockDragEnd,
    freezeMenu,
    unfreezeMenu,
    currentDocumentId,
    workspaceId,
    unresolvedCommentCountByBlockId,
  } = props;
  const [insertHovered, setInsertHovered] = useState<null | "top" | "bottom">(null);
  const [menuOpen, setMenuOpen] = useState(false);
  const [hovering, setHovering] = useState(false);
  const [activeCursorBlockId, setActiveCursorBlockId] = useState<string | null>(null);
  const hoverAreaRef = useRef<HTMLDivElement | null>(null);
  const menuFrozenRef = useRef(false);

  // 说明：Wolai 的附件（file）手柄是在行中线左侧居中显示。
  // BlockNote 的 SideMenu 默认参考点更偏“块顶部”，因此这里通过扩大 hover 区域并把手柄定位到附件行中线来对齐。
  const isFileAttachmentRow = block.type === "media" && (block as any)?.props?.assetType === "file";
  const rowHeightPx = isFileAttachmentRow ? 26 : 24;
  const hoverPadPx = 14;
  const lineGapPx = 3;
  const insertBtnSizePx = 16;
  const handleBtnSizePx = 22;
  const unresolvedCount = unresolvedCommentCountByBlockId?.[block.id] ?? 0;

  useEffect(() => {
    const update = () => {
      const cursor = editor.getTextCursorPosition();
      setActiveCursorBlockId(cursor?.block?.id ?? null);
    };
    update();
    return editor.onSelectionChange(update);
  }, [editor]);

  const setFrozen = useCallback(
    (next: boolean) => {
      if (menuFrozenRef.current === next) return;
      menuFrozenRef.current = next;
      if (next) {
        freezeMenu();
      } else {
        unfreezeMenu();
      }
    },
    [freezeMenu, unfreezeMenu],
  );

  useEffect(() => {
    // 说明：Win+Shift+S 截图会触发窗口失焦/可见性变化；若用右键取消，
    // 有些环境下不会触发正常的 mouseleave，导致手柄状态卡死（看起来像“消失”）。
    // 这里在失焦/隐藏时强制解除冻结并重置 hover 状态。
    const reset = () => {
      setHovering(false);
      setMenuOpen(false);
      setInsertHovered(null);
      setFrozen(false);
    };

    const onBlur = () => reset();
    const onVisibility = () => {
      if (document.visibilityState === "hidden") {
        reset();
      }
    };

    window.addEventListener("blur", onBlur);
    document.addEventListener("visibilitychange", onVisibility);
    return () => {
      window.removeEventListener("blur", onBlur);
      document.removeEventListener("visibilitychange", onVisibility);
    };
  }, [setFrozen]);

  const insertParagraph = useCallback(
    (position: "before" | "after") => {
      const inserted = editor.insertBlocks([{ type: "paragraph" } as any], block as any, position as any)?.[0];
      if (inserted) {
        editor.setTextCursorPosition(inserted as any);
        editor.focus();
      }
    },
    [block, editor],
  );

  const stop = (e: ReactMouseEvent) => {
    e.preventDefault();
    e.stopPropagation();
  };

  const paragraphPlainText = useMemo(() => {
    if (block.type !== "paragraph") return null;
    const content = Array.isArray(block.content) ? (block.content as any[]) : [];
    const text = content
      .map((node) =>
        node && typeof (node as { text?: unknown }).text === "string" ? (node as { text: string }).text : ""
      )
      .join("");
    return text;
  }, [block]);

  const isEmptyParagraph = block.type === "paragraph" && (paragraphPlainText ?? "").trim().length === 0;
  const showEmptyPlus = isEmptyParagraph && (activeCursorBlockId === block.id || hovering || menuOpen);

  const openSlashMenuFromEmptyPlus = (e: ReactMouseEvent) => {
    stop(e);
    if (!isEmptyParagraph) return;
    try {
      editor.setTextCursorPosition(block as any, "start");
    } catch {
      // ignore
    }
    editor.focus();
    // 说明：按 Wolai 手感，点击“+”等同于在空行输入 “/” 打开斜杠菜单。
    // deleteTriggerCharacter=true 会把 “/” 写入编辑器，并在选择条目后由插件清理掉。
    editor.openSuggestionMenu("/", { deleteTriggerCharacter: true, ignoreQueryLength: true });
  };

  const forceShowInsertButtons = block.type === "mindmap" || block.type === "onlineTable";
  // 说明：思维导图/在线表格等嵌入块内部可能接管鼠标事件，导致 hover 状态不稳定。
  // 对这些块直接常驻显示插入控件，避免“看不到横杠/加号”。
  const showInsertButtons = !showEmptyPlus && (forceShowInsertButtons || hovering || menuOpen);
  const handleCenterY = hoverPadPx + rowHeightPx / 2;
  // 说明：插入按钮的位置必须“跟着手柄走”，不能依赖容器上下边界。
  // 否则对于思维导图/在线表格等高块，容器可能被撑高，导致按钮跑到块底部。
  // 说明：插入按钮不能与六点手柄发生重叠，否则 hover/click 会被手柄拦截（表现为“看得见但点不到/hover 没反应”）。
  // 这里用“手柄按钮尺寸 + 插入按钮尺寸 + 间距”计算中心距，确保永不重叠。
  const insertDistPx = handleBtnSizePx / 2 + insertBtnSizePx / 2 + lineGapPx;
  const insertBeforeTopPx = handleCenterY - insertDistPx - insertBtnSizePx / 2;
  const insertAfterTopPx = handleCenterY + insertDistPx - insertBtnSizePx / 2;

  return (
    <Components.Generic.Menu.Root
      onOpenChange={(open: boolean) => {
        setMenuOpen(open);
        setFrozen(open || hovering);
      }}
      position={"left"}
    >
      <div
        ref={hoverAreaRef}
        data-testid="wolai-handle-area"
        className="relative w-7 overflow-visible"
        style={{ height: rowHeightPx + hoverPadPx * 2, marginTop: -hoverPadPx }}
        onPointerEnter={() => {
          setHovering(true);
          setFrozen(menuOpen || true);
        }}
        onPointerLeave={() => {
          setHovering(false);
          setInsertHovered(null);
          setFrozen(menuOpen || false);
        }}
      >
        <button
          type="button"
          data-testid="wolai-insert-before"
          title="在上方插入块"
          className={`absolute left-1/2 z-[2147483647] flex h-4 w-4 -translate-x-1/2 items-center justify-center rounded-[4px] text-[#9B9A97] hover:bg-[#EDEDED] hover:text-[#37352F] ${showInsertButtons ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"}`}
          style={{ top: insertBeforeTopPx }}
          onPointerEnter={() => setInsertHovered("top")}
          onPointerLeave={() => {
            queueMicrotask(() => {
              const stillHovering = !!hoverAreaRef.current?.matches?.(":hover");
              if (!stillHovering) setInsertHovered(null);
            });
          }}
          onMouseDown={stop}
          onClick={(e) => {
            stop(e);
            insertParagraph("before");
          }}
        >
          {insertHovered === "top"
            ? <Plus className="h-3.5 w-3.5" />
            : <span className="block h-px w-3 bg-[#CFCFCF]" />}
        </button>

        <div
          className="absolute left-1/2 z-[2147483647] -translate-x-1/2 -translate-y-1/2"
          style={{ top: hoverPadPx + rowHeightPx / 2 }}
        >
          {showEmptyPlus ? (
            <button
              type="button"
              data-testid="wolai-empty-plus"
              aria-label="打开斜杠命令"
              className="flex h-[22px] w-[22px] items-center justify-center rounded-[4px] text-[#9B9A97] hover:bg-[#EDEDED] hover:text-[#37352F]"
              onMouseDown={stop}
              onClick={openSlashMenuFromEmptyPlus}
            >
              <Plus className="h-4 w-4" />
            </button>
          ) : null}
          {!showEmptyPlus ? (
            <Components.Generic.Menu.Trigger>
              <div
                onPointerEnter={() => {
                  setHovering(true);
                  setFrozen(menuOpen || true);
                }}
                onPointerLeave={() => {
                  // 说明：从手柄移到插入按钮时，也会触发手柄的 pointerleave；
                  // 这里用 :hover 兜底，避免插入按钮一闪而过。
                  queueMicrotask(() => {
                    const stillHovering = !!hoverAreaRef.current?.matches?.(":hover");
                    if (!stillHovering && !menuOpen) {
                      setHovering(false);
                      setInsertHovered(null);
                      setFrozen(false);
                    }
                  });
                }}
              >
                <Components.SideMenu.Button
                  label="块操作"
                  draggable={true}
                  onDragStart={(e) => blockDragStart(e, block)}
                  onDragEnd={blockDragEnd}
                  className={`bn-button transition-opacity duration-200 ${showInsertButtons || hovering || menuOpen ? "opacity-100" : "opacity-0"}`}
                  icon={
                    <span className="relative inline-flex">
                      <FourDotHandleIcon className="h-5 w-5" data-test="dragHandle" />
                      {unresolvedCount > 0 ? (
                        <span className="absolute -right-1 -top-1 rounded-full bg-[#2563eb] px-1 text-[10px] font-semibold leading-[14px] text-white">
                          {unresolvedCount > 9 ? "9+" : unresolvedCount}
                        </span>
                      ) : null}
                    </span>
                  }
                />
              </div>
            </Components.Generic.Menu.Trigger>
          ) : null}
        </div>

        <button
          type="button"
          data-testid="wolai-insert-after"
          title="在下方插入块"
          className={`absolute left-1/2 z-[2147483647] flex h-4 w-4 -translate-x-1/2 items-center justify-center rounded-[4px] text-[#9B9A97] hover:bg-[#EDEDED] hover:text-[#37352F] ${showInsertButtons ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"}`}
          style={{ top: insertAfterTopPx }}
          onPointerEnter={() => setInsertHovered("bottom")}
          onPointerLeave={() => {
            queueMicrotask(() => {
              const stillHovering = !!hoverAreaRef.current?.matches?.(":hover");
              if (!stillHovering) setInsertHovered(null);
            });
          }}
          onMouseDown={stop}
          onClick={(e) => {
            stop(e);
            insertParagraph("after");
          }}
        >
          {insertHovered === "bottom"
            ? <Plus className="h-3.5 w-3.5" />
            : <span className="block h-px w-3 bg-[#CFCFCF]" />}
        </button>
      </div>

      <CustomDragHandleMenu block={block} currentDocumentId={currentDocumentId} workspaceId={workspaceId} />
    </Components.Generic.Menu.Root>
  );
};

export const CustomSideMenu = (props: CustomSideMenuProps) => (
  <SideMenu {...props}>
    <WolaiDragHandleWithInsert {...props} />
  </SideMenu>
);
