use std::collections::VecDeque;
use std::iter::Peekable;

use textwrap::{Options, wrap};
use unicode_width::UnicodeWidthStr as _;

use crate::{
    Line, LineKind, Mapper, MarkdownLink,
    link_tracker::TrackedUrl,
    markdown::{
        ListMarker, MdContainer, MdContent, MdIterator, MdSection, Modifier, Span, TableAlignment,
    },
    wrap::{wrap_md_spans, wrap_md_spans_lines},
};

/// A simplified nesting container.
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum MdLineContainer {
    /// Blockquote level.
    Blockquote,
    /// List item with marker type.
    /// `continuation` is true for content after the first paragraph in a list item,
    /// which renders as indentation (spaces) instead of the marker.
    ListItem {
        marker: ListMarker,
        continuation: bool,
    },
}

/// Calculate marker width using mapper's symbols.
fn marker_width<M: Mapper>(marker: &ListMarker, mapper: &M) -> usize {
    match marker {
        ListMarker::Unordered(b) => mapper.unordered_bullet(*b).width(),
        ListMarker::Ordered(n) => mapper.ordered_marker(*n).width(),
        ListMarker::TaskChecked(b) => {
            mapper.unordered_bullet(*b).width() + mapper.task_checked().width()
        }
        ListMarker::TaskUnchecked(b) => {
            mapper.unordered_bullet(*b).width() + mapper.task_unchecked().width()
        }
    }
}

/// Position of a table border.
#[derive(Debug, Clone, Copy, PartialEq)]
enum BorderPosition {
    Top,
    HeaderSeparator,
    Bottom,
}

/// Iterator that produces `Line` items from parsed markdown.
///
/// This handles the blank line logic between sections, producing lines
/// one at a time with proper spacing.
pub struct LineIterator<'a, M: Mapper> {
    inner: Peekable<MdIterator<'a>>,
    width: u16,
    mapper: &'a M,
    /// Buffer of pending lines to emit
    pending_lines: VecDeque<Line>,
    /// Whether we need a blank line before next content
    needs_blank: bool,
    /// Previous section's nesting for comparison
    prev_nesting: Vec<MdContainer>,
    /// Whether previous section was a blank line
    prev_was_blank: bool,
    /// Whether previous section was in a list
    prev_in_list: bool,
}

impl<'a, M: Mapper> LineIterator<'a, M> {
    pub(crate) fn new(inner: MdIterator<'a>, width: u16, mapper: &'a M) -> Self {
        LineIterator {
            inner: inner.peekable(),
            width,
            mapper,
            pending_lines: VecDeque::new(),
            needs_blank: false,
            prev_nesting: Vec::new(),
            prev_was_blank: false,
            prev_in_list: false,
        }
    }

    /// Process the next MdSection and queue its lines
    fn process_next_section(&mut self) -> bool {
        let Some(section) = self.inner.next() else {
            return false;
        };

        let in_list = section
            .nesting
            .iter()
            .any(|c| matches!(c, MdContainer::ListItem(_)));

        let is_blank_line = section.content.is_blank();

        // Nesting change detection - compare container types, not exact values
        let container_type_matches = |a: &MdContainer, b: &MdContainer| -> bool {
            matches!(
                (a, b),
                (MdContainer::List(_), MdContainer::List(_))
                    | (MdContainer::ListItem(_), MdContainer::ListItem(_))
                    | (MdContainer::Blockquote(_), MdContainer::Blockquote(_))
            )
        };
        let is_type_prefix = |shorter: &[MdContainer], longer: &[MdContainer]| -> bool {
            !shorter.is_empty()
                && shorter.len() < longer.len()
                && shorter
                    .iter()
                    .zip(longer.iter())
                    .all(|(a, b)| container_type_matches(a, b))
        };
        let nesting_change = is_type_prefix(&self.prev_nesting, &section.nesting)
            || is_type_prefix(&section.nesting, &self.prev_nesting);

        // Count list nesting depth (number of List containers)
        let list_depth = |nesting: &[MdContainer]| -> usize {
            nesting
                .iter()
                .filter(|c| matches!(c, MdContainer::List(_)))
                .count()
        };
        let curr_list_depth = list_depth(&section.nesting);
        let prev_list_depth = list_depth(&self.prev_nesting);

        // Check if both sections are at the same top-level list (depth 1) with same List container
        let same_top_level_list =
            if in_list && self.prev_in_list && curr_list_depth == 1 && prev_list_depth == 1 {
                let curr_list = section
                    .nesting
                    .iter()
                    .find(|c| matches!(c, MdContainer::List(_)));
                let prev_list = self
                    .prev_nesting
                    .iter()
                    .find(|c| matches!(c, MdContainer::List(_)));
                curr_list == prev_list
            } else {
                false
            };

        // For nested lists (depth > 1), treat all items at same depth as same context
        let same_nested_context =
            in_list && self.prev_in_list && curr_list_depth > 1 && prev_list_depth > 1;

        let same_list_context = same_top_level_list || same_nested_context;

        // Check if we're exiting to a new top-level list (not part of previous ancestry)
        // Compare by position: the first List in current nesting should match the first in prev
        let exiting_to_new_top_level =
            nesting_change && curr_list_depth == 1 && prev_list_depth > 1 && {
                let curr_first_list = section
                    .nesting
                    .iter()
                    .find(|c| matches!(c, MdContainer::List(_)));
                let prev_first_list = self
                    .prev_nesting
                    .iter()
                    .find(|c| matches!(c, MdContainer::List(_)));
                curr_first_list != prev_first_list
            };

        // Allow blank lines before continuation paragraphs or between different top-level lists,
        // but not during nesting changes (unless exiting to a new top-level list)
        let should_emit_blank = self.needs_blank
            && (!same_list_context || section.is_list_continuation)
            && !is_blank_line
            && !self.prev_was_blank
            && (!nesting_change || exiting_to_new_top_level);

        if should_emit_blank {
            self.pending_lines.push_back(Line {
                spans: Vec::new(),
                kind: LineKind::Paragraph,
                urls: Vec::new(),
            });
        }

        // Only headers don't need space after
        self.needs_blank = !matches!(section.content, MdContent::Header { .. });
        self.prev_nesting.clone_from(&section.nesting);
        self.prev_was_blank = is_blank_line;
        self.prev_in_list = in_list;

        let lines = section_to_lines(self.width, section, self.mapper);
        self.pending_lines.extend(lines);

        true
    }
}

