use std::sync::Arc;
use std::{borrow::Cow, ops::Deref};

use bitflags::bitflags;
use regex::Regex;
use std::sync::LazyLock;
use tree_sitter::{Node, Parser, Tree, TreeCursor};
use unicode_width::UnicodeWidthStr;

pub(crate) struct MdIterator<'a> {
    source: &'a str,
    // Invariant: cursor is dropped before tree
    cursor: TreeCursor<'a>,
    #[expect(dead_code)]
    tree: Box<Tree>,
    done: bool,
    inline_parser: &'a mut Parser,
    /// Current container ancestry with depth for tracking when to pop.
    context: Vec<(usize, MdContainer)>,
    /// Current depth in the tree.
    depth: usize,
    /// Depth of the last ListItem that has emitted content (for continuation detection).
    list_item_content_depth: Option<usize>,
    /// Sequential ordered-list counter stack. `None` = preserve-source mode.
    /// In sequential mode each entry corresponds to one open ordered list (innermost last);
    /// `None` entry = no item seen yet, `Some(n)` = last assigned number at that depth.
    list_ordinal: Option<Vec<Option<u32>>>,
}

impl<'a> MdIterator<'a> {
    pub(crate) fn new(tree: Tree, inline_parser: &'a mut Parser, source: &'a str) -> Self {
        let tree = Box::new(tree);
        let cursor =
            // SAFETY:
            // cursor references tree, cursor must be dropped before tree, so order of fields
            // matters.
            unsafe { std::mem::transmute::<TreeCursor<'_>, TreeCursor<'static>>(tree.walk()) };

        MdIterator {
            source,
            cursor,
            tree,
            done: false,
            inline_parser,
            context: Vec::new(),
            depth: 0,
            list_item_content_depth: None,
            list_ordinal: Some(Vec::new()),
        }
    }

    pub(crate) fn preserve_list_ordinals(&mut self, preserve_list_ordinals: bool) {
        self.list_ordinal = if preserve_list_ordinals {
            None
        } else {
            Some(Vec::new())
        };
    }
}

impl Iterator for MdIterator<'_> {
    type Item = MdSection;

    fn next(&mut self) -> Option<Self::Item> {
        loop {
            if self.done {
                return None;
            }

            let node = self.cursor.node();

            // Check if current node is a container and push to context
            if let Some(container) = self.node_to_container(node) {
                if let Some(stack) = &mut self.list_ordinal {
                    match &container {
                        MdContainer::List(ListMarker::Ordered(_)) => stack.push(None),
                        MdContainer::ListItem(ListMarker::Ordered(n)) => {
                            if let Some(top) = stack.last_mut() {
                                *top = Some(*n);
                            }
                        }
                        _ => {}
                    }
                }
                self.context.push((self.depth, container));
            }

            // Advance cursor
            if self.cursor.goto_first_child() {
                self.depth += 1;
            } else {
                while !self.cursor.goto_next_sibling() {
                    if self.cursor.goto_parent() {
                        self.depth -= 1;
                        // Pop containers that are no longer ancestors
                        while self.context.last().is_some_and(|(d, _)| *d >= self.depth) {
                            let popped = self.context.pop();
                            match popped {
                                Some((d, MdContainer::ListItem(_))) => {
                                    if self.list_item_content_depth == Some(d) {
                                        self.list_item_content_depth = None;
                                    }
                                }
                                Some((_, MdContainer::List(ListMarker::Ordered(_)))) => {
                                    if let Some(stack) = &mut self.list_ordinal {
                                        stack.pop();
                                    }
                                }
                                _ => {}
                            }
                        }
                    } else {
                        self.done = true;
                        break;
                    }
                }
            }

            if let Some(content) = self.parse_node(node) {
                // Build nesting from container ancestry
                let nesting: Vec<MdContainer> =
                    self.context.iter().map(|(_, c)| c.clone()).collect();

                // Check if this is a continuation within a list item
                let list_item_depth = self
                    .context
                    .iter()
                    .filter(|(_, c)| matches!(c, MdContainer::ListItem(_)))
                    .map(|(d, _)| *d)
                    .next_back();

                let is_list_continuation = if let Some(depth) = list_item_depth {
                    if self.list_item_content_depth == Some(depth) {
                        // We've already emitted content for this list item
                        true
                    } else {
                        // First content for this list item
                        self.list_item_content_depth = Some(depth);
                        false
                    }
                } else {
                    false
                };

                return Some(MdSection {
                    content,
                    nesting,
                    is_list_continuation,
                });
            }
        }
    }
}

