"use client";

import "@blocknote/core/style.css";
import "@blocknote/react/style.css";
import "@blocknote/mantine/style.css";

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { BlockNoteView } from "@blocknote/mantine";
import {
  SideMenuController,
  useCreateBlockNote,
  type SideMenuProps,
} from "@blocknote/react";
import { HocuspocusProvider } from "@hocuspocus/provider";
import * as Y from "yjs";
import type { Block } from "@blocknote/core";
import { cn } from "@/lib/utils";
import type { Json } from "@/types/supabase";
import type { DocumentStats } from "@/types/page-options";
import type { MediaAsset } from "@/types/media";
import { customBlockSchema, type CustomBlockSchema } from "./schema";
import { CustomSideMenu } from "./menus/CustomSideMenu";
import { CustomSlashMenu } from "./menus/CustomSlashMenu";
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
import { DocumentToc, type TocEntry } from "@/components/editor/document-toc";
import { useSearchPaletteStore } from "@/store/search-palette";
import { useEditorBridgeStore } from "@/store/editor-bridge";
import type { ReferenceTarget } from "@/types/search";
import FullScreenTableEditor from "@/components/online-table/FullScreenTableEditor";
import { clamp, MIN_BLOCK_LEVEL, MAX_BLOCK_LEVEL } from "@/lib/constants";
import { ASSETS_CHANGED_EVENT, ASSETS_RESTORED_EVENT, emitAssetsChanged } from "@/lib/events";
import { deleteOnlineTable } from "@/lib/online-table";
import { useAppPreferencesStore } from "@/store/app-preferences";
import { useCommentsUiStore } from "@/store/comments-ui";
import { useConvexAuth, useQuery } from "convex/react";
import { api } from "@/lib/convex/api";
import { buildDocumentSavePayload } from "@/lib/documents/save-contract";
import { executePageBodySavePayload } from "@/lib/documents/page-command-client";
import type { EditorReferenceBridge } from "@/store/editor-bridge";
import type { BlockNoteEditorProps } from "@/components/editor/editor-host-types";

const extractInitialBlocks = (content: unknown): Json | undefined => {
  if (Array.isArray(content) && content.length > 0) {
    return content as Json;
  }
  if (content && typeof content === "object") {
    const maybeBlocks = (content as Record<string, unknown>).blocks;
    if (Array.isArray(maybeBlocks) && maybeBlocks.length > 0) {
      return maybeBlocks as Json;
    }
  }
  return undefined;
};

const extractInlineText = (block: Block<CustomBlockSchema>): string => {
  const inlineNodes = (block.content ?? []) as Array<{ text?: string }>;
  return inlineNodes.map((node) => (typeof node.text === "string" ? node.text : "")).join("").trim();
};

const buildHeadingToc = (blocks: Block<CustomBlockSchema>[]): TocEntry[] => {
  const counters = [0, 0, 0, 0, 0];
  const entries: TocEntry[] = [];

  const walk = (targetBlocks: Block<CustomBlockSchema>[]) => {
    targetBlocks.forEach((block) => {
      if (block.type === "heading") {
        const level = clamp(Number(block.props.level) || 1, MIN_BLOCK_LEVEL, MAX_BLOCK_LEVEL);
        counters[level - 1] += 1;
        for (let i = level; i < counters.length; i += 1) {
          counters[i] = 0;
        }
        const numbering = counters.slice(0, level).filter((value) => value > 0).join(".");
        entries.push({
          id: block.id,
          level,
          numbering,
          title: extractInlineText(block),
        });
      }
      if (block.children && block.children.length > 0) {
        walk(block.children as Block<CustomBlockSchema>[]);
      }
    });
  };

  walk(blocks);
  return entries;
};

const findBlockById = (
  blocks: Block<CustomBlockSchema>[],
  id: string,
): Block<CustomBlockSchema> | undefined => {
  for (const block of blocks) {
    if (block.id === id) return block;
    if (block.children && block.children.length > 0) {
      const child = findBlockById(block.children as Block<CustomBlockSchema>[], id);
      if (child) return child;
    }
  }
  return undefined;
};

const syncProgressMeters = (editorInstance: ReturnType<typeof useCreateBlockNote>) => {
  if (!editorInstance) {
    return;
  }
  const blocks = editorInstance.topLevelBlocks as Block<CustomBlockSchema>[];
  const progressStats = new Map<
    string,
    { done: number; doing: number; total: number }
  >();
  let activeProgressId: string | null = null;

  const traverse = (targetBlocks: Block<CustomBlockSchema>[]) => {
    targetBlocks.forEach((block) => {
      if (block.type === "progressMeter" && block.props.auto) {
        activeProgressId = block.id;
        progressStats.set(block.id, { done: 0, doing: 0, total: 0 });
      } else if (block.type === "progressMeter" && !block.props.auto) {
        activeProgressId = null;
      } else if (block.type === "heading") {
        activeProgressId = null;
      } else if (block.type === "advancedTodo" && activeProgressId) {
        const currentStat = progressStats.get(activeProgressId);
        if (!currentStat) return;
        if (block.props.status === "cancelled") {
          return;
        }
        currentStat.total += 1;
        if (block.props.status === "done") {
          currentStat.done += 1;
        } else if (block.props.status === "doing") {
          currentStat.doing += 1;
        }
      }

      if (block.children && block.children.length > 0) {
        traverse(block.children as Block<CustomBlockSchema>[]);
      }
    });
  };

  traverse(blocks);

  progressStats.forEach((stat, progressId) => {
    const block = findBlockById(blocks, progressId);
    if (!block) return;
    if (block.type !== "progressMeter") return;
    const weightedDone = stat.done + stat.doing * 0.5;
    const percent = stat.total === 0 ? 0 : Math.min(100, Math.round((weightedDone / stat.total) * 100));
    const summary = stat.total === 0 ? "暂无条目" : `${stat.done}/${stat.total} 完成`;

    if (block.props.percent !== percent || block.props.summary !== summary) {
      editorInstance.updateBlock(block, {
        props: {
          percent,
          summary,
        },
      });
    }
  });
};

