use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::fmt;
use ts_rs::TS;

use super::inline::TextSpan;

/// Canonical block-type strings.
///
/// Centralising these here prevents typos sneaking into operation dispatch,
/// input rules, and validation paths: every site that constructs a block
/// type, compares against one, or embeds one in an error message should
/// reference these constants rather than repeating the literal string. The
/// helpers on [`Block`] (`is_table`, `is_paragraph`, …) already compare
/// against them, so readers get the same guarantee from both directions.
pub mod block_types {
    pub const PARAGRAPH: &str = "paragraph";
    pub const HEADING: &str = "heading";
    pub const TODO: &str = "todo";
    pub const LIST: &str = "list";
    pub const TOGGLE: &str = "toggle";
    pub const QUOTE: &str = "quote";
    pub const CODE: &str = "code";
    pub const DIVIDER: &str = "divider";
    pub const IMAGE: &str = "image";
    pub const TABLE: &str = "table";
    pub const TABLE_ROW: &str = "table_row";
    pub const TABLE_CELL: &str = "table_cell";
}

/// Canonical prop-key strings shared by the transform, apply, and import
/// paths. Kept alongside [`block_types`] so contributors have a single
/// reference point when working on block semantics.
pub mod prop_keys {
    /// Indent level (i64) applied to list / block-nesting.
    pub const INDENT: &str = "indent";
    /// Alignment string ("center" / "right" / "justify"); absence = left.
    pub const ALIGN: &str = "align";
    /// Master cell id for merged-cell coverage tracking.
    pub const COVERED_BY: &str = "covered_by";
    /// Heading level (1..=3).
    pub const LEVEL: &str = "level";
    /// Todo checkbox state.
    pub const CHECKED: &str = "checked";
    /// Toggle collapsed state.
    pub const COLLAPSED: &str = "collapsed";
    /// Code fence language.
    pub const LANGUAGE: &str = "language";
    /// Table column definitions (Vec<{width, …}>).
    pub const COLUMNS: &str = "columns";
    /// Collapsed-cursor pending marks (serialized Vec<Mark>).
    pub const PENDING_MARKS: &str = "pendingMarks";
}

/// Text alignment applied to a block. The `Left` variant is the default and
/// is stored as the *absence* of the `align` prop so documents stay minimal.
///
/// Serializes to lowercase strings (`"left"`, `"center"`, `"right"`, `"justify"`)
/// to match the JavaScript layer and CSS `text-align` values.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "lowercase")]
pub enum BlockAlignment {
    #[default]
    Left,
    Center,
    Right,
    Justify,
}

impl BlockAlignment {
    /// Lowercase string used both for serialization into `props["align"]` and
    /// as the CSS `text-align` value.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Left => "left",
            Self::Center => "center",
            Self::Right => "right",
            Self::Justify => "justify",
        }
    }

    /// Parse from the stored string. Unknown values fall back to `Left` so
    /// that legacy documents or typos never cause hard failures.
    #[must_use]
    pub fn from_str_lossy(s: &str) -> Self {
        match s {
            "center" => Self::Center,
            "right" => Self::Right,
            "justify" => Self::Justify,
            _ => Self::Left,
        }
    }
}

impl fmt::Display for BlockAlignment {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct BlockId(pub String);

impl BlockId {
    pub fn new(id: impl Into<String>) -> Self {
        Self(id.into())
    }
}

impl fmt::Display for BlockId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl From<String> for BlockId {
    fn from(s: String) -> Self {
        Self(s)
    }
}

impl From<&str> for BlockId {
    fn from(s: &str) -> Self {
        Self(s.to_string())
    }
}

/// A block in the document. Block type is extensible via `ty` string.
///
/// Built-in types: "paragraph", "heading", "todo"
/// Custom types: "callout", "code", "quote", "divider", "image", etc.
///
/// `props` carries type-specific properties AND rendering hints:
///   - Heading: `{ "level": 1 }`
///   - Todo: `{ "checked": false }`
///   - Rendering: `{ "tag": "h1", "className": "custom", "style": { ... } }`
///   - Custom: any key-value pairs the plugin defines
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct Block {
    pub id: BlockId,
    pub parent: Option<BlockId>,
    pub children: Vec<BlockId>,
    #[serde(rename = "type")]
    pub ty: String,
    #[serde(default, skip_serializing_if = "Map::is_empty")]
    pub props: Map<String, Value>,
    pub content: Vec<TextSpan>,
}

impl Block {
    pub fn new(id: BlockId, block_type: impl Into<String>) -> Self {
        Self {
            id,
            parent: None,
            children: Vec::new(),
            ty: block_type.into(),
            props: Map::new(),
            content: Vec::new(),
        }
    }

    pub fn with_props(
        id: BlockId,
        block_type: impl Into<String>,
        props: Map<String, Value>,
    ) -> Self {
        Self {
            id,
            parent: None,
            children: Vec::new(),
            ty: block_type.into(),
            props,
            content: Vec::new(),
        }
    }

    pub fn new_paragraph(id: BlockId) -> Self {
        Self::new(id, "paragraph")
    }

