//! Markdown Tree Parser — Phase-90 (T90.2)
//!
//! Parses Markdown documents into hierarchical tree structures using heading levels.
//!
//! # Algorithm
//!
//! 1. **Extract headings**: Scan lines for `# Title` patterns (respecting code blocks).
//! 2. **Assign text content**: Each heading owns the text from its line to the next heading.
//! 3. **Build tree**: Use a level-based stack to nest sections into a tree hierarchy.
//!
//! This is a pure-Rust, zero-LLM implementation inspired by PageIndex's `page_index_md.py`.

use super::types::{TreeNode, TreeIndexConfig, TreeIndexResult};

/// A flat heading record extracted from Markdown.
#[derive(Debug, Clone)]
struct HeadingRecord {
    /// The heading title text (without `#` prefix).
    title: String,
    /// The heading level (1-6).
    level: u8,
    /// The 1-based line number in the original document.
    line_num: usize,
}

/// Markdown tree parser.
///
/// Converts Markdown text into a `Vec<TreeNode>` (one root per H1 or top-level heading).
///
/// # Example
///
/// ```ignore
/// use aifastdb_core::tree_index::MarkdownTreeParser;
///
/// let md = "# Chapter 1\nSome text\n## Section 1.1\nMore text";
/// let parser = MarkdownTreeParser::new();
/// let roots = parser.parse(md).unwrap();
/// assert_eq!(roots.len(), 1);
/// assert_eq!(roots[0].children.len(), 1);
/// ```
pub struct MarkdownTreeParser {
    config: TreeIndexConfig,
}

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

impl MarkdownTreeParser {
    /// Create a parser with default configuration.
    pub fn new() -> Self {
        Self {
            config: TreeIndexConfig::default(),
        }
    }

    /// Create a parser with custom configuration.
    pub fn with_config(config: TreeIndexConfig) -> Self {
        Self { config }
    }

    /// Parse Markdown text into a list of root tree nodes.
    ///
    /// Returns one or more root nodes, each representing a top-level section.
    pub fn parse(&self, markdown: &str) -> TreeIndexResult<Vec<TreeNode>> {
        // Step 1: Extract heading records
        let lines: Vec<&str> = markdown.lines().collect();
        let headings = self.extract_headings(&lines);

        if headings.is_empty() {
            // No headings found — return entire document as a single root node
            let content = markdown.trim().to_string();
            if content.is_empty() {
                return Ok(Vec::new());
            }
            let node = TreeNode::new("0001", "Document", 1)
                .with_content(content)
                .with_line_num(1);
            return Ok(vec![node]);
        }

        // Step 2: Assign text content to each heading
        let nodes_with_content = self.assign_text_content(&headings, &lines);

        // Step 3: Build tree from flat list
        let tree = self.build_tree(nodes_with_content);

        Ok(tree)
    }

    /// Extract heading records from Markdown lines.
    ///
    /// Respects fenced code blocks (``` ... ```) — headings inside code blocks are ignored.
    fn extract_headings(&self, lines: &[&str]) -> Vec<HeadingRecord> {
        let mut headings = Vec::new();
        let mut in_code_block = false;

        for (idx, line) in lines.iter().enumerate() {
            let trimmed = line.trim();

            // Toggle code block state
            if trimmed.starts_with("```") {
                in_code_block = !in_code_block;
                continue;
            }

            // Skip lines inside code blocks
            if in_code_block {
                continue;
            }

            // Match heading pattern: #{1,6} followed by space and title
            if let Some(heading) = Self::parse_heading_line(trimmed) {
                if heading.level <= self.config.max_heading_level {
                    headings.push(HeadingRecord {
                        title: heading.title,
                        level: heading.level,
                        line_num: idx + 1, // 1-based
                    });
                }
            }
        }

        headings
    }

    /// Try to parse a line as a Markdown heading.
    ///
    /// Returns `Some((level, title))` if the line is a valid heading.
    fn parse_heading_line(line: &str) -> Option<HeadingRecord> {
        // Must start with # and not be just hashes
        if !line.starts_with('#') {
            return None;
        }

        // Count leading '#' characters
        let hash_count = line.chars().take_while(|c| *c == '#').count();
        if hash_count == 0 || hash_count > 6 {
            return None;
        }

        // Must be followed by a space (or the line is just `####`)
        let rest = &line[hash_count..];
        if rest.is_empty() {
            return None;
        }
        if !rest.starts_with(' ') {
            return None;
        }

        let title = rest.trim().to_string();
        if title.is_empty() {
            return None;
        }

        Some(HeadingRecord {
            title,
            level: hash_count as u8,
            line_num: 0, // Will be set by caller
        })
    }

