use ratatui::{
    style::{Color, Style},
    text::{Line, Span},
};
use syntect::easy::HighlightLines;
use syntect::highlighting::ThemeSet;
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;
use std::sync::LazyLock;

use super::theme::THEME;
use unicode_width::UnicodeWidthStr;

/// Clamp a `Line` to fit within `width` terminal columns.
/// Walks spans left-to-right, truncating/dropping once the budget is exceeded.
/// Avoids rendering artifacts from lines that exceed terminal width.
pub(crate) fn clamp_line(line: Line<'static>, width: usize) -> Line<'static> {
    if width == 0 {
        return Line::from("");
    }
    let mut remaining = width;
    let mut clamped: Vec<Span<'static>> = Vec::new();
    for span in line.spans {
        let span_width = UnicodeWidthStr::width(span.content.as_ref());
        if remaining == 0 {
            break;
        }
        if span_width <= remaining {
            remaining -= span_width;
            clamped.push(span);
        } else {
            // Truncate this span by display width. Multi-column chars must be
            // counted by terminal cells, not scalar values, or a supposedly
            // clamped line can still overrun the viewport and leave artifacts.
            let mut used = 0;
            let mut truncated = String::new();
            for ch in span.content.chars() {
                let ch_width = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
                if used + ch_width > remaining {
                    break;
                }
                used += ch_width;
                truncated.push(ch);
            }
            clamped.push(Span::styled(truncated, span.style));
            remaining = 0;
        }
    }
    Line::from(clamped)
}

static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(SyntaxSet::load_defaults_newlines);
static THEME_SET: LazyLock<ThemeSet> = LazyLock::new(ThemeSet::load_defaults);

/// Highlight a code block using syntect
pub(crate) fn highlight_code_block(code: &str, lang: &str, prefix: &str) -> Vec<Line<'static>> {
    let ss = &*SYNTAX_SET;
    let ts = &*THEME_SET;
    let theme = &ts.themes["base16-ocean.dark"];

    let syntax = ss.find_syntax_by_token(lang)
        .unwrap_or_else(|| ss.find_syntax_plain_text());

    let mut h = HighlightLines::new(syntax, theme);
    let mut lines: Vec<Line> = Vec::new();

    for line in LinesWithEndings::from(code) {
        let ranges = h.highlight_line(line, ss).unwrap_or_default();
        let mut spans: Vec<Span> = Vec::new();
        spans.push(Span::styled(
            format!("{}  \u{2502} ", prefix),
            Style::default().fg(THEME.load().muted),
        ));
        for (style, text) in ranges {
            let fg = Color::Rgb(style.foreground.r, style.foreground.g, style.foreground.b);
            let content = text.trim_end_matches('\n').to_string();
            if !content.is_empty() {
                spans.push(Span::styled(content, Style::default().fg(fg).bg(THEME.load().code_bg)));
            }
        }
        lines.push(Line::from(spans));
    }
    lines
}

/// Try to syntax-highlight a single tool output line.
/// Highlight code lines for tool params (write content, edit old/new) — clean style matching read output
pub(crate) fn highlight_tool_code(lines: &[&str], ext: &str, margin: &str, marker: &str, marker_color: Color) -> Vec<Line<'static>> {
    let ss = &*SYNTAX_SET;
    let ts = &*THEME_SET;
    let theme = &ts.themes["base16-ocean.dark"];

    let syntax = ss.find_syntax_by_extension(ext)
        .unwrap_or_else(|| ss.find_syntax_plain_text());

    let mut h = HighlightLines::new(syntax, theme);
    let mut result = Vec::new();

    // Determine tint based on marker (red for old, green for new, neutral for content)
    let tint = match marker {
        "−" => (40i16, -60i16, -60i16),     // shift toward red: boost red, crush green/blue
        "+" => (-15i16, 10i16, -15i16),      // shift toward green: reduce red/blue
        _ => (0i16, 0i16, 0i16),             // neutral for write content
    };

    for (i, line) in lines.iter().enumerate() {
        let code_with_nl = format!("{}\n", line);
        let ranges = h.highlight_line(&code_with_nl, ss).unwrap_or_default();

        let mut spans = vec![
            Span::styled(
                format!("{}    {:>3} {} ", margin, i + 1, marker),
                Style::default().fg(marker_color),
            ),
        ];
        for (sty, text) in ranges {
            let r = (sty.foreground.r as i16 + tint.0).clamp(0, 255) as u8;
            let g = (sty.foreground.g as i16 + tint.1).clamp(0, 255) as u8;
            let b = (sty.foreground.b as i16 + tint.2).clamp(0, 255) as u8;
            let fg = Color::Rgb(r, g, b);
            let content = text.trim_end_matches('\n').to_string();
            if !content.is_empty() {
                spans.push(Span::styled(content, Style::default().fg(fg)));
            }
        }
        result.push(Line::from(spans));
    }

    result
}

