use std::collections::{HashSet, VecDeque};
use std::env;
use std::fs::{self, File};
use std::io::{self, BufRead, BufReader, IsTerminal, Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Instant;

const DEFAULT_SAMPLES: usize = 5;
const DEFAULT_LIMIT: usize = 200;
const DEFAULT_SCAN_DEPTH: usize = 3;
const DEFAULT_DISCOVER_DEPTH: usize = 4;
const MAX_DISCOVER_DIRS: usize = 20_000;

#[derive(Clone, Debug)]
struct Config {
    roots: Vec<PathBuf>,
    iccid: String,
    date: Option<String>,
    interface: Option<String>,
    contains: Vec<String>,
    lines_mode: bool,
    count_only: bool,
    list_roots: bool,
    choose_roots: bool,
    interactive: bool,
    include_gz: bool,
    samples: usize,
    limit: usize,
    threads: usize,
    max_depth: usize,
    max_files: Option<usize>,
}

impl Default for Config {
    fn default() -> Self {
        let threads = thread::available_parallelism()
            .map(|n| n.get())
            .unwrap_or(4)
            .clamp(1, 16);
        Self {
            roots: Vec::new(),
            iccid: String::new(),
            date: None,
            interface: None,
            contains: Vec::new(),
            lines_mode: false,
            count_only: false,
            list_roots: false,
            choose_roots: false,
            interactive: false,
            include_gz: true,
            samples: DEFAULT_SAMPLES,
            limit: DEFAULT_LIMIT,
            threads,
            max_depth: DEFAULT_SCAN_DEPTH,
            max_files: None,
        }
    }
}

#[derive(Clone, Debug)]
struct RootCandidate {
    path: PathBuf,
    log_files: usize,
    gz_files: usize,
}

#[derive(Clone, Debug)]
struct SampleLine {
    file: PathBuf,
    line_no: usize,
    line: String,
}

#[derive(Debug)]
struct FileResult {
    path: PathBuf,
    matched: usize,
    scanned_lines: usize,
    samples: Vec<SampleLine>,
    error: Option<String>,
}

#[derive(Debug)]
struct MatchFilter {
    iccid: String,
    date: Option<String>,
    interface_terms: Vec<String>,
    contains: Vec<String>,
}

fn main() {
    let start = Instant::now();
    let mut cfg = match parse_args() {
        Ok(cfg) => cfg,
        Err(msg) => {
            eprintln!("{msg}");
            print_usage(io::stderr());
            std::process::exit(2);
        }
    };

    if cfg.list_roots {
        let roots = if cfg.roots.is_empty() {
            discover_roots(DEFAULT_DISCOVER_DEPTH)
        } else {
            discover_log_dirs(&cfg.roots, cfg.max_depth, cfg.include_gz)
        };
        print_root_candidates(&roots);
        return;
    }

    if cfg.interactive {
        cfg = run_wizard(cfg).unwrap_or_else(|err| {
            eprintln!("交互配置失败: {err}");
            std::process::exit(2);
        });
    }

    if cfg.iccid.is_empty() {
        eprintln!("必须传入 --iccid，避免误扫大日志");
        print_usage(io::stderr());
        std::process::exit(2);
    }

    let roots_were_provided = !cfg.roots.is_empty() || !roots_from_args().is_empty();
    if cfg.roots.is_empty() {
        cfg.roots = resolve_default_roots();
    }

    if cfg.choose_roots || (!roots_were_provided && !cfg.interactive && io::stdin().is_terminal()) {
        cfg.roots = choose_roots(&cfg.roots, cfg.max_depth, cfg.include_gz).unwrap_or_else(|err| {
            eprintln!("选择目录失败: {err}");
            std::process::exit(2);
        });
    }

    if cfg.roots.is_empty() {
        eprintln!("没有可用日志目录。先运行 --list-roots 查看自动发现结果，或确认日志目录已挂载。");
        std::process::exit(2);
    }

    let filter = MatchFilter {
        iccid: cfg.iccid.clone(),
        date: cfg.date.clone(),
        interface_terms: interface_terms(cfg.interface.as_deref()).unwrap_or_else(|err| {
            eprintln!("{err}");
            std::process::exit(2);
        }),
        contains: cfg.contains.clone(),
    };

    let mut files = collect_log_files(
        &cfg.roots,
        cfg.max_depth,
        cfg.include_gz,
        cfg.date.as_deref(),
    );
    files.sort_by(|a, b| b.modified_score.cmp(&a.modified_score));
    if let Some(max_files) = cfg.max_files {
        files.truncate(max_files);
    }

    if files.is_empty() {
        eprintln!("没有找到可扫描日志文件。");
        std::process::exit(1);
    }

    let scan_total = files.len();
    let queue = Arc::new(Mutex::new(VecDeque::from(files)));
    let results = Arc::new(Mutex::new(Vec::<FileResult>::new()));
    let filter = Arc::new(filter);
    let cfg_arc = Arc::new(cfg.clone());

    let mut handles = Vec::new();
    for _ in 0..cfg.threads {
        let queue = Arc::clone(&queue);
        let results = Arc::clone(&results);
        let filter = Arc::clone(&filter);
        let cfg_arc = Arc::clone(&cfg_arc);
        handles.push(thread::spawn(move || loop {
            let file = {
                let mut guard = queue.lock().expect("文件队列锁异常");
                guard.pop_front()
            };
            let Some(file) = file else {
                break;
            };
            let result = scan_file(&file.path, &filter, &cfg_arc);
            results.lock().expect("结果锁异常").push(result);
        }));
    }

    for handle in handles {
        if handle.join().is_err() {
            eprintln!("有扫描线程异常退出");
        }
    }

    let mut results = Arc::try_unwrap(results)
        .expect("结果仍被引用")
        .into_inner()
        .expect("结果锁异常");
    results.sort_by(|a, b| b.matched.cmp(&a.matched).then_with(|| a.path.cmp(&b.path)));

    print_results(&cfg, &results, scan_total, start.elapsed().as_millis());
}

#[derive(Clone, Debug)]
struct LogFile {
    path: PathBuf,
    modified_score: u64,
}

fn parse_args() -> Result<Config, String> {
    let mut cfg = Config::default();
    let raw_args: Vec<String> = env::args().skip(1).collect();
    if raw_args.is_empty() && io::stdin().is_terminal() {
        cfg.interactive = true;
    }
    let mut args = raw_args.into_iter().peekable();
    while let Some(arg) = args.next() {
        match arg.as_str() {
            "--root" => cfg
                .roots
                .push(PathBuf::from(next_value(&mut args, "--root")?)),
            "--roots" => add_roots(&mut cfg.roots, &next_value(&mut args, "--roots")?),
            "--iccid" => cfg.iccid = next_value(&mut args, "--iccid")?,
            "--date" => cfg.date = Some(next_value(&mut args, "--date")?),
            "--interface" | "--api" => cfg.interface = Some(next_value(&mut args, "--interface")?),
            "--contains" => cfg.contains.push(next_value(&mut args, "--contains")?),
            "--lines" => cfg.lines_mode = true,
            "--count-only" => cfg.count_only = true,
            "--list-roots" => cfg.list_roots = true,
            "--choose" => cfg.choose_roots = true,
            "--interactive" | "-i" => cfg.interactive = true,
            "--no-gz" => cfg.include_gz = false,
            "--samples" => {
                cfg.samples = parse_usize(&next_value(&mut args, "--samples")?, "--samples")?
            }
            "--limit" => cfg.limit = parse_usize(&next_value(&mut args, "--limit")?, "--limit")?,
            "--threads" => {
                cfg.threads =
                    parse_usize(&next_value(&mut args, "--threads")?, "--threads")?.clamp(1, 64)
            }
            "--max-depth" => {
                cfg.max_depth = parse_usize(&next_value(&mut args, "--max-depth")?, "--max-depth")?
            }
            "--max-files" => {
                cfg.max_files = Some(parse_usize(
                    &next_value(&mut args, "--max-files")?,
                    "--max-files",
                )?)
            }
            "-h" | "--help" => {
                print_usage(io::stdout());
                std::process::exit(0);
            }
            _ => return Err(format!("未知参数: {arg}")),
        }
    }
    if cfg.roots.is_empty() {
        for root in roots_from_args() {
            cfg.roots.push(root);
        }
    }
    Ok(cfg)
}

fn next_value(
    args: &mut std::iter::Peekable<impl Iterator<Item = String>>,
    name: &str,
) -> Result<String, String> {
    args.next().ok_or_else(|| format!("{name} 缺少参数"))
}

fn parse_usize(raw: &str, name: &str) -> Result<usize, String> {
    raw.parse::<usize>()
        .map_err(|_| format!("{name} 需要整数，当前值: {raw}"))
}

fn roots_from_args() -> Vec<PathBuf> {
    let mut roots = Vec::new();
    if let Ok(raw) = env::var("JUNHONG_WORKER_LOG_ROOTS") {
        add_roots(&mut roots, &raw);
    }
    roots
}

fn add_roots(roots: &mut Vec<PathBuf>, raw: &str) {
    for part in raw.split(':') {
        let trimmed = part.trim();
        if !trimmed.is_empty() {
            roots.push(PathBuf::from(trimmed));
        }
    }
}

fn run_wizard(mut cfg: Config) -> io::Result<Config> {
    clear_screen();
    print_banner();

    let root_candidates = if cfg.roots.is_empty() {
        discover_roots(DEFAULT_DISCOVER_DEPTH)
    } else {
        cfg.roots
            .iter()
            .filter(|p| p.is_dir())
            .flat_map(|p| discover_log_dirs(&[p.clone()], cfg.max_depth, cfg.include_gz))
            .collect()
    };

    if root_candidates.is_empty() {
        println!("{}", color("没有自动发现日志目录。", "31;1"));
        println!("可先确认日志挂载目录，或设置 JUNHONG_LOG_DISCOVERY_BASES 后重新运行。");
        return Ok(cfg);
    }

    cfg.roots = choose_root_candidates_pretty(&root_candidates)?;

    println!();
    cfg.iccid = prompt_required("ICCID", "输入要查询的卡 ICCID")?;

    println!();
    cfg.date = choose_date_pretty()?;

    println!();
    cfg.interface = choose_interface_pretty()?;

    println!();
    let extra = prompt_optional("追加关键词过滤（可直接回车跳过）", None)?;
    if !extra.is_empty() {
        cfg.contains.push(extra);
    }

    println!();
    cfg.lines_mode = prompt_yes_no("是否输出原始命中行？默认只看摘要，避免刷屏", false)?;
    if cfg.lines_mode {
        let raw_limit = prompt_optional("最多输出多少行", Some(&cfg.limit.to_string()))?;
        if !raw_limit.is_empty() {
            if let Ok(limit) = raw_limit.parse::<usize>() {
                cfg.limit = limit;
            }
        }
    }

    cfg.include_gz = prompt_yes_no("是否扫描 .gz 压缩轮转日志", true)?;

    println!();
    println!("{}", color("即将扫描", "36;1"));
    println!("  目录数: {}", cfg.roots.len());
    for root in &cfg.roots {
        println!("    - {}", root.display());
    }
    println!("  ICCID: {}", cfg.iccid);
    println!("  日期: {}", cfg.date.as_deref().unwrap_or("不限"));
    println!("  接口: {}", cfg.interface.as_deref().unwrap_or("不限"));
    println!(
        "  输出: {}",
        if cfg.lines_mode {
            "原始行"
        } else {
            "摘要"
        }
    );
    pause("按回车开始扫描")?;
    clear_screen();
    Ok(cfg)
}

fn clear_screen() {
    if io::stdout().is_terminal() {
        print!("\x1b[2J\x1b[H");
        let _ = io::stdout().flush();
    }
}

fn supports_color() -> bool {
    io::stdout().is_terminal() && env::var_os("NO_COLOR").is_none()
}

fn color(text: &str, code: &str) -> String {
    if supports_color() {
        format!("\x1b[{code}m{text}\x1b[0m")
    } else {
        text.to_string()
    }
}

fn print_banner() {
    println!(
        "{}",
        color("╭────────────────────────────────────────────╮", "36;1")
    );
    println!(
        "{}",
        color("│       君鸿 Worker 日志查询向导             │", "36;1")
    );
    println!(
        "{}",
        color("╰────────────────────────────────────────────╯", "36;1")
    );
    println!("{}", color("默认只输出摘要；需要原始行时再开启。", "2"));
    println!();
}

fn choose_root_candidates_pretty(candidates: &[RootCandidate]) -> io::Result<Vec<PathBuf>> {
    println!("{}", color("1. 日志目录", "36;1"));
    println!("已自动发现以下候选目录，默认选择全部：");
    println!();
    for (index, item) in candidates.iter().enumerate() {
        let marker = if item.gz_files > 0 {
            "含压缩"
        } else {
            "普通"
        };
        println!(
            "  {} {:<48}  {:>3} 个日志  {:>3} 个 gz  {}",
            color(&format!("{:>2}.", index + 1), "32;1"),
            truncate_path(&item.path, 48),
            item.log_files,
            item.gz_files,
            color(marker, "2")
        );
    }
    println!();
    println!(
        "{}",
        color(
            "回车=全部；输入 1,3-4=只选部分；all,!2,!5-7=排除部分；q=退出",
            "2"
        )
    );
    let input = prompt_optional("选择目录", Some("all"))?;
    if input.eq_ignore_ascii_case("q") {
        std::process::exit(0);
    }
    if input.is_empty() || input.eq_ignore_ascii_case("all") {
        return Ok(candidates.iter().map(|c| c.path.clone()).collect());
    }

    let selected = select_candidate_paths(candidates, &input);
    if selected.is_empty() {
        println!("{}", color("没有选中有效目录，使用全部候选目录。", "33"));
        return Ok(candidates.iter().map(|c| c.path.clone()).collect());
    }
    Ok(selected)
}

fn truncate_path(path: &Path, width: usize) -> String {
    let text = path.display().to_string();
    let count = text.chars().count();
    if count <= width {
        return text;
    }
    let keep = width.saturating_sub(1);
    let suffix: String = text
        .chars()
        .rev()
        .take(keep)
        .collect::<String>()
        .chars()
        .rev()
        .collect();
    format!("…{suffix}")
}

fn prompt_required(label: &str, help: &str) -> io::Result<String> {
    loop {
        println!("{}", color(help, "2"));
        let value = prompt_optional(label, None)?;
        if !value.trim().is_empty() {
            return Ok(value.trim().to_string());
        }
        println!("{}", color("不能为空。", "31"));
    }
}

fn prompt_optional(label: &str, default: Option<&str>) -> io::Result<String> {
    match default {
        Some(default) => print!("{} [{}]: ", color(label, "33;1"), default),
        None => print!("{}: ", color(label, "33;1")),
    }
    io::stdout().flush()?;
    let mut input = String::new();
    io::stdin().read_line(&mut input)?;
    let value = input.trim().to_string();
    if value.is_empty() {
        Ok(default.unwrap_or("").to_string())
    } else {
        Ok(value)
    }
}

fn prompt_yes_no(label: &str, default: bool) -> io::Result<bool> {
    let default_text = if default { "Y/n" } else { "y/N" };
    loop {
        let value = prompt_optional(label, Some(default_text))?;
        if value == default_text || value.is_empty() {
            return Ok(default);
        }
        match value.to_ascii_lowercase().as_str() {
            "y" | "yes" | "是" => return Ok(true),
            "n" | "no" | "否" => return Ok(false),
            _ => println!("{}", color("请输入 y 或 n。", "31")),
        }
    }
}

fn pause(label: &str) -> io::Result<()> {
    print!("{}...", color(label, "33;1"));
    io::stdout().flush()?;
    let mut input = String::new();
    io::stdin().read_line(&mut input)?;
    Ok(())
}

fn choose_date_pretty() -> io::Result<Option<String>> {
    println!("{}", color("2. 查询日期", "36;1"));
    let today = command_output("date", &["+%F"]).unwrap_or_default();
    let yesterday = command_output("date", &["-d", "yesterday", "+%F"])
        .or_else(|| command_output("date", &["-v-1d", "+%F"]))
        .unwrap_or_default();
    let today_label = if today.is_empty() {
        "今天".to_string()
    } else {
        format!("今天 {today}")
    };
    let yesterday_label = if yesterday.is_empty() {
        "昨天".to_string()
    } else {
        format!("昨天 {yesterday}")
    };
    println!("  {} {}", color("1.", "32;1"), today_label);
    println!("  {} {}", color("2.", "32;1"), yesterday_label);
    println!("  {} 自定义日期", color("3.", "32;1"));
    println!("  {} 不限制日期", color("4.", "32;1"));
    let choice = prompt_optional("选择", Some("2"))?;
    match choice.as_str() {
        "" | "2" => Ok(if yesterday.is_empty() {
            None
        } else {
            Some(yesterday)
        }),
        "1" => Ok(if today.is_empty() { None } else { Some(today) }),
        "3" => {
            let date = prompt_required("日期", "格式：YYYY-MM-DD，例如 2026-05-24")?;
            Ok(Some(date))
        }
        "4" => Ok(None),
        other if other.len() == 10 && other.chars().nth(4) == Some('-') => {
            Ok(Some(other.to_string()))
        }
        _ => Ok(if yesterday.is_empty() {
            None
        } else {
            Some(yesterday)
        }),
    }
}

fn choose_interface_pretty() -> io::Result<Option<String>> {
    println!("{}", color("3. 接口类型", "36;1"));
    println!("  {} 实名 realname", color("1.", "32;1"));
    println!("  {} 流量 flow/carddata", color("2.", "32;1"));
    println!("  {} 卡状态 card_status", color("3.", "32;1"));
    println!("  {} 停机 stop", color("4.", "32;1"));
    println!("  {} 复机 start", color("5.", "32;1"));
    println!("  {} 不限制接口", color("6.", "32;1"));
    println!("  {} 自定义 Gateway 路径", color("7.", "32;1"));
    let choice = prompt_optional("选择", Some("1"))?;
    let value = match choice.as_str() {
        "" | "1" => Some("realname".to_string()),
        "2" => Some("flow".to_string()),
        "3" => Some("card_status".to_string()),
        "4" => Some("stop".to_string()),
        "5" => Some("start".to_string()),
        "6" => None,
        "7" => Some(prompt_required("Gateway 路径", "例如 /flow-card/realName")?),
        other => Some(other.to_string()),
    };
    Ok(value)
}

fn command_output(program: &str, args: &[&str]) -> Option<String> {
    let output = Command::new(program).args(args).output().ok()?;
    if !output.status.success() {
        return None;
    }
    let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if text.is_empty() {
        None
    } else {
        Some(text)
    }
}

fn print_usage<W: Write>(mut writer: W) {
    let _ = writeln!(
        writer,
        r#"用法:
  rustc scripts/ops/search-worker-logs.rs -O -o /tmp/jh-logq
  /tmp/jh-logq
  /tmp/jh-logq --iccid <ICCID> [选项]

核心选项:
  --root <dir>              增加一个日志目录，可重复
  --roots <a:b:c>           一次传多个日志目录
  --list-roots              自动发现候选日志目录并退出；配合 --root/--roots 时只列指定范围
  --choose                  从扫描范围内的日志目录交互选择；无需手动输入目录路径
  --iccid <ICCID>           必填，按 ICCID 过滤
  --date <YYYY-MM-DD>       按日志行日期过滤，并按文件名日期剪枝
  --interface <name/path>   realname, flow, carddata, card_status, stop, start，或直接传 /flow-card/realName
  --contains <text>         追加文本过滤，可重复

输出控制:
  默认只输出汇总、命中文件和少量样例，不刷屏。
  --lines                   输出原始命中行
  --limit <N>               --lines 最大输出行数，默认 200
  --samples <N>             汇总模式样例行数，默认 5
  --count-only              只输出命中文件和行数

性能选项:
  --threads <N>             并发扫描线程数，默认自动
  --max-depth <N>           每个 root 下递归深度，默认 3
  --max-files <N>           最多扫描 N 个文件
  --no-gz                   跳过 .gz 文件

环境变量:
  JUNHONG_WORKER_LOG_ROOTS  默认日志目录，冒号分隔
"#
    );
}

fn resolve_default_roots() -> Vec<PathBuf> {
    let mut roots = roots_from_args();
    if !roots.is_empty() {
        return dedup_paths(roots);
    }

    for path in ["/opt/junhong_cmp/logs", "/app/logs", "./logs"] {
        let p = PathBuf::from(path);
        if p.is_dir() {
            roots.push(p);
        }
    }

    if roots.is_empty() {
        roots = discover_roots(DEFAULT_DISCOVER_DEPTH)
            .into_iter()
            .map(|c| c.path)
            .collect();
    }

    dedup_paths(roots)
}

fn dedup_paths(paths: Vec<PathBuf>) -> Vec<PathBuf> {
    let mut seen = HashSet::new();
    let mut result = Vec::new();
    for path in paths {
        let key = path.to_string_lossy().to_string();
        if seen.insert(key) {
            result.push(path);
        }
    }
    result
}

fn discover_roots(max_depth: usize) -> Vec<RootCandidate> {
    let mut bases = Vec::new();
    if let Ok(raw) = env::var("JUNHONG_LOG_DISCOVERY_BASES") {
        add_roots(&mut bases, &raw);
    } else {
        for path in [".", "/opt/junhong_cmp", "/app", "/data", "/srv"] {
            let p = PathBuf::from(path);
            if p.is_dir() {
                bases.push(p);
            }
        }
    }

    let mut candidates = Vec::new();
    let mut visited = HashSet::new();
    let mut visited_dirs = 0usize;
    for base in bases {
        let mut queue = VecDeque::new();
        queue.push_back((base, 0usize));
        while let Some((dir, depth)) = queue.pop_front() {
            if visited_dirs >= MAX_DISCOVER_DIRS {
                break;
            }
            visited_dirs += 1;

            let key = dir.to_string_lossy().to_string();
            if !visited.insert(key) {
                continue;
            }
            let Ok(entries) = fs::read_dir(&dir) else {
                continue;
            };
            let mut log_files = 0usize;
            let mut gz_files = 0usize;
            let mut subdirs = Vec::new();

            for entry in entries.flatten() {
                let path = entry.path();
                if path.is_dir() {
                    if depth < max_depth && !should_skip_dir(&path) {
                        subdirs.push(path);
                    }
                    continue;
                }
                if is_log_file(&path, true) {
                    log_files += 1;
                    if is_gz(&path) {
                        gz_files += 1;
                    }
                }
            }

            if log_files > 0 {
                candidates.push(RootCandidate {
                    path: dir.clone(),
                    log_files,
                    gz_files,
                });
            }
            for subdir in subdirs {
                queue.push_back((subdir, depth + 1));
            }
        }
    }

    candidates.sort_by(|a, b| a.path.cmp(&b.path));
    candidates
}

fn discover_log_dirs(roots: &[PathBuf], max_depth: usize, include_gz: bool) -> Vec<RootCandidate> {
    let mut candidates = Vec::new();
    let mut visited = HashSet::new();
    let mut visited_dirs = 0usize;

    for root in roots {
        if !root.is_dir() {
            continue;
        }

        let mut queue = VecDeque::new();
        queue.push_back((root.clone(), 0usize));
        while let Some((dir, depth)) = queue.pop_front() {
            if visited_dirs >= MAX_DISCOVER_DIRS {
                break;
            }
            visited_dirs += 1;

            let key = dir.to_string_lossy().to_string();
            if !visited.insert(key) {
                continue;
            }

            let (log_files, gz_files) = count_immediate_logs(&dir, include_gz);
            if log_files > 0 {
                candidates.push(RootCandidate {
                    path: dir.clone(),
                    log_files,
                    gz_files,
                });
            }

            if depth >= max_depth {
                continue;
            }

            let Ok(entries) = fs::read_dir(&dir) else {
                continue;
            };
            for entry in entries.flatten() {
                let path = entry.path();
                if path.is_dir() && !should_skip_dir(&path) {
                    queue.push_back((path, depth + 1));
                }
            }
        }
    }

    candidates.sort_by(|a, b| a.path.cmp(&b.path));
    candidates
}

fn should_skip_dir(path: &Path) -> bool {
    let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
        return false;
    };
    matches!(
        name,
        ".git" | ".omx" | "node_modules" | "vendor" | "tmp" | "cache"
    )
}