impl<M: Mapper> Iterator for LineIterator<'_, M> {
    type Item = Line;

    fn next(&mut self) -> Option<Self::Item> {
        // Return buffered line if available
        if let Some(line) = self.pending_lines.pop_front() {
            return Some(line);
        }

        // Process sections until we have a line to return
        while self.process_next_section() {
            if let Some(line) = self.pending_lines.pop_front() {
                return Some(line);
            }
        }

        None
    }
}

/// Convert a markdown section to output lines.
/// Applies mapper decorators before wrapping so widths are correct.
fn section_to_lines<M: Mapper>(width: u16, section: MdSection, mapper: &M) -> Vec<Line> {
    let nesting = convert_nesting(&section.nesting, section.is_list_continuation);

    match section.content {
        MdContent::Paragraph(p) if p.is_empty() => {
            vec![Line {
                spans: nesting_to_prefix_spans(&nesting, mapper),
                kind: LineKind::Paragraph,
                urls: Vec::new(),
            }]
        }
        MdContent::Paragraph(p) => {
            let prefix_width: usize = nesting
                .iter()
                .map(|c| match c {
                    MdLineContainer::Blockquote => mapper.blockquote_bar().width(),
                    MdLineContainer::ListItem { marker, .. } => marker_width(marker, mapper),
                })
                .sum();
            let decorated_spans = apply_decorators(p.spans, mapper);
            let wrapped_lines = wrap_md_spans(width, decorated_spans, prefix_width, mapper);
            wrapped_to_lines(wrapped_lines, nesting, mapper)
        }
        MdContent::Header { tier, text, links } => {
            let mut lines = if mapper.has_text_size_protocol() {
                // We only pre-wrap headers for text-size-protocol. When rendering image, the
                // wrapping is taken care of the image renderer, as the font size is not known
                // here.
                let spans = vec![Span::from(text.clone())];
                let (n, d) = match tier {
                    1 => (7, 7),
                    2 => (5, 6),
                    3 => (3, 4),
                    4 => (2, 3),
                    5 => (3, 5),
                    _ => (1, 3),
                };
                let scaled_width = width / 2 * d / n;
                let wrapped = wrap_md_spans(scaled_width, spans, 0, mapper);
                wrapped
                    .into_iter()
                    .map(|(spans, urls)| Line {
                        spans,
                        kind: LineKind::Header(tier),
                        urls,
                    })
                    .collect()
            } else {
                vec![Line {
                    spans: vec![Span::from(text.clone())],
                    kind: LineKind::Header(tier),
                    urls: Vec::new(),
                }]
            };

            if !links.is_empty()
                && let Some(joined) = links.into_iter().reduce(|mut acc, next| {
                    acc.push(Span::new(String::from(" "), Modifier::default()));
                    acc.extend(next);
                    acc
                })
            {
                let decorated_spans = apply_decorators(joined, mapper);
                let wrapped_lines = wrap_md_spans(width, decorated_spans, 0, mapper);
                lines.extend(wrapped_to_lines(wrapped_lines, Vec::new(), mapper));
            }

            lines
        }
        MdContent::CodeBlock { language, code } => {
            code_block_to_lines(width, language, code, nesting, mapper)
        }
        MdContent::HorizontalRule => {
            let prefix_spans = nesting_to_prefix_spans(&nesting, mapper);
            let prefix_width: usize = prefix_spans.iter().map(|s| s.content.width()).sum();
            let available = (width as usize).saturating_sub(prefix_width);

            let mut spans = prefix_spans;
            spans.push(Span::new(
                mapper.horizontal_rule_char().repeat(available),
                Modifier::HorizontalRule,
            ));
            vec![Line {
                spans,
                kind: LineKind::HorizontalRule,
                urls: Vec::new(),
            }]
        }
        MdContent::Table {
            header,
            rows,
            alignments,
        } => table_to_lines(width, &header, &rows, &alignments, nesting, mapper),
        MdContent::Html { html } => html
            .split("\n")
            .map(|linestr| Line {
                spans: vec![Span::from(linestr.to_owned())],
                kind: LineKind::Paragraph,
                urls: Vec::new(),
            })
            .collect(),
        MdContent::LinkReferenceDefinition { reference, url } => {
            let spans = vec![
                Span::new("[".to_owned(), Modifier::LinkDescriptionWrapper),
                Span::new(reference.clone(), Modifier::LinkDescription),
                Span::new("]".to_owned(), Modifier::LinkDescriptionWrapper),
                Span::new(": ".to_owned(), Modifier::LinkURLWrapper),
                Span::new(url.clone(), Modifier::BareLink | Modifier::LinkURL),
            ];
            let wrapped = wrap_md_spans_lines(width, spans, mapper);
            let n = wrapped.len();

            let (url_line, url_start) = wrapped
                .iter()
                .enumerate()
                .find_map(|(i, line)| {
                    let mut col: u16 = 0;
                    for s in line {
                        if s.modifiers.contains(Modifier::BareLink | Modifier::LinkURL) {
                            return Some((i, col));
                        }
                        col += s.content.width() as u16;
                    }
                    None
                })
                .unwrap_or((n.saturating_sub(1), 0));

            let last_end: u16 = wrapped
                .last()
                .map(|l| l.iter().map(|s| s.content.width() as u16).sum())
                .unwrap_or(0);

            wrapped
                .into_iter()
                .enumerate()
                .map(|(i, mut line_spans)| {
                    if i >= url_line && i + 1 < n {
                        let w: u16 = line_spans.iter().map(|s| s.content.width() as u16).sum();
                        // Fill to end of line like LinkTracker.
                        if w < width {
                            line_spans.push(Span::new(
                                " ".repeat((width - w) as usize),
                                Modifier::BareLink | Modifier::LinkURL,
                            ));
                        }
                    }
                    Line {
                        spans: line_spans,
                        kind: LineKind::LinkReferenceDefinitions,
                        urls: if i + 1 == n {
                            vec![TrackedUrl::link(
                                url.as_str(),
                                url_start,
                                last_end,
                                n.saturating_sub(url_line + 1),
                            )]
                        } else {
                            vec![]
                        },
                    }
                })
                .collect()
        }
    }
}