/// Highlight bash tool output with blue tint and pattern detection
pub(crate) fn highlight_bash_output(lines: &[&str], margin: &str) -> Vec<Line<'static>> {
    let mut result = Vec::new();

    for raw_line in lines {
        // Replace tabs with spaces — ratatui doesn't handle \t correctly and causes overlap artifacts
        let line = raw_line.replace('\t', "    ");
        let trimmed = line.trim();
        let mut spans = vec![
            Span::styled(format!("{}       ", margin), Style::default().fg(THEME.load().tool_result_color)),
        ];

        if trimmed.is_empty() {
            result.push(Line::from(spans));
            continue;
        }

        // Detect patterns and colorize
        let lc = trimmed.to_ascii_lowercase();
        if lc.starts_with("error") || lc.starts_with("fatal") {
            // Errors → red
            spans.push(Span::styled(line.to_string(), Style::default().fg(THEME.load().error_color)));
        } else if lc.starts_with("warning") || lc.starts_with("warn") {
            // Warnings → yellow
            spans.push(Span::styled(line.to_string(), Style::default().fg(THEME.load().warning_color)));
        } else if trimmed.starts_with("✅") || trimmed.starts_with("ok") || trimmed.starts_with("OK")
            || trimmed.starts_with("done") || trimmed.starts_with("Done") || trimmed.starts_with("success") {
            // Success → green with blue tint
            spans.push(Span::styled(line.to_string(), Style::default().fg(THEME.load().tool_result_ok)));
        } else {
            // Default: blue-tinted with smart coloring
            let mut remaining = line.as_str();
            while !remaining.is_empty() {
                // Find paths (contain /)
                if let Some(slash_pos) = remaining.find('/') {
                    // Output text before the path
                    if slash_pos > 0 {
                        let before = &remaining[..slash_pos];
                        // Find the start of the path (walk back to whitespace)
                        let path_start = before.rfind(|c: char| c.is_whitespace())
                            .map(|p| p + before[p..].chars().next().unwrap().len_utf8())
                            .unwrap_or(0);
                        if path_start > 0 {
                            spans.push(Span::styled(
                                remaining[..path_start].to_string(),
                                Style::default().fg(THEME.load().tool_result_color),
                            ));
                        }
                        // Path portion
                        let after_slash = &remaining[path_start..];
                        let path_end = after_slash.find(|c: char| c.is_whitespace() || c == ':' || c == ')' || c == ']')
                            .unwrap_or(after_slash.len());
                        // Guard: if path_end is 0, we'd loop forever — consume at least 1 char
                        if path_end == 0 {
                            let first_char_len = after_slash.chars().next().map(|c| c.len_utf8()).unwrap_or(1);
                            spans.push(Span::styled(
                                after_slash[..first_char_len].to_string(),
                                Style::default().fg(THEME.load().tool_result_color),
                            ));
                            remaining = &after_slash[first_char_len..];
                        } else {
                            spans.push(Span::styled(
                                after_slash[..path_end].to_string(),
                                Style::default().fg(THEME.load().tool_label),
                            ));
                            remaining = &after_slash[path_end..];
                        }
                    } else {
                        let path_end = remaining.find(|c: char| c.is_whitespace() || c == ':' || c == ')' || c == ']')
                            .unwrap_or(remaining.len());
                        // Guard: if path_end is 0, consume at least 1 char to avoid infinite loop
                        if path_end == 0 {
                            let first_char_len = remaining.chars().next().map(|c| c.len_utf8()).unwrap_or(1);
                            spans.push(Span::styled(
                                remaining[..first_char_len].to_string(),
                                Style::default().fg(THEME.load().tool_result_color),
                            ));
                            remaining = &remaining[first_char_len..];
                        } else {
                            spans.push(Span::styled(
                                remaining[..path_end].to_string(),
                                Style::default().fg(THEME.load().tool_label),
                            ));
                            remaining = &remaining[path_end..];
                        }
                    }
                } else {
                    // No more paths — output the rest with blue tint
                    spans.push(Span::styled(
                        remaining.to_string(),
                        Style::default().fg(THEME.load().tool_result_color),
                    ));
                    break;
                }
            }
        }

        result.push(Line::from(spans));
    }

    result
}