fn print_root_candidates(candidates: &[RootCandidate]) {
    if candidates.is_empty() {
        println!("没有发现候选日志目录。可设置 JUNHONG_LOG_DISCOVERY_BASES 后重试。");
        return;
    }
    println!("发现 {} 个候选日志目录:", candidates.len());
    for (index, item) in candidates.iter().enumerate() {
        println!(
            "{:>2}. {}  (日志文件: {}, gz: {})",
            index + 1,
            item.path.display(),
            item.log_files,
            item.gz_files
        );
    }
}

fn choose_roots(roots: &[PathBuf], max_depth: usize, include_gz: bool) -> io::Result<Vec<PathBuf>> {
    let candidates = discover_log_dirs(roots, max_depth, include_gz);

    print_root_candidates(&candidates);
    if candidates.is_empty() {
        return Ok(Vec::new());
    }

    print!("选择目录编号，例如 1,3-4，或 all,!2,!5-7: ");
    io::stdout().flush()?;
    let mut input = String::new();
    io::stdin().read_line(&mut input)?;
    let input = input.trim();
    if input.eq_ignore_ascii_case("all") || input.is_empty() {
        return Ok(candidates.into_iter().map(|c| c.path).collect());
    }

    let selected = select_candidate_paths(&candidates, input);
    Ok(selected)
}

