"use client";

import { useEditor, EditorContent, ReactNodeViewRenderer, type Editor as TiptapEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { useEffect, useState, useRef } from "react";
import { AIAssist } from "./ai-assist";
import {
    Sparkles,
    Plus,
    GripVertical,
    ChevronUp,
    ChevronDown,
    MoreHorizontal,
    Copy,
    Trash2,
    Type,
    Heading1,
    Heading2,
    Heading3,
    List,
    ListOrdered,
    Quote,
    Code,
} from "lucide-react";
import { Toolbar } from "./editor/toolbar";
import { SlashCommand, getSuggestionItems, renderSuggestionItems } from "./editor/slash-command";
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
import { lowlight } from "lowlight";
import { CodeBlockComponent } from "./editor/code-block";
import Subscript from "@tiptap/extension-subscript";
import Superscript from "@tiptap/extension-superscript";
import Highlight from "@tiptap/extension-highlight";
import TaskList from "@tiptap/extension-task-list";
import TaskItem from "@tiptap/extension-task-item";
import { Callout } from "./editor/extensions/callout";
import { AIMark } from "./editor/extensions/ai-mark";
import { TaskItemComponent } from "./editor/extensions/task-item";
import { useSettingsStore, type AIPrompt } from "@/lib/settings-store";
import { Table } from "@tiptap/extension-table";
import TableRow from "@tiptap/extension-table-row";
import TableCell from "@tiptap/extension-table-cell";
import TableHeader from "@tiptap/extension-table-header";
import Image from "@tiptap/extension-image";
import Youtube from "@tiptap/extension-youtube";
import TextAlign from "@tiptap/extension-text-align";
import { Fragment, type Node as ProseMirrorNode, type Schema } from "@tiptap/pm/model";
import { TextSelection } from "@tiptap/pm/state";
import { buildDropIndicatorText, mergeBlockTexts, moveContiguousSpan } from "@/lib/editor-block-ops";

interface EditorProps {
    content: string;
    onChange: (content: string) => void;
    onEditorReady?: (editor: TiptapEditor) => void;
    onToggleAI?: () => void;
    onExport?: () => void;
    onOpenWikiLink?: (title: string) => void;
    editable?: boolean;
}

function resolveWikiLinkAtPos(doc: TiptapEditor["state"]["doc"], pos: number): string | null {
    const $pos = doc.resolve(pos);
    const start = $pos.start($pos.depth);
    const end = $pos.end($pos.depth);
    const text = doc.textBetween(start, end, "\n");
    const offset = pos - start;
    const re = /\[\[([^[\]]+)\]\]/g;

    let match: RegExpExecArray | null;
    while ((match = re.exec(text)) !== null) {
        const from = match.index;
        const to = from + match[0].length;
        if (offset >= from && offset <= to) {
            const title = (match[1] || "").trim();
            return title || null;
        }
    }
    return null;
}

function moveTopLevelBlockByIndex(editor: TiptapEditor, index: number, direction: "up" | "down"): number {
    const { doc } = editor.state;
    if (index < 0 || index >= doc.childCount) return index;
    if (direction === "up" && index === 0) return index;
    if (direction === "down" && index >= doc.childCount - 1) return index;

    const targetIndex = direction === "up" ? index - 1 : index + 1;
    const currentNode = doc.child(index);
    const targetNode = doc.child(targetIndex);

    let currentStart = 0;
    for (let i = 0; i < index; i += 1) {
        currentStart += doc.child(i).nodeSize;
    }
    const currentEnd = currentStart + currentNode.nodeSize;

    let targetStart = 0;
    for (let i = 0; i < targetIndex; i += 1) {
        targetStart += doc.child(i).nodeSize;
    }
    const targetEnd = targetStart + targetNode.nodeSize;

    const rangeStart = Math.min(currentStart, targetStart);
    const rangeEnd = Math.max(currentEnd, targetEnd);
    const ordered = direction === "up" ? [currentNode, targetNode] : [targetNode, currentNode];

    const tr = editor.state.tr.replaceWith(rangeStart, rangeEnd, Fragment.fromArray(ordered));
    const nextPos = direction === "up" ? targetStart : currentStart + targetNode.nodeSize;
    tr.setSelection(TextSelection.near(tr.doc.resolve(Math.max(1, nextPos + 1))));
    editor.view.dispatch(tr.scrollIntoView());
    editor.commands.focus();
    return targetIndex;
}

function moveTopLevelBlockSpanToInsertionIndex(
    editor: TiptapEditor,
    sourceStartIndex: number,
    sourceEndIndex: number,
    insertionIndex: number
): number {
    const { doc } = editor.state;
    if (sourceStartIndex < 0 || sourceStartIndex >= doc.childCount) return sourceStartIndex;
    if (sourceEndIndex < sourceStartIndex || sourceEndIndex >= doc.childCount) return sourceStartIndex;

    const nodes = Array.from({ length: doc.childCount }, (_, i) => doc.child(i));
    const { reordered, targetStart, changed } = moveContiguousSpan(nodes, sourceStartIndex, sourceEndIndex, insertionIndex);
    if (!changed) return sourceStartIndex;
    const movedCount = sourceEndIndex - sourceStartIndex + 1;

    const tr = editor.state.tr.replaceWith(0, doc.content.size, Fragment.fromArray(reordered));
    setSelectionToTopLevelBlockSpan(tr, targetStart, targetStart + movedCount - 1);
    editor.view.dispatch(tr.scrollIntoView());
    editor.commands.focus();
    return targetStart;
}

function getTopLevelBlockRange(doc: TiptapEditor["state"]["doc"], index: number): { start: number; end: number } | null {
    if (index < 0 || index >= doc.childCount) return null;
    let start = 0;
    for (let i = 0; i < index; i += 1) {
        start += doc.child(i).nodeSize;
    }
    const end = start + doc.child(index).nodeSize;
    return { start, end };
}

function getTopLevelBlockSelectionRange(
    doc: TiptapEditor["state"]["doc"],
    startIndex: number,
    endIndex: number
): { from: number; to: number } | null {
    if (startIndex < 0 || endIndex < startIndex || endIndex >= doc.childCount) return null;

    let startPos = 0;
    for (let i = 0; i < startIndex; i += 1) {
        startPos += doc.child(i).nodeSize;
    }

    let endPos = startPos;
    for (let i = startIndex; i <= endIndex; i += 1) {
        endPos += doc.child(i).nodeSize;
    }

    const from = Math.max(1, startPos + 1);
    const to = Math.max(from, endPos - 1);
    return { from, to };
}

function setSelectionToTopLevelBlockSpan(tr: Parameters<TiptapEditor["view"]["dispatch"]>[0], startIndex: number, endIndex: number): void {
    const range = getTopLevelBlockSelectionRange(tr.doc, startIndex, endIndex);
    if (!range) return;
    tr.setSelection(TextSelection.create(tr.doc, range.from, range.to));
}

function runOnTopLevelBlock(editor: TiptapEditor, index: number, action: () => boolean): boolean {
    const range = getTopLevelBlockRange(editor.state.doc, index);
    if (!range) return false;
    const from = Math.max(1, range.start + 1);
    const to = Math.max(from, range.end - 1);
    const tr = editor.state.tr.setSelection(TextSelection.create(editor.state.tr.doc, from, to));
    editor.view.dispatch(tr);
    editor.commands.focus();
    return action();
}

function getSelectedTopLevelBlockIndices(editor: TiptapEditor): number[] {
    const { doc, selection } = editor.state;
    if (doc.childCount === 0) return [];

    const from = Math.min(selection.from, selection.to);
    const to = Math.max(selection.from, selection.to);
    const indices: number[] = [];

    let cursor = 0;
    for (let i = 0; i < doc.childCount; i += 1) {
        const node = doc.child(i);
        const start = cursor;
        const end = cursor + node.nodeSize;
        const overlap = to > start && from < end;
        if (overlap) indices.push(i);
        cursor = end;
    }

    if (indices.length > 0) return indices;
    const currentIndex = getCurrentTopLevelBlockIndex(editor);
    return currentIndex === null ? [] : [currentIndex];
}

function resolveTargetTopLevelBlockIndices(editor: TiptapEditor, preferredIndex?: number | null): number[] {
    const selected = getSelectedTopLevelBlockIndices(editor);
    const hasRangeSelection = editor.state.selection.from !== editor.state.selection.to;
    // A text range spanning multiple top-level blocks should always stay a multi-block target.
    if (hasRangeSelection && selected.length > 1) return selected;
    if (preferredIndex === null || preferredIndex === undefined) return selected;
    if (preferredIndex < 0 || preferredIndex >= editor.state.doc.childCount) return selected;
    if (selected.length > 1 && selected.includes(preferredIndex)) return selected;
    return [preferredIndex];
}

function convertTopLevelBlock(editor: TiptapEditor, index: number, type: string): boolean {
    return runOnTopLevelBlock(editor, index, () => {
        const node = editor.state.selection.$from.node(1);
        switch (type) {
            case "paragraph":
                return editor.chain().focus().setParagraph().run();
            case "heading1":
                if (node.type.name === "heading" && node.attrs.level === 1) {
                    return editor.chain().focus().setParagraph().run();
                }
                return editor.chain().focus().setNode("heading", { level: 1 }).run();
            case "heading2":
                if (node.type.name === "heading" && node.attrs.level === 2) {
                    return editor.chain().focus().setParagraph().run();
                }
                return editor.chain().focus().setNode("heading", { level: 2 }).run();
            case "heading3":
                if (node.type.name === "heading" && node.attrs.level === 3) {
                    return editor.chain().focus().setParagraph().run();
                }
                return editor.chain().focus().setNode("heading", { level: 3 }).run();
            case "bulletList":
                return editor.chain().focus().toggleBulletList().run();
            case "orderedList":
                return editor.chain().focus().toggleOrderedList().run();
            case "blockquote":
                return editor.chain().focus().toggleBlockquote().run();
            case "codeBlock":
                if (node.type.name === "codeBlock") {
                    return editor.chain().focus().setParagraph().run();
                }
                return editor.chain().focus().setNode("codeBlock").run();
            default:
                return false;
        }
    });
}

function createTextOrNull(schema: Schema, text: string): ProseMirrorNode | null {
    return text ? schema.text(text) : null;
}

function createParagraphNode(schema: Schema, text: string): ProseMirrorNode {
    return schema.nodes.paragraph.create(null, createTextOrNull(schema, text) ?? undefined);
}

function createListItemNode(schema: Schema, text: string): ProseMirrorNode {
    return schema.nodes.listItem.create(null, createParagraphNode(schema, text));
}

function extractListItemsFromTopLevelNode(schema: Schema, node: ProseMirrorNode): ProseMirrorNode[] {
    if (node.type.name === "bulletList" || node.type.name === "orderedList") {
        const items: ProseMirrorNode[] = [];
        for (let i = 0; i < node.childCount; i += 1) {
            const listItem = node.child(i);
            items.push(createListItemNode(schema, listItem.textContent || ""));
        }
        return items;
    }
    return [createListItemNode(schema, node.textContent || "")];
}

function createConvertedNodeByType(
    schema: Schema,
    sourceNode: ProseMirrorNode,
    type: string,
    text: string
): ProseMirrorNode {
    switch (type) {
        case "paragraph":
            return createParagraphNode(schema, text);
        case "heading1":
            if (sourceNode.type.name === "heading" && sourceNode.attrs.level === 1) return createParagraphNode(schema, text);
            return schema.nodes.heading.create({ level: 1 }, createTextOrNull(schema, text) ?? undefined);
        case "heading2":
            if (sourceNode.type.name === "heading" && sourceNode.attrs.level === 2) return createParagraphNode(schema, text);
            return schema.nodes.heading.create({ level: 2 }, createTextOrNull(schema, text) ?? undefined);
        case "heading3":
            if (sourceNode.type.name === "heading" && sourceNode.attrs.level === 3) return createParagraphNode(schema, text);
            return schema.nodes.heading.create({ level: 3 }, createTextOrNull(schema, text) ?? undefined);
        case "blockquote":
            if (sourceNode.type.name === "blockquote") return createParagraphNode(schema, text);
            return schema.nodes.blockquote.create(null, createParagraphNode(schema, text));
        default:
            return sourceNode;
    }
}

function convertSelectedTopLevelBlocks(editor: TiptapEditor, type: string, preferredIndex?: number | null): boolean {
    const indices = resolveTargetTopLevelBlockIndices(editor, preferredIndex);
    if (indices.length === 0) return false;

    const start = indices[0];
    const end = indices[indices.length - 1];

    if (indices.length > 1 && type === "codeBlock") {
        const { doc, schema } = editor.state;
        const nodes = Array.from({ length: doc.childCount }, (_, i) => doc.child(i));
        const mergedText = mergeBlockTexts(nodes.slice(start, end + 1).map((node) => node.textContent || ""));
        const mergedCodeBlock = schema.nodes.codeBlock.create(null, createTextOrNull(schema, mergedText) ?? undefined);
        const transformed = [...nodes.slice(0, start), mergedCodeBlock, ...nodes.slice(end + 1)];

        const tr = editor.state.tr.replaceWith(0, doc.content.size, Fragment.fromArray(transformed));
        setSelectionToTopLevelBlockSpan(tr, start, start);
        editor.view.dispatch(tr.scrollIntoView());
        editor.commands.focus();
        return true;
    }

    if (indices.length > 1 && (type === "bulletList" || type === "orderedList")) {
        const { doc, schema } = editor.state;
        const nodes = Array.from({ length: doc.childCount }, (_, i) => doc.child(i));
        const listItems = nodes.slice(start, end + 1).flatMap((node) => extractListItemsFromTopLevelNode(schema, node));
        const listNode = schema.nodes[type].create(null, listItems);
        const transformed = [...nodes.slice(0, start), listNode, ...nodes.slice(end + 1)];
        const tr = editor.state.tr.replaceWith(0, doc.content.size, Fragment.fromArray(transformed));
        setSelectionToTopLevelBlockSpan(tr, start, start);
        editor.view.dispatch(tr.scrollIntoView());
        editor.commands.focus();
        return true;
    }

    if (indices.length > 1 && (type === "paragraph" || type === "heading1" || type === "heading2" || type === "heading3" || type === "blockquote")) {
        const { doc, schema } = editor.state;
        const selectedSet = new Set(indices);
        const nodes = Array.from({ length: doc.childCount }, (_, i) => doc.child(i));
        const transformed = nodes.map((node, index) => {
            if (!selectedSet.has(index)) return node;
            return createConvertedNodeByType(schema, node, type, node.textContent || "");
        });
        const tr = editor.state.tr.replaceWith(0, doc.content.size, Fragment.fromArray(transformed));
        const nextEnd = Math.min(start + indices.length - 1, tr.doc.childCount - 1);
        setSelectionToTopLevelBlockSpan(tr, start, nextEnd);
        editor.view.dispatch(tr.scrollIntoView());
        editor.commands.focus();
        return true;
    }

    let ok = false;
    // Process from tail to head so earlier transformations won't invalidate later indices.
    for (let i = indices.length - 1; i >= 0; i -= 1) {
        ok = convertTopLevelBlock(editor, indices[i], type) || ok;
    }

    if (ok && indices.length > 1) {
        const nextEnd = Math.min(start + indices.length - 1, editor.state.doc.childCount - 1);
        const range = getTopLevelBlockSelectionRange(editor.state.doc, start, nextEnd);
        if (range) {
            const tr = editor.state.tr.setSelection(TextSelection.create(editor.state.doc, range.from, range.to));
            editor.view.dispatch(tr.scrollIntoView());
            editor.commands.focus();
        }
    }
    return ok;
}

function findScrollableParent(el: HTMLElement | null): HTMLElement | null {
    let cursor: HTMLElement | null = el;
    while (cursor) {
        const style = window.getComputedStyle(cursor);
        const canScroll = (style.overflowY === "auto" || style.overflowY === "scroll") && cursor.scrollHeight > cursor.clientHeight;
        if (canScroll) return cursor;
        cursor = cursor.parentElement;
    }
    return null;
}

function getCurrentTopLevelBlockIndex(editor: TiptapEditor): number | null {
    const { doc, selection } = editor.state;
    const { $from } = selection;
    if ($from.depth < 1) return null;
    const index = $from.index(0);
    return index >= 0 && index < doc.childCount ? index : null;
}

function getTopLevelBlockElement(editor: TiptapEditor, index: number): HTMLElement | null {
    const { doc } = editor.state;
    if (index < 0 || index >= doc.childCount) return null;

    let pos = 0;
    for (let i = 0; i < index; i += 1) {
        pos += doc.child(i).nodeSize;
    }

    try {
        const domAt = editor.view.domAtPos(Math.max(1, pos + 1));
        let el: HTMLElement | null = domAt.node instanceof HTMLElement ? domAt.node : domAt.node.parentElement;
        const root = editor.view.dom;
        while (el && el.parentElement !== root) {
            el = el.parentElement;
        }
        return el && el.parentElement === root ? el : null;
    } catch {
        return null;
    }
}

function applyTopLevelBlockSelectionClasses(editor: TiptapEditor, indices: number[]): void {
    const root = editor.view.dom as HTMLElement;
    const selected = new Set(indices);
    const children = Array.from(root.children);
    for (let i = 0; i < children.length; i += 1) {
        const el = children[i];
        if (!(el instanceof HTMLElement)) continue;
        el.classList.toggle("block-selected", selected.has(i));
    }
}

function resolveInsertionIndex(editor: TiptapEditor, hostEl: HTMLElement, pointerY: number): { index: number; top: number } {
    const { doc } = editor.state;
    if (doc.childCount === 0) return { index: 0, top: 0 };

    const hostRect = hostEl.getBoundingClientRect();
    const y = pointerY - hostRect.top;

    const tops: number[] = [];
    const mids: number[] = [];
    const bottoms: number[] = [];
    for (let i = 0; i < doc.childCount; i += 1) {
        const el = getTopLevelBlockElement(editor, i);
        if (!el) continue;
        const rect = el.getBoundingClientRect();
        const top = rect.top - hostRect.top;
        const bottom = rect.bottom - hostRect.top;
        tops.push(top);
        mids.push(top + rect.height / 2);
        bottoms.push(bottom);
    }

    if (tops.length === 0) return { index: 0, top: 0 };

    for (let i = 0; i < mids.length; i += 1) {
        if (y < mids[i]) {
            return { index: i, top: tops[i] };
        }
    }

    return { index: mids.length, top: bottoms[bottoms.length - 1] };
}

function duplicateTopLevelBlockByIndex(editor: TiptapEditor, index: number): boolean {
    const range = getTopLevelBlockRange(editor.state.doc, index);
    if (!range) return false;
    const node = editor.state.doc.child(index);
    const tr = editor.state.tr.insert(range.end, node.copy(node.content));
    editor.view.dispatch(tr.scrollIntoView());
    editor.commands.focus();
    return true;
}

function duplicateSelectedTopLevelBlocks(editor: TiptapEditor, preferredIndex?: number | null): number | null {
    const indices = resolveTargetTopLevelBlockIndices(editor, preferredIndex);
    if (indices.length === 0) return null;

    if (indices.length === 1) {
        const ok = duplicateTopLevelBlockByIndex(editor, indices[0]);
        return ok ? indices[0] + 1 : null;
    }

    const { doc } = editor.state;
    const start = indices[0];
    const end = indices[indices.length - 1];
    const nodes = Array.from({ length: doc.childCount }, (_, i) => doc.child(i));
    const copied = nodes.slice(start, end + 1).map((node) => node.copy(node.content));
    const reordered = [...nodes.slice(0, end + 1), ...copied, ...nodes.slice(end + 1)];

    const tr = editor.state.tr.replaceWith(0, doc.content.size, Fragment.fromArray(reordered));
    const firstInsertedIndex = end + 1;
    setSelectionToTopLevelBlockSpan(tr, firstInsertedIndex, firstInsertedIndex + (end - start));
    editor.view.dispatch(tr.scrollIntoView());
    editor.commands.focus();
    return firstInsertedIndex;
}

function moveSelectedTopLevelBlocks(editor: TiptapEditor, direction: "up" | "down", preferredIndex?: number | null): number | null {
    const indices = resolveTargetTopLevelBlockIndices(editor, preferredIndex);
    if (indices.length === 0) return null;

    const start = indices[0];
    const end = indices[indices.length - 1];
    const { doc } = editor.state;

    if (direction === "up" && start === 0) return start;
    if (direction === "down" && end >= doc.childCount - 1) return start;

    if (indices.length === 1) {
        return moveTopLevelBlockByIndex(editor, start, direction);
    }

    const nodes = Array.from({ length: doc.childCount }, (_, i) => doc.child(i));
    const selected = nodes.slice(start, end + 1);
    let reordered: typeof nodes;
    let nextStart = start;

    if (direction === "up") {
        const before = nodes[start - 1];
        reordered = [...nodes.slice(0, start - 1), ...selected, before, ...nodes.slice(end + 1)];
        nextStart = start - 1;
    } else {
        const after = nodes[end + 1];
        reordered = [...nodes.slice(0, start), after, ...selected, ...nodes.slice(end + 2)];
        nextStart = start + 1;
    }

    const tr = editor.state.tr.replaceWith(0, doc.content.size, Fragment.fromArray(reordered));
    setSelectionToTopLevelBlockSpan(tr, nextStart, nextStart + (end - start));
    editor.view.dispatch(tr.scrollIntoView());
    editor.commands.focus();
    return nextStart;
}

async function copySelectedTopLevelBlocks(editor: TiptapEditor, preferredIndex?: number | null): Promise<boolean> {
    const indices = resolveTargetTopLevelBlockIndices(editor, preferredIndex);
    if (indices.length === 0) return false;
    const lines = indices.map((index) => editor.state.doc.child(index).textContent || "");
    await navigator.clipboard.writeText(lines.join("\n\n"));
    return true;
}

function deleteSelectedTopLevelBlocks(editor: TiptapEditor, preferredIndex?: number | null): number | null {
    const indices = resolveTargetTopLevelBlockIndices(editor, preferredIndex);
    if (indices.length === 0) return null;

    const { doc } = editor.state;
    if (indices.length >= doc.childCount) {
        editor.commands.setContent("<p></p>");
        editor.commands.focus();
        return 0;
    }

    const tr = editor.state.tr;
    for (let i = indices.length - 1; i >= 0; i -= 1) {
        const range = getTopLevelBlockRange(tr.doc, indices[i]);
        if (!range) continue;
        tr.delete(range.start, range.end);
    }

    const nextIndex = Math.max(0, Math.min(indices[0], tr.doc.childCount - 1));
    let cursorPos = 0;
    for (let i = 0; i < nextIndex; i += 1) {
        cursorPos += tr.doc.child(i).nodeSize;
    }
    tr.setSelection(TextSelection.near(tr.doc.resolve(Math.max(1, cursorPos + 1))));
    editor.view.dispatch(tr.scrollIntoView());
    editor.commands.focus();
    return nextIndex;
}

export function Editor({ content, onChange, onEditorReady, onToggleAI, onExport, onOpenWikiLink, editable = true }: EditorProps) {
    const [showAI, setShowAI] = useState(false);
    const [isGenerating, setIsGenerating] = useState(false);
    const [showBlockMenu, setShowBlockMenu] = useState(false);
    const [blockMenuTop, setBlockMenuTop] = useState(0);
    const [showBlockActionMenu, setShowBlockActionMenu] = useState(false);
    const [showShortcutHelp, setShowShortcutHelp] = useState(false);
    const [activeBlockIndex, setActiveBlockIndex] = useState<number | null>(null);
    const [actionToast, setActionToast] = useState<string | null>(null);
    const [dragPreview, setDragPreview] = useState<{
        visible: boolean;
        sourceStartIndex: number;
        sourceEndIndex: number;
        targetInsertionIndex: number;
        movedCount: number;
        insertionText: string;
        ghostTop: number;
        ghostLeft: number;
        ghostWidth: number;
        ghostText: string;
        indicatorTop: number;
    }>({
        visible: false,
        sourceStartIndex: -1,
        sourceEndIndex: -1,
        targetInsertionIndex: -1,
        movedCount: 0,
        insertionText: "",
        ghostTop: 0,
        ghostLeft: 0,
        ghostWidth: 0,
        ghostText: "",
        indicatorTop: 0,
    });
    const abortControllerRef = useRef<AbortController | null>(null);
    const contentWrapRef = useRef<HTMLDivElement>(null);
    const blockActionMenuRef = useRef<HTMLDivElement>(null);
    const showBlockActionMenuRef = useRef(false);
    const actionToastTimerRef = useRef<number | null>(null);
    const dragStateRef = useRef<{
        dragging: boolean;
        lastY: number;
        lastPointerY: number;
        rafId: number | null;
    }>({ dragging: false, lastY: 0, lastPointerY: 0, rafId: null });
    const dragScrollRef = useRef<HTMLElement | null>(null);
    const dragPreviewRef = useRef<{
        sourceStartIndex: number;
        sourceEndIndex: number;
        targetInsertionIndex: number;
    }>({ sourceStartIndex: -1, sourceEndIndex: -1, targetInsertionIndex: -1 });
    const activeBlockIndexRef = useRef<number | null>(null);
    const suppressNextUpdateRef = useRef(false);
    const allowOnUpdateRef = useRef(false);
    const { fontFamily, fontSize, lineHeight, tableLineHeight } = useSettingsStore();

    const editor = useEditor({
        extensions: [
            StarterKit.configure({
                heading: {
                    levels: [1, 2, 3],
                },
                link: {
                    openOnClick: false,
                    HTMLAttributes: {
                        class: "cursor-pointer text-blue-600 dark:text-blue-400 hover:underline hover:text-blue-800 dark:hover:text-blue-300 transition-colors",
                    },
                },
                codeBlock: false,
                bulletList: {
                    keepMarks: true,
                    keepAttributes: false,
                },
                orderedList: {
                    keepMarks: true,
                    keepAttributes: false,
                },
            }),
            SlashCommand.configure({
                suggestion: {
                    items: getSuggestionItems,
                    render: renderSuggestionItems,
                },
            }),
            CodeBlockLowlight.extend({
                addNodeView() {
                    return ReactNodeViewRenderer(CodeBlockComponent);
                },
            }).configure({ lowlight, defaultLanguage: "plaintext" }),
            Subscript,
            Superscript,
            Highlight.configure({
                multicolor: true,
            }),
            TaskList,
            TaskItem.configure({
                nested: true,
            }).extend({
                addNodeView() {
                    return ReactNodeViewRenderer(TaskItemComponent);
                },
            }),
            Callout,
            AIMark,
            Table.configure({
                resizable: true,
            }),
            TableRow,
            TableHeader,
            TableCell,
            Image.configure({
                inline: true,
                allowBase64: true,
            }),
            Youtube.configure({
                controls: false,
            }),
            TextAlign.configure({
                types: ["heading", "paragraph"],
            }),
        ],
        content,
        onUpdate: ({ editor }) => {
            if (suppressNextUpdateRef.current) {
                suppressNextUpdateRef.current = false;
                return;
            }
            if (!allowOnUpdateRef.current) return;
            if (editor.getHTML() !== content) {
                onChange(editor.getHTML());
            }
        },
        editorProps: {
            attributes: {
                class: "prose prose-zinc dark:prose-invert max-w-none focus:outline-none min-h-[300px]",
                style: `font-family: ${fontFamily}; font-size: ${fontSize}px; --editor-line-height: ${lineHeight}; --table-line-height: ${tableLineHeight};`,
                spellcheck: "false",
            },
            handleKeyDown: (_view, event) => {
                allowOnUpdateRef.current = true;
                if (!editable) return false;

                const editorInstance = editor;
                if (!editorInstance) return false;

                // Alt + Shift + ArrowUp / ArrowDown: move current block.
                if (event.altKey && event.shiftKey && (event.key === "ArrowUp" || event.key === "ArrowDown")) {
                    event.preventDefault();
                    const direction = event.key === "ArrowUp" ? "up" : "down";
                    const next = moveSelectedTopLevelBlocks(editorInstance, direction);
                    if (next !== null) setActiveBlockIndex(next);
                    return true;
                }

                // Ctrl/Cmd + Shift + D: duplicate current block.
                if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key.toLowerCase() === "d") {
                    event.preventDefault();
                    const next = duplicateSelectedTopLevelBlocks(editorInstance);
                    if (next !== null) setActiveBlockIndex(next);
                    return true;
                }

                return false;
            },
            handleClick: (view, pos, event) => {
                allowOnUpdateRef.current = true;
                if (!onOpenWikiLink) return false;
                const pointerEvent = event as MouseEvent;
                if (!pointerEvent.metaKey && !pointerEvent.ctrlKey) return false;

                const title = resolveWikiLinkAtPos(view.state.doc, pos);
                if (!title) return false;

                pointerEvent.preventDefault();
                onOpenWikiLink(title);
                return true;
            },
            handleDOMEvents: {
                mousedown: () => {
                    allowOnUpdateRef.current = true;
                    return false;
                },
                paste: () => {
                    allowOnUpdateRef.current = true;
                    return false;
                },
                beforeinput: () => {
                    allowOnUpdateRef.current = true;
                    return false;
                },
            },
        },
        editable,
        immediatelyRender: false,
    });

    useEffect(() => {
        if (editor) {
            editor.setEditable(editable);
        }
    }, [editor, editable]);

    useEffect(() => {
        const dragState = dragStateRef.current;
        return () => {
            if (dragState.rafId) {
                cancelAnimationFrame(dragState.rafId);
            }
            window.onpointermove = null;
            window.onpointerup = null;
        };
    }, []);

    useEffect(() => {
        activeBlockIndexRef.current = activeBlockIndex;
    }, [activeBlockIndex]);

    useEffect(() => {
        showBlockActionMenuRef.current = showBlockActionMenu;
    }, [showBlockActionMenu]);

    useEffect(() => {
        return () => {
            if (actionToastTimerRef.current) {
                window.clearTimeout(actionToastTimerRef.current);
            }
        };
    }, []);

    useEffect(() => {
        dragPreviewRef.current = {
            sourceStartIndex: dragPreview.sourceStartIndex,
            sourceEndIndex: dragPreview.sourceEndIndex,
            targetInsertionIndex: dragPreview.targetInsertionIndex,
        };
    }, [dragPreview.sourceStartIndex, dragPreview.sourceEndIndex, dragPreview.targetInsertionIndex]);

    const stopHandleDrag = () => {
        dragStateRef.current.dragging = false;
        dragStateRef.current.lastY = 0;
        dragStateRef.current.lastPointerY = 0;
        setDragPreview((prev) => ({ ...prev, visible: false }));
        dragScrollRef.current = null;
        if (dragStateRef.current.rafId) {
            cancelAnimationFrame(dragStateRef.current.rafId);
            dragStateRef.current.rafId = null;
        }
        window.onpointermove = null;
        window.onpointerup = null;
    };

    const startHandleDrag = (startY: number) => {
        if (!editor || !contentWrapRef.current) return;
        const tiptap = editor;
        const currentIndex = activeBlockIndexRef.current ?? getCurrentTopLevelBlockIndex(tiptap);
        if (currentIndex === null) return;
        const indices = resolveTargetTopLevelBlockIndices(tiptap, currentIndex);
        if (indices.length === 0) return;
        const sourceStart = indices[0];
        const sourceEnd = indices[indices.length - 1];
        const blockEl = getTopLevelBlockElement(tiptap, sourceStart);
        if (!blockEl) return;
        const blockEndEl = getTopLevelBlockElement(tiptap, sourceEnd);
        if (!blockEndEl) return;
        const hostRect = contentWrapRef.current.getBoundingClientRect();
        const blockRect = blockEl.getBoundingClientRect();
        const blockEndRect = blockEndEl.getBoundingClientRect();
        dragScrollRef.current = findScrollableParent(contentWrapRef.current);
        const movedCount = sourceEnd - sourceStart + 1;

        dragStateRef.current.dragging = true;
        dragStateRef.current.lastY = startY;
        dragStateRef.current.lastPointerY = startY;
        activeBlockIndexRef.current = sourceStart;
        setActiveBlockIndex(sourceStart);
        setDragPreview({
            visible: true,
            sourceStartIndex: sourceStart,
            sourceEndIndex: sourceEnd,
            targetInsertionIndex: sourceEnd + 1,
            movedCount,
            insertionText: buildDropIndicatorText(sourceEnd + 1, tiptap.state.doc.childCount),
            ghostTop: Math.max(0, blockRect.top - hostRect.top),
            ghostLeft: 0,
            ghostWidth: Math.max(180, blockRect.width),
            ghostText:
                indices.length > 1
                    ? `${(tiptap.state.doc.child(sourceStart).textContent || "空块").slice(0, 80)} 等 ${indices.length} 个块`
                    : (tiptap.state.doc.child(sourceStart).textContent || "空块").slice(0, 140),
            indicatorTop: Math.max(0, blockEndRect.bottom - hostRect.top),
        });

        window.onpointermove = (ev: PointerEvent) => {
            if (!dragStateRef.current.dragging) return;
            if (!contentWrapRef.current) return;
            dragStateRef.current.lastPointerY = ev.clientY;
            const host = contentWrapRef.current;
            const hostBounds = host.getBoundingClientRect();

            const scroller = dragScrollRef.current;
            if (scroller) {
                const rect = scroller.getBoundingClientRect();
                const edge = 56;
                const maxStep = 22;
                if (ev.clientY < rect.top + edge) {
                    const ratio = (rect.top + edge - ev.clientY) / edge;
                    scroller.scrollTop -= Math.ceil(maxStep * Math.min(1, Math.max(0, ratio)));
                } else if (ev.clientY > rect.bottom - edge) {
                    const ratio = (ev.clientY - (rect.bottom - edge)) / edge;
                    scroller.scrollTop += Math.ceil(maxStep * Math.min(1, Math.max(0, ratio)));
                }
            }
            const { index, top } = resolveInsertionIndex(tiptap, host, ev.clientY);
            setDragPreview((prev) => ({
                ...prev,
                targetInsertionIndex: index,
                insertionText: buildDropIndicatorText(index, tiptap.state.doc.childCount),
                indicatorTop: Math.max(0, top),
                ghostTop: Math.max(0, ev.clientY - hostBounds.top - 18),
            }));
        };

        window.onpointerup = () => {
            const sourceStartIndex = dragPreviewRef.current.sourceStartIndex === -1 ? sourceStart : dragPreviewRef.current.sourceStartIndex;
            const sourceEndIndex = dragPreviewRef.current.sourceEndIndex === -1 ? sourceEnd : dragPreviewRef.current.sourceEndIndex;
            const targetInsertionIndex =
                dragPreviewRef.current.targetInsertionIndex === -1 ? sourceEnd + 1 : dragPreviewRef.current.targetInsertionIndex;
            const nextIndex = moveTopLevelBlockSpanToInsertionIndex(tiptap, sourceStartIndex, sourceEndIndex, targetInsertionIndex);
            setActiveBlockIndex(nextIndex);
            stopHandleDrag();
        };
    };

    useEffect(() => {
        if (editor && content !== editor.getHTML()) {
            // 不再调用 cleanupAccidentalStandaloneInlineCode，直接使用原始内容
            suppressNextUpdateRef.current = true;
            allowOnUpdateRef.current = false;
            queueMicrotask(() => {
                editor.commands.setContent(content);
            });
        }
        if (editor && onEditorReady) {
            onEditorReady(editor);
        }
    }, [content, editor, onEditorReady]);

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

        const updateBlockMenu = () => {
            if (!editable || !contentWrapRef.current) {
                applyTopLevelBlockSelectionClasses(editor, []);
                setShowBlockMenu(false);
                setShowBlockActionMenu(false);
                setActiveBlockIndex(null);
                return;
            }
            if (!editor.isFocused) {
                applyTopLevelBlockSelectionClasses(editor, []);
                setShowBlockMenu(false);
                setShowBlockActionMenu(false);
                setActiveBlockIndex(null);
                return;
            }

            const { selection } = editor.state;
            const selectedIndices = getSelectedTopLevelBlockIndices(editor);
            applyTopLevelBlockSelectionClasses(editor, selectedIndices);
            const $from = selection.$from;
            if (!$from.parent.isTextblock) {
                setShowBlockMenu(false);
                setShowBlockActionMenu(false);
                setActiveBlockIndex(null);
                return;
            }

            try {
                const coords = editor.view.coordsAtPos(selection.from);
                const hostRect = contentWrapRef.current.getBoundingClientRect();
                const nextTop = Math.max(0, coords.top - hostRect.top - 2);
                setBlockMenuTop(nextTop);
                setShowBlockMenu(true);
                setActiveBlockIndex($from.index(0));
            } catch {
                setShowBlockMenu(false);
                setShowBlockActionMenu(false);
                setActiveBlockIndex(null);
            }
        };

        updateBlockMenu();

        editor.on("focus", updateBlockMenu);
        editor.on("blur", () => {
            applyTopLevelBlockSelectionClasses(editor, []);
            setShowBlockMenu(false);
            // Keep active block index for action menu clicks after focus loss.
            if (!showBlockActionMenuRef.current) {
                setActiveBlockIndex(null);
            }
        });
        editor.on("selectionUpdate", updateBlockMenu);
        editor.on("transaction", updateBlockMenu);
        window.addEventListener("resize", updateBlockMenu);

        return () => {
            applyTopLevelBlockSelectionClasses(editor, []);
            editor.off("focus", updateBlockMenu);
            editor.off("selectionUpdate", updateBlockMenu);
            editor.off("transaction", updateBlockMenu);
            window.removeEventListener("resize", updateBlockMenu);
        };
    }, [editor, editable]);

    useEffect(() => {
        if (!showBlockActionMenu) return;
        const onMouseDown = (event: MouseEvent) => {
            if (!blockActionMenuRef.current) return;
            if (blockActionMenuRef.current.contains(event.target as Node)) return;
            setShowBlockActionMenu(false);
        };
        window.addEventListener("mousedown", onMouseDown);
        return () => window.removeEventListener("mousedown", onMouseDown);
    }, [showBlockActionMenu]);

    const handleStopAI = () => {
        if (abortControllerRef.current) {
            abortControllerRef.current.abort();
            abortControllerRef.current = null;
        }
        setIsGenerating(false);
    };

    const showActionToastMessage = (message: string) => {
        setActionToast(message);
        if (actionToastTimerRef.current) {
            window.clearTimeout(actionToastTimerRef.current);
        }
        actionToastTimerRef.current = window.setTimeout(() => {
            setActionToast(null);
            actionToastTimerRef.current = null;
        }, 1100);
    };

    const handleAISuggest = async (prompt: AIPrompt) => {
        if (!editor) return;

        const { aiConfig } = useSettingsStore.getState();
        if (!aiConfig.apiKey) {
            alert("请先在设置中配置 AI API Key");
            return;
        }

        const { from, to } = editor.state.selection;
        const selectedText = editor.state.doc.textBetween(from, to, " ");

        const systemPrompt = prompt.systemPrompt || "你是一个乐于助人的助手。";
        let userPrompt = selectedText;

        if (!userPrompt) {
            const pos = from;
            userPrompt = editor.state.doc.textBetween(Math.max(0, pos - 1000), pos, "\n");
        }

        setShowAI(false);
        setIsGenerating(true);
        abortControllerRef.current = new AbortController();

        try {
            const response = await fetch("/api/ai/chat", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    config: aiConfig,
                    messages: [
                        { role: "system", content: systemPrompt },
                        { role: "user", content: userPrompt },
                    ],
                }),
                signal: abortControllerRef.current.signal,
            });

            if (!response.ok || !response.body) {
                throw new Error(await response.text());
            }

            const reader = response.body.getReader();
            const decoder = new TextDecoder();

            editor.chain().focus().insertContent("\n\n").toggleMark("aiMark").run();

            while (true) {
                const { done, value } = await reader.read();
                if (done) break;

                const chunk = decoder.decode(value);
                const lines = chunk.split("\n");
                for (const line of lines) {
                    if (line.startsWith("data: ") && line !== "data: [DONE]") {
                        try {
                            const data = JSON.parse(line.slice(6));
                            const delta = data.choices?.[0]?.delta;
                            const content = typeof delta?.content === "string" ? delta.content : "";
                            if (content) {
                                editor.commands.insertContent(content);
                            }
                        } catch {
                            // Ignore malformed stream chunk.
                        }
                    }
                }
            }
            editor.chain().focus().insertContent("\n\n").unsetMark("aiMark").run();
        } catch (e: unknown) {
            if (e instanceof Error && e.name === "AbortError") {
                editor.chain().focus().insertContent(" [已停止]").unsetMark("aiMark").run();
            } else {
                console.error("AI Error", e);
                alert("AI 请求失败，请检查配置或网络");
            }
        } finally {
            setIsGenerating(false);
            abortControllerRef.current = null;
        }
    };

    if (!editor) return null;

    return (
        <div className="relative group/editor flex flex-col min-h-full">
            <Toolbar editor={editor} onToggleAI={onToggleAI} onExport={onExport} />

            {showAI && (
                <div className="absolute top-12 right-4 z-50">
                    <AIAssist isOpen={showAI} onSuggest={handleAISuggest} onClose={() => setShowAI(false)} />
                </div>
            )}

            <div ref={contentWrapRef} className="relative mt-3 flex-1 md:mt-4">
                {dragPreview.visible && (
                    <div
                        className="pointer-events-none absolute left-0 right-0 z-[14] h-[3px] rounded-full bg-sky-500 shadow-[0_0_0_1px_hsl(205_100%_62%/0.45),0_0_16px_hsl(205_100%_62%/0.55)]"
                        style={{ top: dragPreview.indicatorTop }}
                        aria-hidden="true"
                    />
                )}
                {dragPreview.visible && (
                    <div
                        className="pointer-events-none absolute left-0 z-[15] rounded-md border border-sky-400/40 bg-background/90 px-2 py-1 text-[11px] text-foreground shadow-sm"
                        style={{ top: Math.max(0, dragPreview.indicatorTop - 30) }}
                        aria-hidden="true"
                    >
                        {dragPreview.insertionText}
                    </div>
                )}
                {dragPreview.visible && (
                    <div
                        className="pointer-events-none absolute z-[16] text-[1.02rem] leading-[var(--editor-line-height,1.5)] text-foreground/55"
                        style={{
                            top: dragPreview.ghostTop,
                            left: dragPreview.ghostLeft,
                            width: dragPreview.ghostWidth,
                            fontFamily,
                            fontSize: `${fontSize}px`,
                        }}
                        aria-hidden="true"
                    >
                        <div className="mb-1 inline-flex rounded-md border border-sky-400/35 bg-sky-500/10 px-1.5 py-0.5 text-[10px] font-medium text-sky-700 dark:text-sky-300">
                            将移动 {dragPreview.movedCount} 个块
                        </div>
                        <div className="truncate">{dragPreview.ghostText}</div>
                    </div>
                )}
                {showBlockMenu && (
                    <div
                        className="absolute left-0 z-20 hidden -translate-x-[calc(100%+0.35rem)] items-center gap-1 md:flex"
                        style={{ top: blockMenuTop }}
                        aria-label="块级操作"
                    >
                        <button
                            onMouseDown={(e) => e.preventDefault()}
                            onClick={() => {
                                editor.chain().focus().insertContent("/").run();
                            }}
                            className="ui-icon-btn h-7 w-7 rounded-md bg-background/80"
                            title="插入块（/）"
                            aria-label="插入块（斜杠命令）"
                        >
                            <Plus size={14} />
                        </button>
                        <button
                            onMouseDown={(e) => e.preventDefault()}
                            onPointerDown={(e) => {
                                e.preventDefault();
                                startHandleDrag(e.clientY);
                            }}
                            className="ui-icon-btn h-7 w-7 cursor-grab rounded-md bg-background/80 active:cursor-grabbing"
                            title="拖动以重排块"
                            aria-label="拖动以重排块"
                        >
                            <GripVertical size={14} />
                        </button>
                        <button
                            onMouseDown={(e) => e.preventDefault()}
                            onClick={() => {
                                const idx = getCurrentTopLevelBlockIndex(editor);
                                if (idx !== null) {
                                    activeBlockIndexRef.current = idx;
                                    setActiveBlockIndex(idx);
                                }
                                if (!showBlockActionMenu) {
                                    setShowShortcutHelp(false);
                                }
                                setShowBlockActionMenu((v) => !v);
                            }}
                            className="ui-icon-btn h-7 w-7 rounded-md bg-background/80"
                            title="块菜单"
                            aria-label="打开块菜单"
                        >
                            <MoreHorizontal size={14} />
                        </button>
                        {showBlockActionMenu && (
                            <div
                                ref={blockActionMenuRef}
                                className="ui-card absolute left-7 top-8 z-30 max-h-[62vh] min-w-[220px] overflow-y-auto p-1.5 shadow-xl"
                                role="menu"
                                aria-label="块操作菜单"
                                onMouseDown={(e) => e.preventDefault()}
                            >
                                <button
                                    onMouseDown={(e) => e.preventDefault()}
                                    onClick={() => {
                                        const menuIndex = activeBlockIndexRef.current ?? getCurrentTopLevelBlockIndex(editor);
                                        const next = moveSelectedTopLevelBlocks(editor, "up", menuIndex);
                                        if (next !== null) setActiveBlockIndex(next);
                                        if (next !== null) {
                                            const count = resolveTargetTopLevelBlockIndices(editor, menuIndex).length;
                                            showActionToastMessage(`已上移 ${count} 个块`);
                                        }
                                        setShowBlockActionMenu(false);
                                    }}
                                    className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-foreground hover:bg-accent"
                                >
                                    <ChevronUp size={14} />
                                    上移
                                </button>
                                <button
                                    onMouseDown={(e) => e.preventDefault()}
                                    onClick={() => {
                                        const menuIndex = activeBlockIndexRef.current ?? getCurrentTopLevelBlockIndex(editor);
                                        const next = moveSelectedTopLevelBlocks(editor, "down", menuIndex);
                                        if (next !== null) setActiveBlockIndex(next);
                                        if (next !== null) {
                                            const count = resolveTargetTopLevelBlockIndices(editor, menuIndex).length;
                                            showActionToastMessage(`已下移 ${count} 个块`);
                                        }
                                        setShowBlockActionMenu(false);
                                    }}
                                    className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-foreground hover:bg-accent"
                                >
                                    <ChevronDown size={14} />
                                    下移
                                </button>
                                <button
                                    onMouseDown={(e) => e.preventDefault()}
                                    onClick={async () => {
                                        const menuIndex = activeBlockIndexRef.current ?? getCurrentTopLevelBlockIndex(editor);
                                        const count = resolveTargetTopLevelBlockIndices(editor, menuIndex).length;
                                        await copySelectedTopLevelBlocks(editor, menuIndex);
                                        showActionToastMessage(`已复制 ${count} 个块文本`);
                                        setShowBlockActionMenu(false);
                                    }}
                                    className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-foreground hover:bg-accent"
                                >
                                    <Copy size={14} />
                                    复制选中块文本
                                </button>
                                <button
                                    onMouseDown={(e) => e.preventDefault()}
                                    onClick={() => {
                                        const menuIndex = activeBlockIndexRef.current ?? getCurrentTopLevelBlockIndex(editor);
                                        const count = resolveTargetTopLevelBlockIndices(editor, menuIndex).length;
                                        const next = deleteSelectedTopLevelBlocks(editor, menuIndex);
                                        if (next !== null) setActiveBlockIndex(next);
                                        if (next !== null) showActionToastMessage(`已删除 ${count} 个块`);
                                        setShowBlockActionMenu(false);
                                    }}
                                    className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-destructive hover:bg-destructive/10"
                                >
                                    <Trash2 size={14} />
                                    删除选中块
                                </button>
                                <button
                                    onMouseDown={(e) => e.preventDefault()}
                                    onClick={() => {
                                        const menuIndex = activeBlockIndexRef.current ?? getCurrentTopLevelBlockIndex(editor);
                                        const count = resolveTargetTopLevelBlockIndices(editor, menuIndex).length;
                                        const next = duplicateSelectedTopLevelBlocks(editor, menuIndex);
                                        if (next !== null) setActiveBlockIndex(next);
                                        if (next !== null) showActionToastMessage(`已插入 ${count} 个块副本`);
                                        setShowBlockActionMenu(false);
                                    }}
                                    className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-foreground hover:bg-accent"
                                >
                                    <Copy size={14} />
                                    插入选中块副本
                                </button>
                                <div className="my-1 h-px bg-border/70" />
                                <div className="px-2 py-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">转换块类型</div>
                                <div className="grid grid-cols-2 gap-1 p-1">
                                    <button
                                        onMouseDown={(e) => e.preventDefault()}
                                        onClick={() => {
                                            const menuIndex = activeBlockIndexRef.current ?? getCurrentTopLevelBlockIndex(editor);
                                            convertSelectedTopLevelBlocks(editor, "paragraph", menuIndex);
                                            showActionToastMessage("已转换为段落");
                                            setShowBlockActionMenu(false);
                                        }}
                                        className="flex items-center gap-1.5 rounded-md px-2 py-1.5 text-xs text-foreground hover:bg-accent"
                                    >
                                        <Type size={13} />
                                        段落
                                    </button>
                                    <button
                                        onMouseDown={(e) => e.preventDefault()}
                                        onClick={() => {
                                            const menuIndex = activeBlockIndexRef.current ?? getCurrentTopLevelBlockIndex(editor);
                                            convertSelectedTopLevelBlocks(editor, "heading1", menuIndex);
                                            showActionToastMessage("已转换为标题 1");
                                            setShowBlockActionMenu(false);
                                        }}
                                        className="flex items-center gap-1.5 rounded-md px-2 py-1.5 text-xs text-foreground hover:bg-accent"
                                    >
                                        <Heading1 size={13} />
                                        标题1
                                    </button>
                                    <button
                                        onMouseDown={(e) => e.preventDefault()}
                                        onClick={() => {
                                            const menuIndex = activeBlockIndexRef.current ?? getCurrentTopLevelBlockIndex(editor);
                                            convertSelectedTopLevelBlocks(editor, "heading2", menuIndex);
                                            showActionToastMessage("已转换为标题 2");
                                            setShowBlockActionMenu(false);
                                        }}
                                        className="flex items-center gap-1.5 rounded-md px-2 py-1.5 text-xs text-foreground hover:bg-accent"
                                    >
                                        <Heading2 size={13} />
                                        标题2
                                    </button>
                                    <button
                                        onMouseDown={(e) => e.preventDefault()}
                                        onClick={() => {
                                            const menuIndex = activeBlockIndexRef.current ?? getCurrentTopLevelBlockIndex(editor);
                                            convertSelectedTopLevelBlocks(editor, "heading3", menuIndex);
                                            showActionToastMessage("已转换为标题 3");
                                            setShowBlockActionMenu(false);
                                        }}
                                        className="flex items-center gap-1.5 rounded-md px-2 py-1.5 text-xs text-foreground hover:bg-accent"
                                    >
                                        <Heading3 size={13} />
                                        标题3
                                    </button>
                                    <button
                                        onMouseDown={(e) => e.preventDefault()}
                                        onClick={() => {
                                            const menuIndex = activeBlockIndexRef.current ?? getCurrentTopLevelBlockIndex(editor);
                                            convertSelectedTopLevelBlocks(editor, "bulletList", menuIndex);
                                            showActionToastMessage("已转换为无序列表");
                                            setShowBlockActionMenu(false);
                                        }}
                                        className="flex items-center gap-1.5 rounded-md px-2 py-1.5 text-xs text-foreground hover:bg-accent"
                                    >
                                        <List size={13} />
                                        无序列表
                                    </button>
                                    <button
                                        onMouseDown={(e) => e.preventDefault()}
                                        onClick={() => {
                                            const menuIndex = activeBlockIndexRef.current ?? getCurrentTopLevelBlockIndex(editor);
                                            convertSelectedTopLevelBlocks(editor, "orderedList", menuIndex);
                                            showActionToastMessage("已转换为有序列表");
                                            setShowBlockActionMenu(false);
                                        }}
                                        className="flex items-center gap-1.5 rounded-md px-2 py-1.5 text-xs text-foreground hover:bg-accent"
                                    >
                                        <ListOrdered size={13} />
                                        有序列表
                                    </button>
                                    <button
                                        onMouseDown={(e) => e.preventDefault()}
                                        onClick={() => {
                                            const menuIndex = activeBlockIndexRef.current ?? getCurrentTopLevelBlockIndex(editor);
                                            convertSelectedTopLevelBlocks(editor, "blockquote", menuIndex);
                                            showActionToastMessage("已转换为引用");
                                            setShowBlockActionMenu(false);
                                        }}
                                        className="flex items-center gap-1.5 rounded-md px-2 py-1.5 text-xs text-foreground hover:bg-accent"
                                    >
                                        <Quote size={13} />
                                        引用
                                    </button>
                                    <button
                                        onMouseDown={(e) => e.preventDefault()}
                                        onClick={() => {
                                            const menuIndex = activeBlockIndexRef.current ?? getCurrentTopLevelBlockIndex(editor);
                                            convertSelectedTopLevelBlocks(editor, "codeBlock", menuIndex);
                                            showActionToastMessage("已转换为代码块");
                                            setShowBlockActionMenu(false);
                                        }}
                                        className="flex items-center gap-1.5 rounded-md px-2 py-1.5 text-xs text-foreground hover:bg-accent"
                                    >
                                        <Code size={13} />
                                        代码块
                                    </button>
                                </div>
                                <div className="my-1 h-px bg-border/70" />
                                <button
                                    onMouseDown={(e) => e.preventDefault()}
                                    onClick={() => setShowShortcutHelp((v) => !v)}
                                    className="flex w-full items-center justify-between rounded-md px-2 py-1.5 text-xs text-muted-foreground hover:bg-accent"
                                >
                                    <span>查看快捷键</span>
                                    <span>{showShortcutHelp ? "收起" : "展开"}</span>
                                </button>
                                {showShortcutHelp && (
                                    <div className="px-2 pb-1 pt-0.5 text-[11px] text-muted-foreground">
                                        <div>Alt + Shift + ↑/↓: 上下移动选中块</div>
                                        <div>Ctrl/Cmd + Shift + D: 插入块副本</div>
                                        <div>/ : 打开插入命令</div>
                                    </div>
                                )}
                            </div>
                        )}
                    </div>
                )}
                <EditorContent editor={editor} />
            </div>
            {actionToast && (
                <div className="pointer-events-none fixed bottom-[max(1rem,env(safe-area-inset-bottom))] left-1/2 z-40 -translate-x-1/2 rounded-md border border-border/70 bg-card/95 px-3 py-1.5 text-xs text-foreground shadow-md md:bottom-6">
                    {actionToast}
                </div>
            )}

            <button
                onClick={(e) => {
                    e.preventDefault();
                    if (isGenerating) {
                        handleStopAI();
                    } else {
                        setShowAI(!showAI);
                    }
                }}
                className={`fixed bottom-[max(1rem,env(safe-area-inset-bottom))] right-4 p-3 rounded-full shadow-lg hover:scale-110 transition-transform z-40 md:bottom-8 md:right-8 ${isGenerating ? "bg-red-500 text-white animate-pulse" : "bg-primary text-primary-foreground"
                    }`}
                title={isGenerating ? "停止生成" : "AI 助手"}
            >
                {isGenerating ? <div className="h-5 w-5 bg-current rounded-sm" /> : <Sparkles size={20} />}
            </button>
        </div>
    );
}