/// Try to highlight a grep output line (filepath:linenum:content)
pub(crate) fn try_highlight_grep_line(line: &str, margin: &str) -> Option<Vec<Span<'static>>> {
    // Grep format: filepath:linenum:content  or  filepath-linenum-content (context)
    // Also: filepath:linenum:  (empty match line)
    let first_colon = line.find(':')?;
    let filepath = &line[..first_colon];

    // Filepath should look like a path (contain / or .)
    if !filepath.contains('/') && !filepath.contains('.') {
        return None;
    }

    let rest = &line[first_colon + 1..];
    let second_sep = rest.find([':', '-'])?;
    let linenum = &rest[..second_sep];

    // Line number should be numeric
    if !linenum.chars().all(|c| c.is_ascii_digit()) || linenum.is_empty() {
        return None;
    }

    let sep_char = rest.as_bytes()[second_sep] as char;
    let content = if second_sep + 1 < rest.len() { &rest[second_sep + 1..] } else { "" };

    let is_context = sep_char == '-';

    Some(vec![
        Span::styled(format!("{}       ", margin), Style::default().fg(THEME.load().tool_result_color)),
        Span::styled(filepath.to_string(), Style::default().fg(THEME.load().tool_label)),
        Span::styled(":", Style::default().fg(THEME.load().muted)),
        Span::styled(linenum.to_string(), Style::default().fg(THEME.load().list_bullet_color)),
        Span::styled(format!("{}", sep_char), Style::default().fg(THEME.load().muted)),
        Span::styled(
            content.to_string(),
            if is_context {
                Style::default().fg(THEME.load().muted)
            } else {
                Style::default().fg(THEME.load().tool_result_color)
            },
        ),
    ])
}

/// Check if tool output looks like read tool output (line-numbered with tabs)
pub(crate) fn is_read_tool_output(lines: &[&str]) -> bool {
    if lines.is_empty() { return false; }
    // Check first few non-empty lines for "number\tcontent" pattern
    let mut checked = 0;
    let mut matches = 0;
    for line in lines.iter().take(10) {
        if line.trim().is_empty() { continue; }
        checked += 1;
        if let Some(tab_idx) = line.find('\t') {
            if line[..tab_idx].trim().chars().all(|c| c.is_ascii_digit()) && !line[..tab_idx].trim().is_empty() {
                matches += 1;
            }
        }
    }
    checked > 0 && matches * 2 >= checked // At least half the lines match
}