fn select_candidate_paths(candidates: &[RootCandidate], input: &str) -> Vec<PathBuf> {
    let indexes = parse_selection_indexes(input, candidates.len());
    indexes
        .into_iter()
        .filter_map(|idx| candidates.get(idx - 1).map(|c| c.path.clone()))
        .collect()
}

fn parse_selection_indexes(input: &str, len: usize) -> Vec<usize> {
    if len == 0 {
        return Vec::new();
    }

    let mut selected = vec![false; len];
    let mut touched = false;
    for raw_part in input.split(',') {
        let part = raw_part.trim();
        if part.is_empty() {
            continue;
        }
        if part.eq_ignore_ascii_case("all") {
            selected.fill(true);
            touched = true;
            continue;
        }

        let (exclude, range_part) = match part.strip_prefix('!') {
            Some(rest) => (true, rest.trim()),
            None => (false, part),
        };
        for idx in parse_index_range(range_part, len) {
            selected[idx - 1] = !exclude;
            touched = true;
        }
    }

    if !touched {
        return Vec::new();
    }

    selected
        .into_iter()
        .enumerate()
        .filter_map(|(idx, is_selected)| if is_selected { Some(idx + 1) } else { None })
        .collect()
}

fn parse_index_range(raw: &str, len: usize) -> Vec<usize> {
    if let Some((start, end)) = raw.split_once('-') {
        let start = start.trim().parse::<usize>().unwrap_or(0);
        let end = end.trim().parse::<usize>().unwrap_or(0);
        if start == 0 || end == 0 {
            return Vec::new();
        }
        let start = start.min(len);
        let end = end.min(len);
        let (start, end) = if start <= end {
            (start, end)
        } else {
            (end, start)
        };
        return (start..=end).collect();
    }

    let idx = raw.trim().parse::<usize>().unwrap_or(0);
    if idx == 0 || idx > len {
        Vec::new()
    } else {
        vec![idx]
    }
}

