//! Parse markdown text into styled ratatui lines with syntax-highlighted code blocks.

use minimad::{Composite, CompositeStyle, Compound, Line, TableRow};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line as RLine, Span};

use crate::highlighter::CodeHighlighter;
use crate::theme::MarkdownTheme;

/// Span of lines within a parsed document that form a fenced code block.
pub struct CodeBlock {
    pub start_line: usize,
    pub end_line: usize,
    pub source: String,
}

/// Result of parsing a markdown string: styled lines and extracted code blocks.
pub struct ParsedMarkdown {
    pub lines: Vec<RLine<'static>>,
    pub code_blocks: Vec<CodeBlock>,
}

/// Parse markdown text into themed, syntax-highlighted ratatui lines.
pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
    md: &str,
    theme: &T,
    highlighter: &H,
) -> ParsedMarkdown {
    let processed = preprocess_markdown(md);
    let text = minimad::Text::from(processed.as_str());
    let mut lines = Vec::new();

    let mut code_line_nr: usize = 0;
    let mut table_buffer: Vec<TableRow> = Vec::new();
    let mut code_blocks: Vec<CodeBlock> = Vec::new();
    let mut current_block_start: Option<usize> = None;
    let mut current_block_source: Vec<String> = Vec::new();

    let flush_table = |buf: &mut Vec<TableRow>, out: &mut Vec<RLine<'static>>, theme: &T| {
        if buf.is_empty() {
            return;
        }
        let col_widths = compute_column_widths(buf);
        for (row_idx, row) in buf.drain(..).enumerate() {
            out.push(render_table_row(row, row_idx, &col_widths, theme));
        }
    };

    let close_block = |start: Option<usize>,
                       source: &mut Vec<String>,
                       blocks: &mut Vec<CodeBlock>,
                       lines: &[RLine<'static>]| {
        if let Some(start) = start {
            blocks.push(CodeBlock {
                start_line: start,
                end_line: lines.len(),
                source: std::mem::take(source).join("\n"),
            });
        }
    };

    for line in text.lines {
        let is_code = matches!(&line, Line::Normal(c) if c.style == CompositeStyle::Code);
        if !is_code {
            close_block(
                current_block_start.take(),
                &mut current_block_source,
                &mut code_blocks,
                &lines,
            );
        }

        match line {
            Line::Normal(composite) if composite.style == CompositeStyle::Code => {
                flush_table(&mut table_buffer, &mut lines, theme);
                code_line_nr += 1;
                if current_block_start.is_none() {
                    current_block_start = Some(lines.len());
                }
                let raw: String = composite
                    .compounds
                    .iter()
                    .map(|c: &minimad::Compound| c.src)
                    .collect();
                current_block_source.push(raw.clone());
                let mut spans = vec![
                    Span::styled(format!(" {code_line_nr:>2} "), theme.code_border()),
                    Span::styled("│ ", theme.code_border()),
                ];
                spans.extend(
                    highlighter
                        .highlight(&raw)
                        .into_iter()
                        .map(|(style, text)| Span::styled(text, style)),
                );
                lines.push(RLine::from(spans));
            }
            Line::Normal(composite) => {
                flush_table(&mut table_buffer, &mut lines, theme);
                code_line_nr = 0;
                lines.push(composite_to_line(composite, theme));
            }
            Line::TableRow(row) => {
                code_line_nr = 0;
                table_buffer.push(row);
            }
            Line::TableRule(_) => {}
            _ => {
                flush_table(&mut table_buffer, &mut lines, theme);
                code_line_nr = 0;
                lines.push(RLine::from(""));
            }
        }
    }
    close_block(
        current_block_start.take(),
        &mut current_block_source,
        &mut code_blocks,
        &lines,
    );
    flush_table(&mut table_buffer, &mut lines, theme);

    ParsedMarkdown { lines, code_blocks }
}

