use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
use ratatui::{
    style::{Color, Modifier, Style},
    text::{Line, Span, Text},
};

#[derive(Clone, Debug)]
struct ListContext {
    next_index: Option<u64>,
}

#[derive(Clone, Debug)]
struct ItemContext {
    marker: String,
    indent: String,
    first_line: bool,
}

#[derive(Clone, Debug)]
struct LinkContext {
    destination: String,
}

pub(crate) fn render_markdown_text(input: &str) -> Text<'static> {
    render_markdown_text_inner(input, false)
}

pub(crate) fn render_markdown_text_spaced(input: &str) -> Text<'static> {
    render_markdown_text_inner(input, true)
}

fn render_markdown_text_inner(input: &str, block_spacing: bool) -> Text<'static> {
    let mut options = Options::empty();
    options.insert(Options::ENABLE_STRIKETHROUGH);

    let parser = Parser::new_ext(input, options);
    let mut renderer = MarkdownRenderer {
        block_spacing,
        ..Default::default()
    };
    for event in parser {
        renderer.handle_event(event);
    }
    renderer.finish()
}

#[derive(Default)]
struct MarkdownRenderer {
    text: Text<'static>,
    current_line: Vec<Span<'static>>,
    inline_styles: Vec<Style>,
    list_stack: Vec<ListContext>,
    item_stack: Vec<ItemContext>,
    link_stack: Vec<LinkContext>,
    blockquote_depth: usize,
    heading_style: Option<Style>,
    in_code_block: bool,
    show_code_block_fence: bool,
    pending_blank_line: bool,
    block_spacing: bool,
}

impl MarkdownRenderer {
    fn handle_event(&mut self, event: Event<'_>) {
        match event {
            Event::Start(tag) => self.start_tag(tag),
            Event::End(tag) => self.end_tag(tag),
            Event::Text(text) => self.push_text(&text),
            Event::Code(code) => {
                let style = self.current_style().patch(code_style());
                self.push_span(Span::styled(code.into_string(), style));
            }
            Event::SoftBreak | Event::HardBreak => self.push_line_break(),
            Event::Rule => {
                self.ensure_blank_line_between_blocks();
                self.push_spans(vec![Span::styled(
                    "───".to_string(),
                    Style::default().fg(Color::DarkGray),
                )]);
                self.push_line_break();
                self.pending_blank_line = true;
            }
            Event::Html(html) | Event::InlineHtml(html) => self.push_text(&html),
            Event::FootnoteReference(_) | Event::TaskListMarker(_) => {}
            Event::InlineMath(math) | Event::DisplayMath(math) => self.push_text(&math),
        }
    }

    fn start_tag(&mut self, tag: Tag<'_>) {
        match tag {
            Tag::Paragraph => self.ensure_blank_line_between_blocks(),
            Tag::Heading { level, .. } => {
                self.ensure_blank_line_between_blocks();
                let style = heading_style(level as usize);
                self.heading_style = Some(style);
                self.push_span(Span::styled(
                    format!("{} ", "#".repeat(level as usize)),
                    style,
                ));
            }
            Tag::BlockQuote(_) => {
                self.ensure_blank_line_between_blocks();
                self.blockquote_depth += 1;
            }
            Tag::CodeBlock(kind) => {
                self.ensure_blank_line_between_blocks();
                let fence = match kind {
                    CodeBlockKind::Fenced(lang) => {
                        let value = lang.split_whitespace().next().unwrap_or_default();
                        if value.is_empty() {
                            Some("```".to_string())
                        } else {
                            Some(format!("```{value}"))
                        }
                    }
                    CodeBlockKind::Indented => None,
                };
                self.show_code_block_fence = fence.is_some();
                if let Some(fence) = fence {
                    self.push_spans(vec![Span::styled(
                        fence,
                        Style::default().fg(Color::DarkGray),
                    )]);
                    self.push_line_break();
                }
                self.in_code_block = true;
            }
            Tag::List(start) => {
                if self.list_stack.is_empty() {
                    self.ensure_blank_line_between_blocks();
                }
                self.list_stack.push(ListContext { next_index: start });
            }
            Tag::Item => self.start_item(),
            Tag::Emphasis => self.push_style(Style::default().add_modifier(Modifier::ITALIC)),
            Tag::Strong => self.push_style(Style::default().add_modifier(Modifier::BOLD)),
            Tag::Strikethrough => {
                self.push_style(Style::default().add_modifier(Modifier::CROSSED_OUT))
            }
            Tag::Link { dest_url, .. } => {
                self.push_style(link_style());
                self.link_stack.push(LinkContext {
                    destination: dest_url.into_string(),
                });
            }
            Tag::Image { .. }
            | Tag::Table(_)
            | Tag::TableHead
            | Tag::TableRow
            | Tag::TableCell
            | Tag::FootnoteDefinition(_)
            | Tag::HtmlBlock
            | Tag::DefinitionList
            | Tag::DefinitionListTitle
            | Tag::DefinitionListDefinition
            | Tag::Superscript
            | Tag::Subscript
            | Tag::MetadataBlock(_) => {}
        }
    }

