use ratatui::text::Span;

use super::{
    checkpoints::HighlightCheckpointIndex,
    util::{push_span_window, take_while},
};
use crate::viewer::palette::{key_style, plain_style, punctuation_style, string_style};

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct MarkdownFenceState {
    fence: Option<MarkdownFence>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct MarkdownFence {
    marker: char,
    marker_len: usize,
    syntax: super::SyntaxKind,
}

impl MarkdownFenceState {
    pub(crate) fn line_syntax(self, line: &str) -> super::SyntaxKind {
        match (self.fence, fence_line(line)) {
            (Some(fence), Some(line_fence)) if line_fence.closes(fence) => {
                super::SyntaxKind::Markdown
            }
            (Some(fence), _) => fence.syntax,
            (None, _) => super::SyntaxKind::Markdown,
        }
    }

    pub(crate) fn advance(&mut self, line: &str) {
        let Some(line_fence) = fence_line(line) else {
            return;
        };

        match self.fence {
            Some(fence) if line_fence.closes(fence) => self.fence = None,
            Some(_) => {}
            None => {
                self.fence = Some(MarkdownFence {
                    marker: line_fence.marker,
                    marker_len: line_fence.marker_len,
                    syntax: syntax_for_fence_info(&line[line_fence.marker_end..]),
                });
            }
        }
    }
}

#[cfg(test)]
pub(crate) fn markdown_line_syntaxes(
    lines: &[String],
    mut state: MarkdownFenceState,
) -> Vec<super::SyntaxKind> {
    lines
        .iter()
        .map(|line| {
            let syntax = state.line_syntax(line);
            state.advance(line);
            syntax
        })
        .collect()
}

pub(crate) fn highlight_markdown_line_window(
    line: &str,
    window_start: usize,
    window_end: usize,
    _index: Option<&mut HighlightCheckpointIndex>,
) -> Vec<Span<'static>> {
    let mut spans = Vec::new();
    let first = take_while(line, 0, char::is_whitespace);

    if first > 0 {
        push_plain(line, 0, first, window_start, window_end, &mut spans);
    }

    if first >= line.len() {
        return spans;
    }

    if let Some(marker_end) = fence_marker_end(line, first) {
        push_punctuation(
            line,
            first,
            marker_end,
            window_start,
            window_end,
            &mut spans,
        );
        if marker_end < line.len() {
            push_span_window(
                &mut spans,
                line,
                marker_end,
                line.len(),
                string_style(),
                window_start,
                window_end,
            );
        }
        return spans;
    }

    if let Some(marker_end) = heading_marker_end(line, first) {
        push_punctuation(
            line,
            first,
            marker_end,
            window_start,
            window_end,
            &mut spans,
        );
        highlight_inline(
            line,
            marker_end,
            line.len(),
            key_style(),
            window_start,
            window_end,
            &mut spans,
        );
        return spans;
    }

    if line[first..].starts_with('>') {
        push_punctuation(line, first, first + 1, window_start, window_end, &mut spans);
        highlight_inline(
            line,
            first + 1,
            line.len(),
            plain_style(),
            window_start,
            window_end,
            &mut spans,
        );
        return spans;
    }

    if let Some(marker_end) = list_marker_end(line, first) {
        push_punctuation(
            line,
            first,
            marker_end,
            window_start,
            window_end,
            &mut spans,
        );
        highlight_inline(
            line,
            marker_end,
            line.len(),
            plain_style(),
            window_start,
            window_end,
            &mut spans,
        );
        return spans;
    }

    highlight_inline(
        line,
        first,
        line.len(),
        plain_style(),
        window_start,
        window_end,
        &mut spans,
    );
    spans
}

fn highlight_inline(
    line: &str,
    start: usize,
    end: usize,
    base_style: ratatui::style::Style,
    window_start: usize,
    window_end: usize,
    spans: &mut Vec<Span<'static>>,
) {
    let mut cursor = start;
    let mut plain_start = start;

    while cursor < end {
        let Some((token_start, token)) = next_inline_token(line, cursor, end) else {
            break;
        };

        push_span_window(
            spans,
            line,
            plain_start,
            token_start,
            base_style,
            window_start,
            window_end,
        );

        let token_end = match token {
            InlineToken::Code => {
                highlight_inline_code(line, token_start, end, window_start, window_end, spans)
            }
            InlineToken::Link => {
                highlight_link(line, token_start, end, window_start, window_end, spans)
            }
            InlineToken::Emphasis(marker) => highlight_emphasis(
                line,
                token_start,
                end,
                marker,
                window_start,
                window_end,
                spans,
            ),
        };
        cursor = token_end;
        plain_start = cursor;
    }

    push_span_window(
        spans,
        line,
        plain_start,
        end,
        base_style,
        window_start,
        window_end,
    );
}