fn count_immediate_logs(dir: &Path, include_gz: bool) -> (usize, usize) {
    let Ok(entries) = fs::read_dir(dir) else {
        return (0, 0);
    };
    let mut log_files = 0usize;
    let mut gz_files = 0usize;
    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_file() && is_log_file(&path, include_gz) {
            log_files += 1;
            if is_gz(&path) {
                gz_files += 1;
            }
        }
    }
    (log_files, gz_files)
}

fn collect_log_files(
    roots: &[PathBuf],
    max_depth: usize,
    include_gz: bool,
    date: Option<&str>,
) -> Vec<LogFile> {
    let mut result = Vec::new();
    let mut seen = HashSet::new();

    for root in roots {
        if root.is_file() {
            maybe_add_file(root, include_gz, date, &mut result, &mut seen);
            continue;
        }
        if !root.is_dir() {
            eprintln!("跳过不存在的目录: {}", root.display());
            continue;
        }

        let mut queue = VecDeque::new();
        queue.push_back((root.clone(), 0usize));
        while let Some((dir, depth)) = queue.pop_front() {
            let Ok(entries) = fs::read_dir(&dir) else {
                continue;
            };
            for entry in entries.flatten() {
                let path = entry.path();
                if path.is_dir() {
                    if depth < max_depth && !should_skip_dir(&path) {
                        queue.push_back((path, depth + 1));
                    }
                    continue;
                }
                maybe_add_file(&path, include_gz, date, &mut result, &mut seen);
            }
        }
    }

    result
}