    fn end_tag(&mut self, tag: TagEnd) {
        match tag {
            TagEnd::Paragraph => {
                self.flush_current_line();
                self.pending_blank_line = true;
            }
            TagEnd::Heading(_) => {
                self.heading_style = None;
                self.flush_current_line();
                self.pending_blank_line = true;
            }
            TagEnd::BlockQuote(_) => {
                self.flush_current_line();
                self.blockquote_depth = self.blockquote_depth.saturating_sub(1);
                self.pending_blank_line = true;
            }
            TagEnd::CodeBlock => {
                self.flush_current_line();
                if self.in_code_block {
                    self.in_code_block = false;
                    if self.show_code_block_fence {
                        self.push_spans(vec![Span::styled(
                            "```".to_string(),
                            Style::default().fg(Color::DarkGray),
                        )]);
                        self.flush_current_line();
                    }
                    self.show_code_block_fence = false;
                }
                self.pending_blank_line = true;
            }
            TagEnd::List(_) => {
                self.list_stack.pop();
                self.pending_blank_line = true;
            }
            TagEnd::Item => {
                self.flush_current_line();
                self.item_stack.pop();
            }
            TagEnd::Emphasis | TagEnd::Strong | TagEnd::Strikethrough => {
                self.inline_styles.pop();
            }
            TagEnd::Link => {
                self.inline_styles.pop();
                if let Some(link) = self.link_stack.pop() {
                    self.push_span(Span::styled(
                        format!(" ({})", link.destination),
                        link_style(),
                    ));
                }
            }
            TagEnd::Image
            | TagEnd::Table
            | TagEnd::TableHead
            | TagEnd::TableRow
            | TagEnd::TableCell
            | TagEnd::FootnoteDefinition
            | TagEnd::HtmlBlock
            | TagEnd::DefinitionList
            | TagEnd::DefinitionListTitle
            | TagEnd::DefinitionListDefinition
            | TagEnd::Superscript
            | TagEnd::Subscript
            | TagEnd::MetadataBlock(_) => {}
        }
    }