export function BlockNoteEditor({
  documentId,
  workspaceId,
  initialContent,
  initialRevision = null,
  initialConflictDetectionKey = null,
  pageOptions,
  readOnly = false,
  onStatsChange,
  onSnapshot,
  onCloseToc,
  onPersistedMetaChange,
}: BlockNoteEditorProps) {
  const [isSaving, setIsSaving] = useState(false);
  const [saveError, setSaveError] = useState<string | null>(null);
  const [tocEntries, setTocEntries] = useState<TocEntry[]>([]);
  const [fullScreenTableId, setFullScreenTableId] = useState<string | null>(null);
  const isFullScreenTableOpen = fullScreenTableId !== null;
  const revisionRef = useRef<number | null>(initialRevision);
  const conflictDetectionKeyRef = useRef<string | null>(initialConflictDetectionKey);
  const onStatsChangeRef = useRef(onStatsChange);
  const onSnapshotRef = useRef(onSnapshot);
  const onPersistedMetaChangeRef = useRef(onPersistedMetaChange);
  const stableInitialContentRef = useRef<{ documentId: string; content: Json | undefined } | null>(null);
  
  const openReferencePalette = useSearchPaletteStore((state) => state.openReference);
  const registerEditorBridge = useEditorBridgeStore((state) => state.registerBridge);
  const spellCheck = useAppPreferencesStore((s) => s.spellCheck);
  const showStructure = useAppPreferencesStore((s) => s.showStructure);
  const openCommentsForBlock = useCommentsUiStore((s) => s.openForBlock);
  const openCommentsForPage = useCommentsUiStore((s) => s.openForPage);
  const { isAuthenticated } = useConvexAuth();
  const threads = useQuery(
    api.comments.listThreadsByDocument,
    isAuthenticated && documentId ? { documentId, includeResolved: false } : "skip",
  );

  const unresolvedCommentCountByBlockId = useMemo(() => {
    const map: Record<string, number> = {};
    if (!Array.isArray(threads)) return map;
    for (const t of threads as any[]) {
      const bid = String(t?.blockId ?? "");
      if (!bid) continue;
      map[bid] = (map[bid] ?? 0) + 1;
    }
    return map;
  }, [threads]);

  const normalizedInitialContent = useMemo(
    () => extractInitialBlocks(initialContent),
    [initialContent],
  );

  if (
    !stableInitialContentRef.current ||
    stableInitialContentRef.current.documentId !== documentId
  ) {
    stableInitialContentRef.current = {
      documentId,
      content: normalizedInitialContent,
    };
  }

  const collaboration = useMemo(() => {
    const url = process.env.NEXT_PUBLIC_HOCUSPOCUS_URL;
    if (!url) return null;
    const doc = new Y.Doc();
    const provider = new HocuspocusProvider({
      url,
      name: `document.${documentId}`,
      document: doc,
    });
    return { doc, provider };
  }, [documentId]);

  const editor = useCreateBlockNote(
    {
      initialContent: stableInitialContentRef.current?.content as never,
      schema: customBlockSchema,
      placeholders: {
        default: "输入'/'选择，按 空格 打开AI...",
        emptyDocument: "输入'/'选择，按 空格 打开AI...",
      },
      collaboration: collaboration
        ? {
            provider: collaboration.provider,
            fragment: collaboration.doc.getXmlFragment("wolai"),
            user: {
              name: "访客",
              color: "#2563eb",
            },
          }
        : undefined,
    },
    [documentId],
  );

  useEffect(
    () => () => {
      collaboration?.provider.destroy();
      collaboration?.doc.destroy();
    },
    [collaboration],
  );

  const editorRef = useRef(editor);
  const insertMediaAssetRef = useRef<(asset: MediaAsset) => void>(() => {});
  const insertMindmapRef = useRef<(args: { documentId: string; mindmapId: string }) => void>(() => {});
  const insertOnlineTableRef = useRef<(args: { documentId: string; tableId: string }) => void>(() => {});
  const setFullScreenTableIdRef = useRef(setFullScreenTableId);

  useEffect(() => {
    editorRef.current = editor;
  }, [editor]);

  useEffect(() => {
    setFullScreenTableIdRef.current = setFullScreenTableId;
  }, [setFullScreenTableId]);

  useEffect(() => {
    revisionRef.current = initialRevision;
  }, [initialRevision]);

  useEffect(() => {
    conflictDetectionKeyRef.current = initialConflictDetectionKey;
  }, [initialConflictDetectionKey]);

  useEffect(() => {
    onStatsChangeRef.current = onStatsChange;
  }, [onStatsChange]);

  useEffect(() => {
    onSnapshotRef.current = onSnapshot;
  }, [onSnapshot]);

  useEffect(() => {
    onPersistedMetaChangeRef.current = onPersistedMetaChange;
  }, [onPersistedMetaChange]);

  const saveContent = useCallback(
    async (content: Json) => {
      setIsSaving(true);
      try {
        setSaveError(null);
        const blockCount = Array.isArray(content) ? content.length : null;
        const persistedMeta = await executePageBodySavePayload(
          buildDocumentSavePayload({
            documentId,
            workspaceId,
            revision: revisionRef.current,
            content,
            conflictDetectionKey: conflictDetectionKeyRef.current,
            snapshotCapturedAt: new Date().toISOString(),
            blockCount,
          }),
        );
        revisionRef.current = persistedMeta.revision ?? null;
        conflictDetectionKeyRef.current = persistedMeta.conflictDetectionKey ?? null;
        onPersistedMetaChangeRef.current?.({
          revision: revisionRef.current,
          conflictDetectionKey: conflictDetectionKeyRef.current,
        });
        setSaveError(null);
      } catch (error) {
        if (error instanceof Error) {
          setSaveError(error.message);
        }
      } finally {
        setIsSaving(false);
      }
    },
    [documentId, workspaceId],
  );

  const debouncedSave = useDebouncedCallback(saveContent, 800);
  const previousAssetsRef = useRef<Set<string>>(new Set());
  const previousMindmapBlockIdsRef = useRef<Set<string>>(new Set());
  const previousOnlineTableIdsRef = useRef<Set<string>>(new Set());
  const onlineTableDeleteTimestampsRef = useRef<Map<string, number>>(new Map());
  const onlineTableRestoreRetryCountRef = useRef<Map<string, number>>(new Map());
  const onlineTableRestoreRetryTimerRef = useRef<Map<string, number>>(new Map());
  const onlineTableRestoringRef = useRef<Set<string>>(new Set());
  const clearMindmapAutosaveCache = useCallback((targetDocumentId: string, mindmapId?: string) => {
    if (typeof window === "undefined") return;
    try {
      const prefix = "wolai-mindmap-autosave-";
      const targetPrefix = `${prefix}${targetDocumentId}`;
      const directKey = mindmapId ? `${targetPrefix}:${mindmapId}` : targetPrefix;
      const keys: string[] = [];
      for (let i = 0; i < window.localStorage.length; i += 1) {
        const k = window.localStorage.key(i);
        if (!k) continue;
        if (mindmapId) {
          if (k === directKey) keys.push(k);
        } else if (k === targetPrefix || k.startsWith(`${targetPrefix}:`)) {
          keys.push(k);
        }
      }
      keys.forEach((k) => window.localStorage.removeItem(k));
    } catch {
      // ignore
    }
  }, []);
  const markMindmapDeleting = useCallback((targetDocumentId: string, mindmapId?: string) => {
    if (typeof window === "undefined") return;
    try {
      const w = window as unknown as {
        __wolaiMindmapDeletingKeys?: Set<string>;
      };
      if (!w.__wolaiMindmapDeletingKeys) {
        w.__wolaiMindmapDeletingKeys = new Set<string>();
      }
      const key = mindmapId ? `${targetDocumentId}:${mindmapId}` : targetDocumentId;
      w.__wolaiMindmapDeletingKeys.add(key);
      window.setTimeout(() => {
        try {
          w.__wolaiMindmapDeletingKeys?.delete(key);
        } catch {
          // ignore
        }
      }, 8000);
    } catch {
      // ignore
    }
  }, []);

  const collectAssets = useCallback((blocks: Block<CustomBlockSchema>[]) => {
    const assetIds = new Set<string>();
    const mindmapBlockIds = new Set<string>();
    const onlineTableIds = new Set<string>();
    const walk = (target: Block<CustomBlockSchema>[]) => {
      target.forEach((b) => {
        if (b.type === "media") {
          const id = (b.props as { assetId?: string })?.assetId;
          if (id) assetIds.add(id);
        }
        if (b.type === "mindmap") {
          mindmapBlockIds.add(b.id);
        }
        if (b.type === "onlineTable") {
          const id = (b.props as { tableId?: string })?.tableId;
          if (id) onlineTableIds.add(id);
        }
        if (Array.isArray(b.children) && b.children.length > 0) {
          walk(b.children as Block<CustomBlockSchema>[]);
        }
      });
    };
    walk(blocks);
    return { assetIds, mindmapBlockIds, onlineTableIds };
  }, []);

  const deleteAssets = useCallback(
    async (assetIds: string[]) => {
      if (assetIds.length === 0) return;
      const resp = await fetch("/api/media/batch", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ action: "delete", assetIds }),
      });
      if (!resp.ok) {
        // 如果后端返回未找到，说明已被其他端删除，忽略即可
        if (resp.status !== 404) {
          console.error("删除附件失败", await resp.text());
        }
        return;
      }
      emitAssetsChanged(documentId);
    },
    [documentId],
  );

  const deleteMindmapAssets = useCallback(
    async (mindmapIds: string[]) => {
      if (mindmapIds.length === 0) return;
      // 关键：当用户在编辑器里“直接删除 mindmap 块”（例如 Backspace/原生删除）时，
      // React 会先卸载 MindmapBlock；若此时未及时标记“删除中”，MindmapBlock 的卸载清理会
      // 把最后一次数据 POST 回 /api/mindmap/...，导致侧边栏的 mindmap 文件看起来没有被同步删除。
      // 因此这里必须在发起 DELETE 前先标记并清理 autosave，确保卸载清理跳过持久化写回。
      const unique = Array.from(new Set(mindmapIds)).filter((id) => typeof id === "string" && id);
      unique.forEach((mindmapId) => {
        markMindmapDeleting(documentId, mindmapId);
        clearMindmapAutosaveCache(documentId, mindmapId);
      });
      await Promise.all(
        mindmapIds.map(async (mindmapId) => {
          try {
            const resp = await fetch(`/api/mindmap/${documentId}/${mindmapId}`, { method: "DELETE" });
            if (!resp.ok) {
              console.error("删除思维导图失败", mindmapId, await resp.text().catch(() => ""));
            }
          } catch (error) {
            console.error("删除思维导图失败", mindmapId, error);
          } finally {
            markMindmapDeleting(documentId, mindmapId);
            clearMindmapAutosaveCache(documentId, mindmapId);
          }
        }),
      );
      emitAssetsChanged(documentId, undefined, undefined, false, mindmapIds);
    },
    [clearMindmapAutosaveCache, documentId, markMindmapDeleting],
  );

  const deleteOnlineTables = useCallback(
    async (tableIds: string[]) => {
      const unique = Array.from(new Set(tableIds)).filter((id) => typeof id === "string" && id);
      if (unique.length === 0) return;

      await Promise.all(
        unique.map(async (tableId) => {
          try {
            await deleteOnlineTable(tableId);
            // 通知侧边栏/其它视图：立即从文件树移除，并触发订阅更新
            if (typeof window !== "undefined") {
              window.dispatchEvent(new CustomEvent("online-table-deleted", { detail: { tableId } }));
              emitAssetsChanged(documentId);
            }
          } catch (error) {
            console.error("删除在线表格失败", tableId, error);
          }
        }),
      );
    },
    [documentId],
  );

  useEffect(() => {
    return () => {
      // 清理 restore 重试计时器，避免页面卸载后继续触发网络请求
      onlineTableRestoreRetryTimerRef.current.forEach((timerId) => {
        try {
          window.clearTimeout(timerId);
        } catch {
          // ignore
        }
      });
      onlineTableRestoreRetryTimerRef.current.clear();
      onlineTableRestoreRetryCountRef.current.clear();
      onlineTableRestoringRef.current.clear();
    };
  }, []);

  const restoreOnlineTableIfNeeded = useCallback(
    async (tableId: string) => {
      const ts = onlineTableDeleteTimestampsRef.current.get(tableId);
      if (!ts) return;

      // 仅对“最近删除”的表格做恢复（用于 Ctrl+Z / Undo），避免首次加载时误触发 restore
      if (Date.now() - ts > 10 * 60 * 1000) {
        onlineTableDeleteTimestampsRef.current.delete(tableId);
        onlineTableRestoreRetryCountRef.current.delete(tableId);
        const pendingTimer = onlineTableRestoreRetryTimerRef.current.get(tableId);
        if (pendingTimer) {
          try {
            window.clearTimeout(pendingTimer);
          } catch {
            // ignore
          }
          onlineTableRestoreRetryTimerRef.current.delete(tableId);
        }
        return;
      }

      if (onlineTableRestoringRef.current.has(tableId)) {
        return;
      }

      onlineTableRestoringRef.current.add(tableId);
      try {
        const resp = await fetch("/api/tables/restore", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ tableId }),
        });
        if (!resp.ok) {
          const prev = onlineTableRestoreRetryCountRef.current.get(tableId) ?? 0;
          const next = prev + 1;
          onlineTableRestoreRetryCountRef.current.set(tableId, next);
          if (next <= 3 && typeof window !== "undefined" && !onlineTableRestoreRetryTimerRef.current.has(tableId)) {
            const timerId = window.setTimeout(() => {
              onlineTableRestoreRetryTimerRef.current.delete(tableId);
              void restoreOnlineTableIfNeeded(tableId);
            }, 600 * next);
            onlineTableRestoreRetryTimerRef.current.set(tableId, timerId);
          }
          return;
        }
        if (typeof window !== "undefined") {
          window.dispatchEvent(new CustomEvent("online-table-saved", { detail: { tableId } }));
          emitAssetsChanged(documentId);
        }
        onlineTableDeleteTimestampsRef.current.delete(tableId);
        onlineTableRestoreRetryCountRef.current.delete(tableId);
      } catch (error) {
        console.error("恢复在线表格失败", tableId, error);
        const prev = onlineTableRestoreRetryCountRef.current.get(tableId) ?? 0;
        const next = prev + 1;
        onlineTableRestoreRetryCountRef.current.set(tableId, next);
        if (next <= 3 && typeof window !== "undefined" && !onlineTableRestoreRetryTimerRef.current.has(tableId)) {
          const timerId = window.setTimeout(() => {
            onlineTableRestoreRetryTimerRef.current.delete(tableId);
            void restoreOnlineTableIfNeeded(tableId);
          }, 600 * next);
          onlineTableRestoreRetryTimerRef.current.set(tableId, timerId);
        }
      } finally {
        onlineTableRestoringRef.current.delete(tableId);
      }
    },
    [documentId],
  );

  // 监听侧边栏删除事件，主动移除编辑区遗留块
  useEffect(() => {
    const handler = (event: Event) => {
      const detail = (event as CustomEvent)?.detail as {
        docId?: string;
        assetIds?: string[];
        mindmapDeleted?: boolean;
        mindmapAssetIds?: string[];
      };
      if (!detail || detail.docId !== documentId) return;
      const assetIds = detail.assetIds ?? [];
      const mindmapDeleted = Boolean(detail.mindmapDeleted);
      const mindmapAssetIds = Array.isArray(detail.mindmapAssetIds)
        ? (detail.mindmapAssetIds.filter((id) => typeof id === "string") as string[])
        : [];
      if (assetIds.length === 0 && !mindmapDeleted && mindmapAssetIds.length === 0) return;
      if (mindmapDeleted || mindmapAssetIds.length > 0) {
        // 标记“删除中”，避免 MindmapBlock 卸载清理里把 autosave 写回导致“复活”
        if (mindmapAssetIds.length > 0) {
          mindmapAssetIds.forEach((id) => markMindmapDeleting(documentId, id));
        } else {
          markMindmapDeleting(documentId);
        }
        // 双保险：等卸载完成后再清一次 autosave，避免删除后再次插入出现旧内容（重复）
        window.setTimeout(() => {
          if (mindmapAssetIds.length > 0) {
            mindmapAssetIds.forEach((id) => clearMindmapAutosaveCache(documentId, id));
          } else {
            clearMindmapAutosaveCache(documentId);
          }
        }, 0);
      }
      const blocks = editor?.topLevelBlocks as Block<CustomBlockSchema>[] | undefined;
      if (!blocks || blocks.length === 0 || !editor) return;
      const toRemove: string[] = [];
      const walk = (target: Block<CustomBlockSchema>[]) => {
        target.forEach((b) => {
          if (b.type === "mindmap") {
            if (mindmapDeleted) {
              toRemove.push(b.id);
            } else if (mindmapAssetIds.length > 0 && mindmapAssetIds.includes(b.id)) {
              toRemove.push(b.id);
            }
          }
          if (assetIds.length > 0 && b.type === "media") {
            const id = (b.props as { assetId?: string })?.assetId;
            if (id && assetIds.includes(id)) {
              toRemove.push(b.id);
            }
          }
          if (Array.isArray(b.children) && b.children.length > 0) {
            walk(b.children as Block<CustomBlockSchema>[]);
          }
        });
      };
      walk(blocks);
      if (toRemove.length > 0) {
        try {
          editor.removeBlocks(toRemove);
        } catch {
          // ignore：可能已被其它链路先行删除（例如块菜单/工具栏触发的 removeBlocks）
        }
      }
    };
    window.addEventListener(ASSETS_CHANGED_EVENT, handler);
    return () => window.removeEventListener(ASSETS_CHANGED_EVENT, handler);
  }, [clearMindmapAutosaveCache, documentId, editor, markMindmapDeleting]);

  // 监听在线表格删除事件（文件树/其它页面触发），主动移除编辑区的 onlineTable 块，并关闭全屏窗口
  useEffect(() => {
    if (!editor) {
      return;
    }
    const handler = (event: Event) => {
      const detail = (event as CustomEvent<{ tableId?: string }>).detail;
      const tableId = typeof detail?.tableId === "string" ? detail.tableId : "";
      if (!tableId) return;
      onlineTableDeleteTimestampsRef.current.set(tableId, Date.now());
      const blocks = editor?.topLevelBlocks as Block<CustomBlockSchema>[] | undefined;
      if (!blocks || blocks.length === 0) return;

      const toRemove: string[] = [];
      const walk = (target: Block<CustomBlockSchema>[]) => {
        target.forEach((b) => {
          if (b.type === "onlineTable") {
            const id = (b.props as { tableId?: string })?.tableId;
            if (id && id === tableId) {
              toRemove.push(b.id);
            }
          }
          if (Array.isArray(b.children) && b.children.length > 0) {
            walk(b.children as Block<CustomBlockSchema>[]);
          }
        });
      };
      walk(blocks);

      if (toRemove.length > 0) {
        try {
          editor.removeBlocks(toRemove);
        } catch {
          // ignore
        }
      }
      setFullScreenTableId((prev) => (prev === tableId ? null : prev));
    };
    window.addEventListener("online-table-deleted", handler as EventListener);
    return () => window.removeEventListener("online-table-deleted", handler as EventListener);
  }, [editor]);

  useEffect(() => {
    if (!editor) {
      return undefined;
    }

    let disposed = false;
    const runSync = () => {
      if (disposed) {
        return;
      }
      const blocks = editor.topLevelBlocks;
      debouncedSave(blocks as Json);
      const typedBlocks = blocks as Block<CustomBlockSchema>[];
      setTocEntries(buildHeadingToc(typedBlocks));
      syncProgressMeters(editor);
      const stats = computeDocumentStats(typedBlocks);
      onStatsChangeRef.current?.(stats);
      onSnapshotRef.current?.({ blocks: blocks as Json, stats });

      // 检测块删除 -> 同步删除附件/思维导图并刷新侧边栏
      const { assetIds, mindmapBlockIds, onlineTableIds } = collectAssets(typedBlocks);
      const prevAssets = previousAssetsRef.current;
      const removedAssets = [...prevAssets].filter((id) => !assetIds.has(id));
      if (removedAssets.length > 0) {
        void deleteAssets(removedAssets);
      }
      previousAssetsRef.current = assetIds;
      const prevMindmaps = previousMindmapBlockIdsRef.current;
      const removedMindmaps = [...prevMindmaps].filter((id) => !mindmapBlockIds.has(id));
      if (removedMindmaps.length > 0) {
        void deleteMindmapAssets(removedMindmaps);
      }
      previousMindmapBlockIdsRef.current = mindmapBlockIds;

      const prevTables = previousOnlineTableIdsRef.current;
      const removedTables = [...prevTables].filter((id) => !onlineTableIds.has(id));
      if (removedTables.length > 0) {
        removedTables.forEach((id) => onlineTableDeleteTimestampsRef.current.set(id, Date.now()));
        void deleteOnlineTables(removedTables);
      }

      const addedTables = [...onlineTableIds].filter((id) => !prevTables.has(id));
      if (addedTables.length > 0) {
        addedTables.forEach((id) => void restoreOnlineTableIfNeeded(id));
      }
      previousOnlineTableIdsRef.current = onlineTableIds;
    };

    runSync();
    const unsubscribe = editor.onEditorContentChange(runSync) as unknown as
      | undefined
      | (() => void);
    return () => {
      disposed = true;
      unsubscribe?.();
    };
  }, [collectAssets, debouncedSave, deleteAssets, deleteMindmapAssets, deleteOnlineTables, editor, restoreOnlineTableIfNeeded]);

  const jumpToHeading = useCallback((headingId: string) => {
    const target = document.querySelector<HTMLElement>(`[data-id="${headingId}"]`);
    if (target) {
      target.scrollIntoView({ behavior: "smooth", block: "center" });
    }
  }, []);

  const editorWrapperClass = cn(
    "relative min-h-[60vh] rounded-2xl border border-transparent bg-white p-4 shadow-sm",
    pageOptions.smallText ? "text-[15px]" : "text-[16px]",
  );