fn maybe_add_file(
    path: &Path,
    include_gz: bool,
    date: Option<&str>,
    result: &mut Vec<LogFile>,
    seen: &mut HashSet<String>,
) {
    if !is_log_file(path, include_gz) {
        return;
    }
    if let Some(date) = date {
        if !filename_can_contain_date(path, date) {
            return;
        }
    }
    let key = path.to_string_lossy().to_string();
    if !seen.insert(key) {
        return;
    }
    let modified_score = path
        .metadata()
        .and_then(|m| m.modified())
        .ok()
        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
        .map(|d| d.as_secs())
        .unwrap_or(0);
    result.push(LogFile {
        path: path.to_path_buf(),
        modified_score,
    });
}

fn is_log_file(path: &Path, include_gz: bool) -> bool {
    let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
        return false;
    };
    let lower = name.to_ascii_lowercase();
    if lower.ends_with(".gz") {
        return include_gz && lower.contains("log");
    }
    lower.ends_with(".log")
        || lower.contains(".log.")
        || lower.ends_with("-json.log")
        || lower == "stdout"
        || lower == "stderr"
}

fn is_gz(path: &Path) -> bool {
    path.extension()
        .and_then(|s| s.to_str())
        .map(|s| s.eq_ignore_ascii_case("gz"))
        .unwrap_or(false)
}