    fn finish(mut self) -> Text<'static> {
        self.flush_current_line();
        while self
            .text
            .lines
            .last()
            .is_some_and(|line| line.spans.iter().all(|span| span.content.is_empty()))
        {
            self.text.lines.pop();
        }
        self.text
    }

    fn start_item(&mut self) {
        self.flush_current_line();

        let depth = self.list_stack.len();
        let indent = " ".repeat(depth.saturating_sub(1) * 2);
        let marker = match self
            .list_stack
            .last_mut()
            .and_then(|context| context.next_index.as_mut())
        {
            Some(index) => {
                let current = *index;
                *index = index.saturating_add(1);
                format!("{indent}{current}. ")
            }
            None => format!("{indent}- "),
        };
        let item_indent = " ".repeat(marker.chars().count());

        self.item_stack.push(ItemContext {
            marker,
            indent: item_indent,
            first_line: true,
        });
    }

    fn ensure_blank_line_between_blocks(&mut self) {
        if self.pending_blank_line {
            self.flush_current_line();
            if self.block_spacing
                && self.item_stack.is_empty()
                && self
                    .text
                    .lines
                    .last()
                    .is_some_and(|line| !line.spans.iter().all(|span| span.content.is_empty()))
            {
                self.text.lines.push(Line::default());
            }
            self.pending_blank_line = false;
        }
    }

    fn push_text(&mut self, text: &str) {
        let style = self.current_style();
        for (index, segment) in text.split('\n').enumerate() {
            if index > 0 {
                self.push_line_break();
            }
            if segment.is_empty() {
                continue;
            }
            self.push_span(Span::styled(segment.to_string(), style));
        }
    }

    fn push_line_break(&mut self) {
        self.flush_current_line();
    }

    fn push_style(&mut self, style: Style) {
        let next = self.current_style().patch(style);
        self.inline_styles.push(next);
    }

    fn current_style(&self) -> Style {
        let mut style = self.heading_style.unwrap_or_default();
        if self.in_code_block {
            style = style.patch(code_style());
        }
        if let Some(inline) = self.inline_styles.last().copied() {
            style = style.patch(inline);
        }
        style
    }

    fn push_spans(&mut self, spans: Vec<Span<'static>>) {
        for span in spans {
            self.push_span(span);
        }
    }

    fn push_span(&mut self, span: Span<'static>) {
        self.ensure_blank_line_between_blocks();
        if self.current_line.is_empty() {
            self.current_line.extend(self.line_prefix());
        }
        self.current_line.push(span);
        if let Some(item) = self.item_stack.last_mut() {
            item.first_line = false;
        }
    }

    fn flush_current_line(&mut self) {
        if self.current_line.is_empty() {
            return;
        }
        self.text
            .lines
            .push(Line::from(std::mem::take(&mut self.current_line)));
    }

    fn line_prefix(&self) -> Vec<Span<'static>> {
        let mut spans = Vec::new();

        for _ in 0..self.blockquote_depth {
            spans.push(Span::styled("> ", quote_style()));
        }

        for (index, item) in self.item_stack.iter().enumerate() {
            let prefix = if index + 1 == self.item_stack.len() && item.first_line {
                item.marker.clone()
            } else {
                item.indent.clone()
            };
            spans.push(Span::raw(prefix));
        }

        if self.in_code_block {
            spans.push(Span::styled(
                "    ".to_string(),
                Style::default().fg(Color::DarkGray),
            ));
        }

        spans
    }
}

fn heading_style(level: usize) -> Style {
    let base = Style::default().add_modifier(Modifier::BOLD);
    if level <= 2 {
        base
    } else {
        base.add_modifier(Modifier::ITALIC)
    }
}

fn code_style() -> Style {
    Style::default().fg(Color::Cyan)
}

fn link_style() -> Style {
    Style::default()
        .fg(Color::Blue)
        .add_modifier(Modifier::UNDERLINED)
}

fn quote_style() -> Style {
    Style::default().fg(Color::Green)
}

#[cfg(test)]
mod tests {
    use super::{render_markdown_text, render_markdown_text_spaced};

    fn flatten_lines(input: &str) -> Vec<String> {
        render_markdown_text(input)
            .lines
            .into_iter()
            .map(|line| line.spans.into_iter().map(|span| span.content).collect())
            .collect()
    }

    fn flatten_spaced_lines(input: &str) -> Vec<String> {
        render_markdown_text_spaced(input)
            .lines
            .into_iter()
            .map(|line| line.spans.into_iter().map(|span| span.content).collect())
            .collect()
    }

    #[test]
    fn renders_inline_styles_and_links() {
        let lines = flatten_lines("**Bold** and `code` with [docs](https://example.com)");
        assert_eq!(lines, vec!["Bold and code with docs (https://example.com)"]);
    }

    #[test]
    fn renders_blockquote_and_lists() {
        let lines = flatten_lines("> quoted\n>\n- one\n- two");
        assert_eq!(lines, vec!["> quoted", "- one", "- two"]);
    }

    #[test]
    fn renders_fenced_code_block() {
        let lines = flatten_lines("```rust\nfn main() {}\n```");
        assert_eq!(lines, vec!["```rust", "    fn main() {}", "```"]);
    }

    #[test]
    fn renders_balanced_fenced_code_block_without_language() {
        let lines = flatten_lines("```\nplain\n```");
        assert_eq!(lines, vec!["```", "    plain", "```"]);
    }

    #[test]
    fn renders_indented_code_block() {
        let lines = flatten_lines("    one\n    two");
        assert_eq!(lines, vec!["    one", "    two"]);
    }

    #[test]
    fn spaced_renderer_keeps_one_blank_line_between_top_level_blocks() {
        let lines = flatten_spaced_lines("First\n\nSecond\n\n- one\n- two");
        assert_eq!(lines, vec!["First", "", "Second", "", "- one", "- two"]);
    }

    #[test]
    fn spaced_renderer_keeps_one_blank_line_between_heading_and_body() {
        let lines = flatten_spaced_lines("### Title\n\nBody");
        assert_eq!(lines, vec!["### Title", "", "Body"]);
    }
}