    /// Assign text content to each heading.
    ///
    /// Each heading owns the text from its line to the line before the next heading.
    fn assign_text_content(&self, headings: &[HeadingRecord], lines: &[&str]) -> Vec<FlatNode> {
        let mut nodes = Vec::with_capacity(headings.len());

        for (i, heading) in headings.iter().enumerate() {
            let start_line = heading.line_num - 1; // Convert to 0-based
            let end_line = if i + 1 < headings.len() {
                headings[i + 1].line_num - 1
            } else {
                lines.len()
            };

            let text = lines[start_line..end_line].join("\n").trim().to_string();

            nodes.push(FlatNode {
                title: heading.title.clone(),
                level: heading.level,
                line_num: heading.line_num,
                text,
            });
        }

        nodes
    }

    /// Build a tree from a flat list of nodes using a level-based stack.
    ///
    /// Algorithm (same as PageIndex's `build_tree_from_nodes`):
    /// - Maintain a stack of (node_ref, level) pairs.
    /// - For each node, pop stack entries with level >= current level.
    /// - If stack is empty, this is a root node.
    /// - Otherwise, append as child of the top-of-stack node.
    fn build_tree(&self, flat_nodes: Vec<FlatNode>) -> Vec<TreeNode> {
        if flat_nodes.is_empty() {
            return Vec::new();
        }

        let mut root_nodes: Vec<TreeNode> = Vec::new();
        // Stack holds (index_into_root_or_parent, level)
        // We use a simple recursive approach instead
        let mut stack: Vec<(Vec<usize>, u8)> = Vec::new(); // (path, level)

        let mut node_counter: usize = 1;

        for flat in &flat_nodes {
            let node_id = format!("{:04}", node_counter);
            node_counter += 1;

            let tree_node = TreeNode::new(&node_id, &flat.title, flat.level)
                .with_content(&flat.text)
                .with_line_num(flat.line_num);

            // Pop stack entries with level >= current level
            while let Some((_, stack_level)) = stack.last() {
                if *stack_level >= flat.level {
                    stack.pop();
                } else {
                    break;
                }
            }

            if stack.is_empty() {
                // This is a root node
                root_nodes.push(tree_node);
                let idx = root_nodes.len() - 1;
                stack.push((vec![idx], flat.level));
            } else {
                // Append as child of the current stack top
                let (parent_path, _) = stack.last().unwrap();
                let parent_path = parent_path.clone();

                // Navigate to parent and add child
                let child_idx = add_child_at_path(&mut root_nodes, &parent_path, tree_node);

                // Push new path onto stack
                let mut child_path = parent_path;
                child_path.push(child_idx);
                stack.push((child_path, flat.level));
            }
        }

        root_nodes
    }
}

/// A flat (non-hierarchical) node with assigned text content.
#[derive(Debug)]
struct FlatNode {
    title: String,
    level: u8,
    line_num: usize,
    text: String,
}

/// Add a child node at the given path in the tree and return the child's index.
///
/// `path` is a sequence of indices: `[root_idx, child_idx, grandchild_idx, ...]`.
fn add_child_at_path(roots: &mut [TreeNode], path: &[usize], child: TreeNode) -> usize {
    if path.is_empty() {
        panic!("empty path in add_child_at_path");
    }

    let mut node = &mut roots[path[0]];
    for &idx in &path[1..] {
        node = &mut node.children[idx];
    }

    node.children.push(child);
    node.children.len() - 1
}

