"use client";

import "simple-mind-map/dist/simpleMindMap.esm.css";
import React, {
  useCallback,
  useEffect,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { createPortal } from "react-dom";
import { createReactBlockSpec } from "@blocknote/react";
import type {
  BlockConfig,
  BlockNoteEditor,
  DefaultInlineContentSchema,
  DefaultStyleSchema,
  PropSchema,
  SpecificBlock,
} from "@blocknote/core";
import type { CustomBlockSchema } from "../schema";
import { MindmapToolbar } from "./MindmapToolbar";
import { MindmapSidebar } from "./MindmapSidebar";
import { MindmapSidebarTrigger } from "./MindmapSidebarTrigger";
import { MindmapNavigator } from "./MindmapNavigator";
import { MindmapMiniMap } from "./MindmapMiniMap";
import { MindmapCount } from "./MindmapCount";
import { MindmapContextMenu } from "./MindmapContextMenu";
import type { SidebarPanel } from "./mindmapSidebarConfig";
import type { MindMapNode } from "./mindmapTypes";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight } from "lucide-react";       
import iconConfig from "./mindmapIconConfig";
import { emitAssetsChanged } from "@/lib/events";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { api } from "@/lib/convex/api";
import { useQuery } from "convex/react";
import {
  buildMindmapProjection,
  canonicalizeMindmapData,
  defaultMindmapData,
  extractMindmapTitle,
  type MindMapData,
  type MindmapProjection,
  type MindmapRouteMeta,
} from "@/lib/mindmap/mindmap-projection";

// 避免 SSR/Node 环境提前评估 simple-mind-map 内部依赖 document
const loadIconModules = async () => {
  const { nodeIconList } = await import("simple-mind-map/src/svg/icons.js");    
  const { mergerIconList } = await import("simple-mind-map/src/utils/index.js");
  return { nodeIconList, mergerIconList };
};

// 安全获取图片尺寸
const getImageSizeSafe = (url: string): Promise<{ width: number; height: number } | null> =>
  new Promise((resolve) => {
    if (!url) return resolve(null);
    const img = new Image();
    img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight });
    img.onerror = () => resolve(null);
    img.src = url;
  });

// 类型检查辅助函数
const isRecord = (v: unknown): v is Record<string, unknown> =>
  typeof v === "object" && v !== null && !Array.isArray(v);

const readInitialRequestMeta = (meta: unknown): { requestId: string; traceId: string } | null => {
  if (!isRecord(meta)) return null;
  const requestId = typeof meta.requestId === "string" ? meta.requestId.trim() : "";
  const traceId = typeof meta.traceId === "string" ? meta.traceId.trim() : "";
  if (!requestId || !traceId) return null;
  return { requestId, traceId };
};

const readInitialWorkspaceId = (meta: unknown): string => {
  if (!isRecord(meta)) return "";
  return typeof meta.workspaceId === "string" ? meta.workspaceId.trim() : "";
};

type MindMapInstance = {
  execCommand: (command: string, ...args: unknown[]) => void;
  destroy: () => void;
  setData: (data: unknown) => void;
  getData: (withConfig?: boolean) => unknown;
  on?: <TArgs extends unknown[]>(event: string, handler: (...args: TArgs) => void) => void;
  emit?: (event: string, ...args: unknown[]) => void;
  off?: <TArgs extends unknown[]>(event: string, handler: (...args: TArgs) => void) => void;
  setMode?: (mode: string) => void;
  command: { clearHistory: () => void };
  editNodeClassList?: string[];
  view: {
    fit: () => void;
    scale: number;
    enlarge: () => void;
    narrow: () => void;
    setScale: (scale: number, cx: number, cy: number) => void;
  };
  renderer: {
    activeNodeList: unknown[];
    lastActiveNodeList?: unknown[];
    root?: unknown;
    renderTree: { _node: unknown };
    setRootNodeCenter?: () => void;
    clearActiveNodeList?: () => void;
    addNodeToActiveList?: (node: unknown, isActive?: boolean) => void;
    emitNodeActiveEvent?: (node: unknown) => void;
    findNodeByUid?: (uid: string) => unknown;
    copy?: () => void;
    paste?: () => void;
    beingCopyData?: unknown;
  };
  painter?: { startPainter: () => void };
  doExport?: { export: (type: string, isDownload?: boolean, name?: string) => Promise<unknown> };
  width: number;
  height: number;
  miniMap?: unknown;
  getThemeConfig: (key: string) => unknown;
  setThemeConfig: (config: unknown) => void;
  setTheme: (theme: string) => void;
  setLayout: (layout: string) => void;
};

type MindmapDocumentBridge = {
  updateBlockData?: (data: unknown) => void;
  removeBlock?: () => void;
};

type MindmapPreviewCardProps = {
  docId?: string;
  mindmapId: string;
  title: string;
  projection: MindmapProjection | null;
  onOpenInline: () => void;
  onOpenFullscreen: () => void;
  onOpenStandalone: () => void;
};

type MindmapSurfaceViewProps = {
  docId?: string;
  mindmapId: string;
  initialData?: unknown;
  initialProjection?: MindmapProjection | null;
  standalone?: boolean;
  fullscreen?: boolean;
  onExitFullscreen?: () => void;
  documentBridge?: MindmapDocumentBridge | null;
};

const MindmapPreviewCard = ({
  docId,
  mindmapId,
  title,
  projection,
  onOpenInline,
  onOpenFullscreen,
  onOpenStandalone,
}: MindmapPreviewCardProps) => {
  const previewNodes = projection?.nodes.slice(0, 6) ?? [];

  return (
    <div
      data-testid="mindmap-preview-card"
      data-document-id={docId || undefined}
      data-mindmap-id={mindmapId}
      className="not-prose my-4 w-full overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm"
    >
      <div className="border-b border-slate-200 bg-gradient-to-r from-slate-50 via-white to-emerald-50/60 px-4 py-4">
        <div className="flex flex-wrap items-start justify-between gap-3">
          <div className="min-w-0">
            <div className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">
              文档内嵌导图
            </div>
            <div className="mt-1 truncate text-base font-semibold text-slate-900">{title}</div>
            <div className="mt-1 text-xs text-slate-500">
              默认只加载子树投影预览，进入编辑或沉浸态后再挂载导图内核
            </div>
          </div>
          <div className="flex flex-wrap items-center gap-2">
            <button
              type="button"
              className="rounded-full border border-slate-900 bg-slate-900 px-3 py-1.5 text-xs font-medium text-white transition hover:bg-slate-800"
              onClick={onOpenInline}
            >
              进入编辑
            </button>
            <button
              type="button"
              className="rounded-full border border-slate-300 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 transition hover:border-slate-400 hover:bg-slate-50"
              onClick={onOpenFullscreen}
            >
              沉浸展开
            </button>
            <button
              type="button"
              className="rounded-full border border-slate-300 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 transition hover:border-slate-400 hover:bg-slate-50"
              onClick={onOpenStandalone}
            >
              打开独立页
            </button>
          </div>
        </div>
        <div className="mt-3 flex flex-wrap items-center gap-2 text-xs text-slate-500">
          <span className="rounded-full bg-slate-100 px-2.5 py-1">
            {projection?.nodeCount ?? 1} 个节点
          </span>
          <span className="rounded-full bg-slate-100 px-2.5 py-1">
            {projection?.projection ?? "mindmap_subtree"}
          </span>
          <span className="rounded-full bg-slate-100 px-2.5 py-1">按需挂载编辑器</span>
        </div>
      </div>
      <div className="space-y-3 bg-[linear-gradient(180deg,#ffffff_0%,#f8fafc_100%)] px-4 py-4">
        {previewNodes.length > 0 ? (
          previewNodes.map((node) => (
            <div
              key={`${node.uid}:${node.depth}`}
              className="rounded-2xl border border-slate-200/80 bg-white/85 px-3 py-2 shadow-sm"
              style={{ marginLeft: `${node.depth * 14}px` }}
            >
              <div className="truncate text-sm font-medium text-slate-800">{node.text}</div>
              <div className="mt-1 text-xs text-slate-500">子节点 {node.childCount}</div>
            </div>
          ))
        ) : (
          <div className="rounded-2xl border border-dashed border-slate-200 bg-white/80 px-4 py-6 text-sm text-slate-500">
            当前还没有可预览的子树摘要，进入编辑态后可继续修改。
          </div>
        )}
      </div>
    </div>
  );
};

// 将思维导图数据中的 asset:id 格式转换为实际的签名 URL
// 返回转换后的数据和 signed URL -> asset ID 的映射
const resolveAssetUrls = async (data: MindMapData): Promise<{ data: MindMapData; urlToAssetId: Map<string, string> }> => {
  const assetIds: string[] = [];

  // 递归收集所有 asset:id
  const collectAssetIds = (node: any) => {
    if (!node) return;
    const candidates: unknown[] = [
      node?.data?.image,
      node?.image,
      node?.image?.url,
      node?.data?.image?.url,
    ];
    candidates.forEach((value) => {
      if (typeof value !== "string") return;
      if (!value.startsWith("asset:")) return;
      const id = value.replace(/^asset:/, "").trim();
      if (id) assetIds.push(id);
    });
    if (Array.isArray(node.children)) {
      node.children.forEach(collectAssetIds);
    }
  };
  collectAssetIds(data);

  // 如果没有 asset:id，直接返回原数据和空映射
  if (assetIds.length === 0) return { data, urlToAssetId: new Map() };

  // 批量获取签名 URL
  const urlMap = new Map<string, string>();
  await Promise.all(assetIds.map(async (id) => {
    try {
      const response = await fetch(`/api/media/sign?assetId=${id}`);
      if (response.ok) {
        const result = await response.json();
        urlMap.set(id, result.signedUrl);
      }
    } catch {}
  }));

  // 递归替换 asset:id 为签名 URL，并建立反向映射
  const urlToAssetId = new Map<string, string>();
  const replaceAssetIds = (node: any): any => {
    if (!node) return node;
    const newNode = { ...node };

    const replaceAssetString = (value: unknown): string | null => {
      if (typeof value !== "string") return null;
      if (!value.startsWith("asset:")) return null;
      const id = value.replace(/^asset:/, "").trim();
      if (!id) return null;
      const signedUrl = urlMap.get(id);
      if (signedUrl) {
        urlToAssetId.set(signedUrl, id);
        return signedUrl;
      }
      // 即使签名失败，也保留 asset:id -> id 的映射，方便后续删除/撤销逻辑使用
      urlToAssetId.set(`asset:${id}`, id);
      return `asset:${id}`;
    };

    // 常见：node.data.image = "asset:xxx"
    const nextDataImage = replaceAssetString(newNode?.data?.image);
    if (nextDataImage && newNode.data && typeof newNode.data === "object") {
      newNode.data = { ...newNode.data, image: nextDataImage };
    }

    // 兼容：node.image = "asset:xxx"
    if (typeof newNode.image === "string") {
      const nextImage = replaceAssetString(newNode.image);
      if (nextImage) newNode.image = nextImage;
    }

    // 兼容：node.image.url = "asset:xxx"
    const nextImageUrl = replaceAssetString(newNode?.image?.url);
    if (nextImageUrl && newNode.image && typeof newNode.image === "object") {
      newNode.image = { ...newNode.image, url: nextImageUrl };
    }

    // 兼容：node.data.image.url = "asset:xxx"
    const nextDataImageUrl = replaceAssetString(newNode?.data?.image?.url);
    if (nextDataImageUrl && newNode.data && typeof newNode.data === "object") {
      const dataImage = (newNode.data as any).image;
      if (dataImage && typeof dataImage === "object") {
        (newNode.data as any).image = { ...(dataImage as any), url: nextDataImageUrl };
      }
    }

    if (Array.isArray(newNode.children)) {
      newNode.children = newNode.children.map(replaceAssetIds);
    }
    return newNode;
  };

  return { data: replaceAssetIds(data), urlToAssetId };
};

// 将思维导图数据中的签名 URL 转换回 asset:id 格式（用于保存）
const revertToAssetIds = (data: MindMapData, urlToAssetId: Map<string, string>): MindMapData => {
  const revertNode = (node: any): any => {
    if (!node) return node;
    const newNode = { ...node };

    const revertSigned = (value: unknown): string | null => {
      if (typeof value !== "string") return null;
      const assetId = urlToAssetId.get(value);
      if (!assetId) return null;
      return `asset:${assetId}`;
    };

    // 常见：node.data.image = signedUrl
    const nextDataImage = revertSigned(newNode?.data?.image);
    if (nextDataImage && newNode.data && typeof newNode.data === "object") {
      newNode.data = { ...newNode.data, image: nextDataImage };
    }

    // 兼容：node.image = signedUrl
    if (typeof newNode.image === "string") {
      const nextImage = revertSigned(newNode.image);
      if (nextImage) newNode.image = nextImage;
    }

    // 兼容：node.image.url = signedUrl
    const nextImageUrl = revertSigned(newNode?.image?.url);
    if (nextImageUrl && newNode.image && typeof newNode.image === "object") {
      newNode.image = { ...newNode.image, url: nextImageUrl };
    }

    // 兼容：node.data.image.url = signedUrl
    const nextDataImageUrl = revertSigned(newNode?.data?.image?.url);
    if (nextDataImageUrl && newNode.data && typeof newNode.data === "object") {
      const dataImage = (newNode.data as any).image;
      if (dataImage && typeof dataImage === "object") {
        (newNode.data as any).image = { ...(dataImage as any), url: nextDataImageUrl };
      }
    }

    if (Array.isArray(newNode.children)) {
      newNode.children = newNode.children.map(revertNode);
    }
    return newNode;
  };
  return revertNode(data);
};

const STORAGE_PREFIX = "wolai-mindmap-autosave-";

function downloadJson(data: unknown, name: string) {
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
  const url = URL.createObjectURL(blob);
  const anchor = document.createElement("a");
  anchor.href = url;
  anchor.download = `${name}.json`;
  anchor.click();
  URL.revokeObjectURL(url);
}

const createSimplePrompt = (title: string, placeholder = "") => {
  const value = window.prompt(title, placeholder);
  if (!value || !value.trim()) return null;
  return value.trim();
};

// 修补 svg.js rbox 在节点未挂载时抛出的异常
const patchSvgRbox = async () => {
  // 仅在浏览器环境生效
  if (typeof window === "undefined") return;
  const svgModule = await import("@svgdotjs/svg.js");
  type SvgCtorPrototype = {
    rbox?: (ref?: unknown) => unknown;
    __wolaiRboxPatched?: boolean;
  };
  type SvgCtor = { prototype?: SvgCtorPrototype } | undefined;
  const svgModuleTyped = svgModule as unknown as { Element?: SvgCtor; G?: SvgCtor };
  const candidates = [svgModuleTyped.Element, svgModuleTyped.G];
  let warned = false;
  candidates.forEach((Ctor) => {
    if (!Ctor?.prototype) return;
    if (Ctor.prototype.__wolaiRboxPatched) return;
    const original = Ctor.prototype.rbox;
    if (typeof original !== "function") return;
    Ctor.prototype.rbox = function patchedRbox(ref?: unknown) {
      try {
        return original.call(this, ref);
      } catch (error) {
        // 退化到 DOM 的 getBoundingClientRect 计算，避免初次渲染时出现 (0,0)
        type SvgElementLike = { node?: Element | null; el?: Element | null };
        const maybeThis = this as unknown as SvgElementLike;
        const el = maybeThis?.node ?? maybeThis?.el ?? null;
        const rect =
          el && typeof el.getBoundingClientRect === "function"
            ? el.getBoundingClientRect()
            : null;
        const viewportWidth = typeof window !== "undefined" ? window.innerWidth : 0;
        const viewportHeight =
          typeof window !== "undefined" ? window.innerHeight : 0;
        const defaultWidth = Math.max(120, Math.floor(viewportWidth * 0.2));
        const defaultHeight = Math.max(60, Math.floor(viewportHeight * 0.1));
        const fallback = rect && rect.width && rect.height
          ? {
              x: rect.x,
              y: rect.y,
              width: rect.width,
              height: rect.height,
              x2: rect.x + rect.width,
              y2: rect.y + rect.height,
              cx: rect.x + rect.width / 2,
              cy: rect.y + rect.height / 2,
            }
          : {
              x: Math.max(0, (viewportWidth - defaultWidth) / 2),
              y: Math.max(0, (viewportHeight - defaultHeight) / 2),
              width: defaultWidth,
              height: defaultHeight,
              x2: Math.max(0, (viewportWidth + defaultWidth) / 2),
              y2: Math.max(0, (viewportHeight + defaultHeight) / 2),
              cx: Math.max(0, viewportWidth / 2),
              cy: Math.max(0, viewportHeight / 2),
            };
        if (!warned) warned = true;
        return fallback;
      }
    };
    Ctor.prototype.__wolaiRboxPatched = true;
  });
};

