use std::collections::HashMap;

use serde_json::{Map, Value};

/// Definition of a block type, including its default props and rendering hints.
#[derive(Debug, Clone)]
pub struct BlockTypeDef {
    pub name: String,
    pub default_tag: Option<String>,
    pub default_props: Map<String, Value>,
}

impl BlockTypeDef {
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            default_tag: None,
            default_props: Map::new(),
        }
    }

    #[must_use]
    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
        self.default_tag = Some(tag.into());
        self
    }

    #[must_use]
    pub fn with_props(mut self, props: Map<String, Value>) -> Self {
        self.default_props = props;
        self
    }
}

/// Definition of a mark type, including its default rendering hints.
#[derive(Debug, Clone)]
pub struct MarkTypeDef {
    pub name: String,
    pub default_tag: Option<String>,
}

impl MarkTypeDef {
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            default_tag: None,
        }
    }

    #[must_use]
    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
        self.default_tag = Some(tag.into());
        self
    }
}

/// Registry of known block types and mark types.
/// Validates that operations only use registered types.
#[derive(Debug, Clone)]
pub struct TypeRegistry {
    block_types: HashMap<String, BlockTypeDef>,
    mark_types: HashMap<String, MarkTypeDef>,
}

// Built-in block type constants
pub mod block_type {
    pub const PARAGRAPH: &str = "paragraph";
    pub const HEADING: &str = "heading";
    pub const TODO: &str = "todo";
    pub const LIST: &str = "list";
    pub const QUOTE: &str = "quote";
    pub const TOGGLE: &str = "toggle";
    pub const TABLE: &str = "table";
    pub const TABLE_ROW: &str = "table_row";
    pub const TABLE_CELL: &str = "table_cell";
    /// Void/atomic horizontal rule block. Has no editable text content;
    /// rendered as `<hr>`. Created by the `---` markdown shortcut.
    pub const DIVIDER: &str = "divider";
    /// Void/atomic image block. Has no editable text content; rendered
    /// as `<figure><img>`. Inserted via the React layer's
    /// `uploadImage` callback (`packages/react/src/plugins/image.tsx`).
    /// Carries `src` (resolved URL) and `alt` (accessibility text) in
    /// `props`.
    pub const IMAGE: &str = "image";
}

// Built-in mark type constants
pub mod mark_type {
    pub const BOLD: &str = "bold";
    pub const ITALIC: &str = "italic";
    pub const UNDERLINE: &str = "underline";
    pub const CODE: &str = "code";
    pub const STRIKE: &str = "strike";
}

impl TypeRegistry {
    /// Create a registry with only the built-in types.
    pub fn new() -> Self {
        let mut registry = Self {
            block_types: HashMap::new(),
            mark_types: HashMap::new(),
        };
        registry.register_builtins();
        registry
    }

    fn register_builtins(&mut self) {
        // Block types
        self.register_block_type(BlockTypeDef::new(block_type::PARAGRAPH).with_tag("p"));
        self.register_block_type(
            BlockTypeDef::new(block_type::HEADING)
                .with_tag("h1")
                .with_props({
                    let mut m = Map::new();
                    m.insert("level".into(), Value::Number(1.into()));
                    m
                }),
        );
        self.register_block_type(
            BlockTypeDef::new(block_type::TODO)
                .with_tag("div")
                .with_props({
                    let mut m = Map::new();
                    m.insert("checked".into(), Value::Bool(false));
                    m
                }),
        );
        self.register_block_type(
            BlockTypeDef::new(block_type::LIST)
                .with_tag("div")
                .with_props({
                    let mut m = Map::new();
                    m.insert("style".into(), Value::String("unordered".into()));
                    m.insert("indent".into(), Value::Number(0.into()));
                    m
                }),
        );
        self.register_block_type(BlockTypeDef::new(block_type::QUOTE).with_tag("blockquote"));
        // Divider is a void block: no text content, rendered as <hr>.
        self.register_block_type(BlockTypeDef::new(block_type::DIVIDER).with_tag("hr"));
        // Image is a void block: no text content, rendered as <figure><img>.
        // `src` resolves to the uploaded URL (returned by the consumer-
        // supplied `uploadImage` callback on EditorProvider); `alt`
        // carries the accessibility caption. `width` is the displayed
        // pixel width set by the resize handle drag (height auto-
        // computes from the image's natural aspect ratio); `0` means
        // "use the image's natural width, capped by container".
        self.register_block_type(
            BlockTypeDef::new(block_type::IMAGE)
                .with_tag("figure")
                .with_props({
                    let mut m = Map::new();
                    m.insert("src".into(), Value::String(String::new()));
                    m.insert("alt".into(), Value::String(String::new()));
                    m.insert("width".into(), Value::Number(0.into()));
                    m
                }),
        );
        self.register_block_type(
            BlockTypeDef::new(block_type::TOGGLE)
                .with_tag("div")
                .with_props({
                    let mut m = Map::new();
                    m.insert("collapsed".into(), Value::Bool(false));
                    m
                }),
        );
        self.register_block_type(BlockTypeDef::new(block_type::TABLE).with_tag("table"));
        self.register_block_type(BlockTypeDef::new(block_type::TABLE_ROW).with_tag("tr"));
        self.register_block_type(
            BlockTypeDef::new(block_type::TABLE_CELL)
                .with_tag("td")
                .with_props({
                    let mut m = Map::new();
                    m.insert("colspan".into(), Value::Number(1.into()));
                    m.insert("rowspan".into(), Value::Number(1.into()));
                    m
                }),
        );

        // Mark types
        self.register_mark_type(MarkTypeDef::new(mark_type::BOLD).with_tag("strong"));
        self.register_mark_type(MarkTypeDef::new(mark_type::ITALIC).with_tag("em"));
        self.register_mark_type(MarkTypeDef::new(mark_type::UNDERLINE).with_tag("u"));
        self.register_mark_type(MarkTypeDef::new(mark_type::CODE).with_tag("code"));
        self.register_mark_type(MarkTypeDef::new(mark_type::STRIKE).with_tag("s"));
    }

