//! Block parser for Logseq-compatible content blocks
//!
//! Parses markdown content in outliner format (bullet lists) into structured
//! Block objects. Supports:
//! - Task markers (TODO, DONE, DOING, etc.)
//! - Tags (#card, #important)
//! - Custom properties (`property::` value)
//! - Block references ((uuid))
//! - Page references [[page]]
//! - Nested hierarchy (indentation)

use std::collections::HashMap;
use std::fmt::Write;

use crate::error::Result;
use crate::models::{Block, TaskStatus};
use crate::regex_patterns::{
    LOGSEQ_WIKILINK_PATTERN, PROPERTY_INLINE_PATTERN, PROPERTY_START_PATTERN, TAG_PATTERN,
    UUID_REF_PATTERN,
};

/// Parser for converting markdown outliner format to/from Block structures
pub struct BlockParser;

impl BlockParser {
    /// Parse markdown content into a list of blocks
    ///
    /// # Format
    ///
    /// ```markdown
    /// - Block 1 #tag
    ///   - Nested block
    /// - TODO Block 2
    ///   priority:: high
    /// - Block with [[page-ref]] and ((block-ref))
    /// ```
    ///
    /// # Errors
    ///
    /// Returns an error if the markdown content contains invalid block syntax
    /// or if parsing of individual block lines fails.
    ///
    /// # Panics
    ///
    /// May panic if internal regex compilation fails (should never happen with
    /// hardcoded patterns) or if stack operations fail unexpectedly.
    pub fn parse(content: &str) -> Result<Vec<Block>> {
        let lines: Vec<&str> = content.lines().collect();
        if lines.is_empty() {
            return Ok(Vec::new());
        }

        let mut blocks = Vec::new();
        let mut stack: Vec<(usize, Block)> = Vec::new(); // (indent_level, block)

        for line in lines {
            if line.trim().is_empty() {
                continue;
            }

            let indent = count_indent(line);
            let trimmed = line.trim();

            // Check if this is a property line (key:: value without bullet)
            if !trimmed.starts_with('-') && !trimmed.starts_with('*') {
                // Try to parse as a property and add to the last block
                Self::try_add_property_to_last_block(&mut stack, trimmed);
                continue;
            }

            // Remove bullet marker
            let content_after_bullet = trimmed[1..].trim();

            // Parse the block
            let block = Self::parse_block_line(content_after_bullet);

            // Handle hierarchy based on indentation
            if stack.is_empty() {
                // First block or root level
                stack.push((indent, block));
            } else {
                // Pop blocks with greater or equal indentation and add to their parents
                Self::flush_stack_to_indent(&mut stack, &mut blocks, indent);
                stack.push((indent, block));
            }
        }

        // Flush remaining blocks in stack
        while let Some((_, block)) = stack.pop() {
            if let Some((_, parent)) = stack.last_mut() {
                parent.add_child(block);
            } else {
                blocks.push(block);
            }
        }

        Ok(blocks)
    }

    /// Flush stack entries with indentation >= target indent, adding them to
    /// blocks or parents
    fn flush_stack_to_indent(
        stack: &mut Vec<(usize, Block)>,
        blocks: &mut Vec<Block>,
        target_indent: usize,
    ) {
        while let Some((parent_indent, _)) = stack.last() {
            if target_indent > *parent_indent {
                break;
            }

            let (_, completed_block) = stack.pop().unwrap();

            // Add to parent or root
            if let Some((_, parent)) = stack.last_mut() {
                parent.add_child(completed_block);
            } else {
                blocks.push(completed_block);
            }
        }
    }

    /// Try to parse a property line and add it to the last block in the stack
    fn try_add_property_to_last_block(stack: &mut [(usize, Block)], line: &str) {
        if let Some((_, last_block)) = stack.last_mut() {
            if let Some(cap) = PROPERTY_START_PATTERN.captures(line) {
                let key = cap[1].to_string();
                let value = cap[2].to_string();
                last_block.properties.set_property(key, value);
            }
        }
    }

    /// Parse a single block line (without bullet marker)
    fn parse_block_line(line: &str) -> Block {
        let (task_status, after_status) = Self::extract_task_status(line);
        let (tags, after_tags) = Self::extract_tags(&after_status);
        let (properties, content) = Self::extract_properties(&after_tags);
        let block_refs = Self::extract_block_refs(&content);
        let page_refs = Self::extract_page_refs(&content);

        let mut block = Block::new(content.trim().to_string());
        block.properties.status = task_status;
        block.properties.tags = tags;
        block.properties.custom = properties;
        block.properties.block_refs = block_refs;
        block.properties.page_refs = page_refs;

        block
    }