    pub fn new_heading(id: BlockId, level: u8) -> Self {
        let mut props = Map::new();
        props.insert("level".into(), Value::Number(level.into()));
        Self::with_props(id, "heading", props)
    }

    pub fn new_todo(id: BlockId, checked: bool) -> Self {
        let mut props = Map::new();
        props.insert("checked".into(), Value::Bool(checked));
        Self::with_props(id, "todo", props)
    }

    pub fn new_table(id: BlockId, columns: Vec<Value>) -> Self {
        let mut props = Map::new();
        props.insert("columns".into(), Value::Array(columns));
        Self::with_props(id, "table", props)
    }

    pub fn new_table_row(id: BlockId) -> Self {
        Self::new(id, "table_row")
    }

    pub fn new_table_cell(id: BlockId) -> Self {
        let mut props = Map::new();
        props.insert("colspan".into(), Value::Number(1.into()));
        props.insert("rowspan".into(), Value::Number(1.into()));
        Self::with_props(id, "table_cell", props)
    }

    /// Create a new divider (horizontal rule) block. Dividers are void:
    /// they carry no text content and render as `<hr>`.
    pub fn new_divider(id: BlockId) -> Self {
        Self::new(id, "divider")
    }

    /// Create a new image block. Images are void: they carry no
    /// editable text content and render as `<figure><img>`. The `src`
    /// is the URL returned by the consumer-supplied `uploadImage`
    /// callback; `alt` is the accessibility caption (may be empty).
    /// `width` defaults to `0` ("use natural width"); the resize
    /// handle in `ImageBlockEditor` writes the user-chosen display
    /// pixel width into the prop.
    pub fn new_image(id: BlockId, src: impl Into<String>, alt: impl Into<String>) -> Self {
        let mut props = Map::new();
        props.insert("src".into(), Value::String(src.into()));
        props.insert("alt".into(), Value::String(alt.into()));
        props.insert("width".into(), Value::Number(0.into()));
        Self::with_props(id, "image", props)
    }

    // --- Type checks ---

    pub fn is_type(&self, block_type: &str) -> bool {
        self.ty == block_type
    }

    pub fn is_paragraph(&self) -> bool {
        self.ty == "paragraph"
    }

    pub fn is_heading(&self) -> bool {
        self.ty == "heading"
    }

    pub fn is_todo(&self) -> bool {
        self.ty == "todo"
    }

    pub fn is_list(&self) -> bool {
        self.ty == "list"
    }

    pub fn is_toggle(&self) -> bool {
        self.ty == "toggle"
    }

    pub fn is_quote(&self) -> bool {
        self.ty == "quote"
    }

    /// Fenced code block. Enter inside one inserts `\n` rather than
    /// splitting the block, and Tab inserts two literal spaces
    /// instead of indenting. See
    /// `crates/core/src/command/transform.rs` for the enforced
    /// semantics.
    pub fn is_code(&self) -> bool {
        self.ty == "code"
    }

    pub fn is_table(&self) -> bool {
        self.ty == "table"
    }

    pub fn is_table_row(&self) -> bool {
        self.ty == "table_row"
    }

    pub fn is_table_cell(&self) -> bool {
        self.ty == "table_cell"
    }

    pub fn is_divider(&self) -> bool {
        self.ty == "divider"
    }

    pub fn is_image(&self) -> bool {
        self.ty == "image"
    }

    /// Returns true if this block is part of a table structure
    /// (table, table_row, or table_cell).
    pub fn is_table_related(&self) -> bool {
        self.is_table() || self.is_table_row() || self.is_table_cell()
    }

    /// Returns true if this block is a void/atomic block — one that
    /// accepts no editable text content (e.g. `divider`, `image`).
    /// Renderers should mark these with `contentEditable={false}` and
    /// ignore `content` entirely.
    pub fn is_void(&self) -> bool {
        self.is_divider() || self.is_image()
    }

    // --- Prop accessors ---

    pub fn prop(&self, key: &str) -> Option<&Value> {
        self.props.get(key)
    }

    pub fn prop_str(&self, key: &str) -> Option<&str> {
        self.props.get(key).and_then(|v| v.as_str())
    }

    pub fn prop_u64(&self, key: &str) -> Option<u64> {
        self.props.get(key).and_then(|v| v.as_u64())
    }

    pub fn prop_bool(&self, key: &str) -> Option<bool> {
        self.props.get(key).and_then(|v| v.as_bool())
    }

    #[must_use]
    pub fn indent_level(&self) -> i64 {
        self.props
            .get("indent")
            .and_then(Value::as_i64)
            .unwrap_or(0)
    }

    pub fn heading_level(&self) -> u8 {
        self.prop_u64("level").unwrap_or(1) as u8
    }

    pub fn is_checked(&self) -> bool {
        self.prop_bool("checked").unwrap_or(false)
    }

    /// Column span for a table cell (defaults to 1).
    pub fn colspan(&self) -> usize {
        self.prop_u64("colspan").unwrap_or(1) as usize
    }