impl<'a> MdIterator<'a> {
    /// Parses a node and returns its content if it's a section.
    #[expect(clippy::string_slice)] // In tree-sitter we trust
    fn parse_node(&mut self, node: Node<'a>) -> Option<MdContent> {
        match node.kind() {
            "paragraph" => self.parse_paragraph(&node),
            "atx_heading" => {
                let mut tier = 0;
                let mut text = "";
                let mut links: Vec<Vec<Span>> = Vec::new();
                for child in node.children(&mut node.walk()) {
                    match child.kind() {
                        "inline" => {
                            text = &self.source[child.byte_range()];
                            if let Some(tree) = self.inline_parser.parse(text, None)
                                && let Some(MdContent::Paragraph(p)) =
                                    // TODO: should make a specific parser only for links in
                                    // headers here.
                                    MdParagraph::from_inline(tree.root_node(), text, 0)
                            {
                                let mut link: Vec<Span> = Vec::new();
                                for span in p.spans {
                                    if span.modifiers.contains(Modifier::Link) {
                                        let is_end =
                                            (span.modifiers.contains(Modifier::LinkURLWrapper)
                                                && link
                                                    .last()
                                                    .map(|last| {
                                                        last.modifiers.contains(Modifier::LinkURL)
                                                    })
                                                    .unwrap_or(false))
                                                || (span.modifiers.contains(Modifier::LinkURL)
                                                    && span.content.starts_with("[")
                                                    && span.content.ends_with("]"));
                                        link.push(span);
                                        if is_end {
                                            links.push(std::mem::take(&mut link));
                                        }
                                    }
                                }
                            }
                        }
                        "atx_h1_marker" => tier = 1,
                        "atx_h2_marker" => tier = 2,
                        "atx_h3_marker" => tier = 3,
                        "atx_h4_marker" => tier = 4,
                        "atx_h5_marker" => tier = 5,
                        "atx_h6_marker" => tier = 6,
                        _ => {
                            debug_assert!(false, "heading greater than 6");
                        }
                    }
                }
                Some(MdContent::Header {
                    tier,
                    text: text.to_owned(),
                    links,
                })
            }
            "block_continuation" => {
                // Blank line inside blockquote
                if let Some(parent) = node.parent() {
                    if parent.kind() == "block_quote" {
                        return Some(MdContent::Paragraph(MdParagraph::empty()));
                    }
                }
                None
            }
            "fenced_code_block" => {
                let mut language = String::new();
                let mut code = String::new();

                for child in node.children(&mut node.walk()) {
                    match child.kind() {
                        "info_string" => {
                            language = self.source[child.byte_range()].trim().to_owned();
                        }
                        "code_fence_content" => {
                            code = self.source[child.byte_range()].to_owned();
                        }
                        _ => {}
                    }
                }

                if code.ends_with('\n') {
                    code.pop();
                }

                Some(MdContent::CodeBlock { language, code })
            }
            "indented_code_block" => {
                let code = self.source[node.byte_range()]
                    .lines()
                    .map(|line| line.strip_prefix("    ").unwrap_or(line))
                    .collect::<Vec<_>>()
                    .join("\n");
                Some(MdContent::CodeBlock {
                    language: String::new(),
                    code,
                })
            }
            "thematic_break" => Some(MdContent::HorizontalRule),
            "pipe_table" => Some(self.parse_table(node)),
            "html_block" => Some(self.parse_html(node)),
            "link_reference_definition" => {
                // We parse it with tree-sitter. A simple regex would do, except for escaping
                // edge-case, e.g. `[description][reference\[with-brackets\]]`, and so on.
                self.parse_paragraph(&node).map(|md_content| {
                    let MdContent::Paragraph(p) = md_content else {
                        return md_content;
                    };
                    let mut reference = String::new();
                    let mut url = String::new();
                    for span in &p.spans {
                        if span.modifiers == Modifier::LinkDescription {
                            reference.push_str(&span.content);
                        } else if span.modifiers == Modifier::BareLink | Modifier::LinkURL {
                            url = span.content.clone();
                        }
                    }
                    MdContent::LinkReferenceDefinition { reference, url }
                })
            }
            _ => None, // Processed as inline.
        }
    }

    fn parse_table(&mut self, node: Node<'a>) -> MdContent {
        let mut header: Vec<Vec<Span>> = Vec::new();
        let mut rows: Vec<Vec<Vec<Span>>> = Vec::new();
        let mut alignments: Vec<TableAlignment> = Vec::new();

        for child in node.children(&mut node.walk()) {
            match child.kind() {
                "pipe_table_header" => {
                    header = self.parse_table_row(child);
                }
                "pipe_table_delimiter_row" => {
                    alignments = self.parse_table_alignments(child);
                }
                "pipe_table_row" => {
                    rows.push(self.parse_table_row(child));
                }
                _ => {}
            }
        }

        while alignments.len() < header.len() {
            alignments.push(TableAlignment::default());
        }

        MdContent::Table {
            header,
            rows,
            alignments,
        }
    }