/// Apply mapper decorators to spans (emphasis, code, links, etc).
/// This must happen before wrapping so decorator widths are included.
fn apply_decorators<M: Mapper>(spans: Vec<Span>, mapper: &M) -> Vec<Span> {
    let mut result: Vec<Span> = Vec::with_capacity(spans.len() * 2);
    let mut prev_emphasis = false;
    let mut prev_strong = false;
    let mut prev_code = false;
    let mut prev_strikethrough = false;

    for mut span in spans {
        let has_emphasis = span.modifiers.contains(Modifier::Emphasis);
        let has_strong = span.modifiers.contains(Modifier::StrongEmphasis);
        let has_code = span.modifiers.contains(Modifier::Code);
        let has_strikethrough = span.modifiers.contains(Modifier::Strikethrough);
        // is_newline is true only for soft breaks in flowing text (NewLine without HardLineBreak).
        // HardLineBreak spans (from GFM two-space breaks) do not need
        // the NewLine decorator transfer; their trailing whitespace is handled by wrap.rs.
        let is_newline = span.modifiers.contains(Modifier::NewLine)
            && !span.modifiers.contains(Modifier::HardLineBreak);

        // If this span starts a new line or hard break, trim trailing whitespace from previous span
        // (This matches wrap.rs behavior but must happen before we insert decorators)
        if is_newline || span.modifiers.contains(Modifier::HardLineBreak) {
            if let Some(last) = result.last_mut() {
                last.content.truncate(last.content.trim_end().len());
            }
        }

        // Close decorators that ended (in reverse order of nesting)
        if prev_code && !has_code {
            let close = mapper.code_close();
            if !close.is_empty() {
                result.push(Span::new(close.to_owned(), Modifier::CodeWrapper));
            }
        }
        if prev_strikethrough && !has_strikethrough {
            let close = mapper.strikethrough_close();
            if !close.is_empty() {
                result.push(Span::new(close.to_owned(), Modifier::StrikethroughWrapper));
            }
        }
        if prev_strong && !has_strong {
            let close = mapper.strong_close();
            if !close.is_empty() {
                result.push(Span::new(close.to_owned(), Modifier::StrongEmphasisWrapper));
            }
        }
        if prev_emphasis && !has_emphasis {
            let close = mapper.emphasis_close();
            if !close.is_empty() {
                result.push(Span::new(close.to_owned(), Modifier::EmphasisWrapper));
            }
        }

        // Track if we need to transfer NewLine to the first opening decorator
        let mut newline_transferred = false;

        // Open decorators that started
        // If the content span has NewLine, transfer it to the first decorator we insert
        if has_emphasis && !prev_emphasis {
            let open = mapper.emphasis_open();
            if !open.is_empty() {
                let mods = if is_newline && !newline_transferred {
                    newline_transferred = true;
                    Modifier::EmphasisWrapper | Modifier::NewLine
                } else {
                    Modifier::EmphasisWrapper
                };
                result.push(Span::new(open.to_owned(), mods));
            }
        }
        if has_strong && !prev_strong {
            let open = mapper.strong_open();
            if !open.is_empty() {
                let mods = if is_newline && !newline_transferred {
                    newline_transferred = true;
                    Modifier::StrongEmphasisWrapper | Modifier::NewLine
                } else {
                    Modifier::StrongEmphasisWrapper
                };
                result.push(Span::new(open.to_owned(), mods));
            }
        }
        if has_strikethrough && !prev_strikethrough {
            let open = mapper.strikethrough_open();
            if !open.is_empty() {
                let mods = if is_newline && !newline_transferred {
                    newline_transferred = true;
                    Modifier::StrikethroughWrapper | Modifier::NewLine
                } else {
                    Modifier::StrikethroughWrapper
                };
                result.push(Span::new(open.to_owned(), mods));
            }
        }
        if has_code && !prev_code {
            let open = mapper.code_open();
            if !open.is_empty() {
                let mods = if is_newline && !newline_transferred {
                    newline_transferred = true;
                    Modifier::CodeWrapper | Modifier::NewLine
                } else {
                    Modifier::CodeWrapper
                };
                result.push(Span::new(open.to_owned(), mods));
            }
        }

        // If we transferred NewLine to an opening decorator, remove it from the content span
        if newline_transferred {
            span.modifiers.remove(Modifier::NewLine);
        }

        // Transform link wrappers
        if span.modifiers.contains(Modifier::LinkDescriptionWrapper) {
            span.content = if span.content == "[" {
                mapper.link_desc_open().to_owned()
            } else {
                mapper.link_desc_close().to_owned()
            };
        } else if span.modifiers.contains(Modifier::LinkURLWrapper) {
            span.content = if span.content == "(" {
                mapper.link_url_open().to_owned()
            } else {
                mapper.link_url_close().to_owned()
            };
        }

        let hide = mapper.hide_urls()
            && (span.modifiers.contains(Modifier::LinkURLWrapper)
                && !(span.modifiers.contains(Modifier::BareLink)
                    || span.modifiers.contains(Modifier::Image)));
        if !hide {
            result.push(span);
        } else {
            // LinkURLWrapper may be hidden by setting to "empty content", but we need them to
            // exist for LinkTracker logic.
            result.push(Span {
                content: String::new(),
                modifiers: span.modifiers,
            });
        }

        prev_emphasis = has_emphasis;
        prev_strong = has_strong;
        prev_code = has_code;
        prev_strikethrough = has_strikethrough;
    }

    // Close any remaining open decorators at end
    if prev_code {
        let close = mapper.code_close();
        if !close.is_empty() {
            result.push(Span::new(close.to_owned(), Modifier::CodeWrapper));
        }
    }
    if prev_strikethrough {
        let close = mapper.strikethrough_close();
        if !close.is_empty() {
            result.push(Span::new(close.to_owned(), Modifier::StrikethroughWrapper));
        }
    }
    if prev_strong {
        let close = mapper.strong_close();
        if !close.is_empty() {
            result.push(Span::new(close.to_owned(), Modifier::StrongEmphasisWrapper));
        }
    }
    if prev_emphasis {
        let close = mapper.emphasis_close();
        if !close.is_empty() {
            result.push(Span::new(close.to_owned(), Modifier::EmphasisWrapper));
        }
    }

    result
}

