//! List conversion utilities for code actions.
//!
//! Provides functions to convert lists between loose/compact and bullet/ordered/task styles.

use crate::syntax::{AstNode, List, ListKind, SyntaxKind, SyntaxNode, SyntaxToken};
use tower_lsp_server::ls_types::{Range, TextEdit};

use super::super::conversions::offset_to_position;

/// Find the innermost LIST node at the given position.
pub fn find_list_at_position(tree: &SyntaxNode, offset: usize) -> Option<SyntaxNode> {
    // Find the deepest node at this offset
    let text_size = rowan::TextSize::from(offset as u32);
    let token = tree.token_at_offset(text_size).right_biased()?;

    // Walk up the tree to find the innermost LIST node
    token
        .parent_ancestors()
        .find(|node| node.kind() == SyntaxKind::LIST)
}

/// Detects the marker style for a list using the first list item's marker token.
pub fn detect_list_type(list_node: &SyntaxNode) -> Option<ListKind> {
    let list = List::cast(list_node.clone())?;
    list.kind()
}

/// Convert a compact list to a loose list by inserting blank lines between items.
///
/// Returns a list of TextEdits that insert blank lines after each list item
/// (except the last one, to avoid adding trailing blank lines).
pub fn convert_to_loose(list_node: &SyntaxNode, text: &str) -> Vec<TextEdit> {
    let list = match List::cast(list_node.clone()) {
        Some(l) => l,
        None => return vec![],
    };

    if list.is_loose() {
        // Already loose, nothing to do
        return vec![];
    }

    let mut edits = Vec::new();
    let items: Vec<_> = list.items().collect();

    // Insert blank line after each item except the last
    for (idx, item) in items.iter().enumerate() {
        if idx == items.len() - 1 {
            // Don't add blank line after last item
            continue;
        }

        // Find the end of this item (right after its last newline)
        let item_end = item.syntax().text_range().end().into();

        // Insert a blank line
        let position = offset_to_position(text, item_end);
        edits.push(TextEdit {
            range: Range {
                start: position,
                end: position,
            },
            new_text: "\n".to_string(),
        });
    }

    edits
}

/// Convert a loose list to a compact list by removing blank lines between items.
///
/// Returns a list of TextEdits that remove blank lines between list items.
pub fn convert_to_compact(list_node: &SyntaxNode, text: &str) -> Vec<TextEdit> {
    let list = match List::cast(list_node.clone()) {
        Some(l) => l,
        None => return vec![],
    };

    if list.is_compact() {
        // Already compact, nothing to do
        return vec![];
    }

    let mut edits = Vec::new();

    // Find all BLANK_LINE nodes that are between LIST_ITEM nodes
    let children: Vec<_> = list_node.children_with_tokens().collect();
    let mut prev_was_item = false;

    for (idx, child) in children.iter().enumerate() {
        if let Some(node) = child.as_node() {
            if node.kind() == SyntaxKind::LIST_ITEM {
                prev_was_item = true;
            } else if node.kind() == SyntaxKind::BLANK_LINE && prev_was_item {
                // Check if next non-blank node is also a LIST_ITEM
                let has_next_item = children[idx + 1..]
                    .iter()
                    .find(|c| {
                        c.as_node()
                            .map(|n| n.kind() != SyntaxKind::BLANK_LINE)
                            .unwrap_or(false)
                    })
                    .and_then(|c| c.as_node())
                    .map(|n| n.kind() == SyntaxKind::LIST_ITEM)
                    .unwrap_or(false);

                if has_next_item {
                    // Remove this blank line
                    let start = offset_to_position(text, node.text_range().start().into());
                    let end = offset_to_position(text, node.text_range().end().into());
                    edits.push(TextEdit {
                        range: Range { start, end },
                        new_text: String::new(),
                    });
                }
            }
        } else if let Some(token) = child.as_token()
            && token.kind() == SyntaxKind::BLANK_LINE
            && prev_was_item
        {
            // Check if next non-blank element is also a LIST_ITEM
            let has_next_item = children[idx + 1..]
                .iter()
                .find(|c| {
                    if let Some(n) = c.as_node() {
                        n.kind() != SyntaxKind::BLANK_LINE
                    } else if let Some(t) = c.as_token() {
                        t.kind() != SyntaxKind::BLANK_LINE
                    } else {
                        false
                    }
                })
                .and_then(|c| c.as_node())
                .map(|n| n.kind() == SyntaxKind::LIST_ITEM)
                .unwrap_or(false);

            if has_next_item {
                // Remove this blank line token
                let start = offset_to_position(text, token.text_range().start().into());
                let end = offset_to_position(text, token.text_range().end().into());
                edits.push(TextEdit {
                    range: Range { start, end },
                    new_text: String::new(),
                });
            }
        }
    }

    edits
}

