use std::borrow::Cow;
use std::path::Path;

use serde_json::Value;
use shell_words::split as shell_split;

use super::streams::strip_ansi_codes;

pub(super) fn parse_command_tokens(payload: &Value) -> Option<Vec<String>> {
    if let Some(array) = payload.get("command").and_then(Value::as_array) {
        let mut tokens = Vec::with_capacity(array.len());
        for value in array {
            if let Some(segment) = value.as_str()
                && !segment.is_empty()
            {
                tokens.push(segment.to_string());
            }
        }
        if !tokens.is_empty() {
            return Some(tokens);
        }
    }

    if let Some(command_str) = payload.get("command").and_then(Value::as_str) {
        if command_str.trim().is_empty() {
            return None;
        }
        if let Ok(segments) = shell_split(command_str)
            && !segments.is_empty()
        {
            return Some(segments);
        }
    }
    None
}

fn normalized_command_name(tokens: &[String]) -> Option<String> {
    tokens
        .first()
        .and_then(|cmd| Path::new(cmd).file_name())
        .and_then(|name| name.to_str())
        .map(|name| name.to_ascii_lowercase())
}

fn command_is_multicol_listing(tokens: &[String]) -> bool {
    normalized_command_name(tokens)
        .map(|name| {
            matches!(
                name.as_str(),
                "ls" | "dir" | "vdir" | "gls" | "colorls" | "exa" | "eza"
            )
        })
        .unwrap_or(false)
}

fn listing_has_single_column_flag(tokens: &[String]) -> bool {
    tokens.iter().any(|arg| {
        matches!(
            arg.as_str(),
            "-1" | "--format=single-column"
                | "--long"
                | "-l"
                | "--tree"
                | "--grid=never"
                | "--no-grid"
        )
    })
}

pub(super) fn preprocess_terminal_stdout<'a>(
    tokens: Option<&[String]>,
    stdout: &'a str,
) -> Cow<'a, str> {
    if stdout.trim().is_empty() {
        return Cow::Borrowed(stdout);
    }

    let filtered_text =
        if stdout.contains("MallocStackLogging:") || stdout.contains("malloc: enabling abort()") {
            let mut filtered = String::with_capacity(stdout.len());
            let mut removed_any = false;

            for line in stdout.lines() {
                if line.contains("MallocStackLogging:")
                    || line.contains("malloc: enabling abort()")
                    || line.contains("can't turn off malloc stack logging")
                {
                    removed_any = true;
                    continue;
                }
                if !filtered.is_empty() {
                    filtered.push('\n');
                }
                filtered.push_str(line);
            }

            removed_any.then_some(filtered)
        } else {
            None
        };

    let normalized = if let Some(filtered) = filtered_text {
        let stripped = strip_ansi_codes(&filtered);
        match stripped {
            Cow::Borrowed(text) => Cow::Owned(normalize_carriage_returns(text).into_owned()),
            Cow::Owned(text) => Cow::Owned(normalize_carriage_returns(&text).into_owned()),
        }
    } else {
        let stripped = strip_ansi_codes(stdout);
        match stripped {
            Cow::Borrowed(text) => normalize_carriage_returns(text),
            Cow::Owned(text) => Cow::Owned(normalize_carriage_returns(&text).into_owned()),
        }
    };

    let should_strip_numbers = tokens
        .map(command_can_emit_rust_diagnostics)
        .unwrap_or(false)
        && looks_like_rust_diagnostic(normalized.as_ref());

    if should_strip_numbers {
        if let Some(stripped) = strip_rust_diagnostic_columns_from_str(normalized.as_ref()) {
            return Cow::Owned(stripped);
        }
        return normalized;
    }

    if let Some(parts) = tokens
        && command_is_multicol_listing(parts)
        && !listing_has_single_column_flag(parts)
    {
        let plain = strip_ansi_codes(normalized.as_ref());
        let mut rows = String::with_capacity(plain.len());
        for entry in plain.split_whitespace() {
            if entry.is_empty() {
                continue;
            }
            rows.push_str(entry);
            rows.push('\n');
        }
        return Cow::Owned(rows);
    }

    normalized
}

fn command_can_emit_rust_diagnostics(tokens: &[String]) -> bool {
    tokens
        .first()
        .map(|cmd| matches!(cmd.as_str(), "cargo" | "rustc"))
        .unwrap_or(false)
}

fn looks_like_rust_diagnostic(text: &str) -> bool {
    if text.is_empty() {
        return false;
    }

    let mut snippet_lines = 0usize;
    let mut pointer_lines = 0usize;
    let mut has_location_marker = false;

    for line in text.lines().take(200) {
        let trimmed = line.trim_start();
        if trimmed.starts_with("--> ") {
            has_location_marker = true;
        }
        if trimmed.starts_with('|') {
            pointer_lines += 1;
        }
        if let Some((prefix, _)) = trimmed.split_once('|') {
            let prefix_trimmed = prefix.trim();
            if !prefix_trimmed.is_empty() && prefix_trimmed.chars().all(|ch| ch.is_ascii_digit()) {
                snippet_lines += 1;
            }
        }
        if snippet_lines >= 1 && pointer_lines >= 1 {
            return true;
        }
        if snippet_lines >= 2 && has_location_marker {
            return true;
        }
    }

    false
}

