use leptos::prelude::*;
use leptos_tiptap::{
    TiptapCodeBlockAttributes, TiptapColorAttributes, TiptapContent, TiptapEditorHandle,
    TiptapHeadingLevel, TiptapHighlightAttributes, TiptapInsertContentOptions, TiptapNodeName,
    TiptapRange, TiptapSchemaTarget, TiptapTextAlign, TiptapTocNodeAttrs,
};
use serde::Deserialize;
use serde_json::{json, Value};
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::{spawn_local, JsFuture};
use web_sys::{window, CustomEvent, CustomEventInit, Node, RequestInit, RequestMode, Response};

use crate::editor_runtime::attachment_upload::dispatch_editor_upload_request;
use crate::editor_runtime::block_hover_state::HoveredBlockState;
use crate::editor_runtime::block_menu_document::{
    collect_plain_text, mindmap_paragraph_node, paragraph_node,
};
use crate::editor_runtime::command_sync::sync_persisted_editor_command;
use crate::editor_runtime::persistence::persisted_document_identity;
use crate::editor_runtime::slash_actions::SlashActionKind;
use crate::editor_root_element;

pub(crate) fn prosemirror_node_size(node: &Value) -> Option<u32> {
    match node.get("type").and_then(Value::as_str) {
        Some("text") => Some(
            node.get("text")
                .and_then(Value::as_str)
                .map(|text| text.encode_utf16().count() as u32)
                .unwrap_or(0),
        ),
        Some("hardBreak") | Some("horizontalRule") => Some(1),
        _ => {
            let Some(children) = node.get("content").and_then(Value::as_array) else {
                return Some(match node.get("type").and_then(Value::as_str) {
                    Some("paragraph") | Some("heading") | Some("blockquote")
                    | Some("codeBlock") | Some("bulletList") | Some("orderedList")
                    | Some("taskList") | Some("listItem") | Some("taskItem") | Some("table")
                    | Some("tableRow") | Some("tableCell") | Some("tableHeader") => 2,
                    _ => 1,
                });
            };

            let content_size = children.iter().try_fold(0_u32, |acc, child| {
                prosemirror_node_size(child).map(|size| acc + size)
            })?;
            Some(content_size + 2)
        }
    }
}

fn nested_edge_child_size(node: &Value, at_start: bool) -> Option<u32> {
    let outer = node
        .get("content")
        .and_then(Value::as_array)
        .and_then(|children| {
            if at_start {
                children.first()
            } else {
                children.last()
            }
        })?;
    let inner = outer
        .get("content")
        .and_then(Value::as_array)
        .and_then(|children| {
            if at_start {
                children.first()
            } else {
                children.last()
            }
        })?;
    prosemirror_node_size(inner)
}

pub(crate) fn top_level_block_range(document: &Value, index: usize) -> Option<TiptapRange> {
    let content = document.get("content").and_then(Value::as_array)?;
    let mut position = 0_u32;

    for (current_index, node) in content.iter().enumerate() {
        let node_size = prosemirror_node_size(node)?;
        if current_index == index {
            let from = position + nested_edge_child_size(node, true).unwrap_or(1);
            let mut to = position
                + node_size.saturating_sub(nested_edge_child_size(node, false).unwrap_or(1));
            if to < from {
                to = from;
            }
            return Some(TiptapRange { from, to });
        }
        position += node_size;
    }

    None
}

pub(crate) fn document_content_mut(document: &mut Value) -> Result<&mut Vec<Value>, String> {
    document
        .get_mut("content")
        .and_then(Value::as_array_mut)
        .ok_or_else(|| "文档 JSON 缺少顶层 content 数组".to_string())
}

pub(crate) fn top_level_block_boundary_position(
    document: &Value,
    index: usize,
    before: bool,
) -> Option<u32> {
    let content = document.get("content").and_then(Value::as_array)?;
    let mut position = 0_u32;

    for (current_index, node) in content.iter().enumerate() {
        let node_size = prosemirror_node_size(node)?;
        if current_index == index {
            return Some(if before {
                position
            } else {
                position + node_size
            });
        }
        position += node_size;
    }

    None
}

