use nu_ansi_term::{Color, Style};
use reedline::{Highlighter, StyledText};

use crate::builtins::BuiltinRegistry;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HighlighterPalette {
    command_color: Color,
    builtin_color: Color,
    argument_color: Color,
    flag_color: Color,
    operator_color: Color,
    redirect_color: Color,
}

impl Default for HighlighterPalette {
    fn default() -> Self {
        Self {
            command_color: Color::Cyan,
            builtin_color: Color::Cyan,
            argument_color: Color::Green,
            flag_color: Color::Blue,
            operator_color: Color::Purple,
            redirect_color: Color::Purple,
        }
    }
}

impl HighlighterPalette {
    pub fn new(
        command_color: Color,
        builtin_color: Color,
        argument_color: Color,
        flag_color: Color,
        operator_color: Color,
        redirect_color: Color,
    ) -> Self {
        Self {
            command_color,
            builtin_color,
            argument_color,
            flag_color,
            operator_color,
            redirect_color,
        }
    }
}

#[derive(Debug, Clone, Copy, Default)]
pub struct ShellHighlighter {
    palette: HighlighterPalette,
}

impl ShellHighlighter {
    pub fn new(palette: HighlighterPalette) -> Self {
        Self { palette }
    }
}

impl Highlighter for ShellHighlighter {
    fn highlight(&self, line: &str, _cursor: usize) -> StyledText {
        highlight_line(line, self.palette)
    }
}