/// Build prefix spans from nesting containers.
fn nesting_to_prefix_spans<M: Mapper>(nesting: &[MdLineContainer], mapper: &M) -> Vec<Span> {
    let mut spans = Vec::new();
    let last_list_idx = nesting
        .iter()
        .rposition(|c| matches!(c, MdLineContainer::ListItem { .. }));

    for (i, container) in nesting.iter().enumerate() {
        match container {
            MdLineContainer::Blockquote => {
                spans.push(Span::new(
                    mapper.blockquote_bar().to_owned(),
                    Modifier::BlockquoteBar,
                ));
            }
            MdLineContainer::ListItem {
                marker,
                continuation,
            } => {
                if Some(i) == last_list_idx && !*continuation {
                    let marker_text = match marker {
                        ListMarker::Unordered(b) => mapper.unordered_bullet(*b).to_owned(),
                        ListMarker::Ordered(n) => mapper.ordered_marker(*n),
                        ListMarker::TaskChecked(b) => {
                            format!("{}{}", mapper.unordered_bullet(*b), mapper.task_checked())
                        }
                        ListMarker::TaskUnchecked(b) => {
                            format!("{}{}", mapper.unordered_bullet(*b), mapper.task_unchecked())
                        }
                    };
                    spans.push(Span::new(marker_text, Modifier::ListMarker));
                } else {
                    // Indentation for outer/continuation items
                    let indent_width = marker_width(marker, mapper);
                    spans.push(Span::new(" ".repeat(indent_width), Modifier::empty()));
                }
            }
        }
    }
    spans
}

/// Convert MdContainer nesting to MdLineContainer nesting.
fn convert_nesting(md_nesting: &[MdContainer], is_list_continuation: bool) -> Vec<MdLineContainer> {
    // Find the index of the last ListItem to mark it as continuation if needed
    let last_list_item_idx = md_nesting
        .iter()
        .rposition(|c| matches!(c, MdContainer::ListItem(_)));

    md_nesting
        .iter()
        .enumerate()
        .filter_map(|(idx, c)| match c {
            MdContainer::Blockquote(_) => Some(MdLineContainer::Blockquote),
            MdContainer::ListItem(marker) => {
                let continuation = is_list_continuation && last_list_item_idx == Some(idx);
                Some(MdLineContainer::ListItem {
                    marker: marker.clone(),
                    continuation,
                })
            }
            MdContainer::List(_) => None, // List containers don't produce visual nesting
        })
        .collect()
}

