use crate::cmds::check::file::FileResult;
use crate::errors::KamError;
use regex::Regex;
use serde_json;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::process::Command;

use tree_sitter;
use tree_sitter::Parser;
use tree_sitter_bash;

static SETPROP_RE: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
    Regex::new(r"\bsetprop\b").unwrap_or_else(|e| panic!("setprop regex failed to compile: {e}"))
});

fn command_installed(cmd: &str) -> bool {
    Command::new(cmd).arg("--version").output().is_ok()
}

// 检查shell脚本
// 如果有shellcheck就用它（更准确），否则用自定义的Rust检查（简单但够用）
// 另外，对所有脚本统一执行基于文件名的特判和基于 AST 的高危命令检测（补充 shellcheck 的检测）
///
/// # Errors
///
/// Returns `KamError` when file I/O (read/write), invocation of external tools
/// (e.g., `shellcheck`, `shfmt`), or parser/analysis operations fail.
pub fn check_sh(path: &Path, do_fix: bool) -> Result<FileResult, KamError> {
    // 首先运行已有的检查实现（shellcheck 优先，其次自定义）
    let mut fr = if command_installed("shellcheck") {
        match check_sh_with_tool(path, do_fix) {
            Ok(f) => f,
            Err(e) => {
                // 如果 shellcheck 执行失败，生成一个基本结果并记录警告
                FileResult {
                    path: path.to_string_lossy().to_string(),
                    kind: "sh".to_string(),
                    valid: true,
                    errors: Vec::new(),
                    warnings: vec![format!("shellcheck execution failed: {e}")],
                    fixed: false,
                }
            }
        }
    } else {
        check_sh_custom(path, do_fix)?
    };

    // 读取文件内容以便做额外检查（文件名相关检查 / AST 检查 / 换行格式检查等）
    let s = fs::read_to_string(path)?;

    // 基于文件名的特殊规则（例如 install.sh / post-fs-data.sh 的提示）
    apply_sh_filename_checks(path, &s, &mut fr);

    // AST-aware checks for command substitutions, arithmetic expansions, and backticks.
    // Run these even if shellcheck is installed so we catch unbalanced constructs
    // that shellcheck may miss or when shellcheck isn't available.
    for err in detect_unbalanced_shell_constructs(&s) {
        if !fr.errors.contains(&err) {
            fr.valid = false;
            fr.errors.push(err);
        }
    }

    // 基于树形语法树的高危指令检测（如果 parser 可用）
    let mut parser = Parser::new();
    let language = tree_sitter::Language::new(tree_sitter_bash::LANGUAGE);
    if parser.set_language(&language).is_ok()
        && let Some(tree) = parser.parse(&s, None)
    {
        detect_dangerous_commands(tree.root_node(), &s, &mut fr, path);
    }

    // 通用换行规范：确保使用 UNIX LF（如果要求修复，则写回）
    if s.contains('\r') {
        fr.warnings
            .push("CR/CRLF line endings detected; use UNIX LF line endings".to_string());
        if do_fix {
            let normalized = s.replace("\r\n", "\n").replace('\r', "\n");
            fs::OpenOptions::new()
                .write(true)
                .truncate(true)
                .open(path)?
                .write_all(normalized.as_bytes())?;
            fr.fixed = true;
        }
    }

    Ok(fr)
}