const blocknoteClass = cn(
  "wolai-editor min-h-full",
  (showStructure || pageOptions.showStructure) && "wolai-editor-show-structure",
  pageOptions.showHeadingNumbers && "wolai-heading-numbering",
  isFullScreenTableOpen && "pointer-events-none select-none",
);

const buildDocumentPath = (documentId: string): string => {
  if (typeof window === "undefined" || !window.location) {
    return `/documents/${documentId}`;
  }
  return `${window.location.origin}/documents/${documentId}`;
};

const trimTrailingCharacter = (
  editorInstance: ReturnType<typeof useCreateBlockNote> | null,
  block: Block<CustomBlockSchema>,
  char: string,
) => {
  if (!editorInstance) {
    return;
  }
  const content = (Array.isArray(block.content) ? [...block.content] : []) as any[];
  for (let index = content.length - 1; index >= 0; index -= 1) {
    const node = content[index] as any;
    if (typeof node?.text === "string" && node.text.endsWith(char)) {
      const nextText = node.text.slice(0, -1);
      if (nextText.length === 0) {
        content.splice(index, 1);
      } else {
        content[index] = { ...(node as any), text: nextText } as any;
      }
      editorInstance.updateBlock(block, { content });
      break;
    }
  }
};

const generateBlockId = () => {
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
    return crypto.randomUUID();
  }
  return `ref_${Math.random().toString(36).slice(2, 10)}`;
};