pub(crate) fn insert_editor_paragraph_relative_to_block(
    editor: TiptapEditorHandle,
    index: usize,
    before: bool,
) -> Result<usize, String> {
    let document = editor
        .get_json()
        .map_err(|err| format!("读取当前 JSON 失败：{err}"))?;
    let position = top_level_block_boundary_position(&document, index, before)
        .ok_or_else(|| format!("找不到第 {} 个块的插入位置", index + 1))?;
    editor
        .insert_content_at(
            position,
            TiptapContent::json(json!({ "type": "paragraph" })),
            Some(TiptapInsertContentOptions {
                update_selection: Some(true),
                ..Default::default()
            }),
        )
        .map_err(|err| format!("插入新块失败：{err}"))?;
    let new_index = if before { index } else { index + 1 };
    focus_top_level_block_start(editor, new_index)?;
    Ok(new_index)
}

pub(crate) fn focus_top_level_block_start(
    editor: TiptapEditorHandle,
    index: usize,
) -> Result<(), String> {
    editor
        .focus()
        .map_err(|err| format!("聚焦编辑器失败：{err}"))?;

    if let (Some(root), Some(document), Some(selection)) = (
        editor_root_element(),
        window().and_then(|win| win.document()),
        window().and_then(|win| win.get_selection().ok().flatten()),
    ) {
        if let Some(block) = root.children().item(index as u32) {
            let range = document
                .create_range()
                .map_err(|err| format!("创建新块光标失败：{err:?}"))?;
            range
                .select_node_contents(block.unchecked_ref::<Node>())
                .map_err(|err| format!("选择新块失败：{err:?}"))?;
            range.collapse_with_to_start(true);
            selection
                .remove_all_ranges()
                .map_err(|err| format!("清理旧选区失败：{err:?}"))?;
            selection
                .add_range(&range)
                .map_err(|err| format!("写入新块选区失败：{err:?}"))?;
            return Ok(());
        }
    }

    let document = editor
        .get_json()
        .map_err(|err| format!("读取当前 JSON 失败：{err}"))?;
    let range = top_level_block_range(&document, index)
        .ok_or_else(|| format!("找不到第 {} 个块的选择范围", index + 1))?;
    editor
        .set_text_selection(range.from)
        .map_err(|err| format!("定位新块失败：{err}"))
}

pub(crate) fn apply_text_color(
    editor: TiptapEditorHandle,
    color: &str,
) -> Result<&'static str, String> {
    editor
        .set_color(TiptapColorAttributes {
            color: color.to_string(),
        })
        .map(|_| "已更新文字颜色")
        .map_err(|err| format!("命令执行失败：{err}"))
}

pub(crate) fn clear_text_color(editor: TiptapEditorHandle) -> Result<&'static str, String> {
    editor
        .unset_color()
        .map(|_| "已清除文字颜色")
        .map_err(|err| format!("命令执行失败：{err}"))
}

pub(crate) fn apply_highlight_color(
    editor: TiptapEditorHandle,
    color: Option<&str>,
) -> Result<&'static str, String> {
    match color {
        Some(color) => editor
            .set_highlight(Some(TiptapHighlightAttributes {
                color: Some(color.to_string()),
            }))
            .map(|_| "已更新背景色")
            .map_err(|err| format!("命令执行失败：{err}")),
        None => editor
            .unset_highlight()
            .map(|_| "已清除背景色")
            .map_err(|err| format!("命令执行失败：{err}")),
    }
}

pub(crate) fn apply_text_align(
    editor: TiptapEditorHandle,
    alignment: TiptapTextAlign,
) -> Result<&'static str, String> {
    let message = match alignment {
        TiptapTextAlign::Left => "已切到左对齐",
        TiptapTextAlign::Center => "已切到居中",
        TiptapTextAlign::Right => "已切到右对齐",
        TiptapTextAlign::Justify => "已切到两端对齐",
    };
    editor
        .set_text_align(alignment)
        .map(|_| message)
        .map_err(|err| format!("命令执行失败：{err}"))
}