    #[expect(clippy::string_slice)]
    fn parse_table_row(&mut self, row_node: Node<'a>) -> Vec<Vec<Span>> {
        let mut cells: Vec<Vec<Span>> = Vec::new();

        for child in row_node.children(&mut row_node.walk()) {
            if child.kind() == "pipe_table_cell" {
                let cell_text = self.source[child.byte_range()].trim();
                if cell_text.is_empty() {
                    cells.push(Vec::new());
                } else if let Some(tree) = self.inline_parser.parse(cell_text, None) {
                    let mut p = MdParagraph::empty();
                    p.recurse(tree.root_node(), cell_text, Modifier::default(), 0);
                    cells.push(detect_bare_urls(p.spans));
                } else {
                    cells.push(vec![Span::new(cell_text.to_owned(), Modifier::default())]);
                }
            }
        }

        cells
    }

    #[expect(clippy::string_slice)]
    fn parse_table_alignments(&self, delimiter_node: Node<'a>) -> Vec<TableAlignment> {
        let mut alignments = Vec::new();

        for child in delimiter_node.children(&mut delimiter_node.walk()) {
            if child.kind() == "pipe_table_delimiter_cell" {
                let cell_text = &self.source[child.byte_range()];
                let starts_colon = cell_text.starts_with(':');
                let ends_colon = cell_text.ends_with(':');
                let alignment = match (starts_colon, ends_colon) {
                    (true, true) => TableAlignment::Center,
                    (false, true) => TableAlignment::Right,
                    _ => TableAlignment::Left,
                };
                alignments.push(alignment);
            }
        }

        alignments
    }

    fn node_to_container(&self, node: Node<'a>) -> Option<MdContainer> {
        match node.kind() {
            "list" => {
                for child in node.children(&mut node.walk()) {
                    if child.kind() == "list_item" {
                        return Some(MdContainer::List(self.extract_list_marker(child, None)));
                    }
                }
                Some(MdContainer::List(ListMarker::Unordered(BulletStyle::Dash)))
            }
            "list_item" => {
                let prev_ordinal = self
                    .list_ordinal
                    .as_ref()
                    .and_then(|list| list.last().copied())
                    .flatten();
                Some(MdContainer::ListItem(
                    self.extract_list_marker(node, prev_ordinal),
                ))
            }
            "block_quote" => Some(MdContainer::Blockquote(BlockquoteMarker)),
            _ => None,
        }
    }

    #[expect(clippy::string_slice)]
    fn extract_list_marker(
        &self,
        list_item: Node<'a>,
        // None  = use source number (first item in sequential mode, or preserve mode)
        // Some(n) = last assigned number at this depth; emit n + 1
        prev_ordinal: Option<u32>,
    ) -> ListMarker {
        let mut first_char = '-';
        let mut task: Option<bool> = None;

        for child in list_item.children(&mut list_item.walk()) {
            match child.kind() {
                "list_marker_minus"
                | "list_marker_plus"
                | "list_marker_star"
                | "list_marker_dot"
                | "list_marker_parenthesis" => {
                    let marker_text = self.source[child.byte_range()].trim();
                    first_char = marker_text.chars().next().unwrap_or('-');
                }
                "task_list_marker_checked" => {
                    task = Some(true);
                }
                "task_list_marker_unchecked" => {
                    task = Some(false);
                }
                _ => {}
            }
        }

        let bullet = BulletStyle::from_char(first_char).unwrap_or(BulletStyle::Dash);

        match task {
            Some(true) => ListMarker::TaskChecked(bullet),
            Some(false) => ListMarker::TaskUnchecked(bullet),
            None if first_char.is_ascii_digit() => {
                // Parse ordered list number
                let num: u32 = prev_ordinal
                    .map(|n| n.saturating_add(1))
                    .unwrap_or_else(|| {
                        self.source[list_item.byte_range()]
                            .chars()
                            .take_while(|c| c.is_ascii_digit())
                            .fold(0_u32, |acc, c| {
                                acc.saturating_mul(10)
                                    .saturating_add(c.to_digit(10).unwrap_or(0))
                            })
                    });
                ListMarker::Ordered(if num == 0 { 1 } else { num })
            }
            None => ListMarker::Unordered(bullet),
        }
    }

    fn parse_paragraph(&mut self, node: &Node<'_>) -> Option<MdContent> {
        #[expect(clippy::string_slice)]
        let text = &self.source[node.byte_range()];

        // Skip empty/whitespace-only paragraphs
        if text.trim().is_empty() {
            return None;
        }

        let Some(tree) = self.inline_parser.parse(text, None) else {
            return Some(MdContent::Paragraph(MdParagraph::from(text)));
        };

        // Count blockquote depth for stripping markers from content
        // TODO: why do we need special treatment for blockquote but not list or others?
        let blockquote_depth = self
            .context
            .iter()
            .filter(|(_, c)| matches!(c, MdContainer::Blockquote(_)))
            .count();
        MdParagraph::from_inline(tree.root_node(), text, blockquote_depth)
    }