/// Highlight read tool output as a code block using syntect
pub(crate) fn highlight_read_output(lines: &[&str], ext: &str, margin: &str) -> Option<Vec<Line<'static>>> {
    let ss = &*SYNTAX_SET;
    let ts = &*THEME_SET;
    let theme = &ts.themes["base16-ocean.dark"];

    let syntax = if !ext.is_empty() {
        ss.find_syntax_by_extension(ext).unwrap_or_else(|| ss.find_syntax_plain_text())
    } else {
        ss.find_syntax_plain_text()
    };

    // If plain text, don't bother highlighting
    if syntax.name == "Plain Text" && ext.is_empty() {
        return None;
    }

    let mut h = HighlightLines::new(syntax, theme);
    let mut result = Vec::new();

    for line in lines {
        let (line_num, code) = if let Some(tab_idx) = line.find('\t') {
            let num = line[..tab_idx].trim();
            let code = &line[tab_idx + 1..];
            (num.to_string(), code)
        } else {
            (String::new(), *line)
        };

        let code_with_nl = format!("{}\n", code);
        let ranges = h.highlight_line(&code_with_nl, ss).unwrap_or_default();

        let mut spans = vec![
            Span::styled(
                format!("{}     {:>4} \u{2502} ", margin, line_num),
                Style::default().fg(THEME.load().muted),
            ),
        ];
        for (sty, text) in ranges {
            // Slight cool tint for read output to differentiate from edit
            let r = (sty.foreground.r as i16 - 5).clamp(0, 255) as u8;
            let g = (sty.foreground.g as i16).clamp(0, 255) as u8;
            let b = (sty.foreground.b as i16 + 10).clamp(0, 255) as u8;
            let fg = Color::Rgb(r, g, b);
            let content = text.trim_end_matches('\n').to_string();
            if !content.is_empty() {
                spans.push(Span::styled(content, Style::default().fg(fg)));
            }
        }
        result.push(Line::from(spans));
    }

    Some(result)
}


#[cfg(test)]
mod tests {
    use super::*;
    use ratatui::{style::Style, text::{Line, Span}};
    use unicode_width::UnicodeWidthStr;

    #[test]
    fn clamp_line_truncates_by_display_width_not_char_count() {
        let line = Line::from(vec![Span::styled("ab漢字c", Style::default())]);

        let clamped = clamp_line(line, 5);
        let rendered: String = clamped
            .spans
            .iter()
            .map(|span| span.content.as_ref())
            .collect();

        assert_eq!(rendered, "ab漢");
        assert_eq!(UnicodeWidthStr::width(rendered.as_str()), 4);
    }

    #[test]
    fn clamp_line_preserves_spans_within_display_width_budget() {
        let line = Line::from(vec![
            Span::styled("ab", Style::default()),
            Span::styled("漢", Style::default()),
            Span::styled("cd", Style::default()),
        ]);

        let clamped = clamp_line(line, 4);
        let rendered: String = clamped
            .spans
            .iter()
            .map(|span| span.content.as_ref())
            .collect();

        assert_eq!(rendered, "ab漢");
        assert_eq!(UnicodeWidthStr::width(rendered.as_str()), 4);
    }

    #[test]
    fn highlight_bash_output_handles_nbsp_in_path_context() {
        // Regression: \u{a0} (NBSP) is whitespace but 2 bytes in UTF-8.
        // rfind(whitespace) + 1 landed mid-char → panic on slice.
        // This is the exact crash string from S182.
        let lines = vec![") \\| [395 comments](https://news.ycombinator.com/item?id=47961319) |"];
        let result = highlight_bash_output(&lines, "");
        assert!(!result.is_empty());
    }

    #[test]
    fn highlight_bash_output_handles_nbsp_before_slash() {
        // NBSP (\u{00a0}) right before a slash — rfind finds NBSP as whitespace,
        // +1 byte would land inside the 2-byte char. Must use char boundary.
        let line_with_nbsp = "text\u{00a0}/some/path here";
        let lines = vec![line_with_nbsp];
        let result = highlight_bash_output(&lines, "");
        assert!(!result.is_empty());
    }

    #[test]
    fn highlight_bash_output_handles_multibyte_at_path_boundary() {
        // Multi-byte char (é = 2 bytes) near a slash
        let lines = vec!["café/menu"];
        let result = highlight_bash_output(&lines, "");
        assert!(!result.is_empty());
    }
}