pub(crate) fn apply_image_align(
    editor: TiptapEditorHandle,
    align: &'static str,
) -> Result<&'static str, String> {
    let _ = editor.focus();
    let mut attrs = leptos_tiptap::TiptapAttributes::new();
    attrs.insert("data-align", align);
    editor
        .update_attributes(TiptapSchemaTarget::Node(TiptapNodeName::Image), attrs)
        .map(|_| "已更新图片对齐")
        .map_err(|err| format!("命令执行失败：{err}"))
}

pub(crate) fn run_slash_action(
    editor: TiptapEditorHandle,
    action: SlashActionKind,
) -> Result<&'static str, String> {
    let result = match action {
        SlashActionKind::AiAssistant
        | SlashActionKind::AiWrite
        | SlashActionKind::ContinueWriting
        | SlashActionKind::Summarize
        | SlashActionKind::MoreAi => return Ok("AI 命令入口已打开"),
        SlashActionKind::Paragraph => editor.set_paragraph(),
        SlashActionKind::Heading1 => editor.toggle_heading(TiptapHeadingLevel::H1),
        SlashActionKind::Heading2 => editor.toggle_heading(TiptapHeadingLevel::H2),
        SlashActionKind::Heading3 => editor.toggle_heading(TiptapHeadingLevel::H3),
        SlashActionKind::Heading4 => editor.toggle_heading(TiptapHeadingLevel::H4),
        SlashActionKind::BulletList => editor.toggle_bullet_list(),
        SlashActionKind::OrderedList => editor.toggle_ordered_list(),
        SlashActionKind::Todo | SlashActionKind::AdvancedTodo => editor.toggle_task_list(),
        SlashActionKind::Quote => editor.toggle_blockquote(),
        SlashActionKind::CodeBlock => editor.toggle_code_block(Some(TiptapCodeBlockAttributes {
            language: Some("rust".into()),
        })),
        SlashActionKind::Divider => editor.set_horizontal_rule(),
        SlashActionKind::SimpleTable => {
            let _ = editor.focus();
            editor.insert_table(4, 3, false)
        }
        SlashActionKind::Mindmap => {
            editor.insert_content(TiptapContent::json(mindmap_paragraph_node(next_mindmap_id)), None)
        }
        SlashActionKind::Image => {
            dispatch_editor_upload_request("image", "image/*")?;
            return Ok("已打开图片上传");
        }
        SlashActionKind::UploadAttachment => {
            dispatch_editor_upload_request("attachment", "")?;
            return Ok("已打开附件上传");
        }
        SlashActionKind::Toc => editor.insert_toc_node(TiptapTocNodeAttrs {
            top_offset: Some(0),
            max_show_count: Some(20),
            show_title: Some(true),
        }),
    };

    result
        .map(|_| match action {
            SlashActionKind::AiAssistant
            | SlashActionKind::AiWrite
            | SlashActionKind::ContinueWriting
            | SlashActionKind::Summarize
            | SlashActionKind::MoreAi => "AI 命令入口已打开",
            SlashActionKind::Paragraph => "已切到文本",
            SlashActionKind::Heading1 => "已切到一级标题",
            SlashActionKind::Heading2 => "已切到二级标题",
            SlashActionKind::Heading3 => "已切到三级标题",
            SlashActionKind::Heading4 => "已切到四级标题",
            SlashActionKind::BulletList => "已切到无序列表",
            SlashActionKind::OrderedList => "已切到有序列表",
            SlashActionKind::Todo | SlashActionKind::AdvancedTodo => "已切到待办列表",
            SlashActionKind::Quote => "已切到引述文字",
            SlashActionKind::CodeBlock => "已切到代码块",
            SlashActionKind::Divider => "已插入分割线",
            SlashActionKind::SimpleTable => "已插入简单表格",
            SlashActionKind::Mindmap => "已插入思维导图",
            SlashActionKind::Image => "已打开图片上传",
            SlashActionKind::UploadAttachment => "已打开附件上传",
            SlashActionKind::Toc => "已插入页面目录",
        })
        .map_err(|err| format!("命令执行失败：{err}"))
}