fn preprocess_markdown(md: &str) -> String {
    let mut out = String::with_capacity(md.len());
    for line in md.lines() {
        let line = convert_dash_lists(line);
        let mut result = String::with_capacity(line.len());
        let mut chars = line.char_indices().peekable();
        let bytes = line.as_bytes();
        while let Some((i, c)) = chars.next() {
            if c == '`' {
                result.push(c);
                for (_, ch) in chars.by_ref() {
                    result.push(ch);
                    if ch == '`' {
                        break;
                    }
                }
                continue;
            }
            if c == '_' {
                let before_is_space = i == 0 || bytes[i - 1] == b' ';
                if before_is_space {
                    if let Some(end) = line[i + 1..].find('_') {
                        let inner = &line[i + 1..i + 1 + end];
                        if !inner.is_empty() {
                            result.push('*');
                            result.push_str(inner);
                            result.push('*');
                            for _ in 0..end {
                                chars.next();
                            }
                            chars.next();
                            continue;
                        }
                    }
                }
            }
            result.push(c);
        }
        out.push_str(&result);
        out.push('\n');
    }
    out
}

fn convert_dash_lists(line: &str) -> String {
    let trimmed = line.trim_start();
    if let Some(rest) = trimmed.strip_prefix("- ") {
        let indent = line.len() - trimmed.len();
        format!("{}* {}", " ".repeat(indent), rest)
    } else {
        line.to_string()
    }
}

fn cell_text_width(cell: &Composite) -> usize {
    cell.compounds.iter().map(|c| c.src.chars().count()).sum()
}

fn compute_column_widths(rows: &[TableRow]) -> Vec<usize> {
    let mut widths: Vec<usize> = Vec::new();
    for row in rows {
        for (i, cell) in row.cells.iter().enumerate() {
            let w = cell_text_width(cell);
            if i >= widths.len() {
                widths.push(w);
            } else if w > widths[i] {
                widths[i] = w;
            }
        }
    }
    widths
}

fn render_table_row<T: MarkdownTheme>(
    row: TableRow,
    row_idx: usize,
    col_widths: &[usize],
    theme: &T,
) -> RLine<'static> {
    let is_header = row_idx == 0;
    let bg = if is_header {
        theme.table_header_bg()
    } else if row_idx.is_multiple_of(2) {
        theme.table_row_even()
    } else {
        theme.table_row_odd()
    };

    let base_style = if is_header {
        theme.text().bg(bg).add_modifier(Modifier::BOLD)
    } else {
        theme.text().bg(bg)
    };

    let sep_style = theme.code_border().bg(bg);
    let mut spans: Vec<Span<'static>> = Vec::new();

    for (i, cell) in row.cells.into_iter().enumerate() {
        if i > 0 {
            spans.push(Span::styled(" │ ", sep_style));
        }
        let target_width = col_widths.get(i).copied().unwrap_or(0);
        let cell_width = cell
            .compounds
            .iter()
            .map(|c| c.src.chars().count())
            .sum::<usize>();

        for compound in cell.compounds {
            compound_to_spans(compound, base_style, &mut spans, theme);
        }

        let padding = target_width.saturating_sub(cell_width);
        if padding > 0 {
            spans.push(Span::styled(" ".repeat(padding), base_style));
        }
    }

    RLine::from(spans)
}

fn composite_to_line<T: MarkdownTheme>(composite: Composite, theme: &T) -> RLine<'static> {
    let base_style = match composite.style {
        CompositeStyle::Header(1) => theme.h1(),
        CompositeStyle::Header(2) => theme.h2(),
        CompositeStyle::Header(_) => theme.h3(),
        CompositeStyle::ListItem(_) => theme.list(),
        CompositeStyle::Quote => theme.quote(),
        CompositeStyle::Code => theme.code(),
        CompositeStyle::Paragraph => theme.text(),
    };

    let prefix: String = match composite.style {
        CompositeStyle::ListItem(depth) => {
            let indent = "  ".repeat(depth as usize);
            format!("{indent}• ")
        }
        CompositeStyle::Quote => "  │ ".to_string(),
        _ => String::new(),
    };

    let mut spans: Vec<Span<'static>> = Vec::new();
    if !prefix.is_empty() {
        spans.push(Span::styled(prefix, base_style));
    }

    for compound in composite.compounds {
        compound_to_spans(compound, base_style, &mut spans, theme);
    }

    RLine::from(spans)
}