/// Convert a code block to output lines.
fn code_block_to_lines<M: Mapper>(
    width: u16,
    language: String,
    code: String,
    nesting: Vec<MdLineContainer>,
    mapper: &M,
) -> Vec<Line> {
    let code_lines: Vec<&str> = code.lines().collect();
    let num_lines = code_lines.len();
    if num_lines == 0 {
        return vec![];
    }

    // Calculate prefix and available width
    let prefix_spans = nesting_to_prefix_spans(&nesting, mapper);
    let prefix_width: usize = prefix_spans.iter().map(|s| s.content.width()).sum();
    let available_width = (width as usize).saturating_sub(prefix_width).max(1);

    let mut result = Vec::new();

    for line in code_lines {
        let line_width = line.width();

        if line_width > available_width {
            // Wrap this line
            let options = Options::new(available_width)
                .break_words(true)
                .word_splitter(textwrap::word_splitters::WordSplitter::NoHyphenation);
            let parts: Vec<_> = wrap(line, options).into_iter().collect();

            for part in parts {
                let content_width = part.width();
                let padding = available_width.saturating_sub(content_width);

                let mut spans = prefix_spans.clone();
                spans.push(Span::new(part.into_owned(), Modifier::Code));
                if padding > 0 {
                    spans.push(Span::new(" ".repeat(padding), Modifier::Code));
                }
                result.push(Line {
                    spans,
                    kind: LineKind::CodeBlock {
                        language: language.clone(),
                    },
                    urls: Vec::new(),
                });
            }
        } else {
            // Line fits, pad to fill width
            let padding = available_width.saturating_sub(line_width);

            let mut spans = prefix_spans.clone();
            spans.push(Span::new(line.to_owned(), Modifier::Code));
            if padding > 0 {
                spans.push(Span::new(" ".repeat(padding), Modifier::Code));
            }
            result.push(Line {
                spans,
                kind: LineKind::CodeBlock {
                    language: language.clone(),
                },
                urls: Vec::new(),
            });
        }
    }

    result
}

/// Convert wrapped lines to output Lines with prefix spans.
fn wrapped_to_lines<M: Mapper>(
    wrapped_lines: Vec<(Vec<Span>, Vec<TrackedUrl>)>,
    nesting: Vec<MdLineContainer>,
    mapper: &M,
) -> Vec<Line> {
    let mut lines = Vec::new();

    for (line_idx, (spans, urls)) in wrapped_lines.into_iter().enumerate() {
        let has_content = spans.iter().any(|s| !s.content.trim().is_empty());
        if !has_content && urls.is_empty() {
            continue;
        }

        // For continuation lines (soft-wrapped), mark ListItems as continuation
        let line_nesting = if line_idx == 0 {
            &nesting
        } else {
            &nesting
                .iter()
                .map(|c| match c {
                    MdLineContainer::Blockquote => MdLineContainer::Blockquote,
                    MdLineContainer::ListItem { marker, .. } => MdLineContainer::ListItem {
                        marker: marker.clone(),
                        continuation: true,
                    },
                })
                .collect()
        };

        // Really only replace an image line if it is just a full image on its own line.
        // This excludes images that have been wrapped.
        let is_only_image = spans.len() == 5
            && (spans[0].modifiers == Modifier::Image
                || spans[0].modifiers == Modifier::Image | Modifier::NewLine)
            && spans[0].content == "!["
            && spans[1].modifiers == (Modifier::Image | Modifier::LinkDescription)
            && spans[2].modifiers == Modifier::Image
            && spans[2].content == "]("
            && spans[3].modifiers == (Modifier::Image | Modifier::LinkURL)
            && spans[4].modifiers == Modifier::Image
            && spans[4].content == ")";

        let mut image_lines = Vec::new();
        // Create image lines
        for tracked_url in &urls {
            if let TrackedUrl::Image { desc, url } = tracked_url {
                let spans = vec![
                    Span::new(
                        "![".to_owned(),
                        Modifier::Image | Modifier::LinkDescriptionWrapper,
                    ),
                    if is_only_image {
                        Span::new(desc.clone(), Modifier::Image | Modifier::LinkDescription)
                    } else {
                        Span::new(
                            "Loading...".to_owned(),
                            Modifier::Image | Modifier::LinkDescription,
                        )
                    },
                    Span::new(
                        "]".to_owned(),
                        Modifier::Image | Modifier::LinkDescriptionWrapper,
                    ),
                    Span::new("(".to_owned(), Modifier::Image | Modifier::LinkURLWrapper),
                    Span::new(url.clone(), Modifier::Image | Modifier::LinkURL),
                    Span::new(")".to_owned(), Modifier::Image | Modifier::LinkURLWrapper),
                ];
                image_lines.push(Line {
                    spans,
                    kind: LineKind::Image(MarkdownLink {
                        url: url.clone(),
                        description: desc.clone(),
                    }),
                    urls: vec![TrackedUrl::Image {
                        desc: desc.clone(),
                        url: url.clone(),
                    }],
                });
            }
        }

        // Create text line
        if !is_only_image && !spans.is_empty() {
            let mut nesting_spans = nesting_to_prefix_spans(line_nesting, mapper);
            nesting_spans.extend(spans);
            lines.push(Line {
                spans: nesting_spans,
                kind: LineKind::Paragraph,
                urls,
            });
        }

        lines.extend(image_lines);
    }

    lines
}