    fn parse_html(&self, node: Node<'a>) -> MdContent {
        #[expect(clippy::string_slice)]
        let html = self.source[node.byte_range()].to_owned();
        MdContent::Html { html }
    }
}

bitflags! {
    /// Modifier flags for [`Span`]s.
    ///
    /// Similar to [ratatui](https://ratatui.rs)'s `Modifier`, but more related to the original markdown.
    #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
    pub struct Modifier: u32 {
        const Emphasis = 1 << 0;
        const StrongEmphasis = 1 << 1;
        const Code = 1 << 2;
        const Link = 1 << 3;
        const BareLink = 1 << 4;
        const LinkDescription = 1 << 5;
        const LinkDescriptionWrapper = 1 << 6;
        const LinkURL = 1 << 7;
        const LinkURLWrapper = 1 << 8;
        const Image = 1 << 9;
        const NewLine = 1 << 10;
        // Prefix/structural elements (added for mapper support)
        const BlockquoteBar = 1 << 11;
        const ListMarker = 1 << 12;
        const TableBorder = 1 << 13;
        const HorizontalRule = 1 << 14;
        // Wrapper elements for decorators
        const EmphasisWrapper = 1 << 15;
        const StrongEmphasisWrapper = 1 << 16;
        const CodeWrapper = 1 << 17;
        // Strikethrough
        const Strikethrough = 1 << 18;
        const StrikethroughWrapper = 1 << 19;
        const HardLineBreak = 1 << 20;
    }
}

impl Modifier {
    // Returns true if LinkURL but not Image.
    pub fn is_link_url(&self) -> bool {
        self.contains(Modifier::LinkURL)
            && !self.contains(Modifier::BareLink)
            && !self.contains(Modifier::Image)
    }
    /// Returns true if contains `modifier` but not [`Modifier::Image`].
    pub fn is_link_modifier(&self, modifier: Modifier) -> bool {
        self.contains(modifier) && !self.contains(Modifier::Image)
    }
}

/// Span, `String` content with [`Modifier`]s.
///
/// This is a proto-ratatui-span, we need a different set of modifiers beyond just styling.
#[derive(Debug, Clone, PartialEq)]
pub struct Span {
    pub content: String,
    pub modifiers: Modifier,
}

impl Span {
    pub fn new(content: String, extra: Modifier) -> Self {
        Span {
            content,
            modifiers: extra,
        }
    }

    #[cfg(test)]
    pub fn with(content: &str, modifiers: Modifier) -> Self {
        let content = content.to_owned();
        Span { content, modifiers }
    }
}

impl From<String> for Span {
    fn from(value: String) -> Self {
        Span {
            content: value,
            modifiers: Modifier::default(),
        }
    }
}

#[cfg(test)]
impl From<&str> for Span {
    fn from(value: &str) -> Self {
        Self::from(value.to_owned())
    }
}

#[cfg(test)]
impl std::fmt::Display for Span {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.content)
    }
}

impl UnicodeWidthStr for Span {
    fn width(&self) -> usize {
        self.content.width()
    }

    fn width_cjk(&self) -> usize {
        self.content.width_cjk()
    }
}

/// A source content, such as a URL.
///
/// Derefs to [`Arc<str>`] which is a unique content (e.g. URL).
/// The pointer at [`Arc::as_ptr`] is intentionally shared across multiple [`Span`]s that can
/// result from a single markdown link description, or from line-wrapping.
#[derive(Debug, Clone, PartialEq)]
pub struct SourceContent(Arc<str>);

impl From<&str> for SourceContent {
    fn from(value: &str) -> Self {
        Self(Arc::from(value))
    }
}

impl Deref for SourceContent {
    type Target = Arc<str>;
    fn deref(&self) -> &Arc<str> {
        &self.0
    }
}

#[cfg(test)]
impl std::fmt::Display for SourceContent {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "SourceContent({:?},{})", self.0.as_ptr(), self.0)
    }
}

/// A container in the document structure.
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum MdContainer {
    List(ListMarker),
    ListItem(ListMarker),
    Blockquote(BlockquoteMarker),
}

/// Type of list marker.
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum ListMarker {
    Unordered(BulletStyle),
    Ordered(u32),
    TaskUnchecked(BulletStyle),
    TaskChecked(BulletStyle),
}

/// Bullet style for unordered lists.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BulletStyle {
    Dash,
    Star,
    Plus,
}

impl BulletStyle {
    /// Parse from a character.
    pub fn from_char(c: char) -> Option<Self> {
        match c {
            '-' => Some(BulletStyle::Dash),
            '*' => Some(BulletStyle::Star),
            '+' => Some(BulletStyle::Plus),
            _ => None,
        }
    }
}