const computeDocumentStats = (blocks: Block<CustomBlockSchema>[]): DocumentStats => {
  let characterCount = 0;
  let wordCount = 0;
  let todoTotal = 0;
  let todoDone = 0;
  const accumulate = (targetBlocks: Block<CustomBlockSchema>[]) => {
    targetBlocks.forEach((block) => {
      if (Array.isArray(block.content)) {
        (block.content as any[]).forEach((node: any) => {
          if (typeof node.text === "string") {
            const text = node.text;
            characterCount += text.length;
            const trimmed = text.trim();
            if (trimmed.length === 0) {
              return;
            }
            const tokens = trimmed.split(/\s+/).filter(Boolean);
            if (tokens.length > 1) {
              wordCount += tokens.length;
            } else {
              wordCount += trimmed.replace(/\s+/g, "").length;
            }
          }
        });
      }

      // 待办统计：
      // - advancedTodo：取消不计入总数；done 计为完成
      // - checkListItem（BlockNote 默认块）：按 checked 统计
      if (block.type === "advancedTodo") {
        const status = String((block.props as any)?.status ?? "todo");
        if (status !== "cancelled") {
          todoTotal += 1;
          if (status === "done") {
            todoDone += 1;
          }
        }
      } else if (block.type === "checkListItem") {
        todoTotal += 1;
        if (Boolean((block.props as any)?.checked)) {
          todoDone += 1;
        }
      }
      if (block.children && block.children.length > 0) {
        accumulate(block.children as Block<CustomBlockSchema>[]);
      }
    });
  };
  accumulate(blocks);
  return {
    wordCount,
    characterCount,
    blockCount: blocks.length,
    todoTotal,
    todoDone,
  };
};

  const insertMediaAssetBlock = useCallback(
    (asset: MediaAsset) => {
      if (!editor) {
        return;
      }
      const topBlocks = editor.topLevelBlocks as any[];
      // 避免重复插入（例如：恢复事件重复触发/多端同时恢复）
      const exists = topBlocks.some((b) => {
        if (b.type !== "media") return false;
        const id = (b.props as { assetId?: string })?.assetId;
        return Boolean(id && asset.id && String(id) === String(asset.id));
      });
      if (exists) {
        return;
      }
      const fileUrl = asset.file_url ?? "";
      if (!fileUrl) {
        return;
      }
      const cursor = editor.getTextCursorPosition();
      let referenceBlock: any = cursor?.block ?? topBlocks[topBlocks.length - 1];
      if (!referenceBlock) {
        try {
          // 兜底：部分情况下（例如删除掉最后一个块）topLevelBlocks 可能为空，先补一个段落作为插入锚点
          editor.replaceBlocks(editor.topLevelBlocks, [{ type: "paragraph" } as any]);
          const nextTopBlocks = editor.topLevelBlocks as any[];
          referenceBlock = nextTopBlocks[nextTopBlocks.length - 1];
        } catch {
          // ignore
        }
        if (!referenceBlock) return;
      }
      editor.insertBlocks(
        [
          {
            type: "media",
            props: {
              fileUrl,
              thumbnailUrl: asset.thumbnail_url ?? fileUrl,
              assetId: asset.id,
              assetType: asset.asset_type ?? "image",
              fileName: asset.file_name ?? "",
              fileSize: asset.file_size ?? undefined,
              mimeType: asset.mime_type ?? "",
              ocrStatus: asset.ocr_status ?? "idle",
              documentId,
            },
          },
        ],
        referenceBlock,
        "after",
      );
    },
    [documentId, editor],
  );

  const insertMindmapBlock = useCallback(
    (args: { documentId: string; mindmapId: string }) => {
      if (!editor) return;
      // 仅允许插入到当前打开的页面
      if (!args.documentId || String(args.documentId) !== String(documentId)) return;
      const mindmapId = String(args.mindmapId ?? "").trim();
      if (!mindmapId) return;

      const topBlocks = editor.topLevelBlocks as any[];
      const exists = topBlocks.some((b) => b.type === "mindmap" && String(b.id) === mindmapId);
      if (exists) return;

      const cursor = editor.getTextCursorPosition();
      let referenceBlock: any = cursor?.block ?? topBlocks[topBlocks.length - 1];
      if (!referenceBlock) {
        try {
          editor.replaceBlocks(editor.topLevelBlocks, [{ type: "paragraph" } as any]);
          const nextTopBlocks = editor.topLevelBlocks as any[];
          referenceBlock = nextTopBlocks[nextTopBlocks.length - 1];
        } catch {
          // ignore
        }
        if (!referenceBlock) return;
      }

      editor.insertBlocks(
        [
          {
            id: mindmapId,
            type: "mindmap",
            props: { docId: documentId },
            content: [],
          } as any,
        ],
        referenceBlock,
        "after",
      );
    },
    [documentId, editor],
  );

  const insertOnlineTableBlock = useCallback(
    (args: { documentId: string; tableId: string }) => {
      if (!editor) return;
      // 仅允许插入到当前打开的页面
      if (!args.documentId || String(args.documentId) !== String(documentId)) return;
      const tableId = String(args.tableId ?? "").trim();
      if (!tableId) return;

      const topBlocks = editor.topLevelBlocks as any[];
      const exists = topBlocks.some((b) => {
        if (b.type !== "onlineTable") return false;
        const id = (b.props as { tableId?: string })?.tableId;
        return Boolean(id && String(id) === tableId);
      });
      if (exists) return;

      const cursor = editor.getTextCursorPosition();
      let referenceBlock: any = cursor?.block ?? topBlocks[topBlocks.length - 1];
      if (!referenceBlock) {
        try {
          editor.replaceBlocks(editor.topLevelBlocks, [{ type: "paragraph" } as any]);
          const nextTopBlocks = editor.topLevelBlocks as any[];
          referenceBlock = nextTopBlocks[nextTopBlocks.length - 1];
        } catch {
          // ignore
        }
        if (!referenceBlock) return;
      }

      editor.insertBlocks(
        [
          {
            type: "onlineTable",
            props: { tableId, title: "未命名表格" },
            content: [],
          } as any,
        ],
        referenceBlock,
        "after",
      );
    },
    [documentId, editor],
  );

  useEffect(() => {
    insertMediaAssetRef.current = insertMediaAssetBlock;
  }, [insertMediaAssetBlock]);

  useEffect(() => {
    insertMindmapRef.current = insertMindmapBlock;
  }, [insertMindmapBlock]);

  useEffect(() => {
    insertOnlineTableRef.current = insertOnlineTableBlock;
  }, [insertOnlineTableBlock]);

  useEffect(() => {
    if (!editor) return undefined;

    const handler = (event: Event) => {
      const detail = (event as CustomEvent)?.detail as
        | { docId?: string; kind?: "media"; assetId?: string; asset?: MediaAsset }
        | { docId?: string; kind?: "mindmap"; mindmapId?: string }
        | { docId?: string; kind?: "table"; tableId?: string };
      if (!detail || !detail.docId) return;
      if (String(detail.docId) !== String(documentId)) return;

      if ((detail as any).kind === "mindmap") {
        const mindmapId = String((detail as any).mindmapId ?? "").trim();
        if (!mindmapId) return;
        insertMindmapBlock({ documentId: detail.docId, mindmapId });
        return;
      }

      if ((detail as any).kind === "table") {
        const tableId = String((detail as any).tableId ?? "").trim();
        if (!tableId) return;
        insertOnlineTableBlock({ documentId: detail.docId, tableId });
        return;
      }

      if ((detail as any).kind === "media") {
        const assetId = String((detail as any).assetId ?? "").trim();
        const asset = ((detail as any).asset ?? null) as MediaAsset | null;
        if (!assetId) return;
        void (async () => {
          // 恢复列表里的 file_url 可能为空/不可用，优先用 sign 接口拿最新可访问链接
          let fileUrl = (asset?.file_url ?? "").trim();
          if (!fileUrl) {
            try {
              const res = await fetch(`/api/media/sign?assetId=${encodeURIComponent(assetId)}`);
              if (res.ok) {
                const payload = (await res.json().catch(() => null)) as any;
                fileUrl = String(payload?.signedUrl ?? "").trim();
              }
            } catch {
              // ignore
            }
          }
          if (!fileUrl) return;

          insertMediaAssetBlock({
            ...(asset ?? ({} as MediaAsset)),
            id: assetId,
            document_id: documentId,
            file_url: fileUrl,
            thumbnail_url: (asset?.thumbnail_url ?? fileUrl) as any,
          } as MediaAsset);
        })();
      }
    };

    window.addEventListener(ASSETS_RESTORED_EVENT, handler);
    return () => window.removeEventListener(ASSETS_RESTORED_EVENT, handler);
  }, [documentId, editor, insertMediaAssetBlock, insertMindmapBlock, insertOnlineTableBlock]);

  const uploadClipboardMedia = useCallback(
    async (file: File) => {
      if (!workspaceId) {
        throw new Error("缺少空间信息，无法上传文件");
      }
      const form = new FormData();
      form.append("file", file);
      form.append("workspaceId", workspaceId);
      form.append("documentId", documentId);
      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 { asset: MediaAsset };
      if (!payload.asset) {
        throw new Error("上传返回数据缺失");
      }
      emitAssetsChanged(documentId, payload.asset);
      return payload.asset;
    },
    [documentId, workspaceId],
  );

  const stableEditorBridge = useMemo<EditorReferenceBridge>(
    () => ({
      undo: () => {
        try {
          const currentEditor = editorRef.current;
          if (!currentEditor) return;
          currentEditor.focus();
          currentEditor.undo();
        } catch {
          // ignore
        }
      },
      redo: () => {
        try {
          const currentEditor = editorRef.current;
          if (!currentEditor) return;
          currentEditor.focus();
          currentEditor.redo();
        } catch {
          // ignore
        }
      },
      openTableFullScreen: (tableId: string) => {
        setFullScreenTableIdRef.current(tableId);
      },
      insertMediaAsset: (asset: MediaAsset) => {
        insertMediaAssetRef.current(asset);
      },
      insertMindmapAsset: (args: { documentId: string; mindmapId: string }) => {
        insertMindmapRef.current(args);
      },
      insertOnlineTableAsset: (args: { documentId: string; tableId: string }) => {
        insertOnlineTableRef.current(args);
      },
      insertInlineReference: (target: ReferenceTarget, aliasText?: string) => {
        const currentEditor = editorRef.current;
        if (!currentEditor) return { blockId: null };
        currentEditor.focus();
        const cursor = currentEditor.getTextCursorPosition();
        const blockId = cursor?.block?.id ?? null;
        const text = aliasText || target.title || "无标题";
        currentEditor.insertInlineContent([
          {
            type: "link",
            href: buildDocumentPath(target.id),
            content: text,
          },
          " ",
        ]);
        return { blockId };
      },
      insertEmbedReference: (target: ReferenceTarget) => {
        const currentEditor = editorRef.current;
        if (!currentEditor) return { blockId: null };
        currentEditor.focus();
        const cursor = currentEditor.getTextCursorPosition();
        const referenceBlock =
          cursor?.block ?? currentEditor.topLevelBlocks[currentEditor.topLevelBlocks.length - 1];
        const blockId = generateBlockId();
        currentEditor.insertBlocks(
          [
            {
              id: blockId,
              type: "pageReference",
              props: {
                pageId: target.id,
                title: target.title ?? "无标题",
              },
            },
          ],
          referenceBlock,
          "after",
        );
        return { blockId };
      },
      replaceWithSnapshot: (payload: Json) => {
        const currentEditor = editorRef.current;
        if (!currentEditor) return;
        currentEditor.focus();
        const nextBlocks = Array.isArray(payload)
          ? payload
          : Array.isArray((payload as { blocks?: Json }).blocks)
            ? ((payload as { blocks?: Json }).blocks as Json)
            : [];
        if (!Array.isArray(nextBlocks)) {
          return;
        }
        currentEditor.replaceBlocks(currentEditor.topLevelBlocks, nextBlocks as never);
      },
      getCursorBlockId: () => {
        try {
          const currentEditor = editorRef.current;
          if (!currentEditor) return null;
          const cursor = currentEditor.getTextCursorPosition();
          return cursor?.block?.id ?? null;
        } catch {
          return null;
        }
      },
    }),
    [],
  );

  useEffect(() => {
    registerEditorBridge(editor ? stableEditorBridge : null);
  }, [editor, registerEditorBridge, stableEditorBridge]);

  useEffect(() => {
    return () => registerEditorBridge(null);
  }, [registerEditorBridge]);

  useEffect(() => {
    if (!editor) return;
    if (!workspaceId) return;
    const onKeyDown = (event: KeyboardEvent) => {
      const ctrlOrMeta = event.ctrlKey || event.metaKey;
      if (!ctrlOrMeta) return;
      if (!event.altKey) return;
      const key = String(event.key ?? "").toLowerCase();
      if (key !== "m") return;
      event.preventDefault();
      const cursor = editor.getTextCursorPosition();
      const blockId = cursor?.block?.id ?? null;
      if (blockId) {
        openCommentsForBlock({ workspaceId, documentId, blockId });
      } else {
        openCommentsForPage({ workspaceId, documentId });
      }
    };

    window.addEventListener("keydown", onKeyDown);
    return () => window.removeEventListener("keydown", onKeyDown);
  }, [documentId, editor, openCommentsForBlock, openCommentsForPage, workspaceId]);

  useEffect(() => {
    if (!editor) {
      return undefined;
    }
    // 说明：BlockNote 的 contenteditable 节点不直接暴露 spellcheck props。
    // 这里用 DOM 属性实现“全局选项：拼写检查”。
    try {
      const root = document.querySelector<HTMLElement>(".wolai-editor");
      if (root) {
        root.setAttribute("spellcheck", spellCheck ? "true" : "false");
        root.querySelectorAll<HTMLElement>("[contenteditable]").forEach((el) => {
          el.setAttribute("spellcheck", spellCheck ? "true" : "false");
        });
      }
    } catch {
      // ignore
    }

    const handlePaste = (event: ClipboardEvent) => {
      const activeElement = event.target;
      if (!(activeElement instanceof HTMLElement) || !activeElement.closest(".wolai-editor")) {
        return;
      }
      const items = Array.from(event.clipboardData?.files ?? []);
      if (items.length === 0) {
        return;
      }
      const imageFile = items.find((candidate) => candidate.type?.startsWith("image/"));
      if (!imageFile) {
        return;
      }
      event.preventDefault();
      void (async () => {
        try {
          const asset = await uploadClipboardMedia(imageFile);
          insertMediaAssetBlock(asset);
        } catch (error) {
          console.error(error);
          window.alert((error as Error).message ?? "粘贴图片失败，请稍后重试");
        }
      })();
    };
    window.addEventListener("paste", handlePaste);
    return () => window.removeEventListener("paste", handlePaste);
  }, [editor, insertMediaAssetBlock, spellCheck, uploadClipboardMedia]);

  useEffect(() => {
    if (!editor) {
      return;
    }
    const buffer = { char: "", blockId: "", timestamp: 0 };
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key !== "[" && event.key !== "#") {
        buffer.char = "";
        buffer.blockId = "";
        buffer.timestamp = 0;
        return;
      }
      const activeElement = document.activeElement;
      if (!(activeElement instanceof HTMLElement) || !activeElement.closest(".wolai-editor")) {
        return;
      }
      const { block } = editor.getTextCursorPosition();
      if (!block) {
        return;
      }
      const now = Date.now();
      if (
        buffer.char === event.key &&
        buffer.blockId === block.id &&
        now - buffer.timestamp < 450
      ) {
        event.preventDefault();
        trimTrailingCharacter(editor, block, event.key);
        openReferencePalette({
          referenceMode: event.key === "[" ? "inline" : "embed",
        });
        buffer.char = "";
        buffer.blockId = "";
        buffer.timestamp = 0;
      } else {
        buffer.char = event.key;
        buffer.blockId = block.id;
        buffer.timestamp = now;
      }
    };
    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [editor, openReferencePalette]);

  const layoutClass = cn(
    "relative mx-auto w-full",
    pageOptions.wideLayout ? "max-w-none" : "max-w-[980px]",
  );

  return (
    <>
      <div className={layoutClass}>
        <div className={editorWrapperClass}>
          <BlockNoteView
            editor={editor}
            theme="light"
            slashMenu={false}
            sideMenu={false}
            editable={!pageOptions.protectEditing && !readOnly}
            className={blocknoteClass}
          >
            {!isFullScreenTableOpen && (
              <SideMenuController
                sideMenu={(props: SideMenuProps<CustomBlockSchema>) => (
                  <CustomSideMenu
                    {...props}
                    currentDocumentId={documentId}
                    workspaceId={workspaceId}
                    unresolvedCommentCountByBlockId={unresolvedCommentCountByBlockId}
                  />
                )}
              />
            )}
          <CustomSlashMenu editor={editor} currentDocumentId={documentId} />
          </BlockNoteView>
          <div className="pointer-events-none absolute right-4 top-3 text-xs text-gray-400">
            {isSaving ? "保存中..." : saveError ? saveError : "已保存"}
          </div>
        </div>
        <DocumentToc entries={tocEntries} visible={pageOptions.showToc} onJump={jumpToHeading} onClose={onCloseToc} />
      </div>
      {/* 全屏表格编辑器 Modal */}
      {fullScreenTableId && (
        <FullScreenTableEditor
          tableId={fullScreenTableId}
          onClose={() => setFullScreenTableId(null)}
        />
      )}
    </>
  );
}