// 用shellcheck工具检查（如果可用）
fn check_sh_with_tool(path: &Path, do_fix: bool) -> Result<FileResult, KamError> {
    let s = fs::read_to_string(path)?;
    let mut fr = FileResult {
        path: path.to_string_lossy().to_string(),
        kind: "sh".to_string(),
        valid: true,
        errors: Vec::new(),
        warnings: Vec::new(),
        fixed: false,
    };
    // 运行shellcheck，输出JSON格式（方便解析）
    match Command::new("shellcheck")
        .arg("-x")
        .arg("--format=json")
        .arg(path)
        .output()
    {
        Ok(output) => {
            // Prefer parsing JSON from stdout; handle both legacy {"comments":[...]} and modern array form.
            if !output.stdout.is_empty() {
                match serde_json::from_slice::<serde_json::Value>(&output.stdout) {
                    Ok(v) => {
                        // helper to extract one issue entry into our FileResult
                        let mut push_issue = |c: &serde_json::Value| {
                            let file = c.get("file").and_then(|x| x.as_str()).unwrap_or("");
                            let line = c
                                .get("line")
                                .and_then(serde_json::Value::as_u64)
                                .unwrap_or(0);
                            let column = c
                                .get("column")
                                .and_then(serde_json::Value::as_u64)
                                .unwrap_or(0);
                            let level = c.get("level").and_then(|x| x.as_str()).unwrap_or("");
                            let code = c
                                .get("code")
                                .and_then(serde_json::Value::as_u64)
                                .unwrap_or(0);
                            let message = c.get("message").and_then(|x| x.as_str()).unwrap_or("");
                            let msg =
                                format!("shellcheck [{code}] {file}:{line}:{column} {message}");
                            if level.eq_ignore_ascii_case("error") {
                                fr.valid = false;
                                fr.errors.push(msg);
                            } else {
                                fr.warnings.push(msg);
                            }
                        };

                        // Legacy format: { "comments": [...] }
                        if let Some(comments) = v.get("comments").and_then(|x| x.as_array()) {
                            for c in comments {
                                push_issue(c);
                            }
                        // Modern format: an array of issue objects
                        } else if let Some(arr) = v.as_array() {
                            for c in arr {
                                push_issue(c);
                            }
                        } else {
                            fr.warnings.push(format!(
                                "shellcheck returned unexpected JSON structure: {}",
                                String::from_utf8_lossy(&output.stdout)
                            ));
                        }
                    }
                    Err(e) => {
                        fr.warnings
                            .push(format!("Failed to parse shellcheck JSON: {e}"));
                    }
                }
            } else if !output.stderr.is_empty() {
                let stde = String::from_utf8_lossy(&output.stderr).trim().to_string();
                if !stde.is_empty() {
                    fr.warnings.push(format!("shellcheck stderr: {stde}"));
                }
            }
        }
        Err(e) => {
            // shellcheck运行失败，加个警告，包含错误信息以便排查
            fr.warnings.push(format!("Failed to run shellcheck: {e}"));
        }
    }
    // 如果要求修复且shfmt可用，就用shfmt格式化
    if do_fix && command_installed("shfmt") {
        let before = s;
        let status_res = Command::new("shfmt").arg("-w").arg(path).status();
        if matches!(status_res, Ok(s) if s.success()) {
            // 重新读取文件，比较是否有变化
            let after = fs::read_to_string(path)?;
            if after != before {
                fr.fixed = true;
            }
        }
    }
    Ok(fr)
}

/// Apply file-name based special checks and warnings for common hook scripts and other special files.
/// Examples:
/// - If file is `install.sh`: suggest renaming to `customize.sh`.
/// - If file is `post-fs-data.sh` and contains `setprop`: warning recommending `resetprop` usage.
fn apply_sh_filename_checks(path: &Path, content: &str, fr: &mut FileResult) {
    if let Some(fname) = path.file_name().and_then(|n| n.to_str()) {
        match fname {
            "install.sh" => {
                fr.warnings.push(
                    "Found 'install.sh' - consider renaming to 'customize.sh' to avoid ambiguous behavior"
                        .to_string(),
                );
            }
            "post-fs-data.sh" if SETPROP_RE.is_match(content) => {
                fr.warnings.push(
                    "WARNING: Using setprop will deadlock the boot process! Please use resetprop -n <prop_name> <prop_value> instead."
                        .to_string(),
                );
            }
            "post-mount.sh" => {
                fr.warnings
                    .push("This script will be executed in post-mount stage".to_string());
            }
            "service.sh" => {
                fr.warnings
                    .push("This script will be executed as a late_start service".to_string());
            }
            "boot-completed.sh" => {
                fr.warnings
                    .push("This script will be executed on boot completed".to_string());
            }
            "uninstall.sh" => {
                fr.warnings.push(
                    "This script will be executed when KernelSU removes your module".to_string(),
                );
            }
            "action.sh" => {
                fr.warnings
                    .push("This script will be executed when user clicks the Action button in KernelSU app".to_string());
            }
            "system.prop" => {
                fr.warnings.push(
                    "Properties in this file will be loaded as system properties by resetprop"
                        .to_string(),
                );
            }
            "sepolicy.rule" => {
                fr.warnings.push(
                    "SEPolicy rules may affect device security; review carefully".to_string(),
                );
            }
            _ => {}
        }
    }
}