/// Column alignment for tables.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub(crate) enum TableAlignment {
    #[default]
    Left,
    Center,
    Right,
}

/// Content of a markdown section.
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum MdContent {
    Paragraph(MdParagraph),
    Header {
        tier: u8,
        text: String,
        links: Vec<Vec<Span>>,
    },
    CodeBlock {
        language: String,
        code: String,
    },
    HorizontalRule,
    Table {
        header: Vec<Vec<Span>>,
        rows: Vec<Vec<Vec<Span>>>,
        alignments: Vec<TableAlignment>,
    },
    Html {
        html: String,
    },
    LinkReferenceDefinition {
        reference: String,
        url: String,
    },
}

impl MdContent {
    pub fn is_blank(&self) -> bool {
        matches!(self, MdContent::Paragraph(p) if p.is_empty())
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct MdParagraph {
    pub spans: Vec<Span>,
}

impl MdParagraph {
    pub fn is_empty(&self) -> bool {
        self.spans.is_empty()
    }

    fn from_inline(node: Node<'_>, text: &str, blockquote_depth: usize) -> Option<MdContent> {
        let mut p = MdParagraph { spans: Vec::new() };
        p.recurse(node, text, Modifier::default(), 0);
        p.spans = split_newlines(p.spans);
        p.spans = detect_bare_urls(p.spans);

        // Strip blockquote markers from line-start spans and filter empty/marker-only spans
        for span in &mut p.spans {
            if span.modifiers.contains(Modifier::NewLine) {
                span.content =
                    strip_blockquote_prefix(&span.content, blockquote_depth).into_owned();
            }
        }
        Some(MdContent::Paragraph(p))
    }

    #[expect(clippy::string_slice)]
    pub(crate) fn recurse(&mut self, node: Node<'_>, source: &str, extra: Modifier, _depth: i32) {
        let kind = node.kind();

        if kind.contains("delimiter") {
            return;
        }

        let current_extra = match kind {
            "emphasis" => Modifier::Emphasis,
            "strong_emphasis" => Modifier::StrongEmphasis,
            "strikethrough" => Modifier::Strikethrough,
            "code_span" => {
                // Strip the backtick delimiters from code span content
                let content = &source[node.byte_range()];
                let stripped = content.trim_start_matches('`').trim_end_matches('`').trim(); // Also trim inner whitespace that some code spans have
                self.spans
                    .push(Span::new(stripped.to_owned(), extra.union(Modifier::Code)));
                return;
            }
            "hard_line_break" => {
                // GFM hard line break (two trailing spaces + newline)
                self.spans.push(Span::new(
                    String::new(), // Awkward, but we can't otherwise target the next node.
                    extra.union(Modifier::NewLine | Modifier::HardLineBreak),
                ));
                return;
            }
            "[" | "]" => Modifier::LinkDescriptionWrapper,
            "(" | ")" => Modifier::LinkURLWrapper,
            "link_text" => Modifier::LinkDescription,
            "inline_link" | "full_reference_link" => Modifier::Link,
            "image" => Modifier::Image,
            "image_description" => Modifier::LinkDescription,
            "link_destination" => {
                let url = source[node.byte_range()].to_owned();
                self.spans
                    .push(Span::new(url, extra.union(Modifier::LinkURL)));
                return;
            }
            "link_label" => {
                let label = source[node.byte_range()].to_owned();
                self.spans
                    .push(Span::new(label, extra.union(Modifier::LinkURL)));
                return;
            }
            "link_title" => {
                // ignore.
                return;
            }
            _ => Modifier::default(),
        };
        let extra = extra.union(current_extra);

        if node.child_count() == 0 {
            self.spans.push(Span::new(
                source[node.start_byte()..node.end_byte()].to_owned(),
                extra,
            ));
            return;
        }

        let mut pos = node.start_byte();

        for child in node.children(&mut node.walk()) {
            if is_punctuation(child.kind(), current_extra) {
                continue;
            }
            if child.start_byte() > pos {
                // TODO: is this right?
                self.spans
                    .push(Span::new(source[pos..child.start_byte()].to_owned(), extra));
            }

            self.recurse(child, source, extra, _depth + 1);
            pos = child.end_byte();
        }

        if pos < node.end_byte() {
            self.spans
                .push(Span::new(source[pos..node.end_byte()].to_owned(), extra));
        }
    }

    fn empty() -> MdParagraph {
        Self { spans: Vec::new() }
    }
}

impl From<&str> for MdParagraph {
    fn from(value: &str) -> Self {
        let owned = value.to_owned();
        Self {
            spans: vec![Span::new(owned, Modifier::default())],
        }
    }
}

/// Marker style for blockquotes.
#[derive(Debug, Default, Clone, PartialEq)]
pub(crate) struct BlockquoteMarker;

/// A markdown section with its content and nesting path.
#[derive(Debug)]
pub(crate) struct MdSection {
    pub content: MdContent,
    pub nesting: Vec<MdContainer>,
    /// True if this is a continuation paragraph within a list item (not the first content).
    pub is_list_continuation: bool,
}

fn strip_blockquote_prefix(s: &str, depth: usize) -> Cow<'_, str> {
    if depth == 0 {
        return Cow::Borrowed(s);
    }
    let mut remaining = s;
    for _ in 0..depth {
        if let Some(rest) = remaining.strip_prefix("> ") {
            remaining = rest;
        } else if let Some(rest) = remaining.strip_prefix(">") {
            remaining = rest;
        } else {
            break;
        }
    }
    if remaining.len() == s.len() {
        Cow::Borrowed(s)
    } else {
        Cow::Owned(remaining.to_owned())
    }
}

#[inline]
fn is_punctuation(kind: &str, parent_modifier: Modifier) -> bool {
    match kind {
        "(" | ")" | "[" | "]" if parent_modifier == Modifier::Link => false,
        "!" | "\"" | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" | "+" | "," | "-" | "."
        | "/" | ":" | ";" | "<" | "=" | ">" | "?" | "@" | "[" | "\\" | "]" | "^" | "_" | "`"
        | "{" | "|" | "}" | "~" => true,
        _ => false,
    }
}

/// Regex for detecting bare URLs (http:// or https://).
#[expect(clippy::unwrap_used)]
static URL_REGEX: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"https?://[^\s<>\[\]()]+").unwrap());