    /// Row span for a table cell (defaults to 1).
    pub fn rowspan(&self) -> usize {
        self.prop_u64("rowspan").unwrap_or(1) as usize
    }

    /// Returns true if this cell is covered (hidden) by a merged master cell.
    pub fn is_covered_cell(&self) -> bool {
        self.prop_str("covered_by").is_some()
    }

    /// Returns the master cell's block ID if this cell is covered by a merge.
    pub fn covered_by(&self) -> Option<&str> {
        self.prop_str("covered_by")
    }

    /// The text alignment applied to this block. Defaults to
    /// [`BlockAlignment::Left`] when the `align` prop is absent.
    #[must_use]
    pub fn alignment(&self) -> BlockAlignment {
        self.prop_str("align")
            .map(BlockAlignment::from_str_lossy)
            .unwrap_or_default()
    }

    /// Get the rendering tag hint (e.g., "h1", "p", "div").
    pub fn tag(&self) -> Option<&str> {
        self.prop_str("tag")
    }

    /// Get the rendering class name.
    pub fn class_name(&self) -> Option<&str> {
        self.prop_str("className")
    }

    /// Get the rendering style object.
    pub fn style(&self) -> Option<&Map<String, Value>> {
        self.props.get("style").and_then(|v| v.as_object())
    }

    // --- Content ---

    /// Concatenate the block's own inline text spans into a single
    /// string. **Does NOT traverse `children`** — for tables, toggles,
    /// or any other subtree-bearing block, descendants are ignored.
    ///
    /// This is deliberate: most callers (Markdown export, PDF export,
    /// selection-trim logic) process children separately via their own
    /// walking strategies, and including them here would double-count.
    /// For the clipboard path that DOES need the full subtree, use
    /// [`crate::tree::extract_plain_text`] which takes a `by_id` map
    /// and recurses through `block.children` with table-row /
    /// table-cell aware joining.
    pub fn plain_text(&self) -> String {
        self.content.iter().map(|s| s.text.as_str()).collect()
    }

    pub fn text_len(&self) -> usize {
        self.content.iter().map(TextSpan::len).sum()
    }

    pub fn is_empty(&self) -> bool {
        self.content.is_empty() || self.content.iter().all(|s| s.text.is_empty())
    }
}

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

    #[test]
    fn block_types() {
        let p = Block::new_paragraph(BlockId::new("b1"));
        assert!(p.is_paragraph());
        assert!(!p.is_heading());

        let h = Block::new_heading(BlockId::new("b2"), 2);
        assert!(h.is_heading());
        assert_eq!(h.heading_level(), 2);

        let t = Block::new_todo(BlockId::new("b3"), true);
        assert!(t.is_todo());
        assert!(t.is_checked());
    }

    #[test]
    fn custom_block_type() {
        let mut props = Map::new();
        props.insert("language".into(), Value::String("rust".into()));
        props.insert("tag".into(), Value::String("pre".into()));

        let block = Block::with_props(BlockId::new("b1"), "code", props);
        assert!(block.is_type("code"));
        assert_eq!(block.prop_str("language"), Some("rust"));
        assert_eq!(block.tag(), Some("pre"));
    }

    #[test]
    fn block_with_style() {
        let mut style = Map::new();
        style.insert("background".into(), Value::String("#f0f0f0".into()));

        let mut props = Map::new();
        props.insert("style".into(), Value::Object(style));

        let block = Block::with_props(BlockId::new("b1"), "callout", props);
        assert!(block.style().is_some());
        assert_eq!(
            block.style().unwrap().get("background").unwrap().as_str(),
            Some("#f0f0f0")
        );
    }

    #[test]
    fn block_plain_text() {
        let mut block = Block::new_paragraph(BlockId::new("b1"));
        block.content = vec![TextSpan::plain("hello"), TextSpan::plain(" world")];
        assert_eq!(block.plain_text(), "hello world");
        assert_eq!(block.text_len(), 11);
    }

    #[test]
    fn block_is_empty() {
        let block = Block::new_paragraph(BlockId::new("b1"));
        assert!(block.is_empty());
    }

    #[test]
    fn divider_is_void() {
        let d = Block::new_divider(BlockId::new("b1"));
        assert!(d.is_divider());
        assert!(d.is_void());
        assert!(d.is_empty());
        assert_eq!(d.ty, "divider");
        assert!(!d.is_paragraph());
        assert!(!d.is_heading());
    }

    #[test]
    fn paragraph_is_not_void() {
        let p = Block::new_paragraph(BlockId::new("b1"));
        assert!(!p.is_void());
    }

    #[test]
    fn image_is_void() {
        let img = Block::new_image(BlockId::new("b1"), "https://x.test/a.png", "alt text");
        assert!(img.is_image());
        assert!(img.is_void());
        assert!(img.is_empty());
        assert_eq!(img.ty, "image");
        assert_eq!(img.prop_str("src"), Some("https://x.test/a.png"));
        assert_eq!(img.prop_str("alt"), Some("alt text"));
        // Default width is 0 = "use natural width" sentinel.
        assert_eq!(img.prop_u64("width"), Some(0));
    }
}