fn filename_can_contain_date(path: &Path, date: &str) -> bool {
    let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
        return true;
    };
    let compact = date.replace('-', "");
    let dates = extract_filename_dates(name);
    dates.is_empty() || dates.iter().any(|d| d == date || d == &compact)
}

fn extract_filename_dates(name: &str) -> Vec<String> {
    let bytes = name.as_bytes();
    let mut dates = Vec::new();
    for i in 0..bytes.len() {
        if i + 10 <= bytes.len()
            && bytes[i..].get(4) == Some(&b'-')
            && bytes[i..].get(7) == Some(&b'-')
            && bytes[i..i + 4].iter().all(u8::is_ascii_digit)
            && bytes[i + 5..i + 7].iter().all(u8::is_ascii_digit)
            && bytes[i + 8..i + 10].iter().all(u8::is_ascii_digit)
        {
            dates.push(name[i..i + 10].to_string());
        }
        if i + 8 <= bytes.len() && bytes[i..i + 8].iter().all(u8::is_ascii_digit) {
            let year = &name[i..i + 4];
            if year.starts_with("20") {
                dates.push(name[i..i + 8].to_string());
            }
        }
    }
    dates
}

fn interface_terms(interface: Option<&str>) -> Result<Vec<String>, String> {
    let Some(interface) = interface else {
        return Ok(Vec::new());
    };
    let terms = match interface {
        "realname" => vec!["/flow-card/realName", "realStatus", "实名"],
        "flow" | "carddata" => vec!["/flow-card/flow", "gateway_flow", "流量", "\"used\""],
        "card_status" | "status" => vec!["/flow-card/status", "cardStatus", "卡状态"],
        "stop" => vec!["/flow-card/cardStop", "cardStop", "停机"],
        "start" | "resume" => vec!["/flow-card/cardStart", "cardStart", "复机"],
        other if other.starts_with('/') => vec![other],
        other => return Err(format!("未知接口别名: {other}")),
    };
    Ok(terms.into_iter().map(String::from).collect())
}