const applyToActiveNodes = (
  mindmap: MindMapInstance | null,
  handler: (node: unknown) => void,
) => {
  if (!mindmap) {
    window.alert("思维导图尚未初始化");
    return;
  }
  const renderer = mindmap.renderer;
  const list = renderer?.activeNodeList;
  if (!list || list.length === 0) {
    // 优先尝试 lastActiveNodeList（部分操作会短暂清空 activeNodeList）
    const lastActive = renderer?.lastActiveNodeList?.[0];
    const root = renderer?.root ?? renderer?.renderTree?._node;
    const fallback = lastActive ?? root;
    if (fallback) {
      renderer?.addNodeToActiveList?.(fallback);
      mindmap.execCommand?.("SET_NODE_ACTIVE", fallback, true);
    } else {
      window.alert("请选择至少一个节点再执行该操作");
      return;
    }
  }
  (renderer?.activeNodeList ?? []).forEach(handler);
};

// 确保存在激活节点后再执行命令，若无则自动选中根节点
const ensureActiveBefore = (mindmap?: MindMapInstance | null) => {
  const mm = mindmap ?? null;
  if (!mm) {
    window.alert("思维导图尚未初始化");
    return null;
  }
  const renderer = mm.renderer;
  const list = renderer?.activeNodeList ?? [];
  if (!list || list.length === 0) {
    const lastActive = renderer?.lastActiveNodeList?.[0];
    const root = renderer?.root ?? renderer?.renderTree?._node;
    const fallback = lastActive ?? root;
    if (fallback) {
      // 主动维护激活列表，避免首次插入子节点时因无激活节点导致插入位置错误
      // 先确保根节点居中，避免后续插入节点跑到 (0,0)
      // 注意：setRootNodeCenter 内部依赖 renderer.root（而不是 renderTree._node）。
      // 在“刚初始化/刚插入第二张导图”的窗口期里 renderer.root 可能仍是 null，
      // 此时调用会触发内部解构异常，导致快捷键/工具栏命令中断。
      if (renderer?.root && renderer?.setRootNodeCenter) {
        try {
          renderer.setRootNodeCenter();
        } catch {
          // ignore
        }
      }
      mm.view?.fit?.();
      if (renderer?.clearActiveNodeList) renderer.clearActiveNodeList();
      if (renderer?.addNodeToActiveList) renderer.addNodeToActiveList(fallback, true);
      if (renderer?.emitNodeActiveEvent) renderer.emitNodeActiveEvent(fallback);
      mm.execCommand?.("SET_NODE_ACTIVE", fallback, true);
    } else {
      window.alert("请先选中一个节点");
      return null;
    }
  }
  return mm;
};