pub(crate) fn run_block_turn_into_action(
    editor: TiptapEditorHandle,
    block_index: usize,
    action: SlashActionKind,
) -> Result<&'static str, String> {
    let document = editor
        .get_json()
        .map_err(|err| format!("读取当前 JSON 失败：{err}"))?;
    let range = top_level_block_range(&document, block_index)
        .ok_or_else(|| format!("找不到第 {} 个块的选择范围", block_index + 1))?;

    match action {
        SlashActionKind::SimpleTable => {
            editor
                .set_text_selection(range)
                .map_err(|err| format!("选中当前块失败：{err}"))?;
            editor
                .insert_table(4, 3, false)
                .map(|_| "已把当前块转成简单表格")
                .map_err(|err| format!("命令执行失败：{err}"))
        }
        SlashActionKind::Toc => {
            editor
                .set_text_selection(range)
                .map_err(|err| format!("选中当前块失败：{err}"))?;
            editor
                .insert_toc_node(TiptapTocNodeAttrs {
                    top_offset: Some(0),
                    max_show_count: Some(20),
                    show_title: Some(true),
                })
                .map(|_| "已把当前块转成页面目录")
                .map_err(|err| format!("命令执行失败：{err}"))
        }
        SlashActionKind::Divider => editor
            .insert_content_at(
                range,
                TiptapContent::json(json!({ "type": "horizontalRule" })),
                None,
            )
            .map(|_| "已把当前块转成分割线")
            .map_err(|err| format!("命令执行失败：{err}")),
        SlashActionKind::Mindmap => editor
            .insert_content_at(
                range,
                TiptapContent::json(mindmap_paragraph_node(next_mindmap_id)),
                None,
            )
            .map(|_| "已把当前块转成思维导图")
            .map_err(|err| format!("命令执行失败：{err}")),
        _ => {
            editor
                .set_text_selection(range)
                .map_err(|err| format!("选中当前块失败：{err}"))?;
            editor
                .clear_nodes()
                .map_err(|err| format!("清理当前块样式失败：{err}"))?;

            let message = run_slash_action(editor, action)?;
            let _ = editor.focus();
            let _ = editor.select_textblock_end();
            Ok(message)
        }
    }
}

fn heading_level_from_u8(level: u8) -> TiptapHeadingLevel {
    match level {
        1 => TiptapHeadingLevel::H1,
        2 => TiptapHeadingLevel::H2,
        3 => TiptapHeadingLevel::H3,
        4 => TiptapHeadingLevel::H4,
        5 => TiptapHeadingLevel::H5,
        _ => TiptapHeadingLevel::H6,
    }
}

pub(crate) fn run_block_turn_into_folded_heading_action(
    editor: TiptapEditorHandle,
    block_index: usize,
    level: u8,
) -> Result<&'static str, String> {
    let document = editor
        .get_json()
        .map_err(|err| format!("读取当前 JSON 失败：{err}"))?;
    let range = top_level_block_range(&document, block_index)
        .ok_or_else(|| format!("找不到第 {} 个块的选择范围", block_index + 1))?;

    editor
        .set_text_selection(range)
        .map_err(|err| format!("选中当前块失败：{err}"))?;
    editor
        .clear_nodes()
        .map_err(|err| format!("清理当前块样式失败：{err}"))?;
    editor
        .set_heading_with_collapsed(heading_level_from_u8(level), true)
        .map_err(|err| format!("命令执行失败：{err}"))?;
    let mut attrs = leptos_tiptap::TiptapAttributes::new();
    attrs.insert("collapsed", true);
    editor
        .update_attributes(TiptapSchemaTarget::Node(TiptapNodeName::Heading), attrs)
        .map_err(|err| format!("写入折叠属性失败：{err}"))?;
    let _ = editor.focus();
    let _ = editor.select_textblock_end();

    Ok(match level {
        1 => "已切到折叠主标题",
        2 => "已切到折叠大标题",
        3 => "已切到折叠中标题",
        4 => "已切到折叠小标题",
        _ => "已切到折叠标题",
    })
}

pub(crate) fn top_level_block_is_page(
    editor: TiptapEditorHandle,
    block_index: usize,
) -> bool {
    editor
        .get_json()
        .ok()
        .and_then(|document| {
            document
                .get("content")
                .and_then(Value::as_array)
                .and_then(|content| content.get(block_index).cloned())
        })
        .map(|block| inline_has_page_block_link(&block))
        .unwrap_or(false)
}

