use crate::editor::model::{
    BlockProps, ContentNode, ContentNodePayload, EditorBlock, EditorBlockDocument, EditorBlockType,
    TextMark,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TiptapBlockType {
    Paragraph,
    Heading,
    BulletList,
    OrderedList,
    TaskList,
    ListItem,
    TaskItem,
    Blockquote,
    CodeBlock,
    HorizontalRule,
    Table,
    Image,
    TocNode,
    TableRow,
    TableCell,
    TableHeader,
    Text,
    HardBreak,
    Doc,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TiptapHeadingAttrs {
    pub level: u8,
    #[serde(default)]
    pub block_id: Option<String>,
    #[serde(default)]
    pub text_align: Option<String>,
    #[serde(default)]
    pub collapsed: Option<bool>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct TiptapTaskItemAttrs {
    #[serde(default)]
    pub checked: bool,
    #[serde(default)]
    pub block_id: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct TiptapParagraphAttrs {
    #[serde(default)]
    pub block_id: Option<String>,
    #[serde(default)]
    pub text_align: Option<String>,
    #[serde(default)]
    pub mnote_block_type: Option<String>,
    #[serde(default)]
    pub mindmap_id: Option<String>,
    #[serde(default)]
    pub root_node_id: Option<String>,
    #[serde(default)]
    pub projection_version: Option<u64>,
    #[serde(flatten, default)]
    pub extra: BTreeMap<String, Value>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct TiptapListAttrs {
    #[serde(default)]
    pub block_id: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct TiptapListItemAttrs {
    #[serde(default)]
    pub block_id: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct TiptapBlockquoteAttrs {
    #[serde(default)]
    pub block_id: Option<String>,
    #[serde(default)]
    pub text_align: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TiptapCodeBlockAttrs {
    pub language: Option<String>,
    pub block_id: Option<String>,
    #[serde(default)]
    pub text_align: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct TiptapHorizontalRuleAttrs {
    #[serde(default)]
    pub block_id: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct TiptapImageAttrs {
    pub src: String,
    #[serde(default)]
    pub alt: Option<String>,
    #[serde(default)]
    pub title: Option<String>,
    #[serde(default)]
    pub block_id: Option<String>,
    #[serde(flatten)]
    pub extra: BTreeMap<String, Value>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct TiptapTableAttrs {
    #[serde(default)]
    pub block_id: Option<String>,
    #[serde(flatten)]
    pub extra: BTreeMap<String, Value>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct TiptapTocNodeAttrs {
    #[serde(default)]
    pub top_offset: Option<u32>,
    #[serde(default)]
    pub max_show_count: Option<u32>,
    #[serde(default)]
    pub show_title: Option<bool>,
    #[serde(default)]
    pub block_id: Option<String>,
    #[serde(flatten)]
    pub extra: BTreeMap<String, Value>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct TiptapTableCellAttrs {
    #[serde(default)]
    pub colspan: Option<u32>,
    #[serde(default)]
    pub rowspan: Option<u32>,
    #[serde(default)]
    pub colwidth: Value,
    #[serde(flatten)]
    pub extra: BTreeMap<String, Value>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TiptapMark {
    pub r#type: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum TiptapNode {
    Doc {
        content: Vec<TiptapNode>,
    },
    Paragraph {
        #[serde(default)]
        attrs: TiptapParagraphAttrs,
        #[serde(default)]
        content: Vec<TiptapNode>,
    },
    Heading {
        attrs: TiptapHeadingAttrs,
        #[serde(default)]
        content: Vec<TiptapNode>,
    },
    BulletList {
        #[serde(default)]
        attrs: TiptapListAttrs,
        #[serde(default)]
        content: Vec<TiptapNode>,
    },
    OrderedList {
        #[serde(default)]
        attrs: TiptapListAttrs,
        #[serde(default)]
        content: Vec<TiptapNode>,
    },
    TaskList {
        #[serde(default)]
        attrs: TiptapListAttrs,
        #[serde(default)]
        content: Vec<TiptapNode>,
    },
    ListItem {
        #[serde(default)]
        attrs: TiptapListItemAttrs,
        #[serde(default)]
        content: Vec<TiptapNode>,
    },
    TaskItem {
        attrs: TiptapTaskItemAttrs,
        #[serde(default)]
        content: Vec<TiptapNode>,
    },
    Blockquote {
        #[serde(default)]
        attrs: TiptapBlockquoteAttrs,
        #[serde(default)]
        content: Vec<TiptapNode>,
    },
    CodeBlock {
        attrs: TiptapCodeBlockAttrs,
        #[serde(default)]
        content: Vec<TiptapNode>,
    },
    HorizontalRule {
        #[serde(default)]
        attrs: TiptapHorizontalRuleAttrs,
    },
    Image {
        attrs: TiptapImageAttrs,
    },
    Table {
        #[serde(default)]
        attrs: TiptapTableAttrs,
        #[serde(default)]
        content: Vec<TiptapNode>,
    },
    TocNode {
        #[serde(default)]
        attrs: TiptapTocNodeAttrs,
    },
    TableRow {
        #[serde(default)]
        attrs: BTreeMap<String, Value>,
        #[serde(default)]
        content: Vec<TiptapNode>,
    },
    TableCell {
        #[serde(default)]
        attrs: TiptapTableCellAttrs,
        #[serde(default)]
        content: Vec<TiptapNode>,
    },
    TableHeader {
        #[serde(default)]
        attrs: TiptapTableCellAttrs,
        #[serde(default)]
        content: Vec<TiptapNode>,
    },
    Text {
        text: String,
        #[serde(default)]
        marks: Vec<TiptapMark>,
    },
    HardBreak,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditorBlockDocumentTiptapError {
    UnsupportedNode(&'static str),
    InvalidDocument(&'static str),
}

pub struct EditorBlockDocumentTiptapBridge;

fn text_align_from_props(props: &BlockProps) -> Option<String> {
    props
        .extra
        .get("textAlign")
        .or_else(|| props.extra.get("text_align"))
        .and_then(|value| value.as_str())
        .map(str::trim)
        .filter(|value| matches!(*value, "left" | "center" | "right" | "justify"))
        .map(ToOwned::to_owned)
}

fn table_snapshot_from_props(props: &BlockProps) -> Option<TiptapNode> {
    props
        .extra
        .get("tiptapTable")
        .cloned()
        .and_then(|value| serde_json::from_value::<TiptapNode>(value).ok())
}

fn image_snapshot_from_props(props: &BlockProps) -> Option<TiptapNode> {
    props
        .extra
        .get("tiptapImage")
        .cloned()
        .and_then(|value| serde_json::from_value::<TiptapNode>(value).ok())
}

fn toc_snapshot_from_props(props: &BlockProps) -> Option<TiptapNode> {
    props
        .extra
        .get("tiptapTocNode")
        .or_else(|| props.extra.get("tiptapToc"))
        .cloned()
        .and_then(|value| serde_json::from_value::<TiptapNode>(value).ok())
}

fn props_with_image_snapshot(node: &TiptapNode) -> BlockProps {
    let mut props = BlockProps::default();
    if let Ok(value) = serde_json::to_value(node) {
        props.extra.insert("tiptapImage".into(), value);
    }
    props
}

fn props_with_table_snapshot(node: &TiptapNode) -> BlockProps {
    let mut props = BlockProps::default();
    if let Ok(value) = serde_json::to_value(node) {
        props.extra.insert("tiptapTable".into(), value);
    }
    props
}

fn props_with_toc_snapshot(node: &TiptapNode) -> BlockProps {
    let mut props = BlockProps::default();
    if let Ok(value) = serde_json::to_value(node) {
        props.extra.insert("tiptapTocNode".into(), value);
    }
    props
}

fn collect_table_text_nodes(nodes: &[TiptapNode], out: &mut Vec<ContentNode>) {
    for node in nodes {
        match node {
            TiptapNode::Text { text, marks } => out.push(ContentNode {
                payload: ContentNodePayload::Text {
                    text: text.clone(),
                    marks: marks.iter().filter_map(tiptap_mark_to_text_mark).collect(),
                },
                attrs: BTreeMap::new(),
            }),
            TiptapNode::HardBreak => out.push(ContentNode {
                payload: ContentNodePayload::HardBreak,
                attrs: BTreeMap::new(),
            }),
            TiptapNode::Doc { content }
            | TiptapNode::Paragraph { content, .. }
            | TiptapNode::Heading { content, .. }
            | TiptapNode::BulletList { content, .. }
            | TiptapNode::OrderedList { content, .. }
            | TiptapNode::TaskList { content, .. }
            | TiptapNode::ListItem { content, .. }
            | TiptapNode::TaskItem { content, .. }
            | TiptapNode::Blockquote { content, .. }
            | TiptapNode::CodeBlock { content, .. }
            | TiptapNode::Table { content, .. }
            | TiptapNode::TableRow { content, .. }
            | TiptapNode::TableCell { content, .. }
            | TiptapNode::TableHeader { content, .. } => collect_table_text_nodes(content, out),
            TiptapNode::HorizontalRule { .. }
            | TiptapNode::Image { .. }
            | TiptapNode::TocNode { .. } => {}
        }
    }
}

fn props_with_text_align(text_align: Option<String>) -> BlockProps {
    let mut props = BlockProps::default();
    if let Some(text_align) = text_align
        .as_deref()
        .map(str::trim)
        .filter(|value| matches!(*value, "left" | "center" | "right" | "justify"))
    {
        props.extra.insert("textAlign".into(), text_align.into());
    }
    props
}

fn props_with_mindmap_attrs(attrs: &TiptapParagraphAttrs) -> BlockProps {
    let mut props = props_with_text_align(attrs.text_align.clone());
    if let Some(mindmap_id) = attrs
        .mindmap_id
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        props
            .extra
            .insert("mindmapId".into(), Value::String(mindmap_id.to_string()));
    }
    if let Some(root_node_id) = attrs
        .root_node_id
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        props
            .extra
            .insert("rootNodeId".into(), Value::String(root_node_id.to_string()));
    }
    if let Some(projection_version) = attrs.projection_version {
        props.extra.insert(
            "projectionVersion".into(),
            Value::Number(projection_version.into()),
        );
    }
    for (key, value) in &attrs.extra {
        props.extra.insert(key.clone(), value.clone());
    }
    props
}

fn read_extra_string(props: &BlockProps, key: &str) -> Option<String> {
    props
        .extra
        .get(key)
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToOwned::to_owned)
}

fn read_extra_u64(props: &BlockProps, key: &str) -> Option<u64> {
    props.extra.get(key).and_then(Value::as_u64)
}

impl EditorBlockDocumentTiptapBridge {
    pub fn to_tiptap_doc(
        document: &EditorBlockDocument,
    ) -> Result<TiptapNode, EditorBlockDocumentTiptapError> {
        let mut content = Vec::new();
        for block_id in &document.root_block_ids {
            let block = document
                .blocks
                .iter()
                .find(|block| &block.block_id == block_id)
                .ok_or(EditorBlockDocumentTiptapError::InvalidDocument(
                    "根块不存在",
                ))?;
            content.push(block_to_tiptap_node(block, document)?);
        }
        Ok(TiptapNode::Doc { content })
    }

    pub fn from_tiptap_doc(
        document_id: impl Into<String>,
        doc: &TiptapNode,
    ) -> Result<EditorBlockDocument, EditorBlockDocumentTiptapError> {
        match doc {
            TiptapNode::Doc { content } => {
                let mut blocks = Vec::new();
                let mut root_block_ids = Vec::new();
                for (index, node) in content.iter().enumerate() {
                    let (node_root_ids, mut node_blocks) = node_to_blocks(node, index)?;
                    root_block_ids.extend(node_root_ids);
                    blocks.append(&mut node_blocks);
                }
                Ok(EditorBlockDocument {
                    document_id: document_id.into(),
                    root_block_ids,
                    blocks,
                })
            }
            _ => Err(EditorBlockDocumentTiptapError::InvalidDocument(
                "顶层必须是 doc",
            )),
        }
    }
}

fn node_to_blocks(
    node: &TiptapNode,
    index: usize,
) -> Result<(Vec<String>, Vec<EditorBlock>), EditorBlockDocumentTiptapError> {
    match node {
        TiptapNode::BulletList { attrs, content } => list_node_to_blocks(
            attrs.block_id.as_deref(),
            EditorBlockType::BulletListItem,
            content,
            &format!("block_{}", index + 1),
        ),
        TiptapNode::OrderedList { attrs, content } => list_node_to_blocks(
            attrs.block_id.as_deref(),
            EditorBlockType::NumberedListItem,
            content,
            &format!("block_{}", index + 1),
        ),
        TiptapNode::TaskList { attrs, content } => list_node_to_blocks(
            attrs.block_id.as_deref(),
            EditorBlockType::Todo,
            content,
            &format!("block_{}", index + 1),
        ),
        _ => {
            let block = node_to_block(node, index)?;
            Ok((vec![block.block_id.clone()], vec![block]))
        }
    }
}

fn block_child_tiptap_nodes(
    block: &EditorBlock,
    document: &EditorBlockDocument,
) -> Result<Vec<TiptapNode>, EditorBlockDocumentTiptapError> {
    block
        .child_block_ids
        .iter()
        .map(|child_id| {
            let child = document
                .blocks
                .iter()
                .find(|candidate| &candidate.block_id == child_id)
                .ok_or(EditorBlockDocumentTiptapError::InvalidDocument(
                    "子块不存在",
                ))?;
            block_to_tiptap_node(child, document)
        })
        .collect()
}

fn list_item_content_from_block(
    block: &EditorBlock,
    inline_content: Vec<TiptapNode>,
    document: &EditorBlockDocument,
) -> Result<Vec<TiptapNode>, EditorBlockDocumentTiptapError> {
    let mut content = vec![TiptapNode::Paragraph {
        attrs: TiptapParagraphAttrs {
            block_id: Some(block.block_id.clone()),
            text_align: text_align_from_props(&block.props),
            mnote_block_type: None,
            mindmap_id: None,
            root_node_id: None,
            projection_version: None,
            extra: BTreeMap::new(),
        },
        content: inline_content,
    }];
    content.extend(block_child_tiptap_nodes(block, document)?);
    Ok(content)
}

fn block_to_tiptap_node(
    block: &EditorBlock,
    document: &EditorBlockDocument,
) -> Result<TiptapNode, EditorBlockDocumentTiptapError> {
    let content = text_nodes_to_tiptap(&block.content_nodes)?;
    match block.block_type {
        EditorBlockType::Paragraph => Ok(TiptapNode::Paragraph {
            attrs: TiptapParagraphAttrs {
                block_id: Some(block.block_id.clone()),
                text_align: text_align_from_props(&block.props),
                mnote_block_type: None,
                mindmap_id: None,
                root_node_id: None,
                projection_version: None,
                extra: BTreeMap::new(),
            },
            content,
        }),
        EditorBlockType::Mindmap => {
            let mut extra = block.props.extra.clone();
            extra.remove("textAlign");
            extra.remove("text_align");
            extra.remove("mindmapId");
            extra.remove("rootNodeId");
            extra.remove("projectionVersion");
            if !extra.contains_key("mnoteMindmapData") {
                if let Some(data) = block.props.extra.get("data") {
                    extra.insert("mnoteMindmapData".into(), data.clone());
                }
            }
            Ok(TiptapNode::Paragraph {
                attrs: TiptapParagraphAttrs {
                    block_id: Some(block.block_id.clone()),
                    text_align: text_align_from_props(&block.props),
                    mnote_block_type: Some("mindmap".into()),
                    mindmap_id: read_extra_string(&block.props, "mindmapId")
                        .or_else(|| Some(block.block_id.clone())),
                    root_node_id: read_extra_string(&block.props, "rootNodeId"),
                    projection_version: read_extra_u64(&block.props, "projectionVersion"),
                    extra,
                },
                content: Vec::new(),
            })
        }
        EditorBlockType::Heading => Ok(TiptapNode::Heading {
            attrs: TiptapHeadingAttrs {
                level: block.props.heading_level.unwrap_or(1),
                block_id: Some(block.block_id.clone()),
                text_align: text_align_from_props(&block.props),
                collapsed: block.props.collapsed,
            },
            content,
        }),
        EditorBlockType::BulletListItem => Ok(TiptapNode::BulletList {
            attrs: TiptapListAttrs {
                block_id: Some(block.block_id.clone()),
            },
            content: vec![TiptapNode::ListItem {
                attrs: TiptapListItemAttrs {
                    block_id: Some(block.block_id.clone()),
                },
                content: list_item_content_from_block(block, content, document)?,
            }],
        }),
        EditorBlockType::NumberedListItem => Ok(TiptapNode::OrderedList {
            attrs: TiptapListAttrs {
                block_id: Some(block.block_id.clone()),
            },
            content: vec![TiptapNode::ListItem {
                attrs: TiptapListItemAttrs {
                    block_id: Some(block.block_id.clone()),
                },
                content: list_item_content_from_block(block, content, document)?,
            }],
        }),
        EditorBlockType::Todo => Ok(TiptapNode::TaskList {
            attrs: TiptapListAttrs {
                block_id: Some(block.block_id.clone()),
            },
            content: vec![TiptapNode::TaskItem {
                attrs: TiptapTaskItemAttrs {
                    checked: block.props.checked.unwrap_or(false),
                    block_id: Some(block.block_id.clone()),
                },
                content: list_item_content_from_block(block, content, document)?,
            }],
        }),
        EditorBlockType::Quote => Ok(TiptapNode::Blockquote {
            attrs: TiptapBlockquoteAttrs {
                block_id: Some(block.block_id.clone()),
                text_align: text_align_from_props(&block.props),
            },
            content: vec![TiptapNode::Paragraph {
                attrs: TiptapParagraphAttrs {
                    block_id: Some(block.block_id.clone()),
                    text_align: text_align_from_props(&block.props),
                    mnote_block_type: None,
                    mindmap_id: None,
                    root_node_id: None,
                    projection_version: None,
                    extra: BTreeMap::new(),
                },
                content,
            }],
        }),
        EditorBlockType::CodeBlock => Ok(TiptapNode::CodeBlock {
            attrs: TiptapCodeBlockAttrs {
                language: block.props.language.clone(),
                block_id: Some(block.block_id.clone()),
                text_align: text_align_from_props(&block.props),
            },
            content,
        }),
        EditorBlockType::Divider => Ok(TiptapNode::HorizontalRule {
            attrs: TiptapHorizontalRuleAttrs {
                block_id: Some(block.block_id.clone()),
            },
        }),
        EditorBlockType::Table => table_snapshot_from_props(&block.props).ok_or(
            EditorBlockDocumentTiptapError::InvalidDocument("表格块缺少 Tiptap 快照"),
        ),
        EditorBlockType::Image => image_snapshot_from_props(&block.props).ok_or(
            EditorBlockDocumentTiptapError::InvalidDocument("图片块缺少 Tiptap 快照"),
        ),
        EditorBlockType::Toc => toc_snapshot_from_props(&block.props).ok_or(
            EditorBlockDocumentTiptapError::InvalidDocument("目录块缺少 Tiptap 快照"),
        ),
        _ => Err(EditorBlockDocumentTiptapError::UnsupportedNode(
            "当前块型暂不支持导出到 Tiptap",
        )),
    }
}

fn node_to_block(
    node: &TiptapNode,
    index: usize,
) -> Result<EditorBlock, EditorBlockDocumentTiptapError> {
    let fallback_block_id = format!("block_{}", index + 1);
    match node {
        TiptapNode::Paragraph { attrs, .. }
            if attrs.mnote_block_type.as_deref() == Some("mindmap") =>
        {
            Ok(EditorBlock {
                block_id: attrs
                    .block_id
                    .clone()
                    .or_else(|| attrs.mindmap_id.clone())
                    .unwrap_or(fallback_block_id),
                block_type: EditorBlockType::Mindmap,
                props: props_with_mindmap_attrs(attrs),
                content_nodes: vec![],
                child_block_ids: vec![],
            })
        }
        TiptapNode::Paragraph { attrs, content } => Ok(EditorBlock {
            block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),
            block_type: EditorBlockType::Paragraph,
            props: props_with_text_align(attrs.text_align.clone()),
            content_nodes: text_nodes_from_tiptap(content)?,
            child_block_ids: vec![],
        }),
        TiptapNode::Heading { attrs, content } => Ok(EditorBlock {
            block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),
            block_type: EditorBlockType::Heading,
            props: BlockProps {
                heading_level: Some(attrs.level),
                collapsed: attrs.collapsed,
                ..props_with_text_align(attrs.text_align.clone())
            },
            content_nodes: text_nodes_from_tiptap(content)?,
            child_block_ids: vec![],
        }),
        TiptapNode::BulletList { attrs, content } => list_node_to_block(
            attrs.block_id.clone().unwrap_or(fallback_block_id),
            EditorBlockType::BulletListItem,
            content,
        ),
        TiptapNode::OrderedList { attrs, content } => list_node_to_block(
            attrs.block_id.clone().unwrap_or(fallback_block_id),
            EditorBlockType::NumberedListItem,
            content,
        ),
        TiptapNode::TaskList { attrs, content } => {
            list_task_node_to_block(attrs.block_id.clone().unwrap_or(fallback_block_id), content)
        }
        TiptapNode::Blockquote { attrs, content } => Ok(EditorBlock {
            block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),
            block_type: EditorBlockType::Quote,
            props: props_with_text_align(attrs.text_align.clone()),
            content_nodes: text_nodes_from_tiptap(extract_block_container_content(content)?)?,
            child_block_ids: vec![],
        }),
        TiptapNode::CodeBlock { attrs, content } => Ok(EditorBlock {
            block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),
            block_type: EditorBlockType::CodeBlock,
            props: BlockProps {
                language: attrs.language.clone(),
                ..props_with_text_align(attrs.text_align.clone())
            },
            content_nodes: text_nodes_from_tiptap(content)?,
            child_block_ids: vec![],
        }),
        TiptapNode::HorizontalRule { attrs } => Ok(EditorBlock {
            block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),
            block_type: EditorBlockType::Divider,
            props: BlockProps::default(),
            content_nodes: vec![],
            child_block_ids: vec![],
        }),
        TiptapNode::Table { attrs, content } => {
            let mut content_nodes = Vec::new();
            collect_table_text_nodes(content, &mut content_nodes);
            Ok(EditorBlock {
                block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),
                block_type: EditorBlockType::Table,
                props: props_with_table_snapshot(node),
                content_nodes,
                child_block_ids: vec![],
            })
        }
        TiptapNode::Image { attrs } => Ok(EditorBlock {
            block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),
            block_type: EditorBlockType::Image,
            props: props_with_image_snapshot(node),
            content_nodes: vec![],
            child_block_ids: vec![],
        }),
        TiptapNode::TocNode { attrs } => Ok(EditorBlock {
            block_id: attrs.block_id.clone().unwrap_or(fallback_block_id),
            block_type: EditorBlockType::Toc,
            props: props_with_toc_snapshot(node),
            content_nodes: vec![],
            child_block_ids: vec![],
        }),
        _ => Err(EditorBlockDocumentTiptapError::UnsupportedNode(
            "当前节点暂不支持导入为 EditorBlockDocument",
        )),
    }
}

fn list_node_to_block(
    block_id: String,
    block_type: EditorBlockType,
    content: &[TiptapNode],
) -> Result<EditorBlock, EditorBlockDocumentTiptapError> {
    Ok(EditorBlock {
        block_id,
        block_type,
        props: BlockProps::default(),
        content_nodes: text_nodes_from_tiptap(extract_list_item_inline_content(content)?)?,
        child_block_ids: vec![],
    })
}

fn list_node_to_blocks(
    list_block_id: Option<&str>,
    block_type: EditorBlockType,
    content: &[TiptapNode],
    fallback_prefix: &str,
) -> Result<(Vec<String>, Vec<EditorBlock>), EditorBlockDocumentTiptapError> {
    let mut root_ids = Vec::new();
    let mut blocks = Vec::new();
    let use_list_id_for_single_item = content.len() == 1;

    for (index, item) in content.iter().enumerate() {
        let fallback_block_id = if use_list_id_for_single_item {
            list_block_id
                .map(ToOwned::to_owned)
                .unwrap_or_else(|| fallback_prefix.to_string())
        } else {
            format!("{fallback_prefix}_item_{}", index + 1)
        };
        let (block, mut child_blocks) = list_item_node_to_block(
            item,
            block_type.clone(),
            &fallback_block_id,
            &format!("{fallback_prefix}_child_{}", index + 1),
        )?;
        root_ids.push(block.block_id.clone());
        blocks.push(block);
        blocks.append(&mut child_blocks);
    }

    Ok((root_ids, blocks))
}

fn list_item_node_to_block(
    item: &TiptapNode,
    block_type: EditorBlockType,
    fallback_block_id: &str,
    child_fallback_prefix: &str,
) -> Result<(EditorBlock, Vec<EditorBlock>), EditorBlockDocumentTiptapError> {
    let (block_id, checked, content) = match item {
        TiptapNode::ListItem { attrs, content } => (
            attrs
                .block_id
                .clone()
                .unwrap_or_else(|| fallback_block_id.to_string()),
            None,
            content,
        ),
        TiptapNode::TaskItem { attrs, content } => (
            attrs
                .block_id
                .clone()
                .unwrap_or_else(|| fallback_block_id.to_string()),
            Some(attrs.checked),
            content,
        ),
        _ => {
            return Err(EditorBlockDocumentTiptapError::InvalidDocument(
                "列表节点首子节点类型不正确",
            ))
        }
    };

    let inline_content = extract_block_container_content(content)?;
    let mut child_block_ids = Vec::new();
    let mut child_blocks = Vec::new();
    for (index, child) in content.iter().enumerate() {
        let (child_root_ids, mut nested_blocks) = match child {
            TiptapNode::BulletList { attrs, content } => list_node_to_blocks(
                attrs.block_id.as_deref(),
                EditorBlockType::BulletListItem,
                content,
                &format!("{child_fallback_prefix}_bullet_{}", index + 1),
            )?,
            TiptapNode::OrderedList { attrs, content } => list_node_to_blocks(
                attrs.block_id.as_deref(),
                EditorBlockType::NumberedListItem,
                content,
                &format!("{child_fallback_prefix}_ordered_{}", index + 1),
            )?,
            TiptapNode::TaskList { attrs, content } => list_node_to_blocks(
                attrs.block_id.as_deref(),
                EditorBlockType::Todo,
                content,
                &format!("{child_fallback_prefix}_task_{}", index + 1),
            )?,
            _ => continue,
        };
        child_block_ids.extend(child_root_ids);
        child_blocks.append(&mut nested_blocks);
    }

    Ok((
        EditorBlock {
            block_id,
            block_type,
            props: BlockProps {
                checked,
                ..BlockProps::default()
            },
            content_nodes: text_nodes_from_tiptap(inline_content)?,
            child_block_ids,
        },
        child_blocks,
    ))
}

fn list_task_node_to_block(
    block_id: String,
    content: &[TiptapNode],
) -> Result<EditorBlock, EditorBlockDocumentTiptapError> {
    let Some(first) = content.first() else {
        return Err(EditorBlockDocumentTiptapError::InvalidDocument(
            "任务列表节点缺少 task_item",
        ));
    };
    let (checked, item_content) = match first {
        TiptapNode::TaskItem { attrs, content } => (
            Some(attrs.checked),
            extract_block_container_content(content)?,
        ),
        _ => {
            return Err(EditorBlockDocumentTiptapError::InvalidDocument(
                "任务列表节点首子节点类型不正确",
            ))
        }
    };
    Ok(EditorBlock {
        block_id,
        block_type: EditorBlockType::Todo,
        props: BlockProps {
            checked,
            ..BlockProps::default()
        },
        content_nodes: text_nodes_from_tiptap(item_content)?,
        child_block_ids: vec![],
    })
}

fn extract_list_item_inline_content(
    content: &[TiptapNode],
) -> Result<&[TiptapNode], EditorBlockDocumentTiptapError> {
    let Some(first) = content.first() else {
        return Err(EditorBlockDocumentTiptapError::InvalidDocument(
            "列表节点缺少 list_item/task_item",
        ));
    };
    match first {
        TiptapNode::ListItem { content, .. } | TiptapNode::TaskItem { content, .. } => {
            extract_block_container_content(content)
        }
        _ => Err(EditorBlockDocumentTiptapError::InvalidDocument(
            "列表节点首子节点类型不正确",
        )),
    }
}

fn extract_block_container_content(
    content: &[TiptapNode],
) -> Result<&[TiptapNode], EditorBlockDocumentTiptapError> {
    let Some(first) = content.first() else {
        return Ok(content);
    };
    match first {
        TiptapNode::Paragraph { content, .. } => Ok(content),
        _ => Ok(content),
    }
}

fn text_nodes_to_tiptap(
    nodes: &[ContentNode],
) -> Result<Vec<TiptapNode>, EditorBlockDocumentTiptapError> {
    let mut out = Vec::new();
    for node in nodes {
        match &node.payload {
            ContentNodePayload::Text { text, marks } => out.push(TiptapNode::Text {
                text: text.clone(),
                marks: marks.iter().map(text_mark_to_tiptap).collect(),
            }),
            ContentNodePayload::HardBreak => out.push(TiptapNode::HardBreak),
            ContentNodePayload::ReferenceToken { .. } => {
                return Err(EditorBlockDocumentTiptapError::UnsupportedNode(
                    "引用 token 暂不映射到 Tiptap 文本合同",
                ))
            }
        }
    }
    Ok(out)
}

fn text_nodes_from_tiptap(
    nodes: &[TiptapNode],
) -> Result<Vec<ContentNode>, EditorBlockDocumentTiptapError> {
    let mut out = Vec::new();
    for node in nodes {
        match node {
            TiptapNode::Text { text, marks } => out.push(ContentNode {
                payload: ContentNodePayload::Text {
                    text: text.clone(),
                    marks: marks.iter().filter_map(tiptap_mark_to_text_mark).collect(),
                },
                attrs: BTreeMap::new(),
            }),
            TiptapNode::HardBreak => out.push(ContentNode {
                payload: ContentNodePayload::HardBreak,
                attrs: BTreeMap::new(),
            }),
            _ => {
                return Err(EditorBlockDocumentTiptapError::UnsupportedNode(
                    "当前只支持文本节点内容",
                ))
            }
        }
    }
    Ok(out)
}

fn text_mark_to_tiptap(mark: &TextMark) -> TiptapMark {
    let r#type = match mark {
        TextMark::Bold => "bold",
        TextMark::Italic => "italic",
        TextMark::Underline => "underline",
        TextMark::Strike => "strike",
        TextMark::Code => "code",
    };
    TiptapMark {
        r#type: r#type.to_string(),
    }
}

fn tiptap_mark_to_text_mark(mark: &TiptapMark) -> Option<TextMark> {
    match mark.r#type.as_str() {
        "bold" => Some(TextMark::Bold),
        "italic" => Some(TextMark::Italic),
        "underline" => Some(TextMark::Underline),
        "strike" => Some(TextMark::Strike),
        "code" => Some(TextMark::Code),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn converts_horizontal_rule_to_divider_block() {
        let doc = TiptapNode::Doc {
            content: vec![TiptapNode::HorizontalRule {
                attrs: TiptapHorizontalRuleAttrs {
                    block_id: Some("divider_1".into()),
                },
            }],
        };

        let parsed = EditorBlockDocumentTiptapBridge::from_tiptap_doc("doc_1", &doc)
            .expect("horizontalRule should convert to divider");

        assert_eq!(parsed.root_block_ids, vec!["divider_1"]);
        assert_eq!(parsed.blocks[0].block_type, EditorBlockType::Divider);
        assert!(parsed.blocks[0].content_nodes.is_empty());
    }

    #[test]
    fn converts_divider_block_to_horizontal_rule() {
        let document = EditorBlockDocument {
            document_id: "doc_1".into(),
            root_block_ids: vec!["divider_1".into()],
            blocks: vec![EditorBlock {
                block_id: "divider_1".into(),
                block_type: EditorBlockType::Divider,
                props: BlockProps::default(),
                content_nodes: vec![],
                child_block_ids: vec![],
            }],
        };

        let tiptap = EditorBlockDocumentTiptapBridge::to_tiptap_doc(&document)
            .expect("divider should convert to horizontalRule");

        assert_eq!(
            tiptap,
            TiptapNode::Doc {
                content: vec![TiptapNode::HorizontalRule {
                    attrs: TiptapHorizontalRuleAttrs {
                        block_id: Some("divider_1".into()),
                    },
                }],
            }
        );
    }

    #[test]
    fn converts_nested_bullet_list_to_child_blocks() {
        let doc = TiptapNode::Doc {
            content: vec![TiptapNode::BulletList {
                attrs: TiptapListAttrs {
                    block_id: Some("list_root".into()),
                },
                content: vec![TiptapNode::ListItem {
                    attrs: TiptapListItemAttrs {
                        block_id: Some("parent".into()),
                    },
                    content: vec![
                        TiptapNode::Paragraph {
                            attrs: TiptapParagraphAttrs::default(),
                            content: vec![TiptapNode::Text {
                                text: "E19 parent".into(),
                                marks: vec![],
                            }],
                        },
                        TiptapNode::BulletList {
                            attrs: TiptapListAttrs::default(),
                            content: vec![TiptapNode::ListItem {
                                attrs: TiptapListItemAttrs {
                                    block_id: Some("child".into()),
                                },
                                content: vec![TiptapNode::Paragraph {
                                    attrs: TiptapParagraphAttrs::default(),
                                    content: vec![TiptapNode::Text {
                                        text: "E19 child".into(),
                                        marks: vec![],
                                    }],
                                }],
                            }],
                        },
                    ],
                }],
            }],
        };

        let parsed = EditorBlockDocumentTiptapBridge::from_tiptap_doc("doc_1", &doc)
            .expect("nested list should convert to child blocks");

        assert_eq!(parsed.root_block_ids, vec!["parent"]);
        assert_eq!(parsed.blocks.len(), 2);
        assert_eq!(parsed.blocks[0].block_id, "parent");
        assert_eq!(parsed.blocks[0].block_type, EditorBlockType::BulletListItem);
        assert_eq!(parsed.blocks[0].child_block_ids, vec!["child"]);
        assert_eq!(parsed.blocks[1].block_id, "child");
        assert_eq!(parsed.blocks[1].block_type, EditorBlockType::BulletListItem);
    }

    #[test]
    fn converts_child_blocks_to_nested_bullet_list() {
        let document = EditorBlockDocument {
            document_id: "doc_1".into(),
            root_block_ids: vec!["parent".into()],
            blocks: vec![
                EditorBlock {
                    block_id: "parent".into(),
                    block_type: EditorBlockType::BulletListItem,
                    props: BlockProps::default(),
                    content_nodes: vec![ContentNode {
                        payload: ContentNodePayload::Text {
                            text: "E19 parent".into(),
                            marks: vec![],
                        },
                        attrs: BTreeMap::new(),
                    }],
                    child_block_ids: vec!["child".into()],
                },
                EditorBlock {
                    block_id: "child".into(),
                    block_type: EditorBlockType::BulletListItem,
                    props: BlockProps::default(),
                    content_nodes: vec![ContentNode {
                        payload: ContentNodePayload::Text {
                            text: "E19 child".into(),
                            marks: vec![],
                        },
                        attrs: BTreeMap::new(),
                    }],
                    child_block_ids: vec![],
                },
            ],
        };

        let tiptap = EditorBlockDocumentTiptapBridge::to_tiptap_doc(&document)
            .expect("child blocks should convert to nested list");

        let TiptapNode::Doc { content } = tiptap else {
            panic!("expected doc");
        };
        let Some(TiptapNode::BulletList { content, .. }) = content.first() else {
            panic!("expected root bullet list");
        };
        let Some(TiptapNode::ListItem { content, .. }) = content.first() else {
            panic!("expected root list item");
        };
        assert!(content
            .iter()
            .any(|node| matches!(node, TiptapNode::BulletList { .. })));
    }

    #[test]
    fn preserves_toc_node_snapshot() {
        let doc = TiptapNode::Doc {
            content: vec![TiptapNode::TocNode {
                attrs: TiptapTocNodeAttrs {
                    top_offset: Some(0),
                    max_show_count: Some(20),
                    show_title: Some(true),
                    block_id: Some("toc_1".into()),
                    extra: BTreeMap::new(),
                },
            }],
        };

        let parsed = EditorBlockDocumentTiptapBridge::from_tiptap_doc("doc_1", &doc)
            .expect("tocNode should convert to toc block");
        assert_eq!(parsed.root_block_ids, vec!["toc_1"]);
        assert_eq!(parsed.blocks[0].block_type, EditorBlockType::Toc);
        assert_eq!(
            parsed.blocks[0]
                .props
                .extra
                .get("tiptapTocNode")
                .and_then(|value| value.get("type"))
                .and_then(Value::as_str),
            Some("tocNode")
        );

        let restored = EditorBlockDocumentTiptapBridge::to_tiptap_doc(&parsed)
            .expect("toc block should restore to tocNode");
        assert_eq!(restored, doc);
    }

    #[test]
    fn preserves_paragraph_text_alignment() {
        let doc = TiptapNode::Doc {
            content: vec![TiptapNode::Paragraph {
                attrs: TiptapParagraphAttrs {
                    block_id: Some("align_1".into()),
                    text_align: Some("center".into()),
                    ..TiptapParagraphAttrs::default()
                },
                content: vec![TiptapNode::Text {
                    text: "E20 center".into(),
                    marks: vec![],
                }],
            }],
        };

        let parsed = EditorBlockDocumentTiptapBridge::from_tiptap_doc("doc_1", &doc)
            .expect("textAlign should convert to block props");
        assert_eq!(
            parsed.blocks[0].props.extra.get("textAlign"),
            Some(&serde_json::json!("center"))
        );

        let restored = EditorBlockDocumentTiptapBridge::to_tiptap_doc(&parsed)
            .expect("textAlign should restore to tiptap attrs");
        let TiptapNode::Doc { content } = restored else {
            panic!("expected doc");
        };
        let Some(TiptapNode::Paragraph { attrs, .. }) = content.first() else {
            panic!("expected paragraph");
        };
        assert_eq!(attrs.text_align.as_deref(), Some("center"));
    }

    #[test]
    fn preserves_mindmap_paragraph_placeholder() {
        let doc = TiptapNode::Doc {
            content: vec![TiptapNode::Paragraph {
                attrs: TiptapParagraphAttrs {
                    block_id: Some("mind_1".into()),
                    mnote_block_type: Some("mindmap".into()),
                    mindmap_id: Some("mind_1".into()),
                    root_node_id: Some("root".into()),
                    projection_version: Some(1),
                    ..TiptapParagraphAttrs::default()
                },
                content: vec![],
            }],
        };

        let parsed = EditorBlockDocumentTiptapBridge::from_tiptap_doc("doc_1", &doc)
            .expect("mindmap placeholder should convert to mindmap block");
        assert_eq!(parsed.root_block_ids, vec!["mind_1"]);
        assert_eq!(parsed.blocks[0].block_type, EditorBlockType::Mindmap);
        assert_eq!(
            parsed.blocks[0].props.extra.get("mindmapId"),
            Some(&serde_json::json!("mind_1"))
        );

        let restored = EditorBlockDocumentTiptapBridge::to_tiptap_doc(&parsed)
            .expect("mindmap block should restore to paragraph placeholder");
        assert_eq!(restored, doc);
    }
}