const MindmapSurfaceView = ({
  docId: docIdProp = "",
  mindmapId,
  initialData,
  initialProjection = null,
  standalone = false,
  fullscreen = false,
  onExitFullscreen,
  documentBridge = null,
}: MindmapSurfaceViewProps) => {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const fileInputRef = useRef<HTMLInputElement | null>(null);
  const [mindmap, setMindmap] = useState<MindMapInstance | null>(null);   
  const mindmapReadyRef = useRef(false);
  const mindmapRef = useRef<MindMapInstance | null>(null);
  const hasLocalEditsRef = useRef(false);
  const lastLocalEditAtRef = useRef(0);
  const markLocalEdited = useCallback(() => {
    hasLocalEditsRef.current = true;
    lastLocalEditAtRef.current = Date.now();
  }, []);
  const applyingRemoteRef = useRef(false);
  const [canBack, setCanBack] = useState(false);
  const [canForward, setCanForward] = useState(false);
  const [activeNodes, setActiveNodes] = useState<MindMapNode[]>([]);
  const [painterMode, setPainterMode] = useState(false);
  const [showMiniMap, setShowMiniMap] = useState(false);
  const [activeSidebar, setActiveSidebar] = useState<SidebarPanel | null>(null);
  const [showNoteModal, setShowNoteModal] = useState(false);
  const [noteContent, setNoteContent] = useState("");
  const [localFullscreen, setLocalFullscreen] = useState(false);
  const [showEmbedToolbar, setShowEmbedToolbar] = useState(false);
  const effectiveFullscreen = fullscreen || localFullscreen;
  const wrapperRef = useRef<HTMLDivElement | null>(null);
  const hotkeyScopeRef = useRef(false);
  const lastInteractionAtRef = useRef(0);
  const skipNextPasteRef = useRef(false);
  const persistDataRef = useRef<((data: unknown) => void) | null>(null);
  const recentNodeDblclickRef = useRef(false);
  const deletingRef = useRef(false);
  const textEditOpenRef = useRef(false);
  const suppressTextEditRefocusUntilRef = useRef(0);
  const lastTextEditRefocusAtRef = useRef(0);
  const pendingInitActivateRootRef = useRef(false);
  const pendingRenderEndActivateRootRef = useRef(false);
  // 存储 signed URL 到 asset:id 的映射，用于保存时转换回 asset:id 格式
  const signedUrlToAssetIdRef = useRef(new Map<string, string>());
  // 存储当前思维导图中使用的所有图片 URL，用于检测图片删除
  const currentImageUrlsRef = useRef(new Set<string>());
  // 存储已删除的 asset ID，用于撤销时恢复
  const deletedAssetIdsRef = useRef(new Set<string>());

  const getRootNode = useCallback((mm?: MindMapInstance | null) => {
    const instance = mm ?? mindmap;
    return instance?.renderer?.root ?? instance?.renderer?.renderTree?._node ?? null;
  }, [mindmap]);

  const docId = useMemo(() => docIdProp || "", [docIdProp]);
  const pageId = docId;
  const attachmentId = mindmapId;
  const projectionData = initialProjection?.data ?? null;
  const [requestMeta, setRequestMeta] = useState<{ requestId: string; traceId: string } | null>(
    () => readInitialRequestMeta(initialProjection?.meta),
  );
  const [titleText, setTitleText] = useState(() =>
    initialProjection?.title ?? extractMindmapTitle(projectionData ?? initialData ?? defaultMindmapData),
  );

  const syncTitleText = useCallback((data: unknown) => {
    setTitleText(extractMindmapTitle(data));
  }, []);

  const syncMindmapRouteMeta = useCallback((meta: unknown) => {
    if (!isRecord(meta)) return;
    const requestId = typeof meta.requestId === "string" ? meta.requestId.trim() : "";
    const traceId = typeof meta.traceId === "string" ? meta.traceId.trim() : "";
    if (requestId && traceId) {
      setRequestMeta((prev) =>
        prev?.requestId === requestId && prev?.traceId === traceId
          ? prev
          : { requestId, traceId },
      );
    }
    const nextWorkspaceId = typeof meta.workspaceId === "string" ? meta.workspaceId.trim() : "";
    if (nextWorkspaceId) {
      setWorkspaceId((prev) => (prev === nextWorkspaceId ? prev : nextWorkspaceId));
    }
  }, []);

  // 获取 workspaceId（用于上传图片）
  useEffect(() => {
    let cancelled = false;
    if (!docId) return;
    void (async () => {
      try {
        // 说明：仅用于拿 workspaceId（上传图片需要），避免拉取整套 AI 资产列表导致打开页面变慢。
        const res = await fetch(
          `/api/mindmap-ai/assets?documentId=${encodeURIComponent(docId)}&workspaceOnly=1`,
        );
        const json: unknown = await res.json().catch(() => null);
        if (!res.ok) return;
        if (cancelled) return;
        const obj = isRecord(json) ? json : {};
        setWorkspaceId(String(obj.workspaceId ?? ""));
      } catch {
        // ignore
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [docId]);

  useEffect(() => {
    hasLocalEditsRef.current = false;
    lastLocalEditAtRef.current = 0;
    applyingRemoteRef.current = false;
  }, [docId]);

  const autosaveKey = useMemo(() => {
    if (docId) return `${STORAGE_PREFIX}${docId}:${mindmapId}`;
    return `${STORAGE_PREFIX}${mindmapId}`;
  }, [docId, mindmapId]);

  // 记录本端最近一次成功写入到后端的 updated_at，用于避免 Convex 订阅回放覆盖（清空历史/打断编辑）。
  const lastLocalSavedAtRef = useRef<string | null>(null);
  const lastAppliedRemoteUpdatedAtRef = useRef<string | null>(null);

  const remoteMindmap = useQuery(
    api.mindmaps.get,
    isConvexEnabled() && docId ? { docId, mindmapId } : "skip",
  );
  const initialDataRef = useRef<unknown>(null);
  if (initialDataRef.current === null) {
    const cached = typeof window !== "undefined" ? window.localStorage.getItem(autosaveKey) : null;
    if (cached) {
      try {
        initialDataRef.current = canonicalizeMindmapData(JSON.parse(cached));
      } catch {
        initialDataRef.current = canonicalizeMindmapData(projectionData ?? initialData ?? defaultMindmapData);
      }
    } else {
      initialDataRef.current = canonicalizeMindmapData(projectionData ?? initialData ?? defaultMindmapData);
    }
  }

  useEffect(() => {
    syncTitleText(initialDataRef.current ?? projectionData ?? initialData ?? defaultMindmapData);
  }, [autosaveKey, initialData, projectionData, syncTitleText]);

  useEffect(() => {
    if (!initialProjection?.meta) return;
    syncMindmapRouteMeta(initialProjection.meta);
  }, [initialProjection, syncMindmapRouteMeta]);

  // 优先加载本地文件，其次 Supabase（通过后端 API）
  useEffect(() => {
    let cancelled = false;
    if (!docId) return;
    (async () => {
      try {
        // 多导图：按 docId + mindmapId 拉取
        const resp = await fetch(`/api/mindmap/${docId}/${mindmapId}`);
        if (!resp.ok) return;
        const payload = await resp.json().catch(() => null);
        const data = payload?.data;
        if (!data || cancelled) return;
        syncMindmapRouteMeta(payload?.meta);
        // 若用户已在本地做过编辑，则不再用远端数据覆盖，避免“插入节点后又被重置”
        if (hasLocalEditsRef.current) return;
        initialDataRef.current = canonicalizeMindmapData(data);
        syncTitleText(initialDataRef.current);
        if (mindmap) {
          applyingRemoteRef.current = true;
          try {
            mindmap.setData(initialDataRef.current);
            mindmap.command.clearHistory();
          } finally {
            window.setTimeout(() => {
              applyingRemoteRef.current = false;
            }, 0);
          }
        }
      } catch {}
    })();
    return () => {
      cancelled = true;
    };
  }, [docId, mindmap, mindmapId, syncMindmapRouteMeta, syncTitleText]);

  // Convex 实时订阅：当其它客户端更新/删除导图时，同步到当前实例。
  useEffect(() => {
    if (!remoteMindmap || typeof remoteMindmap !== "object") return;

    const meta = (remoteMindmap as any).meta as Record<string, unknown> | undefined;
    const deletedAt = typeof meta?.deleted_at === "string" ? (meta.deleted_at as string) : null;
    const updatedAt = typeof meta?.updated_at === "string" ? (meta.updated_at as string) : null;

    if (deletedAt) {
      if (!docId || deletingRef.current) return;
      deletingRef.current = true;
      try {
        window.localStorage.removeItem(autosaveKey);
      } catch {
        // ignore
      }

      // 全屏页：退出到文档页
      if (effectiveFullscreen && typeof onExitFullscreen === "function") {
        window.alert("该思维导图已被删除（已移入垃圾桶），将返回页面。");
        onExitFullscreen();
        return;
      }

      // 嵌入编辑器：复用编辑器监听链路移除块
      emitAssetsChanged(docId, undefined, undefined, false, [mindmapId]);
      return;
    }

    if (!updatedAt) return;
    if (lastAppliedRemoteUpdatedAtRef.current === updatedAt) return;
    // 如果这是本端刚刚保存产生的回放，跳过应用，避免清空历史/打断输入
    if (lastLocalSavedAtRef.current && lastLocalSavedAtRef.current === updatedAt) {
      lastAppliedRemoteUpdatedAtRef.current = updatedAt;
      return;
    }
    // 本地仍有未同步编辑时，不覆盖
    if (hasLocalEditsRef.current || deletingRef.current) return;

    const incoming = canonicalizeMindmapData((remoteMindmap as any).data ?? defaultMindmapData);
    lastAppliedRemoteUpdatedAtRef.current = updatedAt;
    initialDataRef.current = incoming;
    syncTitleText(incoming);
    try {
      window.localStorage.setItem(autosaveKey, JSON.stringify(incoming));
    } catch {
      // ignore
    }
    if (!effectiveFullscreen && !standalone) {
      try {
        documentBridge?.updateBlockData?.(incoming);
      } catch {
        // ignore
      }
    }
    if (mindmap) {
      applyingRemoteRef.current = true;
      try {
        mindmap.setData(incoming);
        mindmap.command.clearHistory();
      } finally {
        window.setTimeout(() => {
          applyingRemoteRef.current = false;
        }, 0);
      }
    }
  }, [
    autosaveKey,
    docId,
    effectiveFullscreen,
    mindmap,
    mindmapId,
    onExitFullscreen,
    remoteMindmap,
    standalone,
    documentBridge,
    syncTitleText,
  ]);

  // 记录“最近一次指针交互是否发生在思维导图块内”，用于键盘快捷键作用域
  useEffect(() => {
    const onPointerDownCapture = (e: Event) => {
      const wrapper = wrapperRef.current;
      if (!wrapper) return;
      const targetNode = e.target as Node | null;
      const inWrapper = !!(targetNode && wrapper.contains(targetNode));
      hotkeyScopeRef.current = inWrapper;
      lastInteractionAtRef.current = inWrapper ? Date.now() : 0;

      // 关键：simple-mind-map 的画布/节点有时会 stopPropagation，导致外层 onPointerDown
      // 不触发，从而无法把焦点从 BlockNote 编辑器移到思维导图块上，最终表现为 Enter/Tab
      // 等快捷键不生效。
      // 这里用 capture 阶段兜底：只要点击发生在 wrapper 内，就把焦点拉到 wrapper 上。
      if (!inWrapper) return;

      const target = (targetNode as HTMLElement | null) ?? null;
      if (target) {
        const tag = target.tagName;
        if (tag === "INPUT" || tag === "TEXTAREA") return;
        // 节点文本编辑中不抢焦点，避免影响输入/粘贴（仅判断 simple-mind-map 的节点编辑元素）
        const mm = mindmapRef.current;
        const editClasses = mm?.editNodeClassList;
        if (editClasses) {
          for (const cls of editClasses) {
            if (!cls) continue;
            if (target.classList?.contains(cls)) return;
            const found = target.closest?.(`.${cls}`);
            if (found) return;
          }
        }

      }

      // 这里用 setTimeout(0) 而不是 microtask：BlockNote/ProseMirror 可能会在同一轮事件里重新抢回焦点，
      // 导致“选中节点后 Ctrl+V 把思维导图替换成纯文本”。延后一拍把焦点拉回 wrapper，保证快捷键/粘贴作用域稳定。
      if (effectiveFullscreen) return;
      window.setTimeout(() => {
        try {
          wrapperRef.current?.focus?.({ preventScroll: true });
        } catch {
          // ignore
        }
      }, 0);
    };
    document.addEventListener("pointerdown", onPointerDownCapture, true);       
    document.addEventListener("mousedown", onPointerDownCapture, true);
    return () => {
      document.removeEventListener("pointerdown", onPointerDownCapture, true);  
      document.removeEventListener("mousedown", onPointerDownCapture, true);    
    };
  }, [effectiveFullscreen]);

  // 关键：阻止鼠标事件冒泡到 BlockNote/ProseMirror（它们会在 contenteditable 上处理 mousedown，从而产生 NodeSelection）。
  // 不能用 React 的 onMouseDown（事件委托在 document，太晚了），必须用原生监听挂在 wrapper 上，确保在 bubble 链路中先于 editor DOM。
  useEffect(() => {
    const wrapper = wrapperRef.current;
    if (!wrapper) return;

    const stopBubble = (e: Event) => {
      const target = e.target as HTMLElement | null;
      if (!target) return;
      // 节点文本编辑态 / 输入框：允许事件继续冒泡，避免影响输入法/选择/粘贴
      const tag = target.tagName;
      if (tag === "INPUT" || tag === "TEXTAREA") return;
      const mm = mindmapRef.current;
      const editClasses = mm?.editNodeClassList;
      if (editClasses) {
        for (const cls of editClasses) {
          if (!cls) continue;
          if (target.classList?.contains(cls)) return;
          const found = target.closest?.(`.${cls}`);
          if (found) return;
        }
      }

      // 只要在思维导图块内点击，就阻止冒泡到 editor，避免“Ctrl+V 替换整块”
      e.stopPropagation();
      hotkeyScopeRef.current = true;
      lastInteractionAtRef.current = Date.now();

      // 内嵌视图：同步/异步都尝试一次，把焦点拉到 wrapper，保证快捷键稳定。
      // 全屏（Portal）视图：不能强制 focus wrapper，否则会打断节点双击编辑输入框的焦点。
      if (!effectiveFullscreen) {
        try {
          wrapperRef.current?.focus?.({ preventScroll: true });
        } catch {
          // ignore
        }
        window.setTimeout(() => {
          try {
            wrapperRef.current?.focus?.({ preventScroll: true });
          } catch {
            // ignore
          }
        }, 0);
      }
    };

    wrapper.addEventListener("mousedown", stopBubble);
    wrapper.addEventListener("pointerdown", stopBubble);
    return () => {
      wrapper.removeEventListener("mousedown", stopBubble);
      wrapper.removeEventListener("pointerdown", stopBubble);
    };
  }, [effectiveFullscreen]);

  // 标记“节点双击”事件，避免和“画布双击进入全屏”产生冲突
  useEffect(() => {
    if (!mindmap) return;
    const mark = () => {
      recentNodeDblclickRef.current = true;
      window.setTimeout(() => {
        recentNodeDblclickRef.current = false;
      }, 0);
    };
    mindmap.on?.("node_dblclick", mark);
    return () => {
      mindmap.off?.("node_dblclick", mark);
    };
  }, [mindmap]);

  // 记录“是否处于节点文本编辑态”，并在全屏时做一次“焦点保护”：避免外部组件/浏览器机制
  // 在延迟（例如 1s）后抢走焦点，导致编辑框/光标消失（用户反馈）。
  useEffect(() => {
    if (!mindmap) return;
    const onShow = () => {
      textEditOpenRef.current = true;
      // 刚进入编辑态时允许立即 refocus（不抑制）
      suppressTextEditRefocusUntilRef.current = 0;
      // 兜底：部分环境下焦点会在进入编辑态后稍后被抢走，这里做一次 1s 的检查
      window.setTimeout(() => {
        if (!effectiveFullscreen) return;
        if (!textEditOpenRef.current) return;
        const wrap = document.querySelector(
          ".smm-node-edit-wrap, .smm-richtext-node-edit-wrap",
        ) as HTMLElement | null;
        if (!wrap) return;
        const style = window.getComputedStyle(wrap);
        if (style.display === "none" || style.visibility === "hidden") return;
        const active = document.activeElement as HTMLElement | null;
        if (active && wrap.contains(active)) return;
        const editorEl =
          (wrap.querySelector(".ql-editor") as HTMLElement | null) ?? wrap;
        try {
          editorEl.focus({ preventScroll: true });
        } catch {
          // ignore
        }
      }, 1100);
    };
    const onHide = () => {
      textEditOpenRef.current = false;
      // 用户反馈：全屏下双击节点进入编辑后，约 1s 后光标/输入框会消失。
      // 根因通常是初始化兜底激活（或 render_end 回调）在编辑中途清空 active 列表/重新激活根节点，导致编辑被打断。
      // 这里把这类“激活根节点”的动作延后到结束编辑后再执行。
      if (pendingInitActivateRootRef.current || pendingRenderEndActivateRootRef.current) {
        pendingInitActivateRootRef.current = false;
        pendingRenderEndActivateRootRef.current = false;
        window.setTimeout(() => {
          const mm = mindmapRef.current;
          const renderer = mm?.renderer;
          const root =
            renderer?.root ?? renderer?.renderTree?._node ?? null;
          if (!mm || !renderer || !root) return;
          try {
            renderer?.clearActiveNodeList?.();
            renderer?.addNodeToActiveList?.(root, true);
            renderer?.emitNodeActiveEvent?.(root);
            setActiveNodes([root as MindMapNode]);
          } catch {
            // ignore
          }
        }, 0);
      }
    };
    mindmap.on?.("before_show_text_edit", onShow);
    mindmap.on?.("hide_text_edit", onHide);
    return () => {
      mindmap.off?.("before_show_text_edit", onShow);
      mindmap.off?.("hide_text_edit", onHide);
    };
  }, [mindmap, effectiveFullscreen]);

  // 全屏时：如果编辑框仍在显示但焦点被外部抢走，则自动把焦点拉回编辑框，保证“有光标”的体验。
  // 同时尊重用户的“点击外部结束编辑”：当检测到 pointerdown 发生在编辑框之外时，短暂抑制 refocus，
  // 让 simple-mind-map 正常触发 hide_text_edit。
  useEffect(() => {
    if (!effectiveFullscreen) return;
    if (typeof document === "undefined") return;

    const getEditWrap = () =>
      document.querySelector(
        ".smm-node-edit-wrap, .smm-richtext-node-edit-wrap",
      ) as HTMLElement | null;

    const onPointerDownCapture = (e: Event) => {
      if (!textEditOpenRef.current) return;
      const wrap = getEditWrap();
      if (!wrap) return;
      const target = e.target as Node | null;
      if (target && wrap.contains(target)) return;
      // 用户点击了编辑框外部：允许结束编辑（暂时不强制拉回焦点）
      suppressTextEditRefocusUntilRef.current = Date.now() + 800;
    };

    const onFocusInCapture = (e: Event) => {
      if (!textEditOpenRef.current) return;
      const now = Date.now();
      if (now < suppressTextEditRefocusUntilRef.current) return;
      const wrap = getEditWrap();
      if (!wrap) return;
      const style = window.getComputedStyle(wrap);
      if (style.display === "none" || style.visibility === "hidden") return;
      const target = e.target as Node | null;
      if (target && wrap.contains(target)) return;

      // 节流：避免 focusin -> refocus -> focusin 的循环
      if (now - lastTextEditRefocusAtRef.current < 120) return;
      lastTextEditRefocusAtRef.current = now;

      window.setTimeout(() => {
        if (!textEditOpenRef.current) return;
        if (Date.now() < suppressTextEditRefocusUntilRef.current) return;
        const curWrap = getEditWrap();
        if (!curWrap) return;
        const curStyle = window.getComputedStyle(curWrap);
        if (curStyle.display === "none" || curStyle.visibility === "hidden")
          return;
        const active = document.activeElement as HTMLElement | null;
        if (active && curWrap.contains(active)) return;
        const focusTarget =
          (curWrap.querySelector(".ql-editor") as HTMLElement | null) ?? curWrap;
        try {
          focusTarget.focus({ preventScroll: true });
        } catch {
          // ignore
        }
      }, 0);
    };

    document.addEventListener("pointerdown", onPointerDownCapture, true);
    document.addEventListener("mousedown", onPointerDownCapture, true);
    document.addEventListener("focusin", onFocusInCapture, true);
    return () => {
      document.removeEventListener("pointerdown", onPointerDownCapture, true);
      document.removeEventListener("mousedown", onPointerDownCapture, true);
      document.removeEventListener("focusin", onFocusInCapture, true);
    };
  }, [effectiveFullscreen]);

  const exitLocalFullscreen = useCallback(() => {
    setActiveSidebar(null);
    if (fullscreen) {
      onExitFullscreen?.();
      return;
    }
    if (typeof document === "undefined") {
      setLocalFullscreen(false);
      return;
    }
    if (document.fullscreenElement) {
      document
        .exitFullscreen()
        .catch(() => {
          // ignore
        })
        .finally(() => setLocalFullscreen(false));
      return;
    }
    setLocalFullscreen(false);
  }, [fullscreen, onExitFullscreen]);

  const enterLocalFullscreen = useCallback(() => {
    if (fullscreen) return;
    setLocalFullscreen(true);
    setActiveSidebar(null);
  }, [fullscreen]);

  // 沉浸式全屏：锁定页面滚动、支持 ESC 退出（仅对内嵌全屏生效）
  useEffect(() => {
    if (!localFullscreen) return;
    if (typeof document === "undefined") return;

    const prevOverflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    // 进入全屏后尽量把焦点放到思维导图容器上，保证快捷键立即生效
    queueMicrotask(() => {
      try {
        wrapperRef.current?.focus?.({ preventScroll: true });
      } catch {
        // ignore
      }
    });

    const onKeyDown = (e: KeyboardEvent) => {
      if (e.key !== "Escape") return;
      if (fullscreen) return; // 外部传入的 fullscreen 不在这里处理
      e.preventDefault();
      e.stopPropagation();
      exitLocalFullscreen();
    };

    window.addEventListener("keydown", onKeyDown, true);
    return () => {
      window.removeEventListener("keydown", onKeyDown, true);
      document.body.style.overflow = prevOverflow;
    };
  }, [localFullscreen, fullscreen, exitLocalFullscreen]);

  // 让 Enter/Tab/复制/粘贴在“思维导图块”内生效（避免 BlockNote 抢走快捷键）
  useLayoutEffect(() => {
    const onKeyDownCapture = (e: KeyboardEvent) => {
      const inScope =
        hotkeyScopeRef.current ||
        (lastInteractionAtRef.current > 0 &&
          Date.now() - lastInteractionAtRef.current < 60_000);
      if (!inScope) return;
      const mm = mindmapRef.current;
      if (!mm || !mindmapReadyRef.current) return;

      const target = e.target as HTMLElement | null;
      const editClasses = mm.editNodeClassList;
      const wrapper = wrapperRef.current;
      const isInWrapper = Boolean(wrapper && target && wrapper.contains(target));
      const isNodeTextEditing = (() => {
        if (!target || !isInWrapper) return false;
        const tag = target.tagName;
        if (tag === "INPUT" || tag === "TEXTAREA") return true;
        // 仅在 simple-mind-map 的“节点文本编辑元素”上判定为编辑态；不要用 isContentEditable 泛化判断，
        // 否则会把库内部的隐藏输入层误判成编辑态，导致 Ctrl+C/Ctrl+V 失效。
        if (editClasses) {
          for (const cls of editClasses) {
            if (!cls) continue;
            if (target.classList?.contains(cls)) return true;
            const found = target.closest?.(`.${cls}`);
            if (found) return true;
          }
        }
        return false;
      })();

      const isMod = e.ctrlKey || e.metaKey;
      const key = e.key;
      const lower = key.toLowerCase();
      const stop = () => {
        e.preventDefault();
        e.stopPropagation();
        e.stopImmediatePropagation();
      };

      const pickTargetNode = (inst: MindMapInstance) => {
        const renderer = inst.renderer;
        const active = renderer.activeNodeList ?? [];
        const last = renderer.lastActiveNodeList ?? [];
        const root = renderer.root ?? renderer.renderTree?._node ?? null;
        return active[0] ?? last[0] ?? root ?? null;
      };

      if (key === "Enter" && !e.shiftKey && !e.altKey && !isMod) {
        stop();
        const inst = ensureActiveBefore(mm);
        if (!inst) return;
        const node = pickTargetNode(inst);
        if (!node) return;
        inst.execCommand?.("SET_NODE_ACTIVE", node, true);
        inst.execCommand?.("INSERT_NODE", false, [node]);
        // 兜底：某些情况下 INSERT_NODE 不触发 data_change（例如被外层捕获键盘
        // 事件拦截导致内部 Keyboard 插件不走），这里主动做一次防抖保存，确保
        // 切换全屏/刷新后不会丢失。
        markLocalEdited();
        try {
          const snapshot = inst.getData?.(true) ?? inst.getData?.() ?? null;    
          if (snapshot) persistDataRef.current?.(snapshot);
        } catch {
          // ignore
        }
        return;
      }

      if (key === "Tab" && !e.shiftKey && !e.altKey && !isMod) {
        stop();
        const inst = ensureActiveBefore(mm);
        if (!inst) return;
        const node = pickTargetNode(inst);
        if (!node) return;
        inst.execCommand?.("SET_NODE_ACTIVE", node, true);
        inst.execCommand?.("INSERT_CHILD_NODE", false, [node]);
        markLocalEdited();
        try {
          const snapshot = inst.getData?.(true) ?? inst.getData?.() ?? null;    
          if (snapshot) persistDataRef.current?.(snapshot);
        } catch {
          // ignore
        }
        return;
      }

      // 节点文本编辑中：只接管 Enter/Tab；其余快捷键交给默认输入行为
      //（例如 Ctrl+C/Ctrl+V 作为文本复制粘贴）。
      if (isNodeTextEditing) return;

      if ((key === "Delete" || key === "Backspace") && !e.shiftKey && !e.altKey && !isMod) {
        stop();
        const inst = ensureActiveBefore(mm);
        if (!inst) return;
        inst.execCommand?.("REMOVE_NODE");
        markLocalEdited();
        try {
          const snapshot = inst.getData?.(true) ?? inst.getData?.() ?? null;
          if (snapshot) persistDataRef.current?.(snapshot);
        } catch {
          // ignore
        }
        return;
      }

      if (isMod && lower === "c") {
        stop();
        const inst = ensureActiveBefore(mm);
        if (!inst) return;
        const node = pickTargetNode(inst);
        if (node) {
          inst.execCommand?.("SET_NODE_ACTIVE", node, true);
        }
        inst.renderer.copy?.();
        return;
      }

      if (isMod && lower === "v") {
        // 兜底：某些场景下（尤其是嵌入编辑器时）浏览器仍会触发原生 paste 事件。
        // 这里标记一次，避免我们在 paste capture 里重复执行粘贴逻辑。
        skipNextPasteRef.current = true;
        window.setTimeout(() => {
          skipNextPasteRef.current = false;
        }, 200);
        stop();
        const inst = ensureActiveBefore(mm);
        if (!inst) return;
        const renderer = inst.renderer;
        const copyData = renderer.beingCopyData ?? null;
        if (copyData) {
          inst.execCommand?.("PASTE_NODE", copyData);
          markLocalEdited();
          try {
            const snapshot = inst.getData?.(true) ?? inst.getData?.() ?? null;  
            if (snapshot) persistDataRef.current?.(snapshot);
          } catch {
            // ignore
          }
          return;
        }
        renderer.paste?.();
        markLocalEdited();
        try {
          const snapshot = inst.getData?.(true) ?? inst.getData?.() ?? null;    
          if (snapshot) persistDataRef.current?.(snapshot);
        } catch {
          // ignore
        }
      }
    };

    window.addEventListener("keydown", onKeyDownCapture, true);
    return () => {
      window.removeEventListener("keydown", onKeyDownCapture, true);
    };
  }, [markLocalEdited]);

  // 兜底：在非全屏嵌入 BlockNote 时，Ctrl+V 可能仍然触发编辑器的 paste，导致思维导图块被替换成纯文本。
  // 这里在 capture 阶段拦截 paste：当“最近一次指针交互在思维导图块内”且当前不在节点文本编辑态时，
  // 强制把粘贴交给 simple-mind-map，并阻止 BlockNote 继续处理。
  useLayoutEffect(() => {
    const onPasteCapture = (e: ClipboardEvent) => {
      const inScope =
        hotkeyScopeRef.current ||
        (lastInteractionAtRef.current > 0 &&
          Date.now() - lastInteractionAtRef.current < 60_000);
      if (!inScope) return;
      const mm = mindmapRef.current;
      if (!mm || !mindmapReadyRef.current) return;

      const wrapper = wrapperRef.current;
      const active = (typeof document !== "undefined"
        ? (document.activeElement as HTMLElement | null)
        : null);

      const isTextEditingNow = (() => {
        if (!wrapper || !active) return false;
        if (!wrapper.contains(active)) return false;
        const tag = active.tagName;
        if (tag === "INPUT" || tag === "TEXTAREA") return true;
        const editClasses = mm.editNodeClassList;
        if (editClasses) {
          for (const cls of editClasses) {
            if (!cls) continue;
            if (active.classList?.contains(cls)) return true;
            const found = active.closest?.(`.${cls}`);
            if (found) return true;
          }
        }
        return false;
      })();

      // 节点文本编辑态：允许默认粘贴（粘贴到节点文本）
      if (isTextEditingNow) return;

      e.preventDefault();
      e.stopPropagation();
      e.stopImmediatePropagation?.();

      // 如果刚刚已经在 keydown 里处理过 Ctrl+V，这里只负责阻止 BlockNote 的 paste
      if (skipNextPasteRef.current) {
        skipNextPasteRef.current = false;
        return;
      }

      const inst = ensureActiveBefore(mm);
      if (!inst) return;

      // 优先交给 simple-mind-map 自己的 clipboard 逻辑：支持外部文本/图片/链接以及内部节点复制格式
      try {
        inst.renderer.paste?.();
      } catch {
        // ignore
      }

      // 兜底持久化：避免快速切换视图导致“看起来没保存”
      markLocalEdited();
      try {
        const snapshot = inst.getData?.(true) ?? inst.getData?.() ?? null;      
        if (snapshot) persistDataRef.current?.(snapshot);
      } catch {
        // ignore
      }
    };

    window.addEventListener("paste", onPasteCapture, true);
    return () => {
      window.removeEventListener("paste", onPasteCapture, true);
    };
  }, [markLocalEdited]);

  // 兜底：Ctrl+C 可能被 BlockNote/ProseMirror 先行拦截，导致我们 keydown 捕获不到。
  // 这里直接在 copy 事件的 capture 阶段接管，确保“选中节点 -> Ctrl+C”一定能复制节点数据。
  useLayoutEffect(() => {
    const onCopyCapture = (e: ClipboardEvent) => {
      const inScope =
        hotkeyScopeRef.current ||
        (lastInteractionAtRef.current > 0 &&
          Date.now() - lastInteractionAtRef.current < 60_000);
      if (!inScope) return;

      const mm = mindmapRef.current;
      if (!mm || !mindmapReadyRef.current) return;

      const wrapper = wrapperRef.current;
      const active = (typeof document !== "undefined"
        ? (document.activeElement as HTMLElement | null)
        : null);
      const isTextEditingNow = (() => {
        if (!wrapper || !active) return false;
        if (!wrapper.contains(active)) return false;
        const tag = active.tagName;
        if (tag === "INPUT" || tag === "TEXTAREA") return true;
        const editClasses = mm.editNodeClassList;
        if (editClasses) {
          for (const cls of editClasses) {
            if (!cls) continue;
            if (active.classList?.contains(cls)) return true;
            const found = active.closest?.(`.${cls}`);
            if (found) return true;
          }
        }
        return false;
      })();
      if (isTextEditingNow) return;

      e.preventDefault();
      e.stopPropagation();
      e.stopImmediatePropagation?.();

      const inst = ensureActiveBefore(mm);
      if (!inst) return;
      const renderer = inst.renderer;
      const node =
        renderer.activeNodeList?.[0] ??
        renderer.lastActiveNodeList?.[0] ??
        renderer.root ??
        renderer.renderTree?._node ??
        null;
      if (node) inst.execCommand?.("SET_NODE_ACTIVE", node, true);
      try {
        inst.renderer.copy?.();
      } catch {
        // ignore
      }
    };

    window.addEventListener("copy", onCopyCapture, true);
    return () => {
      window.removeEventListener("copy", onCopyCapture, true);
    };
  }, []);

  // 额外兜底：部分浏览器/编辑器会优先通过 beforeinput(insertFromPaste) 走粘贴路径，
  // 即便我们拦截了 paste，也可能仍触发“替换掉思维导图块”的输入。
  // 这里在 capture 阶段统一阻止 paste 相关的 beforeinput，并把实际粘贴交给 simple-mind-map。
  useLayoutEffect(() => {
    const onBeforeInputCapture = (e: InputEvent) => {
      const inputType = (e as unknown as { inputType?: string }).inputType || "";
      if (!inputType || !inputType.toLowerCase().includes("paste")) return;

      const inScope =
        hotkeyScopeRef.current ||
        (lastInteractionAtRef.current > 0 &&
          Date.now() - lastInteractionAtRef.current < 60_000);
      if (!inScope) return;

      const mm = mindmapRef.current;
      if (!mm || !mindmapReadyRef.current) return;

      // 节点文本编辑态：允许默认粘贴（粘贴到节点文本）
      const wrapper = wrapperRef.current;
      const active = (typeof document !== "undefined"
        ? (document.activeElement as HTMLElement | null)
        : null);
      const isTextEditingNow = (() => {
        if (!wrapper || !active) return false;
        if (!wrapper.contains(active)) return false;
        const tag = active.tagName;
        if (tag === "INPUT" || tag === "TEXTAREA") return true;
        const editClasses = mm.editNodeClassList;
        if (editClasses) {
          for (const cls of editClasses) {
            if (!cls) continue;
            if (active.classList?.contains(cls)) return true;
            const found = active.closest?.(`.${cls}`);
            if (found) return true;
          }
        }
        return false;
      })();
      if (isTextEditingNow) return;

      e.preventDefault();
      e.stopPropagation();
      e.stopImmediatePropagation?.();

      const inst = ensureActiveBefore(mm);
      if (!inst) return;
      try {
        inst.renderer.paste?.();
      } catch {
        // ignore
      }

      markLocalEdited();
      try {
        const snapshot = inst.getData?.(true) ?? inst.getData?.() ?? null;
        if (snapshot) persistDataRef.current?.(snapshot);
      } catch {
        // ignore
      }
    };

    window.addEventListener("beforeinput", onBeforeInputCapture, true);
    return () => {
      window.removeEventListener("beforeinput", onBeforeInputCapture, true);
    };
  }, [markLocalEdited]);

  const persistData = useCallback(
    (data: unknown) => {
      // 关键：全屏/非全屏切换会重建 mindmap 实例，初始化数据必须跟随最新编辑
      // 否则切换视图时会用旧数据，表现为"看起来没保存"
      let safe = canonicalizeMindmapData(data);
      // 将 signed URL 转换回 asset:id 格式用于保存
      if (signedUrlToAssetIdRef.current.size > 0) {
        safe = revertToAssetIds(safe, signedUrlToAssetIdRef.current);
      }
      initialDataRef.current = safe;
      syncTitleText(safe);
      window.localStorage.setItem(autosaveKey, JSON.stringify(safe));
      // 注意：全屏（Portal 覆盖层）下如果调用 updateBlock，BlockNote/ProseMirror
      // 可能会在防抖保存时（例如 ~800ms）抢回焦点，导致节点双击编辑的输入框
      // 被迫退出（表现为"有光标但过一会儿就消失"）。
      // 因此全屏态只做 localStorage + 后端同步，等退出全屏（实例重建/卸载）
      // 时再统一把最新数据写回 block。
      if (!effectiveFullscreen && !standalone) {
        documentBridge?.updateBlockData?.(safe);
      }
      if (docId) {
        // 同步到本地文件 + Convex（弱依赖）
        const requestStartedAt = Date.now();
        fetch(`/api/mindmap/${docId}/${mindmapId}`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ data: safe }),
        })
          .then(async (resp) => {
            if (resp.ok) {
              try {
                const payload = (await resp.json().catch(() => null)) as any;
                syncMindmapRouteMeta(payload?.meta);
                const updatedAt =
                  payload && typeof payload.updated_at === "string"
                    ? payload.updated_at
                    : payload?.meta && typeof payload.meta.updatedAt === "string"
                      ? payload.meta.updatedAt
                      : null;
                if (updatedAt) {
                  lastLocalSavedAtRef.current = updatedAt;
                  // 避免 Convex 订阅回放对本端“已应用的保存”重复 setData/clearHistory 导致闪烁
                  lastAppliedRemoteUpdatedAtRef.current = updatedAt;
                  // 仅当保存期间没有新增编辑时，才允许接收远端更新；否则会出现“插入节点后闪一下又没了”
                  if (lastLocalEditAtRef.current <= requestStartedAt) {
                    hasLocalEditsRef.current = false;
                  }
                }
              } catch {
                // ignore
              }
              const fileName = `mindmap-${mindmapId}.json`;
              emitAssetsChanged(docId, {
                id: mindmapId,
                document_id: docId,
                asset_type: "mindmap",
                file_name: fileName,
                file_url: `/documents/${docId}/${fileName}`,
              });
            }
          })
          .catch(() => {});
      }
    },
    [
      autosaveKey,
      docId,
      mindmapId,
      effectiveFullscreen,
      standalone,
      documentBridge,
      syncMindmapRouteMeta,
      syncTitleText,
    ],
  );
  useEffect(() => {
    persistDataRef.current = persistData;
  }, [persistData]);

  // “有上限的防抖保存”：连续 data_change 只会合并为一次保存，但不会因为持续变化
  // 而无限期推迟（避免测试/用户快速切换全屏时出现“看起来没保存”）。
  const persistTimerRef = useRef<number | null>(null);
  const pendingPersistRef = useRef<unknown | null>(null);
  const schedulePersist = useCallback(
    (data: unknown) => {
      pendingPersistRef.current = data;
      if (typeof window === "undefined") return;
      if (persistTimerRef.current != null) return;
      persistTimerRef.current = window.setTimeout(() => {
        persistTimerRef.current = null;
        const pending = pendingPersistRef.current;
        pendingPersistRef.current = null;
        if (pending) persistData(pending);
      }, 800);
    },
    [persistData],
  );
  useEffect(() => {
    return () => {
      if (persistTimerRef.current != null && typeof window !== "undefined") {
        window.clearTimeout(persistTimerRef.current);
        persistTimerRef.current = null;
      }
      const pending = pendingPersistRef.current;
      pendingPersistRef.current = null;
      if (pending) {
        try {
          persistData(pending);
        } catch {
          // ignore
        }
      }
    };
  }, [persistData]);

  const initialSyncDone = useRef(false);
  useEffect(() => {
    if (!docId || !mindmap || initialSyncDone.current) return;
    initialSyncDone.current = true;
    const data = canonicalizeMindmapData(
      mindmap.getData?.(true) ?? mindmap.getData?.() ?? initialDataRef.current ?? defaultMindmapData,
    );
    (async () => {
      try {
        const resp = await fetch(`/api/mindmap/${docId}/${mindmapId}`, {  
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ data, createOnly: true }),
        });
        if (!resp.ok) {
        } else {
          const payload = (await resp.json().catch(() => null)) as { meta?: MindmapRouteMeta } | null;
          syncMindmapRouteMeta(payload?.meta);
        }
      } catch {} finally {
        // 即便远端失败，也通知侧边栏刷新，保证本地文件树及时更新
        const fileName = `mindmap-${mindmapId}.json`;
        emitAssetsChanged(docId, {
          id: mindmapId,
          document_id: docId,
          asset_type: "mindmap",
          file_name: fileName,
          file_url: `/documents/${docId}/${fileName}`,
        });
      }
    })();
  }, [docId, mindmap, mindmapId, syncMindmapRouteMeta]);

  // 初始选中根节点，后续不强制抢焦点，允许用户自由选择
  useEffect(() => {
    if (!mindmap || activeNodes.length > 0) return;
    const rootNode = mindmap.renderer?.root ?? mindmap.renderer?.renderTree?._node;
    if (rootNode) {
      setActiveNodes([rootNode as MindMapNode]);
      mindmap.renderer.activeNodeList = [rootNode];
      mindmap.renderer.lastActiveNodeList = [rootNode];
      mindmap.emit?.("node_active", rootNode, [rootNode]);
    }
  }, [mindmap, activeNodes.length]);

  // mindmap 实例一旦就绪，立刻向侧边栏广播，确保文件树即时显示 mindmap-<id>.json
  useEffect(() => {
    if (!docId || !mindmap) return;
    const fileName = `mindmap-${mindmapId}.json`;
    emitAssetsChanged(docId, {
      id: mindmapId,
      document_id: docId,
      asset_type: "mindmap",
      file_name: fileName,
      file_url: `/documents/${docId}/${fileName}`,
    });
  }, [docId, mindmap, mindmapId]);

  useEffect(() => {
    let destroyed = false;
    let createdInstance: MindMapInstance | null = null;
    (async () => {
      // 进入/退出全屏会切换渲染树，ref 在某些时序下可能短暂为 null。
      // 若这里直接 return，会导致新实例永远不创建，表现为“切换视图后空白/像没保存”。
      const waitForContainer = async (): Promise<HTMLDivElement | null> => {
        for (let i = 0; i < 60; i += 1) {
          if (destroyed) return null;
          if (containerRef.current) return containerRef.current;
          await new Promise((r) => window.setTimeout(r, 50));
        }
        return containerRef.current;
      };
      const hostContainer = await waitForContainer();
      if (!hostContainer) return;
      const [
        { default: MindMap },
        { default: Painter },
        { default: AssociativeLine },
        { default: OuterFrame },
        { default: Exporter },
        { default: Formula },
        { default: RichText },
        { default: MiniMapPlugin },
        { default: Select },
        { default: Drag },
        { default: KeyboardNavigation },
        { default: NodeImgAdjust },
        { default: Scrollbar },
        { default: RainbowLines },
        { default: Watermark },
        { default: TouchEvent },
        { default: Cooperate },
        { default: Demonstrate },
        { default: MindMapLayoutPro },
        { default: NodeBase64ImageStorage },
        { default: ExportPDF },
        { default: ExportXMind },
        MindMapNodeModule,
      ] = await Promise.all([
        import("simple-mind-map"),
        import("simple-mind-map/src/plugins/Painter.js"),
        import("simple-mind-map/src/plugins/AssociativeLine.js"),
        import("simple-mind-map/src/plugins/OuterFrame.js"),
        import("simple-mind-map/src/plugins/Export.js"),
        import("simple-mind-map/src/plugins/Formula.js"),
        import("simple-mind-map/src/plugins/RichText.js"),
        import("simple-mind-map/src/plugins/MiniMap.js"),
        import("simple-mind-map/src/plugins/Select.js"),
        import("simple-mind-map/src/plugins/Drag.js"),
        import("simple-mind-map/src/plugins/KeyboardNavigation.js"),
        import("simple-mind-map/src/plugins/NodeImgAdjust.js"),
        import("simple-mind-map/src/plugins/Scrollbar.js"),
        import("simple-mind-map/src/plugins/RainbowLines.js"),
        import("simple-mind-map/src/plugins/Watermark.js"),
        import("simple-mind-map/src/plugins/TouchEvent.js"),
        import("simple-mind-map/src/plugins/Cooperate.js"),
        import("simple-mind-map/src/plugins/Demonstrate.js"),
        import("simple-mind-map/src/plugins/MindMapLayoutPro.js"),
        import("simple-mind-map/src/plugins/NodeBase64ImageStorage.js"),        
        import("simple-mind-map/src/plugins/ExportPDF.js"),
        import("simple-mind-map/src/plugins/ExportXMind.js"),
        import("simple-mind-map/src/core/render/node/MindMapNode.js"),     
      ]);

      // React StrictMode（开发环境）会触发 effect 的“挂载-卸载-再挂载”流程：
      // 若异步加载完成时已经卸载，不应再创建实例，否则可能留下残余 DOM，导致出现多个 root。
      if (destroyed) return;

      // 兜底修补 simple-mind-map 在创建富文本节点时 host/缓存未就绪导致的空指针
      type MindMapNodeCtorLike = {
        prototype: {
          __wolaiRichtextPatched?: boolean;
          createRichTextNode?: (...args: unknown[]) => unknown;
        };
      };
      const MindMapNodeCtor = (MindMapNodeModule as unknown as { default?: MindMapNodeCtorLike })?.default;
      if (
        MindMapNodeCtor &&
        !MindMapNodeCtor.prototype.__wolaiRichtextPatched
      ) {
        const originalCreate = MindMapNodeCtor.prototype.createRichTextNode;
        MindMapNodeCtor.prototype.__wolaiRichtextPatched = true;
        MindMapNodeCtor.prototype.createRichTextNode = function patched(...args: unknown[]) {
          type RichtextThis = {
            mindMap?: { el?: HTMLElement | null; commonCaches?: Record<string, unknown> } | null;
          };
          const self = this as unknown as RichtextThis;
          // host 兜底：优先使用实例容器，否则退回 body
          const host: HTMLElement = (self?.mindMap?.el as HTMLElement | null) ?? document.body;
          if (!self.mindMap) {
            self.mindMap = { el: host, commonCaches: {} };
          }
          const el = self.mindMap.el;
          if (!el || typeof el.appendChild !== "function") {
            self.mindMap.el = host;
          }
          const caches = self.mindMap.commonCaches ?? (self.mindMap.commonCaches = {});
          const measureKey = "measureRichtextNodeTextSizeEl";
          if (!caches[measureKey]) {
            const measureDiv = document.createElement("div");
            measureDiv.style.position = "fixed";
            measureDiv.style.left = "-999999px";
            (self.mindMap.el ?? document.body).appendChild(measureDiv);
            caches[measureKey] = measureDiv;
          }
          if (typeof originalCreate === "function") {
            return originalCreate.apply(this, args);
          }
          return null;
        };
      }

      const plugins = [
        { name: "Painter", plugin: Painter },
        { name: "AssociativeLine", plugin: AssociativeLine },
        { name: "OuterFrame", plugin: OuterFrame },
        { name: "Export", plugin: Exporter },
        { name: "Formula", plugin: Formula },
        { name: "RichText", plugin: RichText },
        { name: "MiniMap", plugin: MiniMapPlugin },
        { name: "Select", plugin: Select },
        { name: "Drag", plugin: Drag },
        { name: "KeyboardNavigation", plugin: KeyboardNavigation },
        { name: "NodeImgAdjust", plugin: NodeImgAdjust },
        { name: "Scrollbar", plugin: Scrollbar },
        { name: "RainbowLines", plugin: RainbowLines },
        { name: "Watermark", plugin: Watermark },
        { name: "TouchEvent", plugin: TouchEvent },
        { name: "Cooperate", plugin: Cooperate },
        { name: "Demonstrate", plugin: Demonstrate },
        { name: "MindMapLayoutPro", plugin: MindMapLayoutPro },
        { name: "NodeBase64ImageStorage", plugin: NodeBase64ImageStorage },
        { name: "ExportPDF", plugin: ExportPDF },
        { name: "ExportXMind", plugin: ExportXMind },
      ];

      plugins.forEach(({ plugin }) => {
        if (!plugin) {
          return;
        }
        const MindMapCtor = MindMap as unknown as {
          hasPlugin?: (p: unknown) => number;
          usePlugin?: (p: unknown) => void;
        };
        const hasPlugin = MindMapCtor.hasPlugin;
        const notRegistered =
          typeof hasPlugin === "function" ? hasPlugin(plugin) === -1 : true;
        const registerPlugin = MindMapCtor.usePlugin;
        if (notRegistered && typeof registerPlugin === "function") {
          registerPlugin(plugin);
        }
      });

      const { nodeIconList, mergerIconList } = await loadIconModules();

      await patchSvgRbox();

      if (destroyed) return;

      const hostEl = hostContainer;
      // 防止同一容器残留旧实例的 svg/dom（StrictMode 或异常 destroy 场景）
      try {
        hostEl.replaceChildren();
      } catch {
        hostEl.innerHTML = "";
      }

      // 全屏/非全屏切换会重建实例：创建新实例前优先从本地缓存读取最新数据
      // 以避免“全屏里编辑 → 退出全屏后内容消失 / 反之亦然”。
      const rawForInit = (() => {
        if (typeof window === "undefined") {
          return initialData ?? initialDataRef.current ?? defaultMindmapData;
        }
        try {
          const cached = window.localStorage.getItem(autosaveKey);
          if (cached) {
            return JSON.parse(cached);
          }
        } catch {
          // ignore
        }
        return initialData ?? initialDataRef.current ?? defaultMindmapData;
      })();
      const dataForInitCanonical = canonicalizeMindmapData(rawForInit);
      initialDataRef.current = dataForInitCanonical;
      syncTitleText(dataForInitCanonical);
      // 将 asset:id 转换为签名 URL，并获取映射
      const { data: dataForInit, urlToAssetId } = await resolveAssetUrls(dataForInitCanonical);
      // 保存映射供后续保存时使用
      signedUrlToAssetIdRef.current = urlToAssetId;

      type MindMapConstructor = new (options: {
        el: HTMLElement;
        data: unknown;
        theme: string;
        layout: string;
        mousewheelAction: string;
        enableFreeDrag: boolean;
        enableCtrlKeyNodeSelection: boolean;
        fit: boolean;
        useLeftKeySelectionRightKeyDrag: boolean;
        createNewNodeBehavior: string;
        iconList: unknown[];
        customInnerElsAppendTo?: HTMLElement;
        nodeTextEditZIndex?: number;
      }) => MindMapInstance;
      const MindMapCtor = MindMap as unknown as MindMapConstructor;      

      // Portal 全屏时，simple-mind-map 的节点文本编辑框会 append 到 body，并受 z-index 影响：
      // 若 z-index 低于我们的全屏覆盖层，会出现"进入编辑态但看不到独立输入框/光标"的问题。
      // 这里显式提高节点编辑框 z-index，并把内部浮层挂到 wrapper 上，确保全屏下可见且不会被遮挡。
      const fullscreenTextEditOptions = effectiveFullscreen
        ? {
            // 让编辑框仍 append 到 body（库默认行为），只提高 z-index，避免被全屏覆盖层挡住。
            // 注意：如果把编辑框 append 到 wrapper（overflow-hidden），可能会影响 box-shadow/可见性。
            nodeTextEditZIndex: 100000,
            // 将图片调整遮罩挂载到容器内，确保全屏下可见且事件正常
            customInnerElsAppendTo: containerRef.current || undefined,
          }
        : null;

      const instance = new MindMapCtor({
        el: hostEl,
        data: dataForInit,
        theme: "classic",
        layout: "logicalStructure",
        mousewheelAction: "zoom",
        enableFreeDrag: true,
        enableCtrlKeyNodeSelection: true,
        fit: true,
        useLeftKeySelectionRightKeyDrag: true,
        // 新建节点默认激活，编辑由我们手动触发，避免初次插入时定位到 (0,0)
        createNewNodeBehavior: "activeOnly",
        // 传入扩展图标表，和官方一致
        iconList: mergerIconList([
          ...nodeIconList,
          ...(iconConfig as unknown[]),
        ]),
        ...(fullscreenTextEditOptions ?? {}),
      });
      createdInstance = instance;
      mindmapRef.current = instance;
      // 尽早标记为可用：主页面通过 `/mind` 插入后，若依赖 render_end/尺寸探测，可能导致按钮永远不可用
      //（render_end 可能早于监听注册触发，或容器尺寸长时间为 0）。命令本身不需要等待 render_end。
      mindmapReadyRef.current = true;
      // 确保测量节点的缓存容器始终挂在有效 DOM 上，避免 appendChild 空指针     
      type MindMapWithCaches = MindMapInstance & { commonCaches?: Record<string, unknown> };
      const instanceWithCaches = instance as MindMapWithCaches;
      if (!instanceWithCaches.commonCaches) {
        instanceWithCaches.commonCaches = {};
      }
      const measureKey = "measureRichtextNodeTextSizeEl";
      if (!instanceWithCaches.commonCaches[measureKey]) {
        const measureDiv = document.createElement("div");
        measureDiv.style.position = "fixed";
        measureDiv.style.left = "-999999px";
        instanceWithCaches.commonCaches[measureKey] = measureDiv;
        hostEl.appendChild(measureDiv);
      }
      instance.setMode?.("edit");

      // 兜底：在某些环境/焦点状态下，simple-mind-map 的 data_change 事件可能不会触发，
      // 或者键盘插件直接执行命令但未走到我们的保存逻辑，最终表现为“节点能插入但不会持久化”。
      // 这里对 execCommand 做轻量包装：当执行潜在的“结构变更”命令时，主动触发一次防抖保存。
      const shouldPersistAfterCommand = (cmd: unknown) => {
        if (typeof cmd !== "string") return false;
        if (
          cmd === "INSERT_NODE" ||
          cmd === "INSERT_CHILD_NODE" ||
          cmd === "PASTE_NODE" ||
          cmd === "REMOVE_NODE" ||
          cmd === "DELETE_NODE"
        ) {
          return true;
        }
        return (
          cmd.startsWith("INSERT_") ||
          cmd.startsWith("PASTE_") ||
          cmd.startsWith("REMOVE_") ||
          cmd.startsWith("DELETE_")
        );
      };
      const originalExecCommand =
        typeof instance.execCommand === "function"
          ? instance.execCommand.bind(instance)
          : null;
      if (originalExecCommand) {
        (instance as unknown as { execCommand: (...args: any[]) => any }).execCommand = (
          cmd: unknown,
          ...args: any[]
        ) => {
          const ret = originalExecCommand(cmd as any, ...args);
          if (
            !applyingRemoteRef.current &&
            !deletingRef.current &&
            shouldPersistAfterCommand(cmd)
          ) {
            markLocalEdited();
            try {
              const snapshot =
                instance.getData?.(true) ?? instance.getData?.() ?? null;
              if (snapshot) persistDataRef.current?.(snapshot);
            } catch {
              // ignore
            }
          }
          return ret;
        };
      }
    const centerAndFit = (retry = 0) => {
        try {
          const hostRect = hostEl.getBoundingClientRect?.();
          if (!hostRect || hostRect.width < 10 || hostRect.height < 10) { 
            if (retry < 5) {
              window.setTimeout(() => centerAndFit(retry + 1), 80);
            }
            return;
          }
          // 只要容器尺寸已可用，就允许工具栏命令执行；render_end 可能早于监听注册触发
          //（尤其在首次构造时同步渲染），因此不要完全依赖 render_end 来标记就绪。
          mindmapReadyRef.current = true;
          const renderer = instance.renderer;
          const rootNode = renderer?.root ?? renderer?.renderTree?._node;
          if (rootNode && renderer?.setRootNodeCenter) {
            renderer.setRootNodeCenter();
          }
          instance.view?.fit?.();
        } catch {
          if (retry < 3) {
            window.setTimeout(() => centerAndFit(retry + 1), 50);
          }
        }
      };
      window.requestAnimationFrame(() => centerAndFit());
      window.setTimeout(() => centerAndFit(), 80);

      if (typeof window !== "undefined") {
        // 便于开发阶段在控制台直接调试实例
        window.__mindmapInstance = instance;
        const w = window as unknown as {
          __mindmapInstancesById?: Record<string, MindMapInstance>;
          __mindmapPersistById?: Record<string, (data: unknown) => void>;
        };
        if (!w.__mindmapInstancesById) w.__mindmapInstancesById = {};
        w.__mindmapInstancesById[mindmapId] = instance;
        if (!w.__mindmapPersistById) w.__mindmapPersistById = {};
        w.__mindmapPersistById[mindmapId] = (data: unknown) => {
          persistDataRef.current?.(data);
        };
      }

      setMindmap(instance);

      instance.on?.("back_forward", (index: number, len: number) => {
        setCanBack(index > 0);
        setCanForward(index < len - 1);
      });
      // 渲染完成后再二次居中，并确保根节点被标记为激活
      const handleRenderEnd = () => {
        const renderer = instance.renderer;
        const root = getRootNode(instance);
        if (root) {
          // 用户正在编辑节点文本时，不要清空 active 列表/切回根节点，避免打断编辑（光标/输入框消失）。
          if (textEditOpenRef.current) {
            pendingRenderEndActivateRootRef.current = true;
            mindmapReadyRef.current = true;
            return;
          }
          if (renderer?.clearActiveNodeList) renderer.clearActiveNodeList();
          if (renderer?.addNodeToActiveList) renderer.addNodeToActiveList(root, true);
          if (renderer?.emitNodeActiveEvent) renderer.emitNodeActiveEvent(root);
        }
        mindmapReadyRef.current = true;
        centerAndFit();
      };

      // 注意：部分版本的 simple-mind-map 中 `once` 回调的触发顺序可能早于 `on`，
      // 若在 once 里 off 监听，可能导致 handleRenderEnd 永远不执行，进而使工具栏按钮不可用。
      // 这里统一用 on + 手动自销毁，确保首个 render_end 一定会执行 handleRenderEnd。
      let renderHandled = false;
      const onRenderEndOnce = () => {
        if (renderHandled) return;
        renderHandled = true;
        try {
          handleRenderEnd();
        } finally {
          instance.off?.("render_end", onRenderEndOnce);
        }
      };
      instance.on?.("render_end", onRenderEndOnce);
      instance.on?.("node_active", (_node: unknown, list: unknown[]) => {
        if (!list || list.length === 0) return;
        setActiveNodes((list || []) as MindMapNode[]);
        // 兜底：部分场景下点击节点不会触发外层 pointerdown（被 stopPropagation），
        // 这里用内部事件标记“当前在思维导图作用域内”，确保 Ctrl+C/Ctrl+V 不会被 BlockNote 抢走。
        hotkeyScopeRef.current = true;
        lastInteractionAtRef.current = Date.now();
        // 仅内嵌视图需要把焦点从编辑器拉回本块；全屏（Portal）下强制 focus
        // 可能会抢走节点文本编辑的输入焦点，导致双击编辑不出光标。
        if (!effectiveFullscreen) {
          window.setTimeout(() => {
            try {
              wrapperRef.current?.focus?.({ preventScroll: true });
            } catch {
              // ignore
            }
          }, 0);
        }
      });
      instance.on?.("node_click", (node: unknown) => {
        // 确保点击即可激活（调用节点自身的 active 逻辑，保持和官方一致）       
        const renderer = instance.renderer;
        const nodeWithActive = node as unknown as { active?: () => void };
        if (typeof nodeWithActive.active === "function") {
          nodeWithActive.active();
        } else {
          // 兜底：手动维护激活列表
          if (renderer?.clearActiveNodeList) renderer.clearActiveNodeList();
          if (renderer?.addNodeToActiveList) renderer.addNodeToActiveList(node, true);
          if (renderer?.emitNodeActiveEvent) renderer.emitNodeActiveEvent(node);
        }
        const list = renderer?.activeNodeList ?? [];
        setActiveNodes(
          list.length === 0 && node
            ? [node as MindMapNode]
            : (list as MindMapNode[]),
        );
        hotkeyScopeRef.current = true;
        lastInteractionAtRef.current = Date.now();
        // 仅内嵌视图需要把焦点从编辑器拉回本块；全屏（Portal）下强制 focus
        // 可能会抢走节点文本编辑的输入焦点，导致双击编辑不出光标。
        if (!effectiveFullscreen) {
          window.setTimeout(() => {
            try {
              wrapperRef.current?.focus?.({ preventScroll: true });
            } catch {
              // ignore
            }
          }, 0);
        }
      });
      instance.on?.("painter_start", () => setPainterMode(true));
      instance.on?.("painter_end", () => setPainterMode(false));
        instance.on?.("data_change", () => {
          if (!applyingRemoteRef.current) {
          markLocalEdited();
          }
          // data_change 的参数在部分场景下可能不是“可直接序列化的完整数据”
          //（例如 children 非数组、text 缺失等），全屏切换重建时会触发 RichText 初始化崩溃。
          // 这里统一改为从实例读取快照，确保保存/恢复的数据结构稳定。
        const snapshot =
          instance.getData?.(true) ?? instance.getData?.() ?? null;       
        if (snapshot) schedulePersist(snapshot);
      });
      const renderer = instance.renderer;
      const rootNode = getRootNode(instance);
      if (rootNode) {
        // 初始化时主动标记根节点为选中，确保后续插入子节点有合法的父节点
        if (renderer?.clearActiveNodeList) renderer.clearActiveNodeList();
        if (renderer?.addNodeToActiveList) renderer.addNodeToActiveList(rootNode, true);
        if (renderer?.emitNodeActiveEvent) renderer.emitNodeActiveEvent(rootNode);
        setActiveNodes([rootNode as MindMapNode]);
      }
      // 若首次渲染时 root 仍未就绪，设置兜底延迟激活
      window.setTimeout(() => {
        // 全屏下快速进入节点编辑态时，兜底激活会打断编辑；延后到 hide_text_edit 再处理。
        if (textEditOpenRef.current) {
          pendingInitActivateRootRef.current = true;
          return;
        }
        const root = getRootNode(instance);
        if (!root) return;
        if (renderer?.clearActiveNodeList) renderer.clearActiveNodeList();
        if (renderer?.addNodeToActiveList) renderer.addNodeToActiveList(root, true);
        if (renderer?.emitNodeActiveEvent) renderer.emitNodeActiveEvent(root);
        setActiveNodes([root as MindMapNode]);
      }, 120);

      if (destroyed) {
        instance.destroy();
      }
    })();

    return () => {
      destroyed = true;
      try {
        // 切换“内嵌/全屏”会导致实例重建：这里尽量在销毁前同步一次数据，避免丢失最后一次编辑
        const skipPersist =
          deletingRef.current ||
          (() => {
            if (!docId || typeof window === "undefined") return false;
            try {
              const w = window as unknown as {
                __wolaiMindmapDeletingKeys?: Set<string>;
              };
              const key = `${docId}:${mindmapId}`;
              return Boolean(
                w.__wolaiMindmapDeletingKeys?.has(docId) ||
                  w.__wolaiMindmapDeletingKeys?.has(key),
              );
            } catch {
              return false;
            }
          })();
        if (!skipPersist && createdInstance && typeof window !== "undefined") {
          const data =
            createdInstance.getData?.(true) ?? createdInstance.getData?.();
          if (data) {
            const safe = canonicalizeMindmapData(data);
            initialDataRef.current = safe;
            syncTitleText(safe);
            try {
              window.localStorage.setItem(autosaveKey, JSON.stringify(safe));
            } catch {
              // ignore
            }
            if (!standalone) {
              try {
                documentBridge?.updateBlockData?.(safe);
              } catch {
                // ignore
              }
            }
            if (docId) {
              fetch(`/api/mindmap/${docId}/${mindmapId}`, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ data: safe }),
              }).catch(() => {
                // ignore
              });
            }
          }
        }
        createdInstance?.destroy();
      } catch {
        // ignore
      }
      if (typeof window !== "undefined") {
        if (window.__mindmapInstance === createdInstance) {
          window.__mindmapInstance = null;
        }
        try {
          const w = window as unknown as {
            __mindmapInstancesById?: Record<string, MindMapInstance>;
            __mindmapPersistById?: Record<string, (data: unknown) => void>;
          };
          if (w.__mindmapInstancesById && w.__mindmapInstancesById[mindmapId] === createdInstance) {
            delete w.__mindmapInstancesById[mindmapId];
          }
          if (w.__mindmapPersistById && w.__mindmapPersistById[mindmapId]) {
            delete w.__mindmapPersistById[mindmapId];
          }
        } catch {
          // ignore
        }
      }
      setMindmap(null);
      mindmapRef.current = null;
      mindmapReadyRef.current = false;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [docId, effectiveFullscreen, initialData, mindmapId, standalone, documentBridge, syncTitleText]);

  const getInstanceCandidate = () =>
    mindmap ??
    (typeof window !== "undefined"
      ? ((window as unknown as { __mindmapInstance?: MindMapInstance | null })
          .__mindmapInstance ?? null)
      : null);

  const runWhenReady = (
    fn: (mm: MindMapInstance) => void,
    retry = 0,
  ) => {
    const instance = getInstanceCandidate();
    if (mindmapReadyRef.current && instance) {
      fn(instance);
      return;
    }
    if (retry < 60) {
      window.setTimeout(() => runWhenReady(fn, retry + 1), 50);
    }
  };
  const execWithReflow = (runner: (mm: MindMapInstance) => void) => {
    const attempt = (retry = 0) => {
      const instance = getInstanceCandidate();
      if (!instance || !mindmapReadyRef.current) {
        if (retry < 60) window.setTimeout(() => attempt(retry + 1), 80);
        return;
      }
      const mm = ensureActiveBefore(instance);
      if (!mm) return;
      runner(mm);
    };
    attempt();
  };

  const pickActiveOrRoot = (mm: MindMapInstance) => {
    const list = mm.renderer?.activeNodeList;
    const last = mm.renderer?.lastActiveNodeList;
    return (list && list[0]) || (last && last[0]) || getRootNode(mm) || null;
  };

  const getNodeUid = (node: unknown): string | null => {
    const maybeNode = node as unknown as { getData?: (key: string) => unknown; uid?: unknown };
    const raw = typeof maybeNode.getData === "function" ? maybeNode.getData("uid") : maybeNode.uid;
    return typeof raw === "string" && raw ? raw : null;
  };

  const handleUndo = () => runWhenReady((mm) => mm.execCommand("BACK"));
  const handleRedo = () => runWhenReady((mm) => mm.execCommand("FORWARD"));
  const handlePainter = () => {
    const instance = getInstanceCandidate();
    if (!instance?.painter) {
      window.alert("格式刷插件未就绪");
      return;
    }
    instance.painter.startPainter();
  };
  const handleSibling = () =>
    execWithReflow((mm) => {
      const target = pickActiveOrRoot(mm);
      if (!target) return;
      const targetUid = getNodeUid(target);
      mm.execCommand?.("SET_NODE_ACTIVE", target, true);
      // openEdit=false：保持与 createNewNodeBehavior=activeOnly 的策略一致，避免强制进入编辑模式带来的时序问题
      mm.execCommand?.("INSERT_NODE", false, [target]);

      // 兜底：某些情况下插入后 activeNodeList 会短暂为空，导致下一步“同级/删除”无目标。
      // 这里尝试在下一帧将新插入的节点设为激活（插入同级节点时，新节点位于 target 后）。
      window.setTimeout(() => {
        try {
          const renderer = mm.renderer;
          if ((renderer.activeNodeList?.length ?? 0) > 0) return;
          if (!targetUid || typeof renderer.findNodeByUid !== "function") return;
          const currentTarget = renderer.findNodeByUid(targetUid) as unknown as {
            parent?: { children?: unknown[] } | null;
          };
          const parent = currentTarget?.parent ?? null;
          const siblings = (parent?.children ?? []) as unknown[];
          const idx = siblings.indexOf(currentTarget as unknown);
          const inserted = idx >= 0 ? siblings[idx + 1] : null;
          if (!inserted) return;
          renderer?.clearActiveNodeList?.();
          renderer?.addNodeToActiveList?.(inserted, true);
          renderer.lastActiveNodeList = [inserted];
          renderer?.emitNodeActiveEvent?.(inserted);
          mm.execCommand?.("SET_NODE_ACTIVE", inserted, true);
        } catch {
          /* ignore */
        }
      }, 0);
    });
  const handleChild = () =>
    execWithReflow((mm) => {
      const parent = pickActiveOrRoot(mm);
      if (!parent) return;
      const parentUid = getNodeUid(parent);
      mm.execCommand?.("SET_NODE_ACTIVE", parent, true);
      // openEdit=false：同上，避免插入瞬间强制进入编辑导致异常
      mm.execCommand?.("INSERT_CHILD_NODE", false, [parent]);

      // 兜底：确保新插入的子节点成为激活节点，保证随后点击“同级节点/删除节点”可用
      window.setTimeout(() => {
        try {
          const renderer = mm.renderer;
          if ((renderer.activeNodeList?.length ?? 0) > 0) return;
          if (!parentUid || typeof renderer.findNodeByUid !== "function") return;
          const currentParent = renderer.findNodeByUid(parentUid) as unknown as {
            children?: unknown[];
          };
          const children = (currentParent?.children ?? []) as unknown[];
          const inserted = children.length > 0 ? children[children.length - 1] : null;
          if (!inserted) return;
          renderer?.clearActiveNodeList?.();
          renderer?.addNodeToActiveList?.(inserted, true);
          renderer.lastActiveNodeList = [inserted];
          renderer?.emitNodeActiveEvent?.(inserted);
          mm.execCommand?.("SET_NODE_ACTIVE", inserted, true);
        } catch {
          /* ignore */
        }
      }, 0);
    });
  const handleDelete = () => {
    runWhenReady((instance) => {
      const mm = ensureActiveBefore(instance);
      if (!mm) return;
      window.setTimeout(() => mm.execCommand("REMOVE_NODE"), 0);
    });
  };
  const handleSummary = () => {
    runWhenReady((instance) => {
      const mm = ensureActiveBefore(instance);
      if (mm) {
        window.setTimeout(() => mm.execCommand("ADD_GENERALIZATION"), 0);
      }
    });
  };
  const handleAssociativeLine = () => {
    runWhenReady((instance) => {
      const mm = ensureActiveBefore(instance);
      if (mm) {
        window.setTimeout(() => mm.execCommand("ADD_ASSOCIATIVE_LINE"), 0);     
      }
    });
  };
  const handleOuterFrame = () => {
    runWhenReady((instance) => {
      const mm = ensureActiveBefore(instance);
      if (mm) {
        window.setTimeout(() => mm.execCommand("ADD_OUTER_FRAME"), 0);
      }
    });
  };

  const [showImageModal, setShowImageModal] = useState(false);
  const [imageUrl, setImageUrl] = useState("");
  const [imageWidth, setImageWidth] = useState<number | string>(260);
  const [imageHeight, setImageHeight] = useState<number | string>(200);
  const [imageTitle, setImageTitle] = useState("");
  const [imagePosition, setImagePosition] = useState<string>("top");
  const [uploadingImage, setUploadingImage] = useState(false);
  const [workspaceId, setWorkspaceId] = useState(() => readInitialWorkspaceId(initialProjection?.meta));
  const fileInputForImage = useRef<HTMLInputElement | null>(null);

  // 图片预览（双击节点图片）
  const [showImageViewer, setShowImageViewer] = useState(false);
  const [viewerSrc, setViewerSrc] = useState("");
  const [viewerMeta, setViewerMeta] = useState<{ title?: string; width?: number; height?: number }>({});
  const [viewerZoom, setViewerZoom] = useState(1);

  // 解析图片 URL，如果是 asset_id 格式则获取签名 URL
  const resolveImageUrl = async (urlOrAssetId: string): Promise<string> => {
    // 检查是否是 asset_id 格式
    if (urlOrAssetId.startsWith("asset:")) {
      const assetId = urlOrAssetId.replace("asset:", "");
      try {
        const response = await fetch(`/api/media/sign?assetId=${assetId}`);
        if (response.ok) {
          const data = await response.json();
          return data.signedUrl;
        }
      } catch {}
      return urlOrAssetId;
    }
    // 如果是完整的 URL，直接返回
    return urlOrAssetId;
  };

  // 递归收集思维导图中所有的图片 URL
  const collectImageUrls = (data: any): string[] => {
    if (!data) return [];
    const urls: string[] = [];
    const traverse = (node: any) => {
      if (!node) return;
      const candidates: unknown[] = [
        node?.data?.image,
        node?.image,
        node?.image?.url,
        node?.data?.image?.url,
      ];
      candidates.forEach((value) => {
        if (typeof value === "string" && value) {
          urls.push(value);
        }
      });
      if (Array.isArray(node.children)) {
        node.children.forEach(traverse);
      }
    };
    traverse(data);
    return urls;
  };

  // 处理图片删除（将 asset 移到垃圾桶）
  const deleteImageAssets = useCallback(
    async (assetIds: string[]) => {
      if (!assetIds.length || !docId) return;
      try {
        const response = await fetch("/api/media/batch", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ action: "delete", assetIds }),
        });
        if (response.ok) {
          // 记录到已删除列表
          assetIds.forEach((id) => deletedAssetIdsRef.current.add(id));
          // 通知文件树刷新，传递被删除的 assetIds
          emitAssetsChanged(docId, undefined, assetIds);
        } else {
          const payload = await response.json().catch(() => null);
          console.error("删除图片资源失败", payload?.error);
        }
      } catch (e) {
        console.error("删除图片资源失败", e);
      }
    },
    [docId],
  );

  // 处理图片恢复（从垃圾桶恢复）
  const restoreImageAssets = useCallback(
    async (assetIds: string[]) => {
      if (!assetIds.length || !docId) return;
      try {
        const response = await fetch("/api/media/batch", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ action: "restore", assetIds }),
        });
        if (response.ok) {
          // 从已删除列表移除
          assetIds.forEach((id) => deletedAssetIdsRef.current.delete(id));
          // 通知文件树刷新，传递恢复的 assetIds（空列表表示需要重新获取）
          emitAssetsChanged(docId);
        }
      } catch (e) {
        console.error("恢复图片资源失败", e);
      }
    },
    [docId],
  );
  const imgToolbarRef = useRef<HTMLDivElement | null>(null);
  const [imgToolbarState, setImgToolbarState] = useState({
    show: false,
    x: 0,
    y: 0,
    placement: "top" as "top" | "bottom" | "left" | "right",
  });
  const imgToolbarHover = useRef(false);
  const imgNodeRef = useRef<{ node: MindMapNode | null; imgNode: any }>({ node: null, imgNode: null });

  const handleImage = () => {
    const first = mindmap?.renderer?.activeNodeList?.[0] as MindMapNode | undefined;
    if (first?.getStyle) {
      const placement = first.getStyle("imgPlacement", false) as string;
      if (placement) setImagePosition(placement);
    }
    setShowImageModal(true);
  };

  // 图片工具栏更新位置函数
  const updateImgToolbarPos = useCallback(() => {
    if (!imgToolbarState.show || !imgNodeRef.current.imgNode || !imgToolbarRef.current) return;
    const toolbarRect = imgToolbarRef.current.getBoundingClientRect();
    const imgRbox = imgNodeRef.current.imgNode?.rbox?.();
    if (!imgRbox) return;
    const { width: imgWidth, x, y } = imgRbox;
    setImgToolbarState((s) => ({
      ...s,
      x: x + imgWidth / 2 - toolbarRect.width / 2,
      y: y - toolbarRect.height - 5,
    }));
  }, [imgToolbarState.show]);

  // 预览：监听 mindmap 事件
  useEffect(() => {
    if (!mindmap) return;
    const handler = async (node: MindMapNode, e?: Event) => {
      e?.stopPropagation?.();
      e?.preventDefault?.();
      const srcRaw =
        node?.nodeData?.data?.image ||
        node?.getData?.("image") ||
        node?.data?.image;
      const src = typeof srcRaw === "string" ? srcRaw : "";
      if (src) {
        // 解析图片 URL，如果是 asset_id 格式则获取签名 URL
        const resolvedUrl = await resolveImageUrl(src);
        setViewerSrc(resolvedUrl);
        const sizeRaw = node?.getData?.("imageSize");
        const size =
          sizeRaw && typeof sizeRaw === "object"
            ? (sizeRaw as { width?: unknown; height?: unknown })
            : ({} as { width?: unknown; height?: unknown });
        const title = node?.getData?.("imageTitle") || "";
        setViewerMeta({
          title: typeof title === "string" ? title : "",
          width: Number(size.width) || undefined,
          height: Number(size.height) || undefined,
        });
        setViewerZoom(1); // 重置缩放
        setShowImageViewer(true);
      }
    };
    const onActive = (node: MindMapNode) => {
      if (node === imgNodeRef.current.node) return;
      const list = (mindmap.renderer?.activeNodeList || []) as MindMapNode[];
      const has = list.some((n) => !!n?.getData?.("image"));
      if (!has) {
        setImgToolbarState((s) => ({ ...s, show: false }));
        imgNodeRef.current = { node: null, imgNode: null };
      }
    };
    const showToolbarOnClick = (node: MindMapNode, imgNode: any) => {
      imgNodeRef.current = { node, imgNode };
      const placement = (node?.getStyle?.("imgPlacement") || "top") as "top" | "bottom" | "left" | "right";
      setImagePosition(placement);
      setImgToolbarState((s) => ({
        ...s,
        show: true,
        placement,
      }));
      // 使用 requestAnimationFrame 确保 DOM 更新后再定位
      requestAnimationFrame(() => {
        updateImgToolbarPos();
      });
    };
    const hideToolbar = () => {
      if (!imgToolbarHover.current) {
        setImgToolbarState((s) => ({ ...s, show: false }));
        imgNodeRef.current = { node: null, imgNode: null };
      }
    };
    mindmap.on?.("node_img_dblclick", handler);
    mindmap.on?.("node_active", onActive);
    mindmap.on?.("node_img_click", showToolbarOnClick);
    mindmap.on?.("draw_click", hideToolbar);
    // 添加更多事件监听以正确隐藏/更新工具栏
    mindmap.on?.("svg_mousedown", hideToolbar);
    mindmap.on?.("node_dblclick", hideToolbar);
    mindmap.on?.("scale", updateImgToolbarPos);
    mindmap.on?.("translate", hideToolbar);
    mindmap.on?.("node_img_adjust_btn_mousedown", hideToolbar);
    mindmap.on?.("delete_node_img_from_delete_btn", hideToolbar);
    return () => {
      mindmap.off?.("node_img_dblclick", handler);
      mindmap.off?.("node_active", onActive);
      mindmap.off?.("node_img_click", showToolbarOnClick);
      mindmap.off?.("draw_click", hideToolbar);
      mindmap.off?.("svg_mousedown", hideToolbar);
      mindmap.off?.("node_dblclick", hideToolbar);
      mindmap.off?.("scale", updateImgToolbarPos);
      mindmap.off?.("translate", hideToolbar);
      mindmap.off?.("node_img_adjust_btn_mousedown", hideToolbar);
      mindmap.off?.("delete_node_img_from_delete_btn", hideToolbar);
    };
  }, [mindmap, updateImgToolbarPos]);

  // 处理图片删除/恢复（支持撤销）
  useEffect(() => {
    if (!mindmap || !docId) return;

    // 初始化：收集当前所有图片 URL
    const initialData = mindmap.getData?.();
    if (initialData) {
      const urls = collectImageUrls(initialData);
      currentImageUrlsRef.current = new Set(urls);
    }

    // 处理数据变化
    const handleDataChange = () => {
      const newData = mindmap.getData?.();
      if (!newData) return;

      const newUrls = collectImageUrls(newData);
      const newUrlsSet = new Set(newUrls);
      const oldUrlsSet = currentImageUrlsRef.current;

      // 检测被删除的图片（在旧集合中但不在新集合中）
      const deletedUrls: string[] = [];
      oldUrlsSet.forEach(url => {
        if (!newUrlsSet.has(url)) {
          deletedUrls.push(url);
        }
      });

      // 检测新增的图片（在新集合中但不在旧集合中）
      const addedUrls: string[] = [];
      newUrlsSet.forEach(url => {
        if (!oldUrlsSet.has(url)) {
          addedUrls.push(url);
        }
      });

      // 处理删除的图片
      if (deletedUrls.length > 0) {
        const assetIdsToDelete: string[] = [];

        deletedUrls.forEach(url => {
          const assetId = signedUrlToAssetIdRef.current.get(url);
          if (assetId) {
            assetIdsToDelete.push(assetId);
          }
        });

        if (assetIdsToDelete.length > 0) {
          deleteImageAssets(assetIdsToDelete);
        }
      }

      // 处理恢复的图片（可能是撤销操作）
      if (addedUrls.length > 0) {
        const assetIdsToRestore: string[] = [];
        addedUrls.forEach(url => {
          const assetId = signedUrlToAssetIdRef.current.get(url);
          if (assetId && deletedAssetIdsRef.current.has(assetId)) {
            assetIdsToRestore.push(assetId);
          }
        });
        if (assetIdsToRestore.length > 0) {
          restoreImageAssets(assetIdsToRestore);
        }
      }

      // 更新当前图片集合
      currentImageUrlsRef.current = newUrlsSet;
    };

    // 监听多种事件
    mindmap.on?.("data_change", handleDataChange);
    mindmap.on?.("back_forward", handleDataChange);
    mindmap.on?.("node_data_change", handleDataChange);

    return () => {
      mindmap.off?.("data_change", handleDataChange);
      mindmap.off?.("back_forward", handleDataChange);
      mindmap.off?.("node_data_change", handleDataChange);
    };
  }, [deleteImageAssets, docId, mindmap, restoreImageAssets]);

  // 悬浮图片位置工具条（参考 NodeImgPlacementToolbar.vue）
  const renderImgToolbar = () => {
    // 预览打开时隐藏工具栏
    if (!imgToolbarState.show || showImageViewer) return null;
    const { x, y, placement } = imgToolbarState;

    const setPlacement = (p: "top" | "bottom" | "left" | "right") => {
      setImagePosition(p);
      applyToActiveNodes(mindmap, (n) =>
        mindmap?.execCommand?.("SET_NODE_STYLES", n, { imgPlacement: p }),
      );
      setImgToolbarState((s) => ({ ...s, placement: p }));
    };

    // 位置按钮组件
    const PosBtn = (
      p: typeof placement,
      Icon: React.ComponentType<{ className?: string }>,
      title: string,
    ) => (
      <button
        key={p}
        type="button"
        className={`flex h-8 w-8 items-center justify-center rounded border bg-white text-gray-700 shadow-sm ${
          placement === p ? "border-blue-500 bg-blue-50 text-blue-600" : "border-gray-200 hover:bg-gray-100"
        }`}
        onClick={(e) => {
          e.stopPropagation();
          setPlacement(p);
        }}
        title={title}
      >
        <Icon className="h-4 w-4" />
      </button>
    );

    return (
      <div
        ref={imgToolbarRef}
        className="pointer-events-auto fixed z-[9999] flex items-center gap-1 rounded-lg border border-gray-200 bg-white px-2 py-1.5 shadow-md"
        style={{ left: x, top: y }}
        onClick={(e) => e.stopPropagation()}
      >
        {PosBtn("top", ArrowUp, "顶部")}
        {PosBtn("bottom", ArrowDown, "底部")}
        {PosBtn("left", ArrowLeft, "靠左")}
        {PosBtn("right", ArrowRight, "靠右")}
      </div>
    );
  };

  const handleIcon = () => {
    setActiveSidebar("icons");
  };

  const handleLink = () => {
    const href = createSimplePrompt("请输入超链接");
    if (!href) return;
    applyToActiveNodes(mindmap, (node) => mindmap?.execCommand("SET_NODE_HYPERLINK", node, href, href));
  };

  const handleNote = () => {
    setShowNoteModal(true);
  };

  const handleTag = () => {
    const tagsRaw = createSimplePrompt("请输入标签，使用逗号分隔", "重点,待验证");
    if (!tagsRaw) return;
    const tags = tagsRaw.split(",").map((item) => item.trim()).filter(Boolean);
    applyToActiveNodes(mindmap, (node) => mindmap?.execCommand("SET_NODE_TAG", node, tags));
  };

  const handleFormula = () => {
    setActiveSidebar("formula");
  };

  const handleNoteConfirm = () => {
    const note = noteContent.trim();
    setShowNoteModal(false);
    if (!note) return;
    applyToActiveNodes(mindmap, (node) => mindmap?.execCommand("SET_NODE_NOTE", node, note));
  };

  const handleImageConfirm = async () => {
    const url = imageUrl.trim();
    // 获取原始图片尺寸，如果没有则使用默认值
    const width = Number(imageWidth) || 0;
    const height = Number(imageHeight) || 0;

    if (!url) {
      window.alert("请输入图片链接");
      return;
    }
    setShowImageModal(false);

    // 如果是 asset:id 格式，需要先转换为 signed URL 供显示
    let displayUrl = url;
    if (url.startsWith("asset:")) {
      displayUrl = await resolveImageUrl(url);
      // 记录映射关系供保存/删除/撤销使用（无论是否签名成功）
      const assetId = url.replace(/^asset:/, "").trim();
      if (assetId) {
        signedUrlToAssetIdRef.current.set(displayUrl, assetId);
        signedUrlToAssetIdRef.current.set(`asset:${assetId}`, assetId);
      }
    }

    applyToActiveNodes(mindmap, (node) =>
      mindmap?.execCommand("SET_NODE_IMAGE", node, {
        url: displayUrl,
        width,
        height,
        title: imageTitle || "",
        custom: false,  // 让 simple-mind-map 自动缩放到合适尺寸
      }),
    );
    applyToActiveNodes(mindmap, (node) =>
      mindmap?.execCommand?.("SET_NODE_STYLES", node, {
        imgPlacement: imagePosition || "top",
      }),
    );

    // 等待命令执行后更新 currentImageUrlsRef
    setTimeout(() => {
      const newData = mindmap?.getData?.();
      if (newData) {
        const newUrls = collectImageUrls(newData);
        currentImageUrlsRef.current = new Set(newUrls);
      }
    }, 100);
  };

  const handleAttachment = () => {
    const url = createSimplePrompt("附件链接（http/https）");
    if (!url) return;
    const name = createSimplePrompt("附件名称（可选）", "附件");
    applyToActiveNodes(mindmap, (node) => mindmap?.execCommand("SET_NODE_ATTACHMENT", node, url, name ?? ""));
  };

  const handleAiPlaceholder = () => {
    window.alert("AI 能力占位：后续接入大模型生成/优化节点内容。");
  };

  const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => {  
    const file = event.target.files?.[0];
    if (!file) return;
    const reset = () => {
      event.target.value = "";
    };
    const ext = (file.name.split(".").pop() || "").toLowerCase();
    try {
      // JSON / smm
      if (ext === "json" || ext === "smm") {
        const text = await file.text();
        const data = JSON.parse(text);
        mindmap?.setData(data);
        mindmap?.command.clearHistory();
        persistData(data);
        return;
      }

      // XMind
      if (ext === "xmind") {
        const xmindParser = await import("simple-mind-map/src/parse/xmind.js");
        const blob = new Blob([await file.arrayBuffer()]);
        const data = await xmindParser.default.parseXmindFile(blob, (content) => {
          const list = content;
          if (list.length > 1) {
            window.alert("检测到 XMind 多画布，自动导入第一个画布。");
          }
          return list.length > 0 ? list[0] : content;
        });
        mindmap?.setData(data);
        mindmap?.command.clearHistory();
        persistData(data);
        return;
      }

      // MindManager (.mmap)
      if (ext === "mmap") {
        const { parseMindManagerMmapFile } = await import("./mindmapMindManagerImport");
        const data = await parseMindManagerMmapFile(file);
        mindmap?.setData(data);
        mindmap?.command.clearHistory();
        persistData(data);
        return;
      }

      // Markdown
      if (ext === "md" || ext === "markdown") {
        const { transformMarkdownTo } = await import(
          "simple-mind-map/src/parse/markdownTo.js"
        );
        const text = await file.text();
        const data = transformMarkdownTo(text) as MindMapData;
        if (!data.data) {
          data.data = { text: file.name.replace(/\.(md|markdown)$/i, "") || "中心主题" };
        }
        mindmap?.setData(data);
        mindmap?.command.clearHistory();
        persistData(data);
        return;
      }

      window.alert("暂不支持该文件类型，请选择 .json / .smm / .xmind / .mmap / .md");
    } catch {
      window.alert("导入失败：文件格式或内容错误");
    } finally {
      reset();
    }
  };

  const handleNew = () => {
    if (!window.confirm("确定要新建空白导图吗？当前未保存的修改将被覆盖。")) return;
    mindmap?.setData(defaultMindmapData);
    mindmap?.command.clearHistory();
    persistData(defaultMindmapData);
  };

  const handleOpenDirectory = () => {
    const saved = window.localStorage.getItem(autosaveKey);
    if (!saved) {
      window.alert("暂无本地目录记录，可使用“导入”载入文件。");
      return;
    }
    try {
      const data = JSON.parse(saved);
      mindmap?.setData(data);
      window.alert("已从本地目录恢复最新自动保存版本。");
    } catch {
      window.alert("本地目录数据损坏，建议重新导入。");
    }
  };

  const handleExportJson = () => {
    const data = mindmap?.getData?.(true) ?? initialDataRef.current ?? initialData ?? defaultMindmapData;
    downloadJson(data, "mindmap");
  };

  const handleExport = async (type: string, name = "mindmap") => {
    try {
      await mindmap?.doExport?.export(type, true, name);
    } catch {
      window.alert(`导出 ${type.toUpperCase()} 失败，请稍后再试`);
    }
  };

  const handleExportPng = () => handleExport("png");
  const handleExportSvg = () => handleExport("svg");
  const handleExportPdf = () => handleExport("pdf");
  const handleExportMd = () => handleExport("md");
  const handleExportTxt = () => handleExport("txt");
  const handleExportXmind = () => handleExport("xmind");

  const handleSaveAs = () => handleExportJson();

  const handleDeleteMindmap = useCallback(async () => {
    if (!docId) {
      deletingRef.current = true;
      documentBridge?.removeBlock?.();
      return;
    }
    const confirmed = window.confirm("确认删除当前思维导图？将移入垃圾桶（10 分钟内可恢复），到期将自动清理。");
    if (!confirmed) return;
    deletingRef.current = true;
    const resp = await fetch(`/api/mindmap/${docId}/${mindmapId}`, { method: "DELETE" });
    if (!resp.ok) {
      const payload = await resp.json().catch(() => ({}));
      window.alert(payload?.error ?? "删除思维导图失败");
      deletingRef.current = false;
      return;
    }
    try {
      window.localStorage.removeItem(autosaveKey);
    } catch {
      // ignore
    }
    emitAssetsChanged(docId, undefined, undefined, false, [mindmapId]);   
    // 同时会触发全局删除监听（ASSETS_CHANGED_EVENT）进行块移除，这里做 try/catch 避免重复删除报错
    try {
      documentBridge?.removeBlock?.();
    } catch {
      // ignore
    }
  }, [autosaveKey, docId, mindmapId, documentBridge]);

  const toolbarProps = {
    canBack,
    canForward,
    painterMode,
    onUndo: handleUndo,
    onRedo: handleRedo,
    onPainter: handlePainter,
    onSibling: handleSibling,
    onChild: handleChild,
    onDelete: handleDelete,
    onImage: handleImage,
    onIcon: handleIcon,
    onLink: handleLink,
    onNote: handleNote,
    onTag: handleTag,
    onSummary: handleSummary,
    onAssociativeLine: handleAssociativeLine,
    onFormula: handleFormula,
    onAttachment: handleAttachment,
    onOuterFrame: handleOuterFrame,
    onAi: handleAiPlaceholder,
    onImport: handleImport,
    onNew: handleNew,
    onOpenDirectory: handleOpenDirectory,
    onSaveAs: handleSaveAs,
    onDeleteMindmap: handleDeleteMindmap,
    onExportJson: handleExportJson,
    onExportPng: handleExportPng,
    onExportSvg: handleExportSvg,
    onExportPdf: handleExportPdf,
    onExportMd: handleExportMd,
    onExportTxt: handleExportTxt,
    onExportXmind: handleExportXmind,
    fileInputRef,
  };

  const noteModal = showNoteModal ? (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
      <div className="w-full max-w-4xl rounded-xl bg-white shadow-2xl">
        <div className="flex items-center justify-between border-b px-4 py-3">  
          <span className="text-lg font-semibold text-gray-800">备注</span>     
          <button className="text-gray-400 hover:text-gray-600" onClick={() => setShowNoteModal(false)}>
            ✕
          </button>
        </div>
        <div className="px-4 py-3">
          <textarea
            className="h-64 w-full resize-none rounded-md border border-gray-200 p-3 text-sm focus:border-blue-400 focus:outline-none"
            placeholder="支持富文本/Markdown，内容将写入节点备注"
            value={noteContent}
            onChange={(e) => setNoteContent(e.target.value)}
          />
        </div>
        <div className="flex items-center justify-end gap-3 border-t px-4 py-3">
          <button
            className="rounded-md border border-gray-200 px-4 py-2 text-sm text-gray-600 hover:bg-gray-50"
            onClick={() => setShowNoteModal(false)}
          >
            取消
          </button>
          <button
            className="rounded-md bg-blue-500 px-4 py-2 text-sm text-white hover:bg-blue-600"
            onClick={handleNoteConfirm}
          >
            确定
          </button>
        </div>
      </div>
    </div>
  ) : null;

  const imageViewer = showImageViewer ? (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4" onClick={() => setShowImageViewer(false)}>
      <div
        className="relative max-h-full max-w-5xl text-white"
        onClick={(e) => e.stopPropagation()}
        onWheel={(e) => {
          e.preventDefault();
          e.stopPropagation();
          const delta = e.deltaY > 0 ? -0.1 : 0.1;
          setViewerZoom((prev) => Math.max(0.1, Math.min(5, prev + delta)));
        }}
      >
        <button
          className="absolute -top-3 -right-3 rounded-full bg-white/90 px-2 py-1 text-sm text-gray-700 shadow"
          onClick={(e) => {
            e.stopPropagation();
            setShowImageViewer(false);
          }}
        >
          关闭
        </button>
        <div className="mb-2 flex items-center justify-between gap-4 px-1 text-xs text-gray-200">
          <span className="truncate">{viewerMeta.title || "图片预览"}</span>
          <div className="flex items-center gap-3">
            <span className="shrink-0">{viewerMeta.width && viewerMeta.height ? `${viewerMeta.width} × ${viewerMeta.height}px` : ""}</span>
            <span className="shrink-0 text-blue-300">{Math.round(viewerZoom * 100)}%</span>
            <button
              className="shrink-0 rounded bg-white/20 px-2 py-0.5 hover:bg-white/30"
              onClick={() => setViewerZoom((prev) => Math.max(0.1, prev - 0.2))}
            >
              -
            </button>
            <button
              className="shrink-0 rounded bg-white/20 px-2 py-0.5 hover:bg-white/30"
              onClick={() => setViewerZoom(1)}
            >
              重置
            </button>
            <button
              className="shrink-0 rounded bg-white/20 px-2 py-0.5 hover:bg-white/30"
              onClick={() => setViewerZoom((prev) => Math.min(5, prev + 0.2))}
            >
              +
            </button>
          </div>
        </div>
        {/* eslint-disable-next-line @next/next/no-img-element */}
        <img
          src={viewerSrc}
          alt={viewerMeta.title || "预览"}
          className="max-h-[80vh] max-w-[80vw] rounded-lg shadow-2xl object-contain bg-white transition-transform duration-100"
          style={{ transform: `scale(${viewerZoom})` }}
          onClick={(e) => e.stopPropagation()}
        />
      </div>
    </div>
  ) : null;

  // 使用 createPortal 将工具栏挂载到 body，确保 position: fixed 相对于视口
  const imgToolbar = typeof document !== "undefined" && imgToolbarState.show
    ? createPortal(renderImgToolbar(), document.body)
    : null;

  const imageModal = showImageModal ? (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
      <div className="w-full max-w-3xl rounded-xl bg-white shadow-2xl">
        <div className="flex items-center justify-between border-b px-4 py-3">  
          <span className="text-lg font-semibold text-gray-800">插入图片</span> 
          <button className="text-gray-400 hover:text-gray-600" onClick={() => setShowImageModal(false)}>
            ✕
          </button>
        </div>
          <div className="space-y-4 px-4 py-3">
          <div className="space-y-2">
            <Label className="text-xs font-semibold text-gray-600">方式一：上传图片</Label>
            <div className="flex items-center gap-2">
              <button
                className="rounded-md border border-gray-200 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50"
                onClick={() => fileInputForImage.current?.click()}
              >
                选择文件
              </button>
              <span className="text-xs text-gray-400 truncate">
                {uploadingImage
                  ? "上传中..."
                  : imageUrl.startsWith("asset:")
                  ? "已选择图片"
                  : imageUrl.startsWith("data:")
                  ? "已选择本地图片"
                  : imageUrl
                  ? "已输入图片地址"
                  : "未选择文件"}
              </span>
              <input
                ref={fileInputForImage}
                type="file"
                accept="image/*"
                className="hidden"
                onChange={async (e) => {
                  const file = e.target.files?.[0];
                  if (!file) return;
                  if (!workspaceId || !docId) {
                    window.alert("缺少空间信息，无法上传文件");
                    return;
                  }
                  setUploadingImage(true);
                  try {
                    // 先获取原始图片尺寸
                    const localSize = await getImageSizeSafe(URL.createObjectURL(file));
                    if (localSize) {
                      setImageWidth(localSize.width);
                      setImageHeight(localSize.height);
                    }

                    const form = new FormData();
                    form.append("file", file);
                    form.append("workspaceId", workspaceId);
                    form.append("documentId", docId);
                    if (mindmapId) {
                      form.append("mindmapId", mindmapId);
                    }
                    const response = await fetch("/api/media/upload", {
                      method: "POST",
                      body: form,
                    });
                    if (!response.ok) {
                      const payload = await response.json().catch(() => null);
                      throw new Error(payload?.error ?? "上传失败");
                    }
                    const payload = (await response.json()) as { mindmapUrl?: string };
                    if (!payload.mindmapUrl) {
                      throw new Error("返回数据缺少图片地址");
                    }
                    setImageUrl(payload.mindmapUrl);
                  } catch (e) {
                    window.alert(e instanceof Error ? e.message : String(e));
                  } finally {
                    setUploadingImage(false);
                    if (fileInputForImage.current) fileInputForImage.current.value = "";
                  }
                }}
              />
            </div>
          </div>
          <div className="space-y-2">
            <Label className="text-xs font-semibold text-gray-600">方式二：图片地址</Label>
            <Input
              value={imageUrl}
              onChange={(e) => setImageUrl(e.target.value)}
              placeholder="https://example.com/image.png"
            />
          </div>
          <div className="space-y-2">
            <Label className="text-xs font-semibold text-gray-600">快捷位置</Label>
            <div className="flex gap-2">
              <button
                type="button"
                className={`flex-1 rounded border px-2 py-2 text-sm ${imagePosition === "top" ? "border-blue-500 bg-blue-50 text-blue-600" : "border-gray-200"}`}
                onClick={() => setImagePosition("top")}
                title="顶部"
              >
                <ArrowUp className="mx-auto h-4 w-4" />
              </button>
              <button
                type="button"
                className={`flex-1 rounded border px-2 py-2 text-sm ${imagePosition === "bottom" ? "border-blue-500 bg-blue-50 text-blue-600" : "border-gray-200"}`}
                onClick={() => setImagePosition("bottom")}
                title="底部"
              >
                <ArrowDown className="mx-auto h-4 w-4" />
              </button>
              <button
                type="button"
                className={`flex-1 rounded border px-2 py-2 text-sm ${imagePosition === "left" ? "border-blue-500 bg-blue-50 text-blue-600" : "border-gray-200"}`}
                onClick={() => setImagePosition("left")}
                title="靠左"
              >
                <ArrowLeft className="mx-auto h-4 w-4" />
              </button>
              <button
                type="button"
                className={`flex-1 rounded border px-2 py-2 text-sm ${imagePosition === "right" ? "border-blue-500 bg-blue-50 text-blue-600" : "border-gray-200"}`}
                onClick={() => setImagePosition("right")}
                title="靠右"
              >
                <ArrowRight className="mx-auto h-4 w-4" />
              </button>
            </div>
          </div>
          <div className="grid grid-cols-2 gap-3">
            <div className="space-y-1">
              <Label className="text-xs text-gray-500">宽度（px）</Label>
              <Input
                type="number"
                value={imageWidth}
                onChange={(e) => setImageWidth(e.target.value)}
                min={10}
                placeholder="260"
              />
            </div>
            <div className="space-y-1">
              <Label className="text-xs text-gray-500">高度（px）</Label>
              <Input
                type="number"
                value={imageHeight}
                onChange={(e) => setImageHeight(e.target.value)}
                min={10}
                placeholder="200"
              />
            </div>
          </div>
          <div className="space-y-1">
            <Label className="text-xs text-gray-500">标题（可选）</Label>
            <Input
              value={imageTitle}
              onChange={(e) => setImageTitle(e.target.value)}
              placeholder="图片标题"
            />
          </div>
          <div className="space-y-1">
            <Label className="text-xs text-gray-500">位置</Label>
            <select
              className="h-9 w-full rounded-md border border-gray-200 px-3 text-sm"
              value={imagePosition}
              onChange={(e) => setImagePosition(e.target.value)}
            >
              <option value="center">居中</option>
              <option value="top">顶部</option>
              <option value="bottom">底部</option>
              <option value="left">靠左</option>
              <option value="right">靠右</option>
            </select>
          </div>
          <p className="text-xs text-gray-400">可选择本地图片或粘贴 URL，默认尺寸 260×200。</p>
        </div>
        <div className="flex items-center justify-end gap-3 border-t px-4 py-3">  
          <button
            className="rounded-md border border-gray-200 px-4 py-2 text-sm text-gray-600 hover:bg-gray-50"
            onClick={() => setShowImageModal(false)}
          >
            取消
          </button>
          <button
            className="rounded-md bg-blue-500 px-4 py-2 text-sm text-white hover:bg-blue-600"
            onClick={handleImageConfirm}
          >
            插入
          </button>
        </div>
      </div>
    </div>
  ) : null;

  const toggleLocalFullscreen = () => {
    if (localFullscreen) exitLocalFullscreen();
    else enterLocalFullscreen();
  };

  if (effectiveFullscreen) {
    const fullscreenView = (
      <div
        ref={wrapperRef}
        data-testid="mindmap-fullscreen"
        data-page-id={pageId || undefined}
        data-document-id={docId || undefined}
        data-attachment-id={attachmentId}
        data-mindmap-id={mindmapId}
        data-workspace-id={workspaceId || undefined}
        data-request-id={requestMeta?.requestId}
        data-trace-id={requestMeta?.traceId}
        tabIndex={0}
        className="fixed inset-0 z-[9999] overflow-hidden bg-white"
      >
        <div className="pointer-events-none fixed inset-x-0 top-0 z-20 px-6 py-4">
          <div className="pointer-events-auto mx-auto flex max-w-[1600px] items-start justify-between gap-4">
            <div className="rounded-2xl border border-slate-200/80 bg-white/92 px-4 py-3 shadow-lg shadow-slate-200/70 backdrop-blur">
              <div className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400">
                {standalone ? "独立导图页" : "导图全屏"}
              </div>
              <div className="mt-1 text-lg font-semibold text-slate-900">{titleText}</div>
              <div className="mt-1 text-xs text-slate-500">
                {docId ? `文档 ${docId}` : "未绑定文档"}
              </div>
            </div>
            <div className="flex min-w-0 justify-end">
              <MindmapToolbar {...toolbarProps} />
            </div>
          </div>
        </div>
        <div className="relative h-full w-full pt-0">
          <div
            ref={containerRef}
            className="h-full w-full"
            data-testid="mindmap-canvas"
            data-page-id={pageId || undefined}
            data-document-id={docId || undefined}
            data-attachment-id={attachmentId}
            data-mindmap-id={mindmapId}
            data-workspace-id={workspaceId || undefined}
            data-request-id={requestMeta?.requestId}
            data-trace-id={requestMeta?.traceId}
            contentEditable={false}
          />

          <MindmapSidebarTrigger
            activeSidebar={activeSidebar}
            onSelect={setActiveSidebar}
          />
          <MindmapSidebar
            documentId={docId}
            mindmapId={mindmapId}
            mindmap={mindmap}
            activeNodes={activeNodes}
            activeTab={activeSidebar}
            onClose={() => setActiveSidebar(null)}
          />
          <MindmapNavigator
            mindmap={mindmap}
            fullscreen={effectiveFullscreen}
            toggleFullscreen={
              fullscreen
                ? undefined
                : toggleLocalFullscreen
            }
            onToggleMiniMap={() => setShowMiniMap(!showMiniMap)}
            miniMapOpen={showMiniMap}
          />
          <MindmapCount mindmap={mindmap} />
          <MindmapMiniMap mindmap={mindmap} show={showMiniMap} />
          <MindmapContextMenu mindmap={mindmap} />
          {imgToolbar}
          {noteModal}
          {imageModal}
          {imageViewer}
        </div>
      </div>
    );

    if (typeof document === "undefined") return null;
    return createPortal(fullscreenView, document.body);
  }

  return (
    <div
      ref={wrapperRef}
      data-testid="mindmap-embed"
      data-page-id={pageId || undefined}
      data-document-id={docId || undefined}
      data-attachment-id={attachmentId}
      data-mindmap-id={mindmapId}
      data-workspace-id={workspaceId || undefined}
      data-request-id={requestMeta?.requestId}
      data-trace-id={requestMeta?.traceId}
      tabIndex={0}
      className="not-prose my-4 w-full overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm"
    >
      <div className="border-b border-slate-200 bg-gradient-to-r from-slate-50 via-white to-emerald-50/50 px-4 py-4">
        <div className="flex flex-wrap items-start justify-between gap-3">
          <div className="min-w-0">
            <div className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">
              文档内嵌导图
            </div>
            <div className="mt-1 truncate text-base font-semibold text-slate-900">{titleText}</div>
            <div className="mt-1 text-xs text-slate-500">
              双击画布可临时全屏；独立页已不再依赖文档编辑器上下文
            </div>
          </div>
          <div className="flex flex-wrap items-center gap-2">
            <button
              type="button"
              className="rounded-full border border-slate-300 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 transition hover:border-slate-400 hover:bg-slate-50"
              onClick={() => {
                if (!docId) return;
                window.open(`/mindmap/${docId}/${mindmapId}`, "_blank", "noopener,noreferrer");
              }}
            >
              打开独立页
            </button>
            <button
              type="button"
              className="rounded-full border border-slate-300 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 transition hover:border-slate-400 hover:bg-slate-50"
              onClick={toggleLocalFullscreen}
            >
              沉浸展开
            </button>
            <button
              type="button"
              className={`rounded-full border px-3 py-1.5 text-xs font-medium transition ${
                showEmbedToolbar
                  ? "border-slate-900 bg-slate-900 text-white"
                  : "border-slate-300 bg-white text-slate-700 hover:border-slate-400 hover:bg-slate-50"
              }`}
              onClick={() => setShowEmbedToolbar((prev) => !prev)}
            >
              {showEmbedToolbar ? "收起工具" : "展开工具"}
            </button>
          </div>
        </div>
        <div className="mt-3 flex flex-wrap items-center gap-2 text-xs text-slate-500">
          <span className="rounded-full bg-slate-100 px-2.5 py-1">轻预览</span>
          <span className="rounded-full bg-slate-100 px-2.5 py-1">轻交互</span>
          <span className="rounded-full bg-slate-100 px-2.5 py-1">对象边界已拆离文档上下文</span>
        </div>
        <div
          className={`grid transition-[grid-template-rows,opacity,margin] duration-200 ${
            showEmbedToolbar ? "mt-4 grid-rows-[1fr] opacity-100" : "mt-0 grid-rows-[0fr] opacity-0"
          }`}
        >
          <div className="min-h-0 overflow-hidden">
            <div className="w-full">
              <MindmapToolbar {...toolbarProps} />
            </div>
          </div>
        </div>
      </div>
      <div
        data-testid="mindmap-stage"
        className="relative h-[420px] w-full overflow-hidden bg-[linear-gradient(180deg,#ffffff_0%,#f8fafc_100%)] md:h-[460px]"
        onPointerDown={(e) => {
          // 阻止事件冒泡到 BlockNote/ProseMirror，避免产生 NodeSelection 导致粘贴替换整块
          e.stopPropagation();
          hotkeyScopeRef.current = true;
          lastInteractionAtRef.current = Date.now();
          // 点击后把焦点落到容器上，避免快捷键被编辑器抢走
          try {
            wrapperRef.current?.focus?.({ preventScroll: true });
          } catch {
            // ignore
          }
        }}
        onMouseDown={(e) => {
          e.stopPropagation();
          hotkeyScopeRef.current = true;
          lastInteractionAtRef.current = Date.now();
          // 兼容：某些环境下 pointer 事件不触发
          try {
            wrapperRef.current?.focus?.({ preventScroll: true });
          } catch {
            // ignore
          }
        }}
        onDoubleClick={() => {
          if (recentNodeDblclickRef.current) return;
          enterLocalFullscreen();
        }}
      >
        <div
          ref={containerRef}
          className="h-full w-full"
          data-testid="mindmap-canvas"
          data-page-id={pageId || undefined}
          data-document-id={docId || undefined}
          data-attachment-id={attachmentId}
          data-mindmap-id={mindmapId}
          data-workspace-id={workspaceId || undefined}
          data-request-id={requestMeta?.requestId}
          data-trace-id={requestMeta?.traceId}
          contentEditable={false}
        />
        
        <MindmapSidebarTrigger
            activeSidebar={activeSidebar}
            onSelect={setActiveSidebar}
        />
        <MindmapSidebar
            documentId={docId}
            mindmapId={mindmapId}
            mindmap={mindmap}
            activeNodes={activeNodes}
            activeTab={activeSidebar}
            onClose={() => setActiveSidebar(null)}
        />
        <MindmapNavigator
            mindmap={mindmap}
            fullscreen={effectiveFullscreen}
            toggleFullscreen={
              fullscreen
                ? undefined
                : toggleLocalFullscreen
            }
            onToggleMiniMap={() => setShowMiniMap(!showMiniMap)}
            miniMapOpen={showMiniMap}
        />
        <MindmapCount mindmap={mindmap} />
        <MindmapMiniMap mindmap={mindmap} show={showMiniMap} />
        <MindmapContextMenu mindmap={mindmap} />
        {imgToolbar}
        {noteModal}
        {imageModal}
        {imageViewer}
      </div>
    </div>
  );
};