pub(crate) fn top_level_block_matches_action(
    editor: TiptapEditorHandle,
    block_index: usize,
    action: SlashActionKind,
) -> bool {
    let Some(block) = editor.get_json().ok().and_then(|document| {
        document
            .get("content")
            .and_then(Value::as_array)
            .and_then(|content| content.get(block_index).cloned())
    }) else {
        return false;
    };

    let node_type = block
        .get("type")
        .and_then(Value::as_str)
        .unwrap_or_default();
    match action {
        SlashActionKind::Paragraph => {
            node_type == "paragraph" && !inline_has_page_block_link(&block)
        }
        SlashActionKind::Heading1
        | SlashActionKind::Heading2
        | SlashActionKind::Heading3
        | SlashActionKind::Heading4 => {
            let expected_level = match action {
                SlashActionKind::Heading1 => 1,
                SlashActionKind::Heading2 => 2,
                SlashActionKind::Heading3 => 3,
                SlashActionKind::Heading4 => 4,
                _ => 0,
            };
            node_type == "heading"
                && block
                    .get("attrs")
                    .and_then(|attrs| attrs.get("level"))
                    .and_then(Value::as_i64)
                    .map(|level| level == expected_level)
                    .unwrap_or(false)
        }
        SlashActionKind::BulletList => node_type == "bulletList",
        SlashActionKind::OrderedList => node_type == "orderedList",
        SlashActionKind::Todo | SlashActionKind::AdvancedTodo => node_type == "taskList",
        SlashActionKind::Quote => node_type == "blockquote",
        SlashActionKind::CodeBlock => node_type == "codeBlock",
        SlashActionKind::Divider => node_type == "horizontalRule",
        SlashActionKind::SimpleTable => node_type == "table",
        SlashActionKind::Mindmap => {
            node_type == "paragraph"
                && block
                    .get("attrs")
                    .and_then(|attrs| attrs.get("mnoteBlockType"))
                    .and_then(Value::as_str)
                    == Some("mindmap")
        }
        SlashActionKind::Image => node_type == "image",
        SlashActionKind::UploadAttachment => false,
        SlashActionKind::Toc => node_type == "tocNode",
        SlashActionKind::AiAssistant
        | SlashActionKind::AiWrite
        | SlashActionKind::ContinueWriting
        | SlashActionKind::Summarize
        | SlashActionKind::MoreAi => false,
    }
}

fn next_mindmap_id() -> String {
    format!(
        "mindmap-{}",
        (js_sys::Math::random() * 1_000_000_000.0).round() as u64
    )
}

fn inline_has_page_block_link(node: &Value) -> bool {
    if node
        .get("marks")
        .and_then(Value::as_array)
        .map(|marks| {
            marks.iter().any(|mark| {
                mark.get("type").and_then(Value::as_str) == Some("link")
                    && mark
                        .get("attrs")
                        .and_then(|attrs| attrs.get("class"))
                        .and_then(Value::as_str)
                        .map(|class_name| {
                            class_name
                                .split_whitespace()
                                .any(|part| part == "mnote-page-block-link")
                        })
                        .unwrap_or(false)
            })
        })
        .unwrap_or(false)
    {
        return true;
    }

    node.get("content")
        .and_then(Value::as_array)
        .map(|children| children.iter().any(inline_has_page_block_link))
        .unwrap_or(false)
}

fn normalized_page_block_title(text: &str) -> String {
    let trimmed = text.trim();
    if trimmed.is_empty() {
        "未命名页面".to_string()
    } else {
        trimmed.to_string()
    }
}