#[derive(Debug, Clone, Copy)]
enum InlineToken {
    Code,
    Link,
    Emphasis(char),
}

fn next_inline_token(line: &str, start: usize, end: usize) -> Option<(usize, InlineToken)> {
    line[start..end]
        .char_indices()
        .filter_map(|(relative, ch)| {
            let index = start + relative;
            match ch {
                '`' => Some((index, InlineToken::Code)),
                '[' => Some((index, InlineToken::Link)),
                '*' | '_' => Some((index, InlineToken::Emphasis(ch))),
                _ => None,
            }
        })
        .next()
}

fn highlight_inline_code(
    line: &str,
    start: usize,
    end: usize,
    window_start: usize,
    window_end: usize,
    spans: &mut Vec<Span<'static>>,
) -> usize {
    let Some(close) = line[start + 1..end]
        .find('`')
        .map(|relative| start + 1 + relative)
    else {
        push_plain(line, start, start + 1, window_start, window_end, spans);
        return start + 1;
    };
    push_punctuation(line, start, start + 1, window_start, window_end, spans);
    push_span_window(
        spans,
        line,
        start + 1,
        close,
        string_style(),
        window_start,
        window_end,
    );
    push_punctuation(line, close, close + 1, window_start, window_end, spans);
    close + 1
}

fn highlight_link(
    line: &str,
    start: usize,
    end: usize,
    window_start: usize,
    window_end: usize,
    spans: &mut Vec<Span<'static>>,
) -> usize {
    let Some(label_close) = line[start + 1..end]
        .find(']')
        .map(|relative| start + 1 + relative)
    else {
        push_plain(line, start, start + 1, window_start, window_end, spans);
        return start + 1;
    };
    let url_open = label_close + 1;
    if !line[url_open..end].starts_with('(') {
        push_plain(line, start, start + 1, window_start, window_end, spans);
        return start + 1;
    }
    let Some(url_close) = line[url_open + 1..end]
        .find(')')
        .map(|relative| url_open + 1 + relative)
    else {
        push_plain(line, start, start + 1, window_start, window_end, spans);
        return start + 1;
    };

    push_punctuation(line, start, start + 1, window_start, window_end, spans);
    push_span_window(
        spans,
        line,
        start + 1,
        label_close,
        key_style(),
        window_start,
        window_end,
    );
    push_punctuation(
        line,
        label_close,
        url_open + 1,
        window_start,
        window_end,
        spans,
    );
    push_span_window(
        spans,
        line,
        url_open + 1,
        url_close,
        string_style(),
        window_start,
        window_end,
    );
    push_punctuation(
        line,
        url_close,
        url_close + 1,
        window_start,
        window_end,
        spans,
    );
    url_close + 1
}

fn highlight_emphasis(
    line: &str,
    start: usize,
    end: usize,
    marker: char,
    window_start: usize,
    window_end: usize,
    spans: &mut Vec<Span<'static>>,
) -> usize {
    let marker_len = if line[start + marker.len_utf8()..end].starts_with(marker) {
        marker.len_utf8() * 2
    } else {
        marker.len_utf8()
    };
    let close_marker = marker.to_string().repeat(marker_len / marker.len_utf8());
    let content_start = start + marker_len;
    let Some(content_end) = line[content_start..end]
        .find(&close_marker)
        .map(|relative| content_start + relative)
    else {
        push_plain(
            line,
            start,
            start + marker.len_utf8(),
            window_start,
            window_end,
            spans,
        );
        return start + marker.len_utf8();
    };

    push_punctuation(line, start, content_start, window_start, window_end, spans);
    push_span_window(
        spans,
        line,
        content_start,
        content_end,
        key_style(),
        window_start,
        window_end,
    );
    push_punctuation(
        line,
        content_end,
        content_end + marker_len,
        window_start,
        window_end,
        spans,
    );
    content_end + marker_len
}