/// Convert a table to output lines.
fn table_to_lines<M: Mapper>(
    width: u16,
    header: &[Vec<Span>],
    rows: &[Vec<Vec<Span>>],
    alignments: &[TableAlignment],
    nesting: Vec<MdLineContainer>,
    mapper: &M,
) -> Vec<Line> {
    let mut lines = Vec::new();

    let prefix_spans = nesting_to_prefix_spans(&nesting, mapper);
    let prefix_width: usize = prefix_spans.iter().map(|s| s.content.width()).sum();
    let available_width = (width as usize).saturating_sub(prefix_width);

    let num_cols = header.len();
    if num_cols == 0 {
        return lines;
    }

    // Calculate cell width
    let cell_width = |cell: &[Span]| -> usize { cell.iter().map(|n| n.content.width()).sum() };

    // Find max width for each column
    let mut col_widths: Vec<usize> = header.iter().map(|c| cell_width(c)).collect();
    for row in rows {
        for (i, cell) in row.iter().enumerate() {
            if i < col_widths.len() {
                col_widths[i] = col_widths[i].max(cell_width(cell));
            }
        }
    }

    // Add padding
    let col_widths: Vec<usize> = col_widths.iter().map(|w| w + 2).collect();

    // Scale if too wide
    let table_width: usize = col_widths.iter().sum::<usize>() + num_cols + 1;
    let col_widths: Vec<usize> = if table_width > available_width && available_width > num_cols + 1
    {
        let content_width = available_width - num_cols - 1;
        let total_content: usize = col_widths.iter().sum();
        col_widths
            .iter()
            .map(|w| (w * content_width / total_content).max(3))
            .collect()
    } else {
        col_widths
    };

    // Helper to build border line
    let build_border = |position: BorderPosition| -> Line {
        let (left, mid, right) = match position {
            BorderPosition::Top => (
                mapper.table_top_left(),
                mapper.table_top_junction(),
                mapper.table_top_right(),
            ),
            BorderPosition::HeaderSeparator => (
                mapper.table_left_junction(),
                mapper.table_cross(),
                mapper.table_right_junction(),
            ),
            BorderPosition::Bottom => (
                mapper.table_bottom_left(),
                mapper.table_bottom_junction(),
                mapper.table_bottom_right(),
            ),
        };
        let horizontal = mapper.table_horizontal();

        let mut spans = prefix_spans.clone();
        spans.push(Span::new(left.to_owned(), Modifier::TableBorder));
        for (i, &col_w) in col_widths.iter().enumerate() {
            spans.push(Span::new(horizontal.repeat(col_w), Modifier::TableBorder));
            if i < num_cols - 1 {
                spans.push(Span::new(mid.to_owned(), Modifier::TableBorder));
            }
        }
        spans.push(Span::new(right.to_owned(), Modifier::TableBorder));

        Line {
            spans,
            kind: LineKind::TableBorder,
            urls: Vec::new(),
        }
    };

    // Helper to build row lines
    let build_row_lines = |row: &[Vec<Span>], is_header: bool| -> Vec<Line> {
        let vertical = mapper.table_vertical();

        // Wrap each cell's content (apply decorators first, like paragraphs)
        let wrapped_cells: Vec<Vec<Vec<Span>>> = row
            .iter()
            .enumerate()
            .map(|(i, cell)| {
                let col_width = col_widths.get(i).copied().unwrap_or(3);
                let inner_width = col_width.saturating_sub(2).max(1) as u16;
                let decorated = apply_decorators(cell.clone(), mapper);
                let wrapped = wrap_md_spans_lines(inner_width, decorated, mapper);
                if wrapped.is_empty() {
                    vec![Vec::new()]
                } else {
                    wrapped
                }
            })
            .collect();

        let max_lines = wrapped_cells.iter().map(|c| c.len()).max().unwrap_or(1);
        let mut result = Vec::new();

        for line_idx in 0..max_lines {
            let mut spans = prefix_spans.clone();
            spans.push(Span::new(vertical.to_owned(), Modifier::TableBorder));

            for (i, col_width) in col_widths.iter().enumerate() {
                let alignment = alignments.get(i).copied().unwrap_or(TableAlignment::Left);
                let cell_spans = wrapped_cells
                    .get(i)
                    .and_then(|c| c.get(line_idx))
                    .map_or(&[][..], |v| v.as_slice());

                let content_width: usize = cell_spans.iter().map(|s| s.content.width()).sum();
                let inner_width = col_width.saturating_sub(2);
                let padding_total = inner_width.saturating_sub(content_width);

                let (left_pad, right_pad) = match alignment {
                    TableAlignment::Center => {
                        (padding_total / 2, padding_total - padding_total / 2)
                    }
                    TableAlignment::Right => (padding_total, 0),
                    TableAlignment::Left => (0, padding_total),
                };

                // Left padding + space
                spans.push(Span::new(
                    format!(" {}", " ".repeat(left_pad)),
                    Modifier::empty(),
                ));

                // Cell content (already decorated)
                spans.extend(cell_spans.iter().cloned());

                // Right padding + space
                spans.push(Span::new(
                    format!("{} ", " ".repeat(right_pad)),
                    Modifier::empty(),
                ));
                spans.push(Span::new(vertical.to_owned(), Modifier::TableBorder));
            }

            // Fill missing columns
            for i in row.len()..num_cols {
                let col_width = col_widths.get(i).copied().unwrap_or(3);
                spans.push(Span::new(" ".repeat(col_width), Modifier::empty()));
                spans.push(Span::new(vertical.to_owned(), Modifier::TableBorder));
            }

            result.push(Line {
                spans,
                kind: LineKind::TableRow { is_header },
                urls: Vec::new(),
            });
        }

        result
    };

    // Top border
    lines.push(build_border(BorderPosition::Top));

    // Header row
    lines.extend(build_row_lines(header, true));

    // Header separator
    lines.push(build_border(BorderPosition::HeaderSeparator));

    // Data rows
    for row in rows {
        lines.extend(build_row_lines(row, false));
    }

    // Bottom border
    lines.push(build_border(BorderPosition::Bottom));

    lines
}