#[derive(Debug, Deserialize)]
struct TreeCommandHttpResponse {
    result: Option<TreeCommandResult>,
    message: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TreeCommandResult {
    document_id: Option<String>,
    workspace_id: Option<String>,
    title: Option<String>,
    parent_id: Option<String>,
}

fn dispatch_tree_local_command(body: &Value, result: &Value) {
    let Some(win) = window() else {
        return;
    };
    let Ok(detail) = serde_wasm_bindgen::to_value(&json!({
        "body": body,
        "result": result,
    })) else {
        return;
    };
    let init = CustomEventInit::new();
    init.set_detail(&detail);
    if let Ok(event) = CustomEvent::new_with_event_init_dict("tree:local-command", &init) {
        let _ = win.dispatch_event(&event);
    }
}

async fn create_child_page_via_tree_command(
    parent_id: String,
    workspace_id: Option<String>,
    title: String,
) -> Result<TreeCommandResult, String> {
    let body = json!({
        "action": "create",
        "workspaceId": workspace_id,
        "parentId": parent_id,
        "title": title,
        "accessScope": "private",
        "content": [],
    });
    let body_string =
        serde_json::to_string(&body).map_err(|err| format!("序列化创建子页面请求失败：{err}"))?;

    let request_init = RequestInit::new();
    request_init.set_method("POST");
    request_init.set_mode(RequestMode::SameOrigin);
    request_init.set_body(&JsValue::from_str(&body_string));
    let headers = js_sys::Object::new();
    js_sys::Reflect::set(
        &headers,
        &JsValue::from_str("content-type"),
        &JsValue::from_str("application/json"),
    )
    .map_err(|_| "设置创建子页面请求头失败".to_string())?;
    js_sys::Reflect::set(
        request_init.as_ref(),
        &JsValue::from_str("headers"),
        &headers,
    )
    .map_err(|_| "设置创建子页面请求头失败".to_string())?;

    let win = window().ok_or_else(|| "当前浏览器窗口不可用".to_string())?;
    let response_value =
        JsFuture::from(win.fetch_with_str_and_init("/api/tree/commands", &request_init))
            .await
            .map_err(|err| format!("创建子页面请求失败：{err:?}"))?;
    let response: Response = response_value
        .dyn_into()
        .map_err(|_| "创建子页面响应类型错误".to_string())?;
    let status = response.status();
    let json_value = JsFuture::from(
        response
            .json()
            .map_err(|_| "读取创建子页面响应失败".to_string())?,
    )
    .await
    .map_err(|err| format!("解析创建子页面响应失败：{err:?}"))?;
    let parsed: TreeCommandHttpResponse = serde_wasm_bindgen::from_value(json_value.clone())
        .map_err(|err| format!("创建子页面响应 JSON 不符合预期：{err}"))?;
    if !response.ok() {
        return Err(parsed
            .message
            .unwrap_or_else(|| format!("创建子页面失败：HTTP {status}")));
    }
    let result = parsed
        .result
        .ok_or_else(|| "创建子页面响应缺少 result".to_string())?;
    let document_id = result
        .document_id
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .ok_or_else(|| "创建子页面响应缺少 documentId".to_string())?;
    if document_id == parent_id {
        return Err("创建子页面返回了当前页面 id".to_string());
    }
    if let Some(created_parent_id) = result
        .parent_id
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        if created_parent_id != parent_id {
            return Err(format!(
                "创建子页面父节点不符合预期：期望 {parent_id}，实际 {created_parent_id}"
            ));
        }
    }
    dispatch_tree_local_command(
        &body,
        &serde_wasm_bindgen::from_value(json_value).unwrap_or(Value::Null)["result"],
    );
    Ok(result)
}

fn block_plain_text_from_index(
    editor: TiptapEditorHandle,
    block_index: usize,
) -> Result<String, String> {
    let document = editor
        .get_json()
        .map_err(|err| format!("读取当前 JSON 失败：{err}"))?;
    let block = document
        .get("content")
        .and_then(Value::as_array)
        .and_then(|content| content.get(block_index))
        .ok_or_else(|| format!("找不到第 {} 个块", block_index + 1))?;
    Ok(normalized_page_block_title(&collect_plain_text(block)))
}