fn heading_marker_end(line: &str, start: usize) -> Option<usize> {
    let count = line[start..].chars().take_while(|ch| *ch == '#').count();
    if !(1..=6).contains(&count) {
        return None;
    }
    let end = start + count;
    if end == line.len() || line[end..].starts_with(char::is_whitespace) {
        Some(end)
    } else {
        None
    }
}

fn fence_marker_end(line: &str, start: usize) -> Option<usize> {
    fence_line_from_start(line, start).map(|fence| fence.marker_end)
}

#[derive(Debug, Clone, Copy)]
struct MarkdownFenceLine {
    marker: char,
    marker_len: usize,
    marker_end: usize,
    trailing_is_blank: bool,
}

impl MarkdownFenceLine {
    fn closes(self, fence: MarkdownFence) -> bool {
        self.marker == fence.marker && self.marker_len >= fence.marker_len && self.trailing_is_blank
    }
}

fn fence_line(line: &str) -> Option<MarkdownFenceLine> {
    let start = markdown_marker_start(line)?;
    fence_line_from_start(line, start)
}

fn markdown_marker_start(line: &str) -> Option<usize> {
    let mut spaces = 0_usize;
    for (index, ch) in line.char_indices() {
        match ch {
            ' ' if spaces < 3 => spaces += 1,
            ' ' => return None,
            _ => return Some(index),
        }
    }
    None
}

fn fence_line_from_start(line: &str, start: usize) -> Option<MarkdownFenceLine> {
    let marker = line[start..].chars().next()?;
    if marker != '`' && marker != '~' {
        return None;
    }
    let count = line[start..].chars().take_while(|ch| *ch == marker).count();
    (count >= 3).then_some(MarkdownFenceLine {
        marker,
        marker_len: count,
        marker_end: start + marker.len_utf8() * count,
        trailing_is_blank: line[start + marker.len_utf8() * count..].trim().is_empty(),
    })
}

fn syntax_for_fence_info(info: &str) -> super::SyntaxKind {
    let language = normalized_fence_language(info);
    match language.as_deref() {
        Some("json" | "jsonc" | "jsonl" | "ndjson" | "xml" | "html" | "xhtml" | "svg") => {
            super::SyntaxKind::Structured
        }
        Some("toml") => super::SyntaxKind::Toml,
        Some("jinja" | "jinja2" | "j2" | "html+jinja") => super::SyntaxKind::Jinja,
        Some("md" | "markdown") => super::SyntaxKind::Markdown,
        _ => super::SyntaxKind::Plain,
    }
}

fn normalized_fence_language(info: &str) -> Option<String> {
    let raw = info.split_whitespace().next()?;
    let language = raw
        .trim_matches(|ch| matches!(ch, '{' | '}'))
        .trim_start_matches('.')
        .trim_end_matches(',');
    (!language.is_empty()).then(|| language.to_ascii_lowercase())
}

fn list_marker_end(line: &str, start: usize) -> Option<usize> {
    if line[start..].starts_with("- ")
        || line[start..].starts_with("* ")
        || line[start..].starts_with("+ ")
    {
        return Some(start + 2);
    }

    let digit_end = take_while(line, start, |ch| ch.is_ascii_digit());
    if digit_end == start || digit_end >= line.len() {
        return None;
    }
    let marker = line[digit_end..].chars().next()?;
    let marker_end = digit_end + marker.len_utf8();
    if (marker == '.' || marker == ')')
        && line[marker_end..]
            .chars()
            .next()
            .is_some_and(char::is_whitespace)
    {
        Some(marker_end + 1)
    } else {
        None
    }
}

fn push_plain(
    line: &str,
    start: usize,
    end: usize,
    window_start: usize,
    window_end: usize,
    spans: &mut Vec<Span<'static>>,
) {
    push_span_window(
        spans,
        line,
        start,
        end,
        plain_style(),
        window_start,
        window_end,
    );
}

fn push_punctuation(
    line: &str,
    start: usize,
    end: usize,
    window_start: usize,
    window_end: usize,
    spans: &mut Vec<Span<'static>>,
) {
    push_span_window(
        spans,
        line,
        start,
        end,
        punctuation_style(),
        window_start,
        window_end,
    );
}