#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
    use crate::DefaultMapper;

    use super::*;
    use pretty_assertions::assert_eq;
    use tree_sitter::Parser;

    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 line_iterator_clean_links() {
        let mut parser = make_parser();
        let mut inline_parser = make_inline_parser();
        let source = "[link](http://example.com)\n";
        let tree = parser.parse(source, None).unwrap();
        let iter = MdIterator::new(tree, &mut inline_parser, source);

        struct HideUrlsMapper;
        impl Mapper for HideUrlsMapper {
            fn link_desc_open(&self) -> &str {
                ""
            }
            fn link_desc_close(&self) -> &str {
                ""
            }
            fn hide_urls(&self) -> bool {
                true
            }
        }

        let line_iter = LineIterator::new(iter, 80, &HideUrlsMapper {});
        let lines: Vec<Line> = line_iter.collect();
        assert_eq!(
            lines[0],
            Line {
                spans: vec![
                    Span::with("", Modifier::Link | Modifier::LinkDescriptionWrapper),
                    Span::with("link", Modifier::Link | Modifier::LinkDescription),
                    Span::with("", Modifier::Link | Modifier::LinkDescriptionWrapper),
                    Span::with("", Modifier::Link | Modifier::LinkURLWrapper,),
                    Span::with("http://example.com", Modifier::Link | Modifier::LinkURL),
                    Span::with("", Modifier::Link | Modifier::LinkURLWrapper,),
                ],
                kind: LineKind::Paragraph,
                urls: vec![TrackedUrl::link("http://example.com", 0, 4, 0)]
            }
        );
    }

    #[test]
    fn line_iterator_clean_links_nested_image() {
        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 iter = MdIterator::new(tree, &mut inline_parser, source);

        struct HideUrlsMapper;
        impl Mapper for HideUrlsMapper {
            fn link_desc_open(&self) -> &str {
                ""
            }
            fn link_desc_close(&self) -> &str {
                ""
            }
            fn hide_urls(&self) -> bool {
                true
            }
        }

        let line_iter = LineIterator::new(iter, 80, &HideUrlsMapper {});
        let lines: Vec<Line> = line_iter.collect();
        assert_eq!(
            lines[0],
            Line {
                spans: vec![
                    Span::with("", Modifier::Link | Modifier::LinkDescriptionWrapper),
                    Span::with(
                        "![",
                        Modifier::Link | Modifier::LinkDescription | Modifier::Image
                    ),
                    Span::with(
                        "image",
                        Modifier::Link | Modifier::LinkDescription | Modifier::Image
                    ),
                    Span::with(
                        "](",
                        Modifier::Link | Modifier::LinkDescription | Modifier::Image
                    ),
                    Span::with(
                        "http://example.com/img.png",
                        Modifier::Link
                            | Modifier::LinkDescription
                            | Modifier::Image
                            | Modifier::LinkURL,
                    ),
                    Span::with(
                        ")",
                        Modifier::Link | Modifier::LinkDescription | Modifier::Image
                    ),
                    Span::with("", Modifier::Link | Modifier::LinkDescriptionWrapper),
                    Span::with("", Modifier::Link | Modifier::LinkURLWrapper),
                    Span::with("http://example.com", Modifier::Link | Modifier::LinkURL,),
                    Span::with("", Modifier::Link | Modifier::LinkURLWrapper),
                ],
                kind: LineKind::Paragraph,
                urls: vec![
                    TrackedUrl::image("image", "http://example.com/img.png"),
                    TrackedUrl::link("http://example.com", 0, 36, 0),
                ],
            }
        );
    }

    #[test]
    fn bold_italics_cause_line_wrap_bugfix() {
        let mut parser = make_parser();
        let mut inline_parser = make_inline_parser();
        let source = r#"
I have searched far **and** wide but I have *yet* to come across a show that would leave me so emotionally invested and at the end devastated. The reasons for this I have yet to understand and this blog post is meant to explore them. Who knows where my keyboard will take us."#;
        let tree = parser.parse(source, None).unwrap();
        let iter = MdIterator::new(tree, &mut inline_parser, source);

        let line_iter = LineIterator::new(iter, 80, &DefaultMapper {});
        let lines: Vec<Line> = line_iter.collect();
        assert_eq!(4, lines.len());
        assert_eq!(
            lines[0],
            Line {
                spans: vec![
                    Span::with("I have searched far ", Modifier::default()),
                    Span::with("**", Modifier::StrongEmphasisWrapper),
                    Span::with("and", Modifier::StrongEmphasis),
                    Span::with("**", Modifier::StrongEmphasisWrapper),
                    Span::with(" wide but I have ", Modifier::default()),
                    Span::with("*", Modifier::EmphasisWrapper),
                    Span::with("yet", Modifier::Emphasis),
                    Span::with("*", Modifier::EmphasisWrapper),
                    Span::with(" to come across a show that", Modifier::default()),
                ],
                kind: LineKind::Paragraph,
                urls: Vec::new(),
            }
        );
        assert_eq!(
            lines[1],
            Line {
                spans: vec![Span::with(
                    "would leave me so emotionally invested and at the end devastated. The reasons",
                    Modifier::default()
                ),],
                kind: LineKind::Paragraph,
                urls: Vec::new(),
            }
        );
        assert_eq!(
            lines[2],
            Line {
                spans: vec![Span::with(
                    "for this I have yet to understand and this blog post is meant to explore them.",
                    Modifier::default()
                ),],
                kind: LineKind::Paragraph,
                urls: Vec::new(),
            }
        );
        assert_eq!(
            lines[3],
            Line {
                spans: vec![Span::with(
                    "Who knows where my keyboard will take us.",
                    Modifier::default()
                ),],
                kind: LineKind::Paragraph,
                urls: Vec::new(),
            }
        );
    }

    #[test]
    fn long_line_link_wrapping() {
        let source = "blalalalallalabbalallalaa [![Packaging status](https://repology.org/badge/vertical-allrepos/mdfried.svg)](https://repology.org/project/mdfried/versions)";
        let mut parser = make_parser();
        let mut inline_parser = make_inline_parser();
        let tree = parser.parse(source, None).unwrap();
        let iter = MdIterator::new(tree, &mut inline_parser, source);

        let line_iter = LineIterator::new(iter, 100, &DefaultMapper {});
        let lines: Vec<Line> = line_iter.collect();
        assert_eq!(
            vec![
                "blalalalallalabbalallalaa [![Packaging status](                                                     ",
                "https://repology.org/badge/vertical-allrepos/mdfried.svg)]",
                "![Loading...](https://repology.org/badge/vertical-allrepos/mdfried.svg)",
                "(https://repology.org/project/mdfried/versions)",
            ],
            Line::to_strings(&lines),
        );
    }

    #[test]
    #[ignore]
    fn long_url_link_wrapping() {
        let source = "[![Packaging status](https://repology.org/badge/vertical-allrepos/superloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongurl.svg)](https://repology.org/project/mdfried/versions)";
        let mut parser = make_parser();
        let mut inline_parser = make_inline_parser();
        let tree = parser.parse(source, None).unwrap();
        let iter = MdIterator::new(tree, &mut inline_parser, source);

        let line_iter = LineIterator::new(iter, 100, &DefaultMapper {});
        let lines: Vec<Line> = line_iter.collect();
        assert_eq!(
            vec![
                "[![Packaging status](",
                "https://repology.org/badge/vertical-allrepos/superloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongurl.svg)]",
                "![Loading...](https://repology.org/badge/vertical-allrepos/mdfried.svg)",
                "(https://repology.org/project/mdfried/versions)",
            ],
            Line::to_strings(&lines),
        );
    }

    #[test]
    fn is_only_image() {
        let source = "![image desc](https://image.com)";
        let mut parser = make_parser();
        let mut inline_parser = make_inline_parser();
        let tree = parser.parse(source, None).unwrap();
        let iter = MdIterator::new(tree, &mut inline_parser, source);

        let line_iter = LineIterator::new(iter, 100, &DefaultMapper {});
        let lines: Vec<Line> = line_iter.collect();
        assert_eq!(
            vec!["![image desc](https://image.com)"],
            Line::to_strings(&lines),
        );
    }

    #[test]
    fn link_reference_definition_wrapping() {
        // URL overflows width=20, so it is pushed to its own line(s).
        // textwrap breaks "https://ex.com/abcdefg" at the "/" (a Unicode line-break
        // opportunity), producing "https://ex.com/" (15 chars) and "abcdefg" (7 chars).
        // The intermediate URL line must be padded to width=20 with filler spaces so that
        // the multiline link overlay covers the full line — matching is_mid_link() behaviour
        // for regular link descriptions.
        let source = "[L]: https://ex.com/abcdefg\n";
        let mut parser = make_parser();
        let mut inline_parser = make_inline_parser();
        let tree = parser.parse(source, None).unwrap();
        let iter = MdIterator::new(tree, &mut inline_parser, source);

        let line_iter = LineIterator::new(iter, 20, &DefaultMapper {});
        let lines: Vec<Line> = line_iter.collect();
        assert_eq!(lines.len(), 3);

        let strings = Line::to_strings(&lines);
        assert_eq!(strings[0], "[L]:");
        // "https://ex.com/" (15 chars) padded to width=20 by filler spaces.
        assert_eq!(strings[1], "https://ex.com/     ");
        assert_eq!(strings[2], "abcdefg");

        // TrackedUrl is only on the last line: start=0 (URL at col 0), end=7, lines=1.
        assert!(lines[0].urls.is_empty());
        assert!(lines[1].urls.is_empty());
        assert_eq!(
            lines[2].urls,
            vec![TrackedUrl::link("https://ex.com/abcdefg", 0, 7, 1)],
        );
    }
}