#[inline]
fn strip_rust_diagnostic_columns_from_str(input: &str) -> Option<String> {
    if input.is_empty() {
        return None;
    }

    let mut output = String::with_capacity(input.len());
    let mut changed = false;

    for chunk in input.split_inclusive('\n') {
        let (line, had_newline) = chunk
            .strip_suffix('\n')
            .map(|line| (line, true))
            .unwrap_or((chunk, false));

        if let Some(prefix_end) = rust_diagnostic_prefix_end(line) {
            changed = true;
            output.push_str(&line[prefix_end..]);
        } else {
            output.push_str(line);
        }

        if had_newline {
            output.push('\n');
        }
    }

    if changed { Some(output) } else { None }
}

fn rust_diagnostic_prefix_end(line: &str) -> Option<usize> {
    let bytes = line.as_bytes();
    let len = bytes.len();

    let mut idx = 0usize;
    while idx < len && bytes[idx].is_ascii_whitespace() {
        idx += 1;
    }
    if idx >= len {
        return None;
    }

    if bytes[idx].is_ascii_digit() {
        let mut cursor = idx;
        while cursor < len && bytes[cursor].is_ascii_digit() {
            cursor += 1;
        }
        if cursor == idx {
            return None;
        }
        while cursor < len && bytes[cursor].is_ascii_whitespace() {
            cursor += 1;
        }
        if cursor < len && bytes[cursor] == b'|' {
            cursor += 1;
            if cursor < len && bytes[cursor] == b' ' {
                cursor += 1;
            }
            return Some(cursor);
        }
        return None;
    }

    if bytes[idx] == b'|' {
        let mut cursor = idx + 1;
        if cursor < len && bytes[cursor] == b' ' {
            cursor += 1;
        }
        return Some(cursor);
    }

    None
}

#[inline]
fn normalize_carriage_returns(input: &str) -> Cow<'_, str> {
    if !input.contains('\r') {
        return Cow::Borrowed(input);
    }

    let mut output = String::with_capacity(input.len());
    let mut current_line = String::new();
    let mut chars = input.chars().peekable();

    while let Some(ch) = chars.next() {
        match ch {
            '\r' => {
                if matches!(chars.peek(), Some('\n')) {
                    chars.next();
                    output.push_str(&current_line);
                    output.push('\n');
                    current_line.clear();
                } else {
                    current_line.clear();
                }
            }
            '\n' => {
                output.push_str(&current_line);
                output.push('\n');
                current_line.clear();
            }
            _ => current_line.push(ch),
        }
    }

    if !current_line.is_empty() {
        output.push_str(&current_line);
    }

    Cow::Owned(output)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn preprocess_strips_rust_line_numbers_for_cargo_output() {
        let tokens = vec!["cargo".to_string(), "check".to_string()];
        let input = "\
warning: this is a warning
  --> src/main.rs:12:5
   |
12 |     let x = 5;
   |     ----- value defined here
   |
   = note: additional context
";
        let processed = preprocess_terminal_stdout(Some(&tokens), input);
        let output = processed.to_string();
        assert!(!output.contains("12 |"));
        assert!(output.contains("let x = 5;"));
        assert!(output.contains("----- value defined here"));
    }

    #[test]
    fn detects_rust_diagnostic_shape() {
        let sample = "\
warning: something
  --> src/lib.rs:7:9
   |
 7 |     println!(\"hi\");
   |     ^^^^^^^^^^^^^^^
";
        assert!(
            looks_like_rust_diagnostic(sample),
            "should detect diagnostic structure"
        );
    }

    #[test]
    fn rust_prefix_end_handles_pointer_lines() {
        let line = "   |         ^ expected struct `Foo`, found enum `Bar`";
        let idx = rust_diagnostic_prefix_end(line).expect("prefix");
        assert_eq!(
            &line[idx..],
            "        ^ expected struct `Foo`, found enum `Bar`"
        );
    }

    #[test]
    fn strip_rust_columns_returns_none_when_unmodified() {
        let sample = "no diagnostics here";
        assert!(strip_rust_diagnostic_columns_from_str(sample).is_none());
    }

    #[test]
    fn detects_nested_json_command_output() {
        let nested_json = r#"{"stdout": "test output", "stderr": "test error", "returncode": 0}"#;
        if let Ok(parsed) = serde_json::from_str::<Value>(nested_json) {
            assert!(parsed.get("stdout").is_some());
            assert!(parsed.get("stderr").is_some());
            assert!(parsed.get("returncode").is_some());
        } else {
            panic!("Failed to parse nested JSON");
        }
    }

    #[test]
    fn parse_command_tokens_handles_array_format() {
        let payload = serde_json::json!({
            "command": ["cargo", "fmt"]
        });
        let tokens = parse_command_tokens(&payload);
        assert_eq!(tokens, Some(vec!["cargo".to_string(), "fmt".to_string()]));
    }

    #[test]
    fn parse_command_tokens_handles_string_format() {
        let payload = serde_json::json!({
            "command": "cargo fmt"
        });
        let tokens = parse_command_tokens(&payload);
        assert_eq!(tokens, Some(vec!["cargo".to_string(), "fmt".to_string()]));
    }

    #[test]
    fn parse_command_tokens_returns_none_for_empty_array() {
        let payload = serde_json::json!({
            "command": []
        });
        let tokens = parse_command_tokens(&payload);
        assert_eq!(tokens, None);
    }
}