// ─── Tests ───────────────────────────────────────────────────────────────────

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

    #[test]
    fn test_parse_simple_document() {
        let md = "# Chapter 1\nSome intro text.\n## Section 1.1\nSection content.\n## Section 1.2\nMore content.";
        let parser = MarkdownTreeParser::new();
        let roots = parser.parse(md).unwrap();

        assert_eq!(roots.len(), 1);
        assert_eq!(roots[0].title, "Chapter 1");
        assert_eq!(roots[0].level, 1);
        assert_eq!(roots[0].children.len(), 2);
        assert_eq!(roots[0].children[0].title, "Section 1.1");
        assert_eq!(roots[0].children[1].title, "Section 1.2");
    }

    #[test]
    fn test_parse_multi_root() {
        let md = "# Part A\nContent A.\n# Part B\nContent B.";
        let parser = MarkdownTreeParser::new();
        let roots = parser.parse(md).unwrap();

        assert_eq!(roots.len(), 2);
        assert_eq!(roots[0].title, "Part A");
        assert_eq!(roots[1].title, "Part B");
    }

    #[test]
    fn test_parse_nested_tree() {
        let md = "\
# Root
Root text
## Level 2
L2 text
### Level 3
L3 text
#### Level 4
L4 text
## Another L2
Another L2 text";
        let parser = MarkdownTreeParser::new();
        let roots = parser.parse(md).unwrap();

        assert_eq!(roots.len(), 1);
        let root = &roots[0];
        assert_eq!(root.children.len(), 2);

        let l2 = &root.children[0];
        assert_eq!(l2.title, "Level 2");
        assert_eq!(l2.children.len(), 1);

        let l3 = &l2.children[0];
        assert_eq!(l3.title, "Level 3");
        assert_eq!(l3.children.len(), 1);

        let l4 = &l3.children[0];
        assert_eq!(l4.title, "Level 4");
        assert!(l4.is_leaf());

        let another_l2 = &root.children[1];
        assert_eq!(another_l2.title, "Another L2");
        assert!(another_l2.is_leaf());
    }

    #[test]
    fn test_parse_code_blocks_ignored() {
        let md = "\
# Real Heading
Some text
```
# This is NOT a heading
## Also not a heading
```
## Real Sub Heading
More text";
        let parser = MarkdownTreeParser::new();
        let roots = parser.parse(md).unwrap();

        assert_eq!(roots.len(), 1);
        assert_eq!(roots[0].children.len(), 1);
        assert_eq!(roots[0].children[0].title, "Real Sub Heading");
    }

    #[test]
    fn test_parse_empty_document() {
        let parser = MarkdownTreeParser::new();

        let roots = parser.parse("").unwrap();
        assert!(roots.is_empty());

        let roots = parser.parse("   \n  \n").unwrap();
        assert!(roots.is_empty());
    }

    #[test]
    fn test_parse_no_headings() {
        let md = "Just some text without headings.\nAnother line.";
        let parser = MarkdownTreeParser::new();
        let roots = parser.parse(md).unwrap();

        assert_eq!(roots.len(), 1);
        assert_eq!(roots[0].title, "Document");
        assert!(roots[0].content.text.contains("Just some text"));
    }

    #[test]
    fn test_parse_heading_line() {
        // Valid headings
        assert!(MarkdownTreeParser::parse_heading_line("# Title").is_some());
        assert!(MarkdownTreeParser::parse_heading_line("## Sub").is_some());
        assert!(MarkdownTreeParser::parse_heading_line("###### Deep").is_some());

        // Invalid
        assert!(MarkdownTreeParser::parse_heading_line("####### TooDeep").is_none());
        assert!(MarkdownTreeParser::parse_heading_line("#NoSpace").is_none());
        assert!(MarkdownTreeParser::parse_heading_line("Not a heading").is_none());
        assert!(MarkdownTreeParser::parse_heading_line("# ").is_none());
        assert!(MarkdownTreeParser::parse_heading_line("####").is_none());
    }

    #[test]
    fn test_parse_max_heading_level() {
        let md = "# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6";
        let config = TreeIndexConfig {
            max_heading_level: 3,
            ..Default::default()
        };
        let parser = MarkdownTreeParser::with_config(config);
        let roots = parser.parse(md).unwrap();

        // H4, H5, H6 should be excluded
        let total_nodes = roots.iter().map(|r| r.node_count()).sum::<usize>();
        assert_eq!(total_nodes, 3); // H1, H2, H3
    }

    #[test]
    fn test_node_ids_sequential() {
        let md = "# A\n## B\n## C\n### D";
        let parser = MarkdownTreeParser::new();
        let roots = parser.parse(md).unwrap();

        let ids: Vec<String> = roots[0]
            .iter_preorder()
            .map(|n| n.node_id.clone())
            .collect();
        assert_eq!(ids, vec!["0001", "0002", "0003", "0004"]);
    }

    #[test]
    fn test_node_content_assigned() {
        let md = "# Title\nFirst line.\nSecond line.\n## Sub\nSub content.";
        let parser = MarkdownTreeParser::new();
        let roots = parser.parse(md).unwrap();

        let root = &roots[0];
        assert!(root.content.text.contains("First line."));
        assert!(root.content.text.contains("Second line."));

        let sub = &root.children[0];
        assert!(sub.content.text.contains("Sub content."));
    }

    #[test]
    fn test_line_numbers() {
        let md = "# H1\ntext\n## H2\ntext\n### H3\ntext";
        let parser = MarkdownTreeParser::new();
        let roots = parser.parse(md).unwrap();

        assert_eq!(roots[0].line_num, Some(1));
        assert_eq!(roots[0].children[0].line_num, Some(3));
        assert_eq!(roots[0].children[0].children[0].line_num, Some(5));
    }

    #[test]
    fn test_level_skip() {
        // H1 → H3 (skipping H2) — should still nest correctly
        let md = "# Top\n### Deep\nContent";
        let parser = MarkdownTreeParser::new();
        let roots = parser.parse(md).unwrap();

        assert_eq!(roots.len(), 1);
        // H3 should be a child of H1 (even though H2 is skipped)
        assert_eq!(roots[0].children.len(), 1);
        assert_eq!(roots[0].children[0].title, "Deep");
    }
}