    /// Extract task status from line start
    fn extract_task_status(line: &str) -> (Option<TaskStatus>, String) {
        let trimmed = line.trim();

        let status = if trimmed.starts_with("TODO ") {
            Some(TaskStatus::Todo)
        } else if trimmed.starts_with("DOING ") {
            Some(TaskStatus::Doing)
        } else if trimmed.starts_with("DONE ") {
            Some(TaskStatus::Done)
        } else if trimmed.starts_with("LATER ") {
            Some(TaskStatus::Later)
        } else if trimmed.starts_with("NOW ") {
            Some(TaskStatus::Now)
        } else if trimmed.starts_with("WAITING ") {
            Some(TaskStatus::Waiting)
        } else if trimmed.starts_with("CANCELLED ") {
            Some(TaskStatus::Cancelled)
        } else {
            None
        };

        match status {
            Some(s) => {
                let status_str = s.to_string();
                let after = trimmed.trim_start_matches(&status_str).trim();
                (Some(s), after.to_string())
            }
            None => (None, trimmed.to_string()),
        }
    }

    /// Extract hashtags from content
    fn extract_tags(content: &str) -> (Vec<String>, String) {
        let mut tags = Vec::new();
        let mut clean_content = content.to_string();

        for cap in TAG_PATTERN.captures_iter(content) {
            tags.push(cap[1].to_string());
        }

        // Remove tags from content
        clean_content = TAG_PATTERN.replace_all(&clean_content, "").to_string();

        (tags, clean_content)
    }

    /// Extract custom properties (`key::` value format)
    fn extract_properties(content: &str) -> (HashMap<String, String>, String) {
        let mut properties = HashMap::new();
        let mut clean_content = content.to_string();

        for cap in PROPERTY_INLINE_PATTERN.captures_iter(content) {
            let key = cap[1].to_string();
            let value = cap[2].to_string();
            properties.insert(key, value);
        }

        // Remove properties from content
        clean_content = PROPERTY_INLINE_PATTERN
            .replace_all(&clean_content, "")
            .to_string();

        (properties, clean_content)
    }

    /// Extract block references ((uuid))
    fn extract_block_refs(content: &str) -> Vec<String> {
        UUID_REF_PATTERN
            .captures_iter(content)
            .map(|cap| cap[1].to_string())
            .collect()
    }

    /// Extract page references [[page]]
    fn extract_page_refs(content: &str) -> Vec<String> {
        LOGSEQ_WIKILINK_PATTERN
            .captures_iter(content)
            .map(|cap| cap[1].to_string())
            .collect()
    }

    /// Serialize blocks back to markdown outliner format
    #[must_use]
    pub fn serialize(blocks: &[Block]) -> String {
        let mut output = String::new();
        for block in blocks {
            Self::serialize_block(block, 0, &mut output);
        }
        output
    }

    /// Serialize a single block and its children recursively
    fn serialize_block(block: &Block, indent_level: usize, output: &mut String) {
        let indent = "  ".repeat(indent_level);

        // Start with bullet
        output.push_str(&indent);
        output.push_str("- ");

        // Add task status if present
        if let Some(status) = block.properties.status {
            let _ = write!(output, "{status} ");
        }

        // Add content
        output.push_str(&block.content);

        // Add tags
        for tag in &block.properties.tags {
            let _ = write!(output, " #{tag}");
        }

        // Add page references (if not already in content)
        for page_ref in &block.properties.page_refs {
            if !block.content.contains(&format!("[[{page_ref}]]")) {
                let _ = write!(output, " [[{page_ref}]]");
            }
        }

        // Add block references (if not already in content)
        for block_ref in &block.properties.block_refs {
            if !block.content.contains(&format!("(({block_ref}))")) {
                let _ = write!(output, " (({block_ref}))");
            }
        }

        output.push('\n');

        // Add custom properties on next line(s) if present
        for (key, value) in &block.properties.custom {
            output.push_str(&indent);
            output.push_str("  ");
            let _ = writeln!(output, "{key}:: {value}");
        }

        // Recursively serialize children
        for child in &block.children {
            Self::serialize_block(child, indent_level + 1, output);
        }
    }
}