/// Convert a bullet list to an ordered list by replacing markers with 1., 2., ...
pub fn convert_to_ordered(list_node: &SyntaxNode, text: &str) -> Vec<TextEdit> {
    if detect_list_type(list_node) != Some(ListKind::Bullet) {
        return vec![];
    }

    let Some(list) = List::cast(list_node.clone()) else {
        return vec![];
    };

    list.items()
        .enumerate()
        .filter_map(|(idx, item)| {
            let marker = first_item_marker_token(item.syntax())?;
            let start = offset_to_position(text, marker.text_range().start().into());
            let end = offset_to_position(text, marker.text_range().end().into());
            Some(TextEdit {
                range: Range { start, end },
                new_text: format!("{}.", idx + 1),
            })
        })
        .collect()
}

/// Convert an ordered list to a bullet list by replacing markers with "-".
pub fn convert_to_bullet(list_node: &SyntaxNode, text: &str) -> Vec<TextEdit> {
    if !matches!(
        detect_list_type(list_node),
        Some(ListKind::Ordered | ListKind::Task)
    ) {
        return vec![];
    }

    let Some(list) = List::cast(list_node.clone()) else {
        return vec![];
    };

    list.items()
        .flat_map(|item| {
            let mut edits = Vec::new();
            let Some(marker) = first_item_marker_token(item.syntax()) else {
                return edits;
            };
            let start = offset_to_position(text, marker.text_range().start().into());
            let end = offset_to_position(text, marker.text_range().end().into());
            edits.push(TextEdit {
                range: Range { start, end },
                new_text: "-".to_string(),
            });

            if let Some(checkbox) = task_checkbox_token(item.syntax()) {
                let checkbox_start = offset_to_position(text, checkbox.text_range().start().into());
                let checkbox_end = offset_to_position(
                    text,
                    checkbox_removal_end_offset(text, checkbox.text_range().end().into()),
                );
                edits.push(TextEdit {
                    range: Range {
                        start: checkbox_start,
                        end: checkbox_end,
                    },
                    new_text: String::new(),
                });
            }

            edits
        })
        .collect()
}

/// Convert a non-task list to a task list by inserting an unchecked checkbox after each marker.
///
/// Ordered lists are converted to bullet task lists (`- [ ]`) to match canonical
/// Pandoc/GFM task-list form.
pub fn convert_to_task(list_node: &SyntaxNode, text: &str) -> Vec<TextEdit> {
    let list_type = detect_list_type(list_node);
    if !matches!(list_type, Some(ListKind::Bullet | ListKind::Ordered)) {
        return vec![];
    }

    let Some(list) = List::cast(list_node.clone()) else {
        return vec![];
    };

    list.items()
        .flat_map(|item| {
            let mut edits = Vec::new();
            let marker = first_item_marker_token(item.syntax())?;

            if list_type == Some(ListKind::Ordered) {
                let marker_start = offset_to_position(text, marker.text_range().start().into());
                let marker_end = offset_to_position(text, marker.text_range().end().into());
                edits.push(TextEdit {
                    range: Range {
                        start: marker_start,
                        end: marker_end,
                    },
                    new_text: "-".to_string(),
                });
            }

            let insert_at = offset_to_position(text, marker.text_range().end().into());
            edits.push(TextEdit {
                range: Range {
                    start: insert_at,
                    end: insert_at,
                },
                new_text: " [ ]".to_string(),
            });

            Some(edits)
        })
        .flatten()
        .collect()
}

/// Convert a task list to an ordered list by replacing markers and removing checkboxes.
pub fn convert_task_to_ordered(list_node: &SyntaxNode, text: &str) -> Vec<TextEdit> {
    if detect_list_type(list_node) != Some(ListKind::Task) {
        return vec![];
    }

    let Some(list) = List::cast(list_node.clone()) else {
        return vec![];
    };

    list.items()
        .enumerate()
        .flat_map(|(idx, item)| {
            let mut edits = Vec::new();
            let Some(marker) = first_item_marker_token(item.syntax()) else {
                return edits;
            };

            let marker_start = offset_to_position(text, marker.text_range().start().into());
            let marker_end = offset_to_position(text, marker.text_range().end().into());
            edits.push(TextEdit {
                range: Range {
                    start: marker_start,
                    end: marker_end,
                },
                new_text: format!("{}.", idx + 1),
            });

            if let Some(checkbox) = task_checkbox_token(item.syntax()) {
                let checkbox_start = offset_to_position(text, checkbox.text_range().start().into());
                let checkbox_end = offset_to_position(
                    text,
                    checkbox_removal_end_offset(text, checkbox.text_range().end().into()),
                );
                edits.push(TextEdit {
                    range: Range {
                        start: checkbox_start,
                        end: checkbox_end,
                    },
                    new_text: String::new(),
                });
            }

            edits
        })
        .collect()
}