fn highlight_line(line: &str, palette: HighlighterPalette) -> StyledText {
    let mut out = StyledText::new();

    let plain = Style::new();
    let op_style = Style::new().fg(palette.operator_color).dimmed();
    let redir_style = Style::new().fg(palette.redirect_color).dimmed();
    let cmd_style = Style::new().fg(palette.command_color).italic();
    let builtin_style = Style::new().fg(palette.builtin_color).bold();
    let arg_style = Style::new().fg(palette.argument_color);
    let flag_style = Style::new().fg(palette.flag_color).bold();

    let mut i = 0usize;
    let mut command_expected = true;

    while i < line.len() {
        if let Some((ch, ch_len)) = next_char(line, i)
            && ch.is_whitespace()
        {
            let start = i;
            i += ch_len;
            while i < line.len() {
                let Some((c, n)) = next_char(line, i) else {
                    break;
                };
                if !c.is_whitespace() {
                    break;
                }
                i += n;
            }
            out.push((plain, line[start..i].to_string()));
            continue;
        }

        if let Some((tok, end, kind)) = read_operator_token(line, i) {
            let style = match kind {
                OpKind::Redirect => redir_style,
                OpKind::Control => op_style,
            };

            out.push((style, tok.to_string()));

            match tok {
                "|" | "&&" | "||" | ";" | "(" | "{" => command_expected = true,
                ")" | "}" => command_expected = false,
                _ => {}
            }

            i = end;
            continue;
        }

        let start = i;
        i = read_word_end(line, i);

        let token = &line[start..i];
        let style = if command_expected {
            let candidate = dequote_and_unescape(token);
            if BuiltinRegistry::defaults().contains(&candidate) {
                builtin_style
            } else {
                cmd_style
            }
        } else if is_flag_token(token) {
            flag_style
        } else {
            arg_style
        };

        out.push((style, token.to_string()));

        if command_expected {
            command_expected = false;
        }
    }

    out
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OpKind {
    Control,
    Redirect,
}

fn read_operator_token(line: &str, i: usize) -> Option<(&str, usize, OpKind)> {
    let (ch, ch_len) = next_char(line, i)?;

    if ch == '&' {
        if peek_char(line, i + ch_len) == Some('&') {
            return Some(("&&", i + ch_len + 1, OpKind::Control));
        }
        return Some(("&", i + ch_len, OpKind::Control));
    }

    if ch == '|' {
        if peek_char(line, i + ch_len) == Some('|') {
            return Some(("||", i + ch_len + 1, OpKind::Control));
        }
        return Some(("|", i + ch_len, OpKind::Control));
    }

    if ch == '>' {
        if peek_char(line, i + ch_len) == Some('>') {
            return Some((">>", i + ch_len + 1, OpKind::Redirect));
        }
        return Some((">", i + ch_len, OpKind::Redirect));
    }

    if ch == '<' {
        if peek_char(line, i + ch_len) == Some('<') {
            return Some(("<<", i + ch_len + 1, OpKind::Redirect));
        }
        return Some(("<", i + ch_len, OpKind::Redirect));
    }

    if ch == ';' {
        return Some((";", i + ch_len, OpKind::Control));
    }

    if ch == '(' {
        return Some(("(", i + ch_len, OpKind::Control));
    }

    if ch == '{' {
        return Some(("{", i + ch_len, OpKind::Control));
    }

    if ch == ')' {
        return Some((")", i + ch_len, OpKind::Control));
    }

    if ch == '}' {
        return Some(("}", i + ch_len, OpKind::Control));
    }

    if ch.is_ascii_digit() {
        let mut j = i;
        while j < line.len() {
            let (c, n) = next_char(line, j)?;
            if !c.is_ascii_digit() {
                break;
            }
            j += n;
        }

        let next = peek_char(line, j)?;
        if next == '>' || next == '<' {
            let op_start = j;
            let (op_ch, op_len) = next_char(line, op_start)?;
            if op_ch == '>' && peek_char(line, op_start + op_len) == Some('>') {
                let end = op_start + op_len + 1;
                return Some((&line[i..end], end, OpKind::Redirect));
            }

            let end = op_start + op_len;
            return Some((&line[i..end], end, OpKind::Redirect));
        }
    }

    None
}

fn read_word_end(line: &str, mut i: usize) -> usize {
    while i < line.len() {
        let (ch, ch_len) = match next_char(line, i) {
            Some(x) => x,
            None => break,
        };

        if ch.is_whitespace() || is_operator_break(ch) {
            break;
        }

        if ch == '\'' {
            i += ch_len;
            while i < line.len() {
                let Some((c, n)) = next_char(line, i) else {
                    break;
                };
                i += n;
                if c == '\'' {
                    break;
                }
            }
            continue;
        }

        if ch == '"' {
            i += ch_len;
            while i < line.len() {
                let Some((c, n)) = next_char(line, i) else {
                    break;
                };
                if c == '\\' {
                    i += n;
                    if i < line.len() {
                        if let Some((_, n2)) = next_char(line, i) {
                            i += n2;
                        } else {
                            break;
                        }
                    }
                    continue;
                }
                i += n;
                if c == '"' {
                    break;
                }
            }
            continue;
        }

        if ch == '\\' {
            i += ch_len;
            if i < line.len() {
                if let Some((_, n2)) = next_char(line, i) {
                    i += n2;
                } else {
                    break;
                }
            }
            continue;
        }

        i += ch_len;
    }

    i
}

fn is_operator_break(ch: char) -> bool {
    matches!(ch, '|' | '&' | ';' | '(' | ')' | '{' | '}' | '<' | '>')
}

fn is_flag_token(token: &str) -> bool {
    let token = token.trim();
    token.starts_with('-') && token != "-" && !token.starts_with("->")
}

fn dequote_and_unescape(token: &str) -> String {
    let mut out = String::new();
    let mut i = 0usize;

    while i < token.len() {
        let Some((ch, n)) = next_char(token, i) else {
            break;
        };

        match ch {
            '\'' => {
                i += n;
                while i < token.len() {
                    let Some((c, n2)) = next_char(token, i) else {
                        break;
                    };
                    i += n2;
                    if c == '\'' {
                        break;
                    }
                    out.push(c);
                }
            }
            '"' => {
                i += n;
                while i < token.len() {
                    let Some((c, n2)) = next_char(token, i) else {
                        break;
                    };
                    if c == '\\' {
                        i += n2;
                        if i < token.len() {
                            if let Some((c3, n3)) = next_char(token, i) {
                                i += n3;
                                out.push(c3);
                            } else {
                                break;
                            }
                        }
                        continue;
                    }
                    i += n2;
                    if c == '"' {
                        break;
                    }
                    out.push(c);
                }
            }
            '\\' => {
                i += n;
                if i < token.len() {
                    if let Some((c2, n2)) = next_char(token, i) {
                        i += n2;
                        out.push(c2);
                    } else {
                        break;
                    }
                }
            }
            _ => {
                out.push(ch);
                i += n;
            }
        }
    }

    out
}

fn next_char(s: &str, i: usize) -> Option<(char, usize)> {
    s.get(i..)?.chars().next().map(|ch| (ch, ch.len_utf8()))
}

fn peek_char(s: &str, i: usize) -> Option<char> {
    s.get(i..)?.chars().next()
}

#[cfg(test)]
mod tests {
    use nu_ansi_term::{Color, Style};

    use super::{HighlighterPalette, highlight_line};

    fn palette() -> HighlighterPalette {
        HighlighterPalette::new(
            Color::Cyan,
            Color::LightCyan,
            Color::Green,
            Color::Blue,
            Color::Purple,
            Color::Yellow,
        )
    }

    #[test]
    fn styles_commands_builtins_args_flags_and_operators() {
        let highlighted = highlight_line("ls -la file | cd", palette());

        assert_eq!(
            highlighted.buffer,
            vec![
                (Style::new().fg(Color::Cyan).italic(), "ls".to_string()),
                (Style::new(), " ".to_string()),
                (Style::new().fg(Color::Blue).bold(), "-la".to_string()),
                (Style::new(), " ".to_string()),
                (Style::new().fg(Color::Green), "file".to_string()),
                (Style::new(), " ".to_string()),
                (Style::new().fg(Color::Purple).dimmed(), "|".to_string()),
                (Style::new(), " ".to_string()),
                (Style::new().fg(Color::LightCyan).bold(), "cd".to_string()),
            ]
        );
    }

    #[test]
    fn styles_redirections_separately_from_arguments() {
        let highlighted = highlight_line("echo hello 2>> output.txt", palette());

        assert_eq!(
            highlighted.buffer,
            vec![
                (Style::new().fg(Color::LightCyan).bold(), "echo".to_string()),
                (Style::new(), " ".to_string()),
                (Style::new().fg(Color::Green), "hello".to_string()),
                (Style::new(), " ".to_string()),
                (Style::new().fg(Color::Yellow).dimmed(), "2>>".to_string()),
                (Style::new(), " ".to_string()),
                (Style::new().fg(Color::Green), "output.txt".to_string()),
            ]
        );
    }

    #[test]
    fn keeps_quoted_and_escaped_arguments_as_single_tokens() {
        let highlighted = highlight_line("printf \"hello world\" some\\ file", palette());

        assert_eq!(
            highlighted.buffer,
            vec![
                (Style::new().fg(Color::Cyan).italic(), "printf".to_string()),
                (Style::new(), " ".to_string()),
                (Style::new().fg(Color::Green), "\"hello world\"".to_string(),),
                (Style::new(), " ".to_string()),
                (Style::new().fg(Color::Green), "some\\ file".to_string()),
            ]
        );
    }
}