/// Count leading whitespace (indentation level)
fn count_indent(line: &str) -> usize {
    line.chars().take_while(|c| c.is_whitespace()).count()
}

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

    #[test]
    fn test_parse_simple_block() {
        let content = "- Simple block";
        let blocks = BlockParser::parse(content).unwrap();

        assert_eq!(blocks.len(), 1);
        assert_eq!(blocks[0].content, "Simple block");
        assert!(blocks[0].properties.status.is_none());
        assert!(blocks[0].properties.tags.is_empty());
    }

    #[test]
    fn test_parse_task_block() {
        let content = "- TODO Complete this task";
        let blocks = BlockParser::parse(content).unwrap();

        assert_eq!(blocks.len(), 1);
        assert_eq!(blocks[0].properties.status, Some(TaskStatus::Todo));
        assert_eq!(blocks[0].content, "Complete this task");
    }

    #[test]
    fn test_parse_block_with_tags() {
        let content = "- This is a #card with #tags";
        let blocks = BlockParser::parse(content).unwrap();

        assert_eq!(blocks.len(), 1);
        assert_eq!(blocks[0].properties.tags.len(), 2);
        assert!(blocks[0].properties.tags.contains(&"card".to_string()));
        assert!(blocks[0].properties.tags.contains(&"tags".to_string()));
    }

    #[test]
    fn test_parse_block_with_properties() {
        let content = "- Block with property\n  priority:: high";
        let blocks = BlockParser::parse(content).unwrap();

        assert_eq!(blocks.len(), 1);
        assert_eq!(
            blocks[0].properties.get_property("priority"),
            Some(&"high".to_string())
        );
    }

    #[test]
    fn test_parse_nested_blocks() {
        let content = r#"- Parent block
  - Child block 1
  - Child block 2
    - Grandchild block"#;

        let blocks = BlockParser::parse(content).unwrap();

        assert_eq!(blocks.len(), 1);
        assert_eq!(blocks[0].content, "Parent block");
        assert_eq!(blocks[0].children.len(), 2);
        assert_eq!(blocks[0].children[0].content, "Child block 1");
        assert_eq!(blocks[0].children[1].content, "Child block 2");
        assert_eq!(blocks[0].children[1].children.len(), 1);
        assert_eq!(
            blocks[0].children[1].children[0].content,
            "Grandchild block"
        );
    }

    #[test]
    fn test_parse_page_references() {
        let content = "- Block with [[Page Reference]]";
        let blocks = BlockParser::parse(content).unwrap();

        assert_eq!(blocks.len(), 1);
        assert_eq!(blocks[0].properties.page_refs.len(), 1);
        assert_eq!(blocks[0].properties.page_refs[0], "Page Reference");
    }

    #[test]
    fn test_serialize_simple_block() {
        let block = Block::new("Simple block".to_string());
        let markdown = BlockParser::serialize(&[block]);

        assert_eq!(markdown.trim(), "- Simple block");
    }

    #[test]
    fn test_serialize_task_block() {
        let mut block = Block::new("Complete this task".to_string());
        block.properties.status = Some(TaskStatus::Todo);
        let markdown = BlockParser::serialize(&[block]);

        assert_eq!(markdown.trim(), "- TODO Complete this task");
    }

    #[test]
    fn test_serialize_block_with_tags() {
        let mut block = Block::new("Card content".to_string());
        block.properties.add_tag("card".to_string());
        block.properties.add_tag("important".to_string());
        let markdown = BlockParser::serialize(&[block]);

        assert!(markdown.contains("#card"));
        assert!(markdown.contains("#important"));
    }

    #[test]
    fn test_serialize_nested_blocks() {
        let mut parent = Block::new("Parent".to_string());
        let child1 = Block::new("Child 1".to_string());
        let child2 = Block::new("Child 2".to_string());

        parent.add_child(child1);
        parent.add_child(child2);

        let markdown = BlockParser::serialize(&[parent]);

        assert!(markdown.contains("- Parent"));
        assert!(markdown.contains("  - Child 1"));
        assert!(markdown.contains("  - Child 2"));
    }

    #[test]
    fn test_round_trip() {
        let original = r#"- TODO Parent task #important
  - Child note
  - DONE Completed subtask"#;

        let blocks = BlockParser::parse(original).unwrap();
        let serialized = BlockParser::serialize(&blocks);
        let reparsed = BlockParser::parse(&serialized).unwrap();

        // Verify structure is preserved
        assert_eq!(blocks.len(), reparsed.len());
        assert_eq!(blocks[0].properties.status, reparsed[0].properties.status);
        assert_eq!(blocks[0].children.len(), reparsed[0].children.len());
    }

    #[test]
    fn test_extract_task_status() {
        let (status, content) = BlockParser::extract_task_status("TODO Task content");
        assert_eq!(status, Some(TaskStatus::Todo));
        assert_eq!(content, "Task content");

        let (status, content) = BlockParser::extract_task_status("DONE Completed task");
        assert_eq!(status, Some(TaskStatus::Done));
        assert_eq!(content, "Completed task");

        let (status, content) = BlockParser::extract_task_status("No task here");
        assert_eq!(status, None);
        assert_eq!(content, "No task here");
    }

    #[test]
    fn test_extract_tags() {
        let (tags, content) = BlockParser::extract_tags("Text with #tag1 and #tag2");
        assert_eq!(tags.len(), 2);
        assert!(tags.contains(&"tag1".to_string()));
        assert!(tags.contains(&"tag2".to_string()));
        assert!(content.contains("Text with"));
        assert!(content.contains("and"));
    }

    #[test]
    fn test_extract_properties() {
        let (props, _content) = BlockParser::extract_properties("priority:: high status:: active");
        assert_eq!(props.get("priority"), Some(&"high".to_string()));
        assert_eq!(props.get("status"), Some(&"active".to_string()));
    }

    #[test]
    fn test_count_indent() {
        assert_eq!(count_indent("- No indent"), 0);
        assert_eq!(count_indent("  - Two spaces"), 2);
        assert_eq!(count_indent("    - Four spaces"), 4);
    }
}