fn first_item_marker_token(item_node: &SyntaxNode) -> Option<SyntaxToken> {
    item_node
        .children_with_tokens()
        .find_map(|elem| elem.as_token().cloned())
        .filter(|token| token.kind() == SyntaxKind::LIST_MARKER)
}

fn task_checkbox_token(item_node: &SyntaxNode) -> Option<SyntaxToken> {
    item_node.children_with_tokens().find_map(|elem| {
        elem.as_token()
            .filter(|token| token.kind() == SyntaxKind::TASK_CHECKBOX)
            .cloned()
    })
}

fn checkbox_removal_end_offset(text: &str, checkbox_end: usize) -> usize {
    match text.as_bytes().get(checkbox_end) {
        Some(b' ' | b'\t') => checkbox_end + 1,
        _ => checkbox_end,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::parse;
    use tower_lsp_server::ls_types::Position;

    fn apply_text_edits(input: &str, edits: &[TextEdit]) -> String {
        fn position_to_offset(text: &str, position: Position) -> usize {
            let target_line = position.line as usize;
            let target_char = position.character as usize;
            let mut line = 0usize;
            let mut col = 0usize;

            for (idx, ch) in text.char_indices() {
                if line == target_line && col == target_char {
                    return idx;
                }
                if ch == '\n' {
                    line += 1;
                    col = 0;
                } else {
                    col += 1;
                }
            }
            text.len()
        }

        let mut result = input.to_string();
        let mut sorted = edits.to_vec();
        sorted.sort_by(|a, b| {
            let key_a = (
                a.range.start.line,
                a.range.start.character,
                a.range.end.line,
                a.range.end.character,
            );
            let key_b = (
                b.range.start.line,
                b.range.start.character,
                b.range.end.line,
                b.range.end.character,
            );
            key_b.cmp(&key_a)
        });

        for edit in sorted {
            let start = position_to_offset(&result, edit.range.start);
            let end = position_to_offset(&result, edit.range.end);
            result.replace_range(start..end, &edit.new_text);
        }
        result
    }

    #[test]
    fn find_list_at_position_finds_innermost() {
        let input = "- Outer\n  - Inner\n  - Inner2\n- Outer2\n";
        let tree = parse(input, None);

        // Offset inside "Inner" (should find inner list)
        let offset = input.find("Inner").unwrap();
        let list = find_list_at_position(&tree, offset).expect("Should find list");
        let wrapped = List::cast(list).expect("Should cast to List");

        // The inner list has 2 items
        assert_eq!(wrapped.items().count(), 2);
    }

    #[test]
    fn convert_compact_to_loose() {
        let input = "- First\n- Second\n- Third\n";
        let tree = parse(input, None);

        let list_node = tree
            .descendants()
            .find(|n| n.kind() == SyntaxKind::LIST)
            .expect("Should find list");

        let edits = convert_to_loose(&list_node, input);

        // Should have 2 edits (after first and second items)
        assert_eq!(edits.len(), 2);

        // First edit should insert after "First\n"
        assert_eq!(edits[0].new_text, "\n");
        assert_eq!(edits[0].range.start.line, 1);

        // Second edit should insert after "Second\n"
        assert_eq!(edits[1].new_text, "\n");
        assert_eq!(edits[1].range.start.line, 2);
    }

    #[test]
    fn convert_loose_to_compact() {
        let input = "- First\n\n- Second\n\n- Third\n";
        let tree = parse(input, None);

        let list_node = tree
            .descendants()
            .find(|n| n.kind() == SyntaxKind::LIST)
            .expect("Should find list");

        let edits = convert_to_compact(&list_node, input);

        // Should have 2 edits (remove 2 blank lines)
        assert_eq!(edits.len(), 2);

        // Each edit should remove one blank line
        for edit in &edits {
            assert_eq!(edit.new_text, "");
            assert!(edit.range.start != edit.range.end);
        }
    }

    #[test]
    fn already_loose_returns_empty() {
        let input = "- First\n\n- Second\n";
        let tree = parse(input, None);

        let list_node = tree
            .descendants()
            .find(|n| n.kind() == SyntaxKind::LIST)
            .expect("Should find list");

        let edits = convert_to_loose(&list_node, input);
        assert_eq!(edits.len(), 0, "Should return no edits for already loose");
    }

    #[test]
    fn already_compact_returns_empty() {
        let input = "- First\n- Second\n";
        let tree = parse(input, None);

        let list_node = tree
            .descendants()
            .find(|n| n.kind() == SyntaxKind::LIST)
            .expect("Should find list");

        let edits = convert_to_compact(&list_node, input);
        assert_eq!(edits.len(), 0, "Should return no edits for already compact");
    }

    #[test]
    fn detect_list_type_for_bullet_and_ordered() {
        let bullet_tree = parse("- First\n- Second\n", None);
        let bullet_list = bullet_tree
            .descendants()
            .find(|n| n.kind() == SyntaxKind::LIST)
            .expect("Should find bullet list");
        assert_eq!(detect_list_type(&bullet_list), Some(ListKind::Bullet));

        let ordered_tree = parse("1. First\n2. Second\n", None);
        let ordered_list = ordered_tree
            .descendants()
            .find(|n| n.kind() == SyntaxKind::LIST)
            .expect("Should find ordered list");
        assert_eq!(detect_list_type(&ordered_list), Some(ListKind::Ordered));
    }

    #[test]
    fn detect_list_type_for_task() {
        let task_tree = parse("- [ ] First\n- [x] Second\n", None);
        let task_list = task_tree
            .descendants()
            .find(|n| n.kind() == SyntaxKind::LIST)
            .expect("Should find task list");
        assert_eq!(detect_list_type(&task_list), Some(ListKind::Task));
    }

    #[test]
    fn convert_bullet_to_ordered() {
        let input = "- First\n- Second\n- Third\n";
        let tree = parse(input, None);
        let list_node = tree
            .descendants()
            .find(|n| n.kind() == SyntaxKind::LIST)
            .expect("Should find list");

        let edits = convert_to_ordered(&list_node, input);
        assert_eq!(edits.len(), 3);
        assert_eq!(edits[0].new_text, "1.");
        assert_eq!(edits[1].new_text, "2.");
        assert_eq!(edits[2].new_text, "3.");
    }

    #[test]
    fn convert_ordered_to_bullet() {
        let input = "1. First\n2. Second\n3. Third\n";
        let tree = parse(input, None);
        let list_node = tree
            .descendants()
            .find(|n| n.kind() == SyntaxKind::LIST)
            .expect("Should find list");

        let edits = convert_to_bullet(&list_node, input);
        assert_eq!(edits.len(), 3);
        assert!(edits.iter().all(|edit| edit.new_text == "-"));
    }

    #[test]
    fn convert_bullet_to_task() {
        let input = "- First\n- Second\n";
        let tree = parse(input, None);
        let list_node = tree
            .descendants()
            .find(|n| n.kind() == SyntaxKind::LIST)
            .expect("Should find list");

        let edits = convert_to_task(&list_node, input);
        assert_eq!(edits.len(), 2);
        assert!(edits.iter().all(|edit| edit.new_text == " [ ]"));
    }

    #[test]
    fn convert_ordered_to_task() {
        let input = "1. First\n2. Second\n";
        let tree = parse(input, None);
        let list_node = tree
            .descendants()
            .find(|n| n.kind() == SyntaxKind::LIST)
            .expect("Should find list");

        let edits = convert_to_task(&list_node, input);
        assert_eq!(edits.len(), 4, "marker + checkbox edit per item");
        assert_eq!(edits[0].new_text, "-");
        assert_eq!(edits[1].new_text, " [ ]");
        assert_eq!(edits[2].new_text, "-");
        assert_eq!(edits[3].new_text, " [ ]");
    }

    #[test]
    fn convert_task_to_bullet_removes_checkboxes() {
        let input = "- [ ] First\n- [x] Second\n";
        let tree = parse(input, None);
        let list_node = tree
            .descendants()
            .find(|n| n.kind() == SyntaxKind::LIST)
            .expect("Should find list");

        let edits = convert_to_bullet(&list_node, input);
        assert_eq!(edits.len(), 4, "marker + checkbox edit per item");
        assert_eq!(edits[0].new_text, "-");
        assert_eq!(edits[1].new_text, "");
        assert_eq!(edits[2].new_text, "-");
        assert_eq!(edits[3].new_text, "");
        let output = apply_text_edits(input, &edits);
        assert_eq!(output, "- First\n- Second\n");
    }

    #[test]
    fn convert_task_to_ordered_removes_checkboxes() {
        let input = "- [ ] First\n- [x] Second\n";
        let tree = parse(input, None);
        let list_node = tree
            .descendants()
            .find(|n| n.kind() == SyntaxKind::LIST)
            .expect("Should find list");

        let edits = convert_task_to_ordered(&list_node, input);
        assert_eq!(edits.len(), 4, "marker + checkbox edit per item");
        assert_eq!(edits[0].new_text, "1.");
        assert_eq!(edits[1].new_text, "");
        assert_eq!(edits[2].new_text, "2.");
        assert_eq!(edits[3].new_text, "");
        let output = apply_text_edits(input, &edits);
        assert_eq!(output, "1. First\n2. Second\n");
    }
}