const MindmapBlockView = ({
  block,
  editor,
  fullscreen = false,
  onExitFullscreen,
}: {
  block: SpecificBlock<
    CustomBlockSchema,
    "mindmap",
    DefaultInlineContentSchema,
    DefaultStyleSchema
  >;
  editor: BlockNoteEditor<CustomBlockSchema>;
  fullscreen?: boolean;
  onExitFullscreen?: () => void;
}) => {
  const documentBridge = useMemo<MindmapDocumentBridge>(
    () => ({
      updateBlockData: (data) => {
        editor.updateBlock(block, { props: { ...block.props, data } });
      },
      removeBlock: () => {
        editor.removeBlocks([block.id]);
      },
    }),
    [block, editor],
  );
  const currentBlockKey = `${block.props.docId ?? ""}:${block.id}`;
  const [inlineModeState, setInlineModeState] = useState<{
    blockKey: string;
    mode: "preview" | "editor" | "fullscreen";
  }>({
    blockKey: currentBlockKey,
    mode: "preview",
  });
  const initialProjection = useMemo(
    () =>
      buildMindmapProjection({
        documentId: block.props.docId ?? "",
        mindmapId: block.id,
        data: block.props.data ?? defaultMindmapData,
        meta: null,
      }),
    [block.id, block.props.data, block.props.docId],
  );
  const inlineModeForCurrentBlock =
    inlineModeState.blockKey === currentBlockKey ? inlineModeState.mode : "preview";
  const createOnlySyncedBlockKeyRef = useRef<string | null>(null);

  useEffect(() => {
    if (!block.props.docId) return;
    if (createOnlySyncedBlockKeyRef.current === currentBlockKey) return;
    createOnlySyncedBlockKeyRef.current = currentBlockKey;
    const data = canonicalizeMindmapData(block.props.data ?? defaultMindmapData);

    void fetch(`/api/mindmap/${block.props.docId}/${block.id}`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ data, createOnly: true }),
    })
      .then(async (resp) => {
        if (!resp.ok) return null;
        return (await resp.json().catch(() => null)) as { meta?: MindmapRouteMeta } | null;
      })
      .then((payload) => {
        const fileName = `mindmap-${block.id}.json`;
        emitAssetsChanged(block.props.docId, {
          id: block.id,
          document_id: block.props.docId ?? "",
          asset_type: "mindmap",
          file_name: fileName,
          file_url: `/documents/${block.props.docId}/${fileName}`,
        });
        return payload;
      })
      .catch(() => {
        if (!block.props.docId) return;
        const fileName = `mindmap-${block.id}.json`;
        emitAssetsChanged(block.props.docId, {
          id: block.id,
          document_id: block.props.docId ?? "",
          asset_type: "mindmap",
          file_name: fileName,
          file_url: `/documents/${block.props.docId}/${fileName}`,
        });
      });
  }, [block.id, block.props.data, block.props.docId, currentBlockKey]);

  if (!fullscreen && inlineModeForCurrentBlock === "preview") {
    return (
      <MindmapPreviewCard
        docId={block.props.docId}
        mindmapId={block.id}
        title={initialProjection.title}
        projection={initialProjection}
        onOpenInline={() => {
          setInlineModeState({ blockKey: currentBlockKey, mode: "editor" });
        }}
        onOpenFullscreen={() => {
          setInlineModeState({ blockKey: currentBlockKey, mode: "fullscreen" });
        }}
        onOpenStandalone={() => {
          if (!block.props.docId) return;
          window.open(`/mindmap/${block.props.docId}/${block.id}`, "_blank", "noopener,noreferrer");
        }}
      />
    );
  }

  return (
    <MindmapSurfaceView
      docId={block.props.docId}
      mindmapId={block.id}
      initialData={block.props.data}
      initialProjection={initialProjection}
      fullscreen={fullscreen || inlineModeForCurrentBlock === "fullscreen"}
      onExitFullscreen={
        fullscreen
          ? onExitFullscreen
          : inlineModeForCurrentBlock === "fullscreen"
            ? () => setInlineModeState({ blockKey: currentBlockKey, mode: "preview" })
            : onExitFullscreen
      }
      documentBridge={documentBridge}
    />
  );
};

const StandaloneMindmapView = ({
  docId,
  mindmapId,
  initialProjection = null,
  onExitFullscreen,
}: {
  docId: string;
  mindmapId: string;
  initialProjection?: MindmapProjection | null;
  onExitFullscreen?: () => void;
}) => (
  <MindmapSurfaceView
    docId={docId}
    mindmapId={mindmapId}
    initialData={initialProjection?.data ?? defaultMindmapData}
    initialProjection={initialProjection}
    standalone
    fullscreen
    onExitFullscreen={onExitFullscreen}
  />
);

export { MindmapBlockView, StandaloneMindmapView, defaultMindmapData };

type MindmapBlockViewProps = Parameters<typeof MindmapBlockView>[0];

export const mindmapBlock = createReactBlockSpec(
  {
    type: "mindmap",
    propSchema: {
      docId: { default: "" },
      data: { default: defaultMindmapData },
    },
    content: "none",
  } as unknown as BlockConfig<"mindmap", PropSchema, "none">,
  {
    render: (props) => (
      <MindmapBlockView {...(props as unknown as MindmapBlockViewProps)} />
    ),
  },
);