/// Walk Tree-sitter AST and detect suspicious / high-risk commands.
///
/// Basic heuristics implemented:
/// - rm -rf ... on absolute paths -> error
/// - dd to /dev/* -> error
/// - mkfs* -> error
/// - chmod 777 -R -> warning
/// - chown on /system or /data -> warning
/// - reboot/shutdown/poweroff -> warning
/// - piped download to shell (curl|wget ... | sh) -> warning
/// - setprop usage (generic warning unless already handled specially)
#[allow(clippy::too_many_lines)] // TODO: split into smaller helpers to reduce complexity
fn detect_dangerous_commands(node: tree_sitter::Node, src: &str, fr: &mut FileResult, path: &Path) {
    // Skip comment nodes (do not analyze commented-out code)
    if node.kind() == "comment" {
        return;
    }

    // Detect piped download-to-shell patterns only within command-like nodes (command/pipeline),
    // to avoid false positives coming from comments or other non-command nodes.
    if (node.kind() == "pipeline" || node.kind() == "command")
        && let Ok(node_text_all) = node.utf8_text(src.as_bytes())
    {
        // If this file is the canonical 'install.sh' script, deliberately skip piped-download
        // detection because installers commonly use `curl | sh` patterns and we don't want to flag them.
        if let Some(fname) = path.file_name().and_then(|n| n.to_str()) {
            if fname.eq_ignore_ascii_case("install.sh") {
                // Skip piped-download warning for install.sh
            } else {
                let node_text_low = node_text_all.to_lowercase();
                // Simple substring-based detection: look for curl/wget + pipe
                if (node_text_low.contains("curl") || node_text_low.contains("wget"))
                    && node_text_low.contains('|')
                {
                    // Prefer to show a concise snippet (the line containing the pipeline) instead of dumping the whole node
                    let mut snippet: Option<String> = None;
                    for line in node_text_all.lines() {
                        let ll = line.to_lowercase();
                        if (ll.contains("curl") || ll.contains("wget") || ll.contains('|'))
                            && (ll.contains(" sh")
                                || ll.contains('|')
                                || ll.contains(" bash")
                                || ll.contains("| bash")
                                || ll.contains("sh ")
                                || ll.contains("bash "))
                        {
                            snippet = Some(line.trim().to_string());
                            break;
                        }
                    }
                    // Fallback: extract around the pipe character
                    if snippet.is_none() {
                        if let Some(pipe_pos) = node_text_all.find('|') {
                            let start = node_text_all[..pipe_pos].rfind('\n').map_or(0, |i| i + 1);
                            let end = node_text_all[pipe_pos..]
                                .find('\n')
                                .map_or(node_text_all.len(), |i| pipe_pos + i);
                            snippet = Some(node_text_all[start..end].trim().to_string());
                        } else {
                            snippet = Some(node_text_all.trim().to_string());
                        }
                    }
                    let snippet = snippet.unwrap_or_else(|| "<snippet unavailable>".to_string());
                    let msg = format!("Piped shell install detected (download | shell): {snippet}");
                    if !fr
                        .warnings
                        .iter()
                        .any(|w| w.contains("Piped shell install detected"))
                    {
                        fr.warnings.push(msg);
                    }
                }
            }
        } else {
            // No filename available; fall back to content-based detection as before
            let node_text_low = node_text_all.to_lowercase();
            if (node_text_low.contains("curl") || node_text_low.contains("wget"))
                && node_text_low.contains('|')
            {
                let mut snippet: Option<String> = None;
                for line in node_text_all.lines() {
                    let ll = line.to_lowercase();
                    if (ll.contains("curl") || ll.contains("wget") || ll.contains('|'))
                        && (ll.contains(" sh")
                            || ll.contains("| sh")
                            || ll.contains(" bash")
                            || ll.contains("| bash")
                            || ll.contains("sh ")
                            || ll.contains("bash "))
                    {
                        snippet = Some(line.trim().to_string());
                        break;
                    }
                }
                if snippet.is_none() {
                    if let Some(pipe_pos) = node_text_all.find('|') {
                        let start = node_text_all[..pipe_pos].rfind('\n').map_or(0, |i| i + 1);
                        let end = node_text_all[pipe_pos..]
                            .find('\n')
                            .map_or(node_text_all.len(), |i| pipe_pos + i);
                        snippet = Some(node_text_all[start..end].trim().to_string());
                    } else {
                        snippet = Some(node_text_all.trim().to_string());
                    }
                }
                let snippet = snippet.unwrap_or_else(|| "<snippet unavailable>".to_string());
                let msg = format!("Piped shell install detected (download | shell): {snippet}");
                if !fr
                    .warnings
                    .iter()
                    .any(|w| w.contains("Piped shell install detected"))
                {
                    fr.warnings.push(msg);
                }
            }
        }
    }

    // If this node is a command, try to extract the command name & full text and apply heuristics
    if node.kind() == "command"
        && node.child(0).is_some()
        && let Ok(node_text) = node.utf8_text(src.as_bytes())
    {
        let txt = node_text.trim();
        let mut iter = txt.split_whitespace();
        if let Some(cmd) = iter.next() {
            let cmd_l = cmd.to_lowercase();
            let node_text_low = node_text.to_lowercase();

            match cmd_l.as_str() {
                "rm" if node_text_low.contains("-rf")
                    || (node_text_low.contains("-r") && node_text_low.contains("-f")) =>
                {
                    // More precise check: examine positional arguments for absolute literal paths (skip variables)
                    let args: Vec<&str> = txt.split_whitespace().skip(1).collect();
                    let mut dangerous_abs = false;
                    for a in args {
                        let a_trim = a.trim_matches('"').trim_matches('\'');
                        // Skip variable references and command substitutions
                        if a_trim.starts_with('/') && !a_trim.contains('$') && !a_trim.contains('`')
                        {
                            dangerous_abs = true;
                            break;
                        }
                    }
                    if dangerous_abs {
                        fr.valid = false;
                        let msg = format!(
                            "Dangerous rm -rf usage detected (possible removal of root files): {node_text}"
                        );
                        if !fr.errors.contains(&msg) {
                            fr.errors.push(msg);
                        }
                    } else {
                        let warn_msg =
                            "rm -rf usage detected; ensure this is intentional".to_string();
                        if !fr.warnings.contains(&warn_msg) {
                            fr.warnings.push(warn_msg);
                        }
                    }
                }
                "dd" if node_text_low.contains("/dev/") => {
                    fr.valid = false;
                    fr.errors
                        .push(format!("Potential destructive 'dd' on device: {node_text}"));
                }
                _ if cmd_l.starts_with("mkfs") => {
                    fr.valid = false;
                    fr.errors
                        .push(format!("Filesystem formatting command found: {node_text}"));
                }
                "reboot" | "shutdown" | "poweroff" | "halt" => {
                    fr.warnings
                        .push("Command will reboot or shutdown the device".to_string());
                }
                "chmod" if node_text_low.contains("777") && node_text_low.contains("-r") => {
                    fr.warnings
                        .push(format!("Potentially unsafe 'chmod 777 -R': {node_text}"));
                }
                "chown" if node_text_low.contains("/system") || node_text_low.contains("/data") => {
                    let msg = format!("'chown' on system/data detected: {node_text}");
                    if !fr.warnings.contains(&msg) {
                        fr.warnings.push(msg);
                    }
                }
                "setprop"
                    if path
                        .file_name()
                        .and_then(|n| n.to_str())
                        .is_none_or(|s| s != "post-fs-data.sh") =>
                {
                    // If this is not the post-fs-data special-case (already warned), add a general warning
                    let msg = format!(
                        "Usage of 'setprop' detected; prefer 'resetprop -n' where appropriate: {node_text}"
                    );
                    if !fr.warnings.contains(&msg) {
                        fr.warnings.push(msg);
                    }
                }
                "eval" => {
                    let msg = format!("Use of 'eval' detected: {node_text}");
                    if !fr.warnings.contains(&msg) {
                        fr.warnings.push(msg);
                    }
                }
                _ => {
                    // For 'command' nodes we already handled many checks; leave room for heuristics.
                }
            }
        }
    }

    // Recurse children
    for i in 0..node.child_count() {
        if let Ok(idx) = u32::try_from(i)
            && let Some(child) = node.child(idx)
        {
            detect_dangerous_commands(child, src, fr, path);
        }
    }
}
// 自定义的shell脚本检查（没有shellcheck时用）
// 只做基本的检查：引号匹配、括号匹配、尾随空格、CRLF等
// 虽然不如shellcheck全面，但至少能发现一些明显的问题
#[allow(clippy::too_many_lines)] // TODO: split this function into smaller helpers to reduce complexity
fn check_sh_custom(path: &Path, do_fix: bool) -> Result<FileResult, KamError> {
    let s = fs::read_to_string(path)?;
    let mut fr = FileResult {
        path: path.to_string_lossy().to_string(),
        kind: "sh".to_string(),
        valid: true,
        errors: Vec::new(),
        warnings: Vec::new(),
        fixed: false,
    };
    // 基本检查：不匹配的引号、尾随空格、CRLF
    // 这个检查比较简单，但能发现一些明显的问题
    // 注：不再将不匹配的括号作为错误；某些 Shell 结构（如 case 模式中的 `pattern)`）会包含单独的右括号，原先的简单计数会导致误报。
    let mut single_open = false;
    let mut double_open = false;
    let mut escaped = false;
    // 逐字符扫描，仅跟踪引号的状态（括号匹配检测已移除以避免误报）
    for ch in s.chars() {
        if escaped {
            // 转义字符，跳过下一个字符的特殊处理
            escaped = false;
            continue;
        }
        if ch == '\\' {
            escaped = true;
            continue;
        }
        // 单引号（只在不在双引号内时处理）
        if ch == '\'' && !double_open {
            single_open = !single_open;
        }
        // 双引号（只在不在单引号内时处理）
        if ch == '"' && !single_open {
            double_open = !double_open;
        }
    }
    // 检查引号是否匹配
    if single_open || double_open {
        fr.valid = false;
        fr.errors.push("Unbalanced quotes detected".to_string());
    }

    // AST-aware checks for command substitutions, arithmetic expansions, and backticks.
    // Only check constructs that are syntactic (e.g., $(), $(( )), backticks) so that we
    // avoid false positives from shell constructs like `case` patterns that contain `)`.
    for err in detect_unbalanced_shell_constructs(&s) {
        if !fr.errors.contains(&err) {
            fr.valid = false;
            fr.errors.push(err);
        }
    }
    // 用tree-sitter解析：如果解析器可用，检测语法错误
    // 虽然可能有点慢，但能发现更多问题
    let mut parser = Parser::new();
    // Some tree-sitter-bash bindings export `LANGUAGE` const instead of `language()` function.
    let language = tree_sitter::Language::new(tree_sitter_bash::LANGUAGE);
    if parser.set_language(&language).is_ok()
        && let Some(tree) = parser.parse(&s, None)
    {
        // Walk nodes to find ERROR nodes
        fn walk_node(node: tree_sitter::Node, fr: &mut FileResult) {
            if node.kind() == "ERROR" {
                fr.valid = false;
                let pos = node.start_position();
                fr.errors
                    .push(format!("Parse error at {}:{}", pos.row + 1, pos.column + 1));
            }
            for i in 0..node.child_count() {
                if let Ok(idx) = u32::try_from(i)
                    && let Some(child) = node.child(idx)
                {
                    walk_node(child, fr);
                }
            }
        }
        let root = tree.root_node();
        walk_node(root, &mut fr);

        // Also run the AST-based dangerous command detection so the custom checker
        // reports the same high-risk commands as the main `check_sh` wrapper.
        detect_dangerous_commands(root, &s, &mut fr, path);
    }
    // 基本内容修复（类似markdown）：CRLF、尾随空格、文件末尾换行
    // 这些虽然不影响功能，但统一格式比较好
    let mut content = s.clone();
    if content.contains("\r\n") {
        fr.warnings.push("CRLF line endings detected".to_string());
        if do_fix {
            content = content.replace("\r\n", "\n");
            fr.fixed = true;
        }
    }
    // 去除每行的尾随空格
    let trimmed_lines: Vec<String> = content
        .lines()
        .map(|l| {
            if l.ends_with(' ') {
                if do_fix {
                    fr.fixed = true;
                    l.trim_end().to_string()
                } else {
                    l.to_string()
                }
            } else {
                l.to_string()
            }
        })
        .collect();
    // 确保文件末尾有换行符
    let new_content = trimmed_lines.join("\n") + "\n";
    if new_content != content {
        if do_fix {
            // 写回修复后的内容
            fs::OpenOptions::new()
                .write(true)
                .truncate(true)
                .open(path)?
                .write_all(new_content.as_bytes())?;
            fr.fixed = true;
        } else {
            fr.warnings
                .push("Trailing spaces or missing newline at EOF".to_string());
        }
    }
    // Other suggestions
    if s.contains("eval ") {
        fr.warnings
            .push("Usage of eval detected; consider alternatives".to_string());
    }
    Ok(fr)
}