/// Detect bare URLs in spans and mark them with LinkURL modifier.
/// Skips spans that already have link-related modifiers.
fn detect_bare_urls(mdspans: Vec<Span>) -> Vec<Span> {
    let mut result = Vec::with_capacity(mdspans.len());

    for span in mdspans {
        // Skip spans that are already part of a link or code
        if span
            .modifiers
            .intersects(Modifier::Link | Modifier::LinkURL | Modifier::Code)
            || !URL_REGEX.is_match(&span.content)
        {
            result.push(span);
            continue;
        }

        let mut last_end = 0;
        let content = &span.content;

        let mut mods = span.modifiers;
        for mat in URL_REGEX.find_iter(content) {
            // Text before the URL
            if mat.start() > last_end {
                let mods = if mods.contains(Modifier::NewLine) {
                    std::mem::replace(&mut mods, span.modifiers.difference(Modifier::NewLine))
                } else {
                    mods
                };
                #[expect(clippy::string_slice)]
                result.push(Span::new(content[last_end..mat.start()].to_owned(), mods));
            }

            // The URL itself - marked as LinkURL (never first, wrapper is always before)
            let url = mat.as_str().to_owned();
            result.push(Span::new(
                url,
                mods | Modifier::BareLink | Modifier::LinkURL,
            ));

            last_end = mat.end();
        }

        // Text after the last URL
        #[expect(clippy::string_slice)]
        if last_end < content.len() {
            result.push(Span::new(content[last_end..].to_owned(), mods));
        }
    }

    result
}

fn split_newlines(mdspans: Vec<Span>) -> Vec<Span> {
    let mut trailing_newline = false;
    mdspans
        .into_iter()
        .flat_map(|mut mdspan| {
            if trailing_newline {
                mdspan.modifiers = mdspan.modifiers.union(Modifier::NewLine);
                trailing_newline = false;
            }
            expand_span(mdspan, &mut trailing_newline)
        })
        .collect()
}

fn expand_span(mdspan: Span, trailing_newline: &mut bool) -> Vec<Span> {
    if mdspan.content.is_empty() && mdspan.modifiers.contains(Modifier::HardLineBreak) {
        return vec![mdspan];
    }
    if !mdspan.content.contains('\n') {
        return vec![mdspan];
    }

    let mut parts = Vec::new();
    let mut first = true;
    for part in mdspan.content.split('\n') {
        if part.is_empty() {
            first = false;
            *trailing_newline = true;
            continue;
        }
        *trailing_newline = false;
        parts.push(Span {
            content: part.to_owned(),
            modifiers: if first {
                mdspan.modifiers
            } else {
                mdspan.modifiers.union(Modifier::NewLine)
            },
        });
        first = false;
    }
    parts
}