fn page_reference_paragraph_node(page_id: &str, workspace_id: Option<&str>, title: &str) -> Value {
    let href = if page_id.trim().is_empty() {
        "#".to_string()
    } else if let Some(workspace_id) = workspace_id
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        format!("/documents/{page_id}?workspaceId={workspace_id}")
    } else {
        format!("/documents/{page_id}")
    };
    paragraph_node(vec![json!({
        "type": "text",
        "text": normalized_page_block_title(title),
        "marks": [{
            "type": "link",
            "attrs": {
                "href": href,
                "target": "_self",
                "rel": "noopener noreferrer nofollow",
                "class": "mnote-page-block-link",
            }
        }]
    })])
}

fn replace_block_with_page_reference(
    editor: TiptapEditorHandle,
    block_index: usize,
    page_id: &str,
    workspace_id: Option<&str>,
    title: &str,
) -> Result<(), String> {
    let mut document = editor
        .get_json()
        .map_err(|err| format!("读取当前 JSON 失败：{err}"))?;
    let content = document_content_mut(&mut document)?;
    if block_index >= content.len() {
        return Err(format!("找不到第 {} 个块", block_index + 1));
    }
    content[block_index] = page_reference_paragraph_node(page_id, workspace_id, title);
    let next_payload = document
        .get("content")
        .and_then(Value::as_array)
        .cloned()
        .map(Value::Array)
        .unwrap_or(document);
    editor
        .set_content(TiptapContent::json(next_payload))
        .map_err(|err| format!("转换为页面失败：{err}"))?;
    focus_top_level_block_start(editor, block_index)?;
    Ok(())
}

pub(crate) fn run_block_turn_into_page_action(
    editor: TiptapEditorHandle,
    block_index: usize,
    document_id: ReadSignal<Option<String>>,
    workspace_id: ReadSignal<Option<String>>,
    title: ReadSignal<String>,
    set_dirty_count: WriteSignal<u32>,
    set_html_output: WriteSignal<String>,
    set_document_json: WriteSignal<Value>,
    set_json_output: WriteSignal<String>,
    set_command_feedback: WriteSignal<String>,
    set_block_menu_open: WriteSignal<bool>,
    set_block_menu_anchor: WriteSignal<Option<HoveredBlockState>>,
    set_block_turn_into_open: WriteSignal<bool>,
) {
    let page_title = match block_plain_text_from_index(editor, block_index) {
        Ok(value) => value,
        Err(err) => {
            set_command_feedback.set(err);
            return;
        }
    };
    let Some(parent_document_id) = document_id.get_untracked() else {
        set_command_feedback.set("转换为页面失败：当前页面缺少 documentId".to_string());
        return;
    };
    let workspace_id_value = workspace_id.get_untracked();
    set_command_feedback.set(format!("正在创建子页面：{page_title}"));
    spawn_local(async move {
        match create_child_page_via_tree_command(
            parent_document_id.clone(),
            workspace_id_value.clone(),
            page_title.clone(),
        )
        .await
        {
            Ok(created) => {
                let Some(child_page_id) = created.document_id.as_deref() else {
                    set_command_feedback.set("转换为页面失败：创建响应缺少子页面 id".to_string());
                    return;
                };
                let effective_workspace_id = created
                    .workspace_id
                    .as_deref()
                    .or(workspace_id_value.as_deref());
                let effective_title = created.title.as_deref().unwrap_or(&page_title);
                match replace_block_with_page_reference(
                    editor,
                    block_index,
                    child_page_id,
                    effective_workspace_id,
                    effective_title,
                ) {
                    Ok(()) => {
                        set_block_menu_open.set(false);
                        set_block_menu_anchor.set(None);
                        set_block_turn_into_open.set(false);
                        sync_persisted_editor_command(
                            editor,
                            &persisted_document_identity(
                                document_id.get_untracked(),
                                workspace_id.get_untracked(),
                            ),
                            set_dirty_count,
                            set_html_output,
                            set_document_json,
                            set_json_output,
                            title,
                            set_command_feedback,
                            format!("已转换为子页面：{effective_title}"),
                        );
                    }
                    Err(err) => set_command_feedback.set(err),
                }
            }
            Err(err) => set_command_feedback.set(err),
        }
    });
}