//
// AST-aware unbalanced-construct detection helpers
//

/// Detect unbalanced syntactic shell constructs that are not easily
/// covered by naive bracket counting (and would otherwise false-positive
/// on patterns like `case foo)`).
///
/// We focus on:
/// - command substitutions: `$(` ... `)`
/// - arithmetic expansions: `((` ... `))` (also `$( (` form via `$((`))
/// - backticks: `` `...` ``
///
/// The checks are quote-aware and skip text inside single quotes.
fn detect_unbalanced_shell_constructs(src: &str) -> Vec<String> {
    let mut errors: Vec<String> = Vec::new();
    let bytes = src.as_bytes();
    let mut i: usize = 0;
    let mut single = false;
    let mut double = false;
    let mut escaped = false;

    while i < bytes.len() {
        let ch = bytes[i] as char;
        if escaped {
            escaped = false;
            i += 1;
            continue;
        }
        if ch == '\\' {
            escaped = true;
            i += 1;
            continue;
        }
        if ch == '\'' && !double {
            single = !single;
            i += 1;
            continue;
        }
        if ch == '"' && !single {
            double = !double;
            i += 1;
            continue;
        }
        if single {
            i += 1;
            continue;
        }

        // Detect $(
        if ch == '$' && i + 1 < bytes.len() && bytes[i + 1] as char == '(' {
            let open_idx = i + 1; // index of '('
            if find_matching_paren(src, open_idx).is_none() {
                errors.push("Unbalanced command substitution detected".to_string());
            } else if let Some(j) = find_matching_paren(src, open_idx) {
                i = j + 1;
                continue;
            }
        }

        // Detect backticks
        if ch == '`' {
            let mut j = i + 1;
            let mut found = false;
            let mut esc = false;
            while j < bytes.len() {
                let chj = bytes[j] as char;
                if esc {
                    esc = false;
                    j += 1;
                    continue;
                }
                if chj == '\\' {
                    esc = true;
                    j += 1;
                    continue;
                }
                if chj == '`' {
                    found = true;
                    break;
                }
                j += 1;
            }
            if found {
                i = j + 1;
                continue;
            }
            errors.push("Unbalanced backticks detected".to_string());
        }

        // Detect arithmetic '(( ... ))' (heuristic: previous char should be whitespace or control)
        if ch == '(' && i + 1 < bytes.len() && bytes[i + 1] as char == '(' {
            let prevc = if i == 0 { '\n' } else { bytes[i - 1] as char };
            if prevc.is_whitespace() || prevc == ';' || prevc == '(' || prevc == '|' || prevc == '&'
            {
                let open_idx = i;
                if find_matching_paren(src, open_idx).is_none() {
                    errors.push("Unbalanced arithmetic expansion detected".to_string());
                } else if let Some(j) = find_matching_paren(src, open_idx) {
                    i = j + 1;
                    continue;
                }
            }
        }

        i += 1;
    }

    // Deduplicate messages
    errors.sort();
    errors.dedup();
    errors
}