fn scan_file(path: &Path, filter: &MatchFilter, cfg: &Config) -> FileResult {
    let mut result = FileResult {
        path: path.to_path_buf(),
        matched: 0,
        scanned_lines: 0,
        samples: Vec::new(),
        error: None,
    };

    let scan_result = if is_gz(path) {
        scan_gzip_file(path, filter, cfg, &mut result)
    } else {
        scan_plain_file(path, filter, cfg, &mut result)
    };

    if let Err(err) = scan_result {
        result.error = Some(err);
    }
    result
}

fn scan_plain_file(
    path: &Path,
    filter: &MatchFilter,
    cfg: &Config,
    result: &mut FileResult,
) -> Result<(), String> {
    let file = File::open(path).map_err(|err| format!("打开失败: {err}"))?;
    let reader = BufReader::new(file);
    scan_reader(path, reader, filter, cfg, result)
}

fn scan_gzip_file(
    path: &Path,
    filter: &MatchFilter,
    cfg: &Config,
    result: &mut FileResult,
) -> Result<(), String> {
    let mut child = Command::new("gzip")
        .arg("-cd")
        .arg("--")
        .arg(path)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .map_err(|err| format!("启动 gzip 失败: {err}"))?;
    let stdout = child
        .stdout
        .take()
        .ok_or_else(|| String::from("读取 gzip stdout 失败"))?;
    let reader = BufReader::new(stdout);
    let scan_result = scan_reader(path, reader, filter, cfg, result);
    let output = child
        .wait_with_output()
        .map_err(|err| format!("等待 gzip 失败: {err}"))?;
    if !output.status.success() {
        let mut msg = String::from_utf8_lossy(&output.stderr).trim().to_string();
        if msg.is_empty() {
            msg = format!("gzip 退出码 {:?}", output.status.code());
        }
        return Err(msg);
    }
    scan_result
}