fn compound_to_spans<T: MarkdownTheme>(
    compound: Compound,
    base: Style,
    out: &mut Vec<Span<'static>>,
    theme: &T,
) {
    let mut style = base;

    if compound.bold {
        style = style.add_modifier(Modifier::BOLD);
    }
    if compound.italic {
        style = style.add_modifier(Modifier::ITALIC);
    }
    if compound.code {
        style = theme.code();
    }
    if compound.strikeout {
        style = style.add_modifier(Modifier::CROSSED_OUT);
    }

    let src = compound.src.to_string();
    let link_style = theme.link();

    let mut rest = src.as_str();
    while let Some(start) = rest.find('[') {
        let after_bracket = &rest[start + 1..];
        if let Some(text_end) = after_bracket.find("](") {
            let url_start = start + 1 + text_end + 2;
            if let Some(url_end) = rest[url_start..].find(')') {
                if start > 0 {
                    out.push(Span::styled(rest[..start].to_string(), style));
                }
                let text = &rest[start + 1..start + 1 + text_end];
                let url = &rest[url_start..url_start + url_end];
                if text == url {
                    out.push(Span::styled(url.to_string(), link_style));
                } else {
                    out.push(Span::styled(text.to_string(), link_style));
                    out.push(Span::styled(format!(" ({url})"), theme.link_url()));
                }
                rest = &rest[url_start + url_end + 1..];
                continue;
            }
        }
        out.push(Span::styled(rest[..start + 1].to_string(), style));
        rest = &rest[start + 1..];
    }
    if !rest.is_empty() {
        out.push(Span::styled(rest.to_string(), style));
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::highlighter::NoHighlight;
    use crate::theme::DefaultTheme;

    #[test]
    fn test_preprocess_underscores() {
        assert_eq!(preprocess_markdown("_italic_"), "*italic*\n");
        assert_eq!(preprocess_markdown("word_with_underscores"), "word_with_underscores\n");
        assert_eq!(preprocess_markdown("hello _world_"), "hello *world*\n");
    }

    #[test]
    fn test_preprocess_dash_lists() {
        assert_eq!(convert_dash_lists("- item"), "* item");
        assert_eq!(convert_dash_lists("  - nested"), "  * nested");
        assert_eq!(convert_dash_lists("not-a-list"), "not-a-list");
    }

    #[test]
    fn test_parse_headings() {
        let md = "# H1\n## H2\n### H3";
        let parsed = parse(md, &DefaultTheme, &NoHighlight);
        assert_eq!(parsed.lines.len(), 3);
    }

    #[test]
    fn test_parse_code_block() {
        let md = "```\ncode line\n```";
        let parsed = parse(md, &DefaultTheme, &NoHighlight);
        assert!(!parsed.lines.is_empty());
        assert_eq!(parsed.code_blocks.len(), 1);
        assert_eq!(parsed.code_blocks[0].source, "code line");
    }

    #[test]
    fn test_parse_table() {
        let md = "| A | B |\n|---|---|\n| 1 | 2 |";
        let parsed = parse(md, &DefaultTheme, &NoHighlight);
        assert_eq!(parsed.lines.len(), 2);
    }

    #[test]
    fn test_default_theme_works() {
        let md = "Hello **world**";
        let parsed = parse(md, &DefaultTheme, &NoHighlight);
        assert_eq!(parsed.lines.len(), 1);
    }

    #[test]
    fn test_multiple_code_blocks() {
        let md = "text\n```\nfirst\n```\nmore text\n```\nsecond line 1\nsecond line 2\n```";
        let parsed = parse(md, &DefaultTheme, &NoHighlight);
        assert_eq!(parsed.code_blocks.len(), 2);
        assert_eq!(parsed.code_blocks[0].source, "first");
        assert_eq!(parsed.code_blocks[1].source, "second line 1\nsecond line 2");
    }
}