#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
    use super::*;
    use pretty_assertions::assert_eq;

    #[test]
    fn split_no_empty_spans() {
        let mdspans = split_newlines(vec![
            Span::with("one line", Modifier::default()),
            Span::with(".", Modifier::default()),
            Span::with("\nanother line", Modifier::NewLine),
            Span::with(".", Modifier::default()),
        ]);
        assert_eq!(
            mdspans,
            vec![
                Span::with("one line", Modifier::default()),
                Span::with(".", Modifier::default()),
                Span::with("another line", Modifier::NewLine),
                Span::with(".", Modifier::default()),
            ]
        );
    }

    fn make_parser() -> Parser {
        let mut parser = Parser::new();
        parser
            .set_language(&tree_sitter_md::LANGUAGE.into())
            .unwrap();
        parser
    }

    fn make_inline_parser() -> Parser {
        let mut inline_parser = Parser::new();
        inline_parser
            .set_language(&tree_sitter_md::INLINE_LANGUAGE.into())
            .unwrap();
        inline_parser
    }

    #[test]
    fn blockquote_blank_lines() {
        let mut parser = make_parser();
        let mut inline_parser = make_inline_parser();
        let source = r#"> First paragraph
>
> Second paragraph"#;
        let tree = parser.parse(source, None).unwrap();
        let sections: Vec<_> = MdIterator::new(tree, &mut inline_parser, source).collect();
        assert_eq!(sections.len(), 3);
        assert!(!sections[0].content.is_blank());
        assert!(sections[1].content.is_blank());
        assert!(!sections[2].content.is_blank());
    }

    #[test]
    fn parse_header() {
        let mut parser = make_parser();
        let mut inline_parser = make_inline_parser();
        let source = "# Hello\n";
        let tree = parser.parse(source, None).unwrap();
        let sections: Vec<_> = MdIterator::new(tree, &mut inline_parser, source).collect();
        assert_eq!(sections.len(), 1);
        assert!(matches!(
            sections[0].content,
            MdContent::Header { tier: 1, .. }
        ));
    }

    #[test]
    fn detect_bare_url() {
        let spans = vec![Span::with(
            "Check https://example.com for more.",
            Modifier::default(),
        )];
        let result = detect_bare_urls(spans);
        assert_eq!(
            result,
            vec![
                Span::from("Check "),
                Span::with(
                    "https://example.com",
                    Modifier::BareLink | Modifier::LinkURL
                ),
                Span::from(" for more."),
            ]
        );
    }

    #[test]
    fn detect_bare_url_preserves_existing_modifiers() {
        let spans = vec![Span::with(
            "See https://example.com now",
            Modifier::Emphasis,
        )];
        let result = detect_bare_urls(spans);
        assert_eq!(
            result,
            vec![
                Span::with("See ", Modifier::Emphasis),
                Span::with(
                    "https://example.com",
                    Modifier::Emphasis | Modifier::BareLink | Modifier::LinkURL
                ),
                Span::with(" now", Modifier::Emphasis),
            ]
        );
    }

    #[test]
    fn detect_bare_url_skips_existing_links() {
        let spans = vec![Span::with(
            "https://example.com",
            Modifier::Link | Modifier::LinkURL,
        )];
        let result = detect_bare_urls(spans.clone());
        assert_eq!(result, spans);
    }

    #[test]
    fn detect_bare_url_skips_code() {
        let spans = vec![Span::with("https://example.com", Modifier::Code)];
        let result = detect_bare_urls(spans.clone());
        assert_eq!(result, spans);
    }

    #[test]
    fn angle_bracket_url_preserved() {
        // Angle bracket URLs like <http://example.com> should preserve both < and >
        let spans = vec![Span::with("<http://www.example.com>", Modifier::default())];
        let result = detect_bare_urls(spans);
        assert_eq!(
            result,
            vec![
                Span::from("<"),
                Span::with(
                    "http://www.example.com",
                    Modifier::BareLink | Modifier::LinkURL
                ),
                Span::from(">"),
            ]
        );
    }

    #[test]
    fn image_contained() {
        let mut parser = make_parser();
        let mut inline_parser = make_inline_parser();
        let source = "![text](url)\n";
        let tree = parser.parse(source, None).unwrap();
        let first = MdIterator::new(tree, &mut inline_parser, source)
            .next()
            .unwrap();
        let MdContent::Paragraph(MdParagraph { spans }) = first.content else {
            panic!("expected paragraph");
        };
        assert_eq!(spans[0], Span::with("![", Modifier::Image));
        assert_eq!(
            spans[1],
            Span::with("text", Modifier::Image | Modifier::LinkDescription)
        );
        assert_eq!(spans[2], Span::with("](", Modifier::Image));
        assert_eq!(
            spans[3],
            Span::with("url", Modifier::Image | Modifier::LinkURL)
        );
        assert_eq!(spans[4], Span::with(")", Modifier::Image));
    }

    #[test]
    fn link_code() {
        let mut parser = make_parser();
        let mut inline_parser = make_inline_parser();
        let source = "![text with `code`](http://example.com/?a=1&`code`=2)\n";
        let tree = parser.parse(source, None).unwrap();
        let first = MdIterator::new(tree, &mut inline_parser, source)
            .next()
            .unwrap();
        let MdContent::Paragraph(MdParagraph { spans }) = first.content else {
            panic!("expected paragraph");
        };
        assert_eq!(spans[0], Span::with("![", Modifier::Image));
        assert_eq!(
            spans[1],
            Span::with("text with ", Modifier::Image | Modifier::LinkDescription)
        );
        assert_eq!(
            spans[2],
            Span::with(
                "code",
                Modifier::Image | Modifier::LinkDescription | Modifier::Code
            )
        );
        assert_eq!(spans[3], Span::with("](", Modifier::Image));
        assert_eq!(
            spans[4],
            Span::with(
                "http://example.com/?a=1&`code`=2",
                Modifier::Image | Modifier::LinkURL
            )
        );
        assert_eq!(spans[5], Span::with(")", Modifier::Image));
    }

    #[test]
    fn nested_image_link() {
        let mut parser = make_parser();
        let mut inline_parser = make_inline_parser();
        let source = "[![image](http://example.com/img.png)](http://example.com)\n";
        let tree = parser.parse(source, None).unwrap();
        let first = MdIterator::new(tree, &mut inline_parser, source)
            .next()
            .unwrap();
        let MdContent::Paragraph(MdParagraph { spans }) = first.content else {
            panic!("expected paragraph");
        };
        assert_eq!(
            spans[0],
            Span::with("[", Modifier::Link | Modifier::LinkDescriptionWrapper)
        );
        assert_eq!(
            spans[1],
            Span::with(
                "![",
                Modifier::Link | Modifier::LinkDescription | Modifier::Image
            )
        );
        assert_eq!(
            spans[2],
            Span::with(
                "image",
                Modifier::Link | Modifier::LinkDescription | Modifier::Image
            )
        );
        assert_eq!(
            spans[3],
            Span::with(
                "](",
                Modifier::Link | Modifier::LinkDescription | Modifier::Image
            )
        );
        assert_eq!(
            spans[4],
            Span::with(
                "http://example.com/img.png",
                Modifier::Link | Modifier::LinkDescription | Modifier::LinkURL | Modifier::Image,
            )
        );
        assert_eq!(
            spans[5],
            Span::with(
                ")",
                Modifier::Link | Modifier::LinkDescription | Modifier::Image
            )
        );
        assert_eq!(
            spans[6],
            Span::with("]", Modifier::Link | Modifier::LinkDescriptionWrapper)
        );
        assert_eq!(
            spans[7],
            Span::with("(", Modifier::Link | Modifier::LinkURLWrapper)
        );
        assert_eq!(
            spans[8],
            Span::with("http://example.com", Modifier::Link | Modifier::LinkURL)
        );
        assert_eq!(
            spans[9],
            Span::with(")", Modifier::Link | Modifier::LinkURLWrapper)
        );
    }

    #[test]
    fn header_with_links() {
        let mut parser = make_parser();
        let mut inline_parser = make_inline_parser();
        let source = "# Header [with link!](https://crates.io/crates/mdfried), [second][ref], [third](http://third), and [another][ref].\n";
        let tree = parser.parse(source, None).unwrap();
        let sections: Vec<_> = MdIterator::new(tree, &mut inline_parser, source).collect();
        assert_eq!(sections.len(), 1);
        fn link(desc: &str, url: &str) -> Vec<Span> {
            vec![
                Span::new(
                    "[".to_owned(),
                    Modifier::Link | Modifier::LinkDescriptionWrapper,
                ),
                Span::new(desc.to_owned(), Modifier::Link | Modifier::LinkDescription),
                Span::new(
                    "]".to_owned(),
                    Modifier::Link | Modifier::LinkDescriptionWrapper,
                ),
                Span::new("(".to_owned(), Modifier::Link | Modifier::LinkURLWrapper),
                Span::new(url.to_owned(), Modifier::Link | Modifier::LinkURL),
                Span::new(")".to_owned(), Modifier::Link | Modifier::LinkURLWrapper),
            ]
        }
        fn link_ref(desc: &str, refe: &str) -> Vec<Span> {
            vec![
                Span::new(
                    "[".to_owned(),
                    Modifier::Link | Modifier::LinkDescriptionWrapper,
                ),
                Span::new(desc.to_owned(), Modifier::Link | Modifier::LinkDescription),
                Span::new(
                    "]".to_owned(),
                    Modifier::Link | Modifier::LinkDescriptionWrapper,
                ),
                Span::new(format!("[{refe}]"), Modifier::Link | Modifier::LinkURL),
            ]
        }
        assert_eq!(
            sections[0].content,
            MdContent::Header {
                tier: 1,
                text: String::from(
                    "Header [with link!](https://crates.io/crates/mdfried), [second][ref], [third](http://third), and [another][ref]."
                ),
                links: vec![
                    link("with link!", "https://crates.io/crates/mdfried"),
                    link_ref("second", "ref"),
                    link("third", "http://third"),
                    link_ref("another", "ref"),
                ],
            },
        );
    }
}