fn scan_reader<R: Read>(
    path: &Path,
    reader: BufReader<R>,
    filter: &MatchFilter,
    cfg: &Config,
    result: &mut FileResult,
) -> Result<(), String> {
    for (idx, line) in reader.lines().enumerate() {
        let line = line.map_err(|err| format!("读取失败: {err}"))?;
        result.scanned_lines += 1;
        if matches_line(&line, filter) {
            result.matched += 1;
            let sample_cap = if cfg.lines_mode {
                cfg.limit
            } else {
                cfg.samples
            };
            if sample_cap == 0 || result.samples.len() < sample_cap.min(1_000) {
                result.samples.push(SampleLine {
                    file: path.to_path_buf(),
                    line_no: idx + 1,
                    line,
                });
            }
        }
    }
    Ok(())
}

fn matches_line(line: &str, filter: &MatchFilter) -> bool {
    if !line.contains(&filter.iccid) {
        return false;
    }
    if let Some(date) = &filter.date {
        if !line.contains(date) {
            return false;
        }
    }
    if !filter.interface_terms.is_empty()
        && !filter
            .interface_terms
            .iter()
            .any(|term| line.contains(term))
    {
        return false;
    }
    for term in &filter.contains {
        if !line.contains(term) {
            return false;
        }
    }
    true
}

fn print_results(cfg: &Config, results: &[FileResult], scan_total: usize, elapsed_ms: u128) {
    let total_matches: usize = results.iter().map(|r| r.matched).sum();
    let total_lines: usize = results.iter().map(|r| r.scanned_lines).sum();
    let matched_files: Vec<&FileResult> = results.iter().filter(|r| r.matched > 0).collect();
    let errors: Vec<&FileResult> = results.iter().filter(|r| r.error.is_some()).collect();

    if cfg.count_only {
        for item in matched_files {
            println!("{}\t{}", item.matched, item.path.display());
        }
        if !errors.is_empty() {
            eprintln!(
                "有 {} 个文件读取失败，去掉 --count-only 可看详情",
                errors.len()
            );
        }
        return;
    }

    println!("扫描完成: {} ms", elapsed_ms);
    println!(
        "文件: 扫描 {} 个，命中 {} 个；行: 扫描 {} 行，命中 {} 行",
        scan_total,
        matched_files.len(),
        total_lines,
        total_matches
    );
    println!("ICCID: {}", cfg.iccid);
    if let Some(date) = &cfg.date {
        println!("日期: {}", date);
    }
    if let Some(interface) = &cfg.interface {
        println!("接口过滤: {}", interface);
    }

    if !errors.is_empty() {
        println!();
        println!("读取失败文件（最多 10 个）:");
        for item in errors.iter().take(10) {
            println!(
                "- {}: {}",
                item.path.display(),
                item.error.as_deref().unwrap_or("")
            );
        }
    }

    if total_matches == 0 {
        println!();
        println!("没有命中。建议先去掉 --interface 只按 ICCID+日期查，确认这张卡当天是否有任何轮询日志。");
        return;
    }

    println!();
    println!("命中文件（最多 30 个）:");
    for item in matched_files.iter().take(30) {
        println!("{:>8}  {}", item.matched, item.path.display());
    }
    if matched_files.len() > 30 {
        println!(
            "... 还有 {} 个命中文件，使用 --count-only 查看完整列表",
            matched_files.len() - 30
        );
    }

    let mut samples = Vec::new();
    for item in results {
        for sample in &item.samples {
            samples.push(sample.clone());
        }
    }
    samples.sort_by(|a, b| a.file.cmp(&b.file).then(a.line_no.cmp(&b.line_no)));

    if cfg.lines_mode {
        println!();
        println!("原始命中行（最多 {} 行）:", cfg.limit);
        for sample in samples.into_iter().take(cfg.limit) {
            println!(
                "{}:{}:{}",
                sample.file.display(),
                sample.line_no,
                sample.line
            );
        }
        if total_matches > cfg.limit {
            println!(
                "... 已按 --limit={} 截断，实际命中 {} 行",
                cfg.limit, total_matches
            );
        }
    } else if cfg.samples > 0 {
        println!();
        println!("样例（最多 {} 条；完整原始行用 --lines）:", cfg.samples);
        for sample in samples.into_iter().take(cfg.samples) {
            println!(
                "{}:{}:{}",
                sample.file.display(),
                sample.line_no,
                truncate_line(&sample.line, 260)
            );
        }
    }
}

fn truncate_line(line: &str, max_chars: usize) -> String {
    let mut out = String::new();
    for (idx, ch) in line.chars().enumerate() {
        if idx >= max_chars {
            out.push_str("...");
            return out;
        }
        out.push(ch);
    }
    out
}