/// 将 delta operations 应用到 Tiptap JSON 树（原地修改）。
/// 返回 true 表示树发生了变更。
pub(crate) fn apply_block_delta_to_json(content: &mut Value, operations: &[Value]) -> bool {
    let Some(content_arr) = content.get_mut("content").and_then(Value::as_array_mut) else {
        return false;
    };
    let mut changed = false;

    for op_value in operations {
        let Some(op) = op_value.get("op").and_then(Value::as_str) else {
            continue;
        };
        match op {
            "replace" => {
                let block_id = op_value.get("block_id").and_then(Value::as_str);
                let text = op_value.get("text").and_then(Value::as_str).unwrap_or("");
                let block_type = op_value.get("block_type").and_then(Value::as_str);
                if let Some(bid) = block_id {
                    if apply_replace_block(content_arr, bid, text, block_type) {
                        changed = true;
                    }
                }
            }
            "insert_after" => {
                let anchor = op_value.get("anchor_block_id").and_then(Value::as_str);
                let block_id = op_value.get("block_id").and_then(Value::as_str);
                let text = op_value.get("text").and_then(Value::as_str).unwrap_or("");
                if let (Some(aid), Some(bid)) = (anchor, block_id) {
                    if apply_insert_block_after(content_arr, aid, bid, text) {
                        changed = true;
                    }
                }
            }
            "delete" => {
                let block_id = op_value.get("block_id").and_then(Value::as_str);
                if let Some(bid) = block_id {
                    if apply_delete_block(content_arr, bid) {
                        changed = true;
                    }
                }
            }
            "move_after" => {
                let block_id = op_value.get("block_id").and_then(Value::as_str);
                let anchor = op_value.get("anchor_block_id").and_then(Value::as_str);
                if let (Some(bid), Some(aid)) = (block_id, anchor) {
                    if apply_move_block_after(content_arr, bid, aid) {
                        changed = true;
                    }
                }
            }
            _ => {}
        }
    }
    changed
}

fn find_block_index(blocks: &[Value], block_id: &str) -> Option<usize> {
    blocks.iter().position(|b| {
        b.get("attrs")
            .and_then(|a| a.get("block_id"))
            .and_then(Value::as_str)
            == Some(block_id)
    })
}

fn apply_replace_block(
    blocks: &mut Vec<Value>,
    block_id: &str,
    text: &str,
    block_type: Option<&str>,
) -> bool {
    let Some(idx) = find_block_index(blocks, block_id) else {
        return false;
    };
    let block = &mut blocks[idx];

    if let Some(bt) = block_type {
        if let Some(b) = block.as_object_mut() {
            b.insert("type".into(), json!(bt));
        }
    }

    let new_content: Value = if text.is_empty() {
        json!([])
    } else {
        json!([{ "type": "text", "text": text }])
    };

    if let Some(b) = block.as_object_mut() {
        b.insert("content".into(), new_content);
    }
    true
}

fn apply_insert_block_after(
    blocks: &mut Vec<Value>,
    anchor_block_id: &str,
    new_block_id: &str,
    text: &str,
) -> bool {
    let Some(idx) = find_block_index(blocks, anchor_block_id) else {
        return false;
    };
    let new_block = json!({
        "type": "paragraph",
        "attrs": { "block_id": new_block_id },
        "content": if text.is_empty() {
            json!([])
        } else {
            json!([{ "type": "text", "text": text }])
        }
    });
    blocks.insert(idx + 1, new_block);
    true
}

fn apply_delete_block(blocks: &mut Vec<Value>, block_id: &str) -> bool {
    let Some(idx) = find_block_index(blocks, block_id) else {
        return false;
    };
    blocks.remove(idx);
    true
}

fn apply_move_block_after(blocks: &mut Vec<Value>, block_id: &str, anchor_block_id: &str) -> bool {
    let Some(block_idx) = find_block_index(blocks, block_id) else {
        return false;
    };
    let Some(anchor_idx) = find_block_index(blocks, anchor_block_id) else {
        return false;
    };

    if block_idx == anchor_idx || block_idx == anchor_idx + 1 {
        return false;
    }

    let block = blocks.remove(block_idx);
    let adjusted_anchor = if block_idx < anchor_idx {
        anchor_idx - 1
    } else {
        anchor_idx
    };
    blocks.insert(adjusted_anchor + 1, block);
    true
}