/// Find the index of the matching closing `)` for an opening `(` at `open_idx`.
/// This is quote- and escape-aware and supports nesting.
fn find_matching_paren(src: &str, open_idx: usize) -> Option<usize> {
    let bytes = src.as_bytes();
    if open_idx >= bytes.len() || bytes[open_idx] as char != '(' {
        return None;
    }
    let mut depth: i32 = 1;
    let mut i = open_idx + 1;
    let mut single = false;
    let mut double = false;
    let mut escaped = false;
    while i < bytes.len() {
        let ch = bytes[i] as char;
        if escaped {
            escaped = false;
            i += 1;
            continue;
        }
        if ch == '\\' {
            escaped = true;
            i += 1;
            continue;
        }
        if ch == '\'' && !double {
            single = !single;
            i += 1;
            continue;
        }
        if ch == '"' && !single {
            double = !double;
            i += 1;
            continue;
        }
        if single {
            i += 1;
            continue;
        }
        if ch == '(' {
            depth += 1;
        } else if ch == ')' {
            depth -= 1;
            if depth == 0 {
                return Some(i);
            }
        }
        i += 1;
    }
    None
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::error::Error;
    use std::io::Write;
    use tempfile::NamedTempFile;

    #[test]
    fn detects_unbalanced_command_substitution() -> Result<(), Box<dyn Error>> {
        let mut f = NamedTempFile::new()?;
        writeln!(f, "echo $(date")?;
        let fr = check_sh_custom(f.path(), false)?;
        assert!(!fr.valid);
        assert!(
            fr.errors
                .iter()
                .any(|e| e.contains("Unbalanced command substitution"))
        );
        Ok(())
    }

    #[test]
    fn does_not_flag_case_pattern() -> Result<(), Box<dyn Error>> {
        let mut f = NamedTempFile::new()?;
        write!(f, "case $x in\n  foo)\n    echo ok\n    ;;\nesac\n")?;
        let fr = check_sh_custom(f.path(), false)?;
        assert!(fr.errors.iter().all(|e| !e.contains("Unbalanced")));
        Ok(())
    }

    #[test]
    fn detects_unbalanced_backtick() -> Result<(), Box<dyn Error>> {
        let mut f = NamedTempFile::new()?;
        writeln!(f, "echo `date")?;
        let fr = check_sh_custom(f.path(), false)?;
        assert!(!fr.valid);
        assert!(fr.errors.iter().any(|e| e.contains("Unbalanced backticks")));
        Ok(())
    }

    #[test]
    fn detects_unbalanced_arithmetic() -> Result<(), Box<dyn Error>> {
        let mut f = NamedTempFile::new()?;
        writeln!(f, "for (( i=0; i<10; i++ ; do echo $i; done")?;
        let fr = check_sh_custom(f.path(), false)?;
        assert!(!fr.valid);
        assert!(
            fr.errors
                .iter()
                .any(|e| e.contains("Unbalanced arithmetic expansion"))
        );
        Ok(())
    }

    #[test]
    fn accepts_nested_command_substitution() -> Result<(), Box<dyn Error>> {
        let mut f = NamedTempFile::new()?;
        writeln!(f, "echo $(echo $(date))")?;
        let fr = check_sh_custom(f.path(), false)?;
        assert!(fr.valid);
        Ok(())
    }
}