    // --- Registration ---

    pub fn register_block_type(&mut self, def: BlockTypeDef) {
        self.block_types.insert(def.name.clone(), def);
    }

    pub fn register_mark_type(&mut self, def: MarkTypeDef) {
        self.mark_types.insert(def.name.clone(), def);
    }

    // --- Validation ---

    pub fn is_valid_block_type(&self, name: &str) -> bool {
        self.block_types.contains_key(name)
    }

    pub fn is_valid_mark_type(&self, name: &str) -> bool {
        self.mark_types.contains_key(name)
    }

    // --- Lookup ---

    pub fn get_block_type(&self, name: &str) -> Option<&BlockTypeDef> {
        self.block_types.get(name)
    }

    pub fn get_mark_type(&self, name: &str) -> Option<&MarkTypeDef> {
        self.mark_types.get(name)
    }

    pub fn block_type_names(&self) -> Vec<&str> {
        self.block_types.keys().map(String::as_str).collect()
    }

    pub fn mark_type_names(&self) -> Vec<&str> {
        self.mark_types.keys().map(String::as_str).collect()
    }
}

impl Default for TypeRegistry {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn builtins_registered() {
        let registry = TypeRegistry::new();
        assert!(registry.is_valid_block_type("paragraph"));
        assert!(registry.is_valid_block_type("heading"));
        assert!(registry.is_valid_block_type("todo"));
        assert!(registry.is_valid_block_type("list"));
        assert!(registry.is_valid_block_type("quote"));
        assert!(registry.is_valid_block_type("divider"));
        assert!(registry.is_valid_block_type("image"));
        assert!(registry.is_valid_block_type("toggle"));
        assert!(registry.is_valid_block_type("table"));
        assert!(registry.is_valid_block_type("table_row"));
        assert!(registry.is_valid_block_type("table_cell"));
        assert!(!registry.is_valid_block_type("nonexistent"));

        assert!(registry.is_valid_mark_type("bold"));
        assert!(registry.is_valid_mark_type("italic"));
        assert!(!registry.is_valid_mark_type("rainbow"));
    }

    #[test]
    fn custom_block_type() {
        let mut registry = TypeRegistry::new();
        assert!(!registry.is_valid_block_type("callout"));

        registry.register_block_type(BlockTypeDef::new("callout").with_tag("aside").with_props({
            let mut m = Map::new();
            m.insert("variant".into(), Value::String("info".into()));
            m
        }));

        assert!(registry.is_valid_block_type("callout"));
        let def = registry.get_block_type("callout").unwrap();
        assert_eq!(def.default_tag.as_deref(), Some("aside"));
    }

    #[test]
    fn custom_mark_type() {
        let mut registry = TypeRegistry::new();
        registry.register_mark_type(MarkTypeDef::new("color").with_tag("span"));

        assert!(registry.is_valid_mark_type("color"));
        let def = registry.get_mark_type("color").unwrap();
        assert_eq!(def.default_tag.as_deref(), Some("span"));
    }

    #[test]
    fn registration_does_not_leak() {
        // Register the same type 1000 times — should overwrite, not accumulate.
        let mut registry = TypeRegistry::new();
        for _ in 0..1000 {
            registry.register_block_type(BlockTypeDef::new("temp"));
        }
        // Exactly one entry per unique name.
        let mut count = 0;
        for name in registry.block_type_names() {
            if name == "temp" {
                count += 1;
            }
        }
        assert_eq!(count, 1);
    }
}
