use anyhow::Result;
use clap::Parser;
use log::error;
use moz_cli_version_check::VersionChecker;
use searchfox_lib::{
    call_graph::{format_call_graph_markdown, CallGraphQuery},
    can_gc::GcInfo,
    field_layout::{format_field_layout, FieldLayoutQuery},
    nesting::NestingContext,
    parse_commit_header,
    search::SearchOptions,
    searchfox_url_repo, CategoryFilter, SearchfoxClient,
};
use std::collections::HashMap;

#[derive(Parser, Debug)]
#[command(
    name = "searchfox-cli",
    about = "Searchfox CLI for Mozilla code search",
    long_about = "A command-line interface for searching Mozilla codebases using searchfox.org.\n\nExamples:\n  searchfox-cli -q AudioStream\n  searchfox-cli -q AudioStream -C -l 10\n  searchfox-cli -q '^Audio.*' -r\n  searchfox-cli -q AudioStream -p ^dom/media\n  searchfox-cli -p PContent.ipdl  # Search for files by path only\n  searchfox-cli --get-file dom/media/AudioStream.h\n  searchfox-cli --symbol AudioContext\n  searchfox-cli --symbol 'AudioContext::CreateGain'\n  searchfox-cli --id main\n  searchfox-cli -q 'path:dom/media AudioStream'\n  searchfox-cli -q 'symbol:AudioContext' --context 3\n  searchfox-cli --define 'AudioContext::CreateGain'\n  searchfox-cli --calls-from 'mozilla::dom::AudioContext::CreateGain' --depth 2\n  searchfox-cli --calls-to 'mozilla::dom::AudioContext::CreateGain' --depth 3\n  searchfox-cli --calls-between 'AudioContext,AudioNode' --depth 2\n  searchfox-cli --field-layout 'mozilla::dom::AudioContext'"
)]
struct Args {
    #[arg(short, long, help = "Search query string")]
    query: Option<String>,

    #[arg(
        short = 'R',
        long,
        default_value = "mozilla-central",
        help = "Repository to search in",
        long_help = "Repository to search in. Available repositories:\n  mozilla-central (default) - Main Firefox development\n  autoland - Integration repository\n  mozilla-beta - Beta release branch\n  mozilla-release - Release branch\n  mozilla-esr115 - ESR 115 branch\n  mozilla-esr128 - ESR 128 branch\n  mozilla-esr140 - ESR 140 branch\n  comm-central - Thunderbird development"
    )]
    repo: String,

    #[arg(
        short,
        long,
        help = "Filter results by path prefix (e.g., ^dom/media) or search for files by path",
        long_help = "Filter search results by file path prefix or search for files by path pattern.\nUse regex patterns to match specific directories or files.\nCan be used alone to search for files without a query.\nExamples:\n  -p ^dom/media (with query) - filters results to files starting with dom/media/\n  -p PContent.ipdl (alone) - finds all files matching PContent.ipdl"
    )]
    path: Option<String>,

    #[arg(
        short = 'C',
        long,
        default_value_t = false,
        help = "Enable case-sensitive search"
    )]
    case: bool,

    #[arg(
        short,
        long,
        default_value_t = false,
        help = "Enable regular expression search",
        long_help = "Enable regular expression search mode.\nAllows using regex patterns in the query string.\nExample: '^Audio.*' matches identifiers starting with 'Audio'"
    )]
    regexp: bool,

    #[arg(
        short,
        long,
        default_value_t = 50,
        help = "Maximum number of results to display"
    )]
    limit: usize,

    #[arg(
        long,
        help = "Fetch and display the contents of a specific file",
        long_help = "Fetch and display the contents of a specific file from the repository.\nProvide the file path relative to the repository root.\nExample: --get-file dom/media/AudioStream.h"
    )]
    get_file: Option<String>,

    #[arg(
        long,
        help = "Line range for --get-file (e.g., 10-20, 10, 10-)",
        long_help = "Specify line range when using --get-file.\nFormats:\n  --lines 10-20  (lines 10 through 20)\n  --lines 10     (just line 10)\n  --lines 10-    (from line 10 to end)\n  --lines -20    (from start to line 20)\nExample: --get-file dom/media/AudioStream.h --lines 100-150"
    )]
    lines: Option<String>,

    #[arg(
        long,
        help = "Number of context lines to show around matches",
        long_help = "Show N lines of context above and below each match.\nOnly works with text: or re: queries.\nExample: --context 3"
    )]
    context: Option<usize>,

    #[arg(
        long,
        help = "Search by mangled symbol name (or plain name for C functions)",
        long_help = "Search by mangled symbol name using searchfox's symbol: query.\nMangled names are returned by --calls-from, --calls-to, and --calls-between.\nC functions are unmangled, so their plain name works directly.\nFor C++/JS use --id instead.\nExample: --symbol '_ZN7mozilla3dom24BaseAudioContext_BindingL10createGainE...'\nExample: --symbol 'PR_Sleep'  (C function, no mangling)"
    )]
    symbol: Option<String>,

    #[arg(
        long,
        help = "Find exact identifier matches",
        long_help = "Search for exact identifier matches (not prefix-based).\nUses id: query syntax internally.\nExample: --id main"
    )]
    id: Option<String>,

    #[arg(
        long,
        help = "Find and display the definition of a symbol",
        long_help = "Find the definition of a symbol using searchfox's structured data.\nSearches for symbol definitions and class/struct declarations.\nDisplays the complete method/function body or class declaration.\nExample: --define 'AudioContext::CreateGain' or --define 'AudioContext'"
    )]
    define: Option<String>,

    #[arg(
        long,
        help = "Enable request logging with timing and size information",
        long_help = "Log all HTTP requests made to searchfox with detailed timing information:\n- Request start/end timestamps\n- Response size and duration\n- Network latency measurement via ping\nUseful for performance analysis and infrastructure planning"
    )]
    log_requests: bool,

    #[arg(
        long = "no-cache",
        help = "Disable cache reads and writes for this invocation"
    )]
    no_cache: bool,

    #[arg(
        long = "force-refetch",
        help = "Bypass cached file content and fetch fresh content from searchfox"
    )]
    force_refetch: bool,

    #[arg(
        long = "clear-cache",
        help = "Delete the on-disk cache database and exit"
    )]
    clear_cache: bool,

    #[arg(
        long = "cpp",
        help = "Filter results to C++ files only",
        long_help = "Filter results to C++ files only (.cc, .cpp, .h, .hh, .hpp)"
    )]
    cpp: bool,

    #[arg(
        long = "c",
        help = "Filter results to C files only",
        long_help = "Filter results to C files only (.c, .h)"
    )]
    c_lang: bool,

    #[arg(
        long = "webidl",
        help = "Filter results to WebIDL files only",
        long_help = "Filter results to WebIDL files only (.webidl)"
    )]
    webidl: bool,

    #[arg(
        long = "js",
        help = "Filter results to JavaScript files only",
        long_help = "Filter results to JavaScript files only (.js, .mjs, .ts, .cjs, .jsx, .tsx)"
    )]
    js: bool,

    #[arg(
        long = "java",
        visible_aliases = ["kt"],
        help = "Filter results to Java/Kotlin files only",
        long_help = "Filter results to Java/Kotlin files only (.java, .kt)"
    )]
    java: bool,

    #[arg(
        long = "calls-from",
        help = "Find functions called by the specified symbol",
        long_help = "Search for functions called by the specified symbol using call graph analysis.\nExample: --calls-from 'mozilla::dom::AudioContext::CreateGain'"
    )]
    calls_from: Option<String>,

    #[arg(
        long = "calls-to",
        help = "Find functions that call the specified symbol",
        long_help = "Search for functions that call the specified symbol using call graph analysis.\nExample: --calls-to 'mozilla::dom::AudioContext::CreateGain'"
    )]
    calls_to: Option<String>,

    #[arg(
        long = "calls-between",
        help = "Find function calls between two symbols or classes",
        long_help = "Find function calls between two symbols or classes.\nExample: --calls-between 'AudioContext,AudioNode'"
    )]
    calls_between: Option<String>,

    #[arg(
        long = "depth",
        default_value_t = 1,
        help = "Set traversal depth for call graph searches",
        long_help = "Set the depth of traversal for call graph searches. Higher values show more indirect calls.\nDefault is 1. Example: --depth 3"
    )]
    depth: u32,

    #[arg(
        long = "field-layout",
        visible_aliases = ["class-layout", "struct-layout"],
        help = "Display the field layout of a C++ class or struct",
        long_help = "Display the field layout of a C++ class or struct, showing size and offset information.\nExample: --field-layout 'soundtouch::SoundTouch' or --field-layout 'mozilla::dom::AudioContext'"
    )]
    field_layout: Option<String>,

    #[arg(
        long = "exclude-tests",
        help = "Exclude test files from results",
        conflicts_with_all = ["only_tests", "only_generated", "only_normal"]
    )]
    exclude_tests: bool,

    #[arg(
        long = "exclude-generated",
        help = "Exclude generated files from results",
        conflicts_with_all = ["only_tests", "only_generated", "only_normal"]
    )]
    exclude_generated: bool,

    #[arg(
        long = "only-tests",
        help = "Show only test files",
        conflicts_with_all = ["exclude_tests", "exclude_generated", "only_generated", "only_normal"]
    )]
    only_tests: bool,

    #[arg(
        long = "only-generated",
        help = "Show only generated files",
        conflicts_with_all = ["exclude_tests", "exclude_generated", "only_tests", "only_normal"]
    )]
    only_generated: bool,

    #[arg(
        long = "only-normal",
        help = "Show only normal (non-test, non-generated) files",
        conflicts_with_all = ["exclude_tests", "exclude_generated", "only_tests", "only_generated"]
    )]
    only_normal: bool,

    #[arg(
        long = "can-gc",
        help = "Check if a function can trigger garbage collection",
        long_help = "Check whether a function can trigger garbage collection (GC) in SpiderMonkey.\nShows 'can GC' or 'cannot GC', and the GC call path when applicable.\nOnly works for C++ functions analyzed by the GC hazard analysis.\nExample: --can-gc 'mozilla::dom::AudioContext::CreateGain'"
    )]
    can_gc: Option<String>,

    #[arg(
        long = "function-at",
        help = "Show which function/class contains the given line",
        long_help = "Show which function or class contains a specific source line.\nFormat: path:line (e.g. dom/media/AudioStream.cpp:42)\nOutputs nesting context from innermost to outermost.\nWorks across all languages supported by searchfox."
    )]
    function_at: Option<String>,

    #[arg(
        long = "blame",
        default_value_t = false,
        help = "Show blame/history info for results",
        long_help = "Augment query results with blame information.\nShows commit hash, author, date, and bug for each result line.\nWorks with --define, --symbol, -q, --get-file, etc."
    )]
    blame: bool,

    #[arg(
        long = "link",
        default_value_t = false,
        help = "Output searchfox links to results instead of content",
        conflicts_with = "permalink"
    )]
    link: bool,

    #[arg(
        long = "permalink",
        default_value_t = false,
        help = "Output searchfox permalinks (with commit hash) to results instead of content",
        conflicts_with = "link"
    )]
    permalink: bool,
}

fn is_llm_environment() -> bool {
    std::env::var("CLAUDECODE").is_ok()
        || std::env::var("CODEX_SANDBOX").is_ok()
        || std::env::var("GEMINI_CLI").is_ok()
        || std::env::var("OPENCODE").is_ok()
}

fn print_llm_help() {
    print!(
        r#"searchfox-cli: Mozilla code search
-q <Q> query|-p <P> path filter|-C case|-r regex|-l <N> limit(50)|--context <N>
--symbol <mangled> (from calls-to/from output)|--id <ID> identifier|--define <S> full definition
--get-file <F> [--lines <R>] R=10-20|10|10-|-20
--calls-from <S>|--calls-to <S>|--calls-between <A,B> [--depth <N>]
--can-gc <S> check if function can trigger GC
--function-at <path:line> show which function/class contains a line
--field-layout <C> C++ class memory layout
--cpp|--c|--webidl|--js|--java/--kt file type filters
--exclude-tests|--exclude-generated|--only-tests|--only-generated|--only-normal
--no-cache disable reads/writes|--force-refetch bypass cached file content|--clear-cache delete cache db
-R <repo> mozilla-central(default)|autoland|mozilla-beta|mozilla-release|mozilla-esr*|comm-central
--blame commit info|--log-requests
--link output searchfox links|--permalink output links with commit hash
Ex: -q AudioStream|-q '^Audio.*' -r|-q AudioStream -p ^dom/media --cpp|--get-file dom/media/AudioStream.h --force-refetch
Ex: --define 'Cls::Method'|--calls-from 'Cls::Method' --depth 2|--field-layout 'ns::Cls'
Ex: --define 'AudioContext::AudioContext' --link|--clear-cache
"#
    );
}

#[tokio::main]
async fn main() -> Result<()> {
    #[cfg(unix)]
    unsafe {
        libc::signal(libc::SIGPIPE, libc::SIG_DFL);
    }
    let version_checker = VersionChecker::new("searchfox-cli", env!("CARGO_PKG_VERSION"));
    version_checker.check_async();

    if std::env::args().any(|arg| arg == "--version" || arg == "-V") {
        println!("searchfox-cli {}", env!("CARGO_PKG_VERSION"));
        version_checker.print_warning_sync();
        return Ok(());
    }

    if is_llm_environment() && std::env::args().any(|arg| arg == "--help" || arg == "-h") {
        print_llm_help();
        version_checker.print_warning();
        return Ok(());
    }

    let mut builder = env_logger::Builder::from_default_env();
    if std::env::var("RUST_LOG").is_err() {
        builder.filter_level(log::LevelFilter::Error);
    }
    builder.init();
    let args = Args::parse();

    if args.clear_cache {
        let removed = searchfox_lib::cache::clear()?;
        if removed {
            println!("Cache cleared.");
        } else {
            println!("Cache already empty.");
        }
        version_checker.print_warning();
        return Ok(());
    }

    let mut client = SearchfoxClient::new(args.repo.clone(), args.log_requests)?;
    client.set_cache_enabled(!args.no_cache);
    client.set_force_refetch(args.force_refetch);

    if args.log_requests {
        eprintln!("=== REQUEST LOGGING ENABLED ===");
        if let Err(e) = client.ping().await {
            eprintln!("[PING] Warning: Could not ping searchfox.org: {e}");
        }
        eprintln!("================================");
    }

    let category_filter = if args.only_tests {
        CategoryFilter::OnlyTests
    } else if args.only_generated {
        CategoryFilter::OnlyGenerated
    } else if args.only_normal {
        CategoryFilter::OnlyNormal
    } else if args.exclude_tests && args.exclude_generated {
        CategoryFilter::ExcludeTestsAndGenerated
    } else if args.exclude_tests {
        CategoryFilter::ExcludeTests
    } else if args.exclude_generated {
        CategoryFilter::ExcludeGenerated
    } else {
        CategoryFilter::All
    };

    let search_options = SearchOptions {
        query: args.query.clone(),
        path: args.path.clone(),
        case: args.case,
        regexp: args.regexp,
        limit: args.limit,
        context: args.context,
        symbol: args.symbol.clone(),
        id: args.id.clone(),
        lang: {
            let mut langs = Vec::new();
            if args.cpp {
                langs.push(searchfox_lib::Lang::Cpp);
            }
            if args.c_lang {
                langs.push(searchfox_lib::Lang::C);
            }
            if args.webidl {
                langs.push(searchfox_lib::Lang::WebIdl);
            }
            if args.js {
                langs.push(searchfox_lib::Lang::Js);
            }
            if args.java {
                langs.push(searchfox_lib::Lang::Java);
            }
            langs
        },
        category_filter,
    };

    if let Some(symbol) = &args.define {
        if args.link || args.permalink {
            let hash = if args.permalink {
                Some(client.get_head_hash().await?)
            } else {
                None
            };

            let file_locations = client
                .find_symbol_locations(symbol, args.path.as_deref(), &search_options)
                .await?;

            let is_ctor = symbol.rfind("::").is_some_and(|pos| {
                let class_part = &symbol[..pos];
                let method_part = &symbol[pos + 2..];
                let class_name = class_part.split("::").last().unwrap_or(class_part);
                class_name == method_part
            });
            let context_lines = if is_ctor { 2 } else { 10 };

            for (file_path, line_number) in &file_locations {
                if let Ok(context) = client
                    .get_definition_context(file_path, *line_number, context_lines, Some(symbol))
                    .await
                {
                    if let Some((start, end)) = extract_line_range_from_output(&context) {
                        println!(
                            "{}",
                            generate_link(&client.repo, file_path, start, end, hash.as_deref())
                        );
                    }
                }
            }
        } else {
            let result = client
                .find_and_display_definition(symbol, args.path.as_deref(), &search_options)
                .await?;
            if !result.is_empty() {
                if args.blame {
                    let file_locations = client
                        .find_symbol_locations(symbol, args.path.as_deref(), &search_options)
                        .await?;

                    if let Some((file_path, _)) = file_locations.first() {
                        let line_numbers = extract_line_numbers_from_definition(&result);

                        if !line_numbers.is_empty() {
                            let blame_map =
                                client.get_blame_for_lines(file_path, &line_numbers).await?;
                            print_definition_with_grouped_blame(&result, &blame_map);
                        } else {
                            println!("{}", result);
                        }
                    } else {
                        println!("{}", result);
                    }
                } else {
                    println!("{}", result);
                }
            }
        }
    } else if let Some(path) = &args.get_file {
        if args.link || args.permalink {
            let hash = if args.permalink {
                Some(client.get_head_hash().await?)
            } else {
                None
            };
            let (start, end) = if let Some(ref range) = args.lines {
                let content = client.get_file(path).await?;
                parse_line_range(range, content.lines().count())?
            } else {
                (0, 0)
            };
            println!(
                "{}",
                generate_link(&client.repo, path, start, end, hash.as_deref())
            );
        } else {
            let content = client.get_file(path).await?;

            let (start_line, end_line) = if let Some(ref range) = args.lines {
                parse_line_range(range, content.lines().count())?
            } else {
                (1, content.lines().count())
            };

            let filtered_lines: Vec<(usize, &str)> = content
                .lines()
                .enumerate()
                .map(|(idx, line)| (idx + 1, line))
                .filter(|(line_num, _)| *line_num >= start_line && *line_num <= end_line)
                .collect();

            if args.blame {
                let line_numbers: Vec<usize> = filtered_lines.iter().map(|(num, _)| *num).collect();
                let blame_map = client.get_blame_for_lines(path, &line_numbers).await?;
                let mut formatted_content = String::new();
                for (line_num, line) in filtered_lines {
                    formatted_content.push_str(&format!("    {:4}: {}\n", line_num, line));
                }
                print_definition_with_grouped_blame(&formatted_content, &blame_map);
            } else {
                for (line_num, line) in filtered_lines {
                    if args.lines.is_some() {
                        println!("{:4}: {}", line_num, line);
                    } else {
                        println!("{}", line);
                    }
                }
            }
        }
    } else if args.calls_from.is_some() || args.calls_to.is_some() || args.calls_between.is_some() {
        let query_text = if let Some(ref symbol) = args.calls_from {
            format!("calls-from:'{}' depth:{}", symbol, args.depth)
        } else if let Some(ref symbol) = args.calls_to {
            format!("calls-to:'{}' depth:{}", symbol, args.depth)
        } else if let Some(ref between) = args.calls_between {
            let parts: Vec<&str> = between.split(',').collect();
            if parts.len() == 2 {
                format!(
                    "calls-between-source:'{}' calls-between-target:'{}' depth:{}",
                    parts[0].trim(),
                    parts[1].trim(),
                    args.depth
                )
            } else {
                format!("calls-between:'{}' depth:{}", between, args.depth)
            }
        } else {
            String::from("call-graph query")
        };

        let query = CallGraphQuery {
            calls_from: args.calls_from,
            calls_to: args.calls_to,
            calls_between: args.calls_between.map(|s| {
                let parts: Vec<&str> = s.split(',').collect();
                if parts.len() == 2 {
                    (parts[0].trim().to_string(), parts[1].trim().to_string())
                } else {
                    (s.clone(), String::new())
                }
            }),
            depth: args.depth,
        };

        let result = client.search_call_graph(&query).await?;
        if result.as_object().is_some_and(|o| !o.is_empty())
            || result.as_array().is_some_and(|a| !a.is_empty())
        {
            if std::env::var("DEBUG_JSON").is_ok() {
                println!("{}", serde_json::to_string_pretty(&result)?);
            } else {
                let markdown = format_call_graph_markdown(&query_text, &result);
                print!("{}", markdown);
            }
        } else {
            println!("No call graph results found for the query.");
        }
    } else if let Some(ref location) = args.function_at {
        let (path, line) = parse_path_line(location)?;
        let contexts = client.get_function_at_line(&path, line).await?;
        if contexts.is_empty() {
            println!(
                "{}:{}: (file scope — not inside any function or class)",
                path, line
            );
        } else {
            print_nesting_contexts(&path, line, &contexts);
        }
    } else if let Some(symbol) = &args.can_gc {
        let results = client.get_gc_info(symbol).await?;
        if results.is_empty() {
            println!(
                "No GC information found for '{}'. GC analysis is only available for C++ functions.",
                symbol
            );
        } else {
            for info in &results {
                print_gc_info(info);
            }
        }
    } else if let Some(class_name) = &args.field_layout {
        let query = FieldLayoutQuery {
            class_name: class_name.clone(),
        };

        let result = client.search_field_layout(&query).await?;
        if result.as_object().is_some_and(|o| !o.is_empty())
            || result.as_array().is_some_and(|a| !a.is_empty())
        {
            if std::env::var("DEBUG_JSON").is_ok() {
                println!("{}", serde_json::to_string_pretty(&result)?);
            } else {
                let formatted = format_field_layout(class_name, &result);
                print!("{}", formatted);
            }
        } else {
            println!("No field layout information found for '{}'.", class_name);
            println!("Note: Field layout is only available for C++ classes and structs.");
        }
    } else if args.query.is_some()
        || args.symbol.is_some()
        || args.id.is_some()
        || args.path.is_some()
    {
        let results = client.search(&search_options).await?;

        if args.link || args.permalink {
            let hash = if args.permalink {
                Some(client.get_head_hash().await?)
            } else {
                None
            };
            for result in &results {
                println!(
                    "{}",
                    generate_link(
                        &client.repo,
                        &result.path,
                        result.line_number,
                        result.line_number,
                        hash.as_deref()
                    )
                );
            }
        } else {
            let mut count = 0;
            if args.blame {
                // Group results by file for efficient blame fetching
                let mut results_by_file: HashMap<String, Vec<(usize, String)>> = HashMap::new();
                for result in &results {
                    if result.line_number > 0 {
                        results_by_file
                            .entry(result.path.clone())
                            .or_default()
                            .push((result.line_number, result.line.clone()));
                    }
                }

                // Fetch and display results with blame
                for (path, lines) in results_by_file {
                    let line_numbers: Vec<usize> = lines.iter().map(|(ln, _)| *ln).collect();
                    let blame_map = client.get_blame_for_lines(&path, &line_numbers).await?;

                    for (line_number, line_text) in lines {
                        println!("{}:{}: {}", path, line_number, line_text);

                        if let Some(blame_info) = blame_map.get(&line_number) {
                            if let Some(ref commit_info) = blame_info.commit_info {
                                let parsed = parse_commit_header(&commit_info.header);
                                let short_hash = &blame_info.commit_hash[..8];
                                if let Some(bug) = parsed.bug_number {
                                    println!(
                                        "  [{}] Bug {}: {} ({}, {})",
                                        short_hash, bug, parsed.message, parsed.author, parsed.date
                                    );
                                } else {
                                    println!(
                                        "  [{}] {} ({}, {})",
                                        short_hash, parsed.message, parsed.author, parsed.date
                                    );
                                }
                            }
                        }
                        count += 1;
                    }
                }
            } else {
                // Original output without blame
                for result in &results {
                    if result.line_number == 0 {
                        println!("{}", result.path);
                    } else {
                        println!("{}:{}: {}", result.path, result.line_number, result.line);
                    }
                    count += 1;
                }
            }
            println!("Total matches: {count}");
        }
    } else {
        error!(
            "Either --query, --symbol, --id, --get-file, --define, --calls-from, --calls-to, --calls-between, --can-gc, or --path must be provided"
        );
        std::process::exit(1);
    }

    version_checker.print_warning();
    Ok(())
}

fn generate_link(
    repo: &str,
    path: &str,
    start_line: usize,
    end_line: usize,
    hash: Option<&str>,
) -> String {
    let fragment = if start_line == 0 {
        String::new()
    } else if start_line == end_line {
        format!("#{}", start_line)
    } else {
        format!("#{}-{}", start_line, end_line)
    };
    let rev_repo = searchfox_url_repo(repo);
    match hash {
        Some(h) => format!(
            "https://searchfox.org/{}/rev/{}/{}{}",
            rev_repo, h, path, fragment
        ),
        None => format!(
            "https://searchfox.org/{}/source/{}{}",
            rev_repo, path, fragment
        ),
    }
}

fn extract_line_range_from_output(output: &str) -> Option<(usize, usize)> {
    let numbers = extract_line_numbers_from_definition(output);
    match (numbers.first(), numbers.last()) {
        (Some(&start), Some(&end)) => Some((start, end)),
        _ => None,
    }
}

/// Extract line numbers from definition output
/// Lines are formatted like ">>> 469: code" or "   470: code"
fn extract_line_numbers_from_definition(output: &str) -> Vec<usize> {
    output
        .lines()
        .filter_map(|line| {
            // Look for pattern like ">>> 469:" or "   470:"
            let trimmed = line.trim_start();
            if trimmed.starts_with(">>>") || line.starts_with("   ") {
                // Extract the number after the marker
                let parts: Vec<&str> = trimmed.split(':').collect();
                if parts.len() >= 2 {
                    let num_part = parts[0].trim_start_matches(">>>").trim();
                    num_part.parse::<usize>().ok()
                } else {
                    None
                }
            } else {
                None
            }
        })
        .collect()
}

/// Parse line number from a formatted output line
/// Returns Some(line_number) if the line contains a line number marker
fn parse_line_number_from_output(line: &str) -> Option<usize> {
    let trimmed = line.trim_start();
    if trimmed.starts_with(">>>") || line.starts_with("   ") {
        let parts: Vec<&str> = trimmed.split(':').collect();
        if parts.len() >= 2 {
            let num_part = parts[0].trim_start_matches(">>>").trim();
            num_part.parse::<usize>().ok()
        } else {
            None
        }
    } else {
        None
    }
}

/// Parse line range string (e.g., "10-20", "10", "10-", "-20")
/// Returns (start_line, end_line) inclusive
fn parse_line_range(range: &str, total_lines: usize) -> Result<(usize, usize)> {
    let range = range.trim();

    if range.contains('-') {
        let parts: Vec<&str> = range.split('-').collect();
        if parts.len() != 2 {
            anyhow::bail!(
                "Invalid line range format: '{}'. Expected formats: 10-20, 10, 10-, -20",
                range
            );
        }

        let start = if parts[0].is_empty() {
            1
        } else {
            parts[0]
                .parse::<usize>()
                .map_err(|_| anyhow::anyhow!("Invalid start line number: '{}'", parts[0]))?
        };

        let end = if parts[1].is_empty() {
            total_lines
        } else {
            parts[1]
                .parse::<usize>()
                .map_err(|_| anyhow::anyhow!("Invalid end line number: '{}'", parts[1]))?
        };

        if start < 1 {
            anyhow::bail!("Start line must be >= 1");
        }
        if end > total_lines {
            anyhow::bail!("End line {} exceeds file length {}", end, total_lines);
        }
        if start > end {
            anyhow::bail!("Start line {} is greater than end line {}", start, end);
        }

        Ok((start, end))
    } else {
        // Single line number
        let line_num = range
            .parse::<usize>()
            .map_err(|_| anyhow::anyhow!("Invalid line number: '{}'", range))?;

        if line_num < 1 {
            anyhow::bail!("Line number must be >= 1");
        }
        if line_num > total_lines {
            anyhow::bail!("Line {} exceeds file length {}", line_num, total_lines);
        }

        Ok((line_num, line_num))
    }
}

/// Print definition with blame info, grouping consecutive lines with the same commit
fn print_definition_with_grouped_blame(
    definition: &str,
    blame_map: &HashMap<usize, searchfox_lib::BlameInfo>,
) {
    #[derive(Clone)]
    struct CommitRange {
        start_line: usize,
        end_line: usize,
        commit_hash: String,
        message: String,
    }

    let mut current_range: Option<CommitRange> = None;
    let mut pending_output: Vec<String> = Vec::new();

    for line in definition.lines() {
        pending_output.push(line.to_string());

        if let Some(line_num) = parse_line_number_from_output(line) {
            if let Some(blame_info) = blame_map.get(&line_num) {
                if let Some(ref commit_info) = blame_info.commit_info {
                    let parsed = parse_commit_header(&commit_info.header);
                    let short_hash = blame_info.commit_hash[..8].to_string();

                    let message = if let Some(bug) = parsed.bug_number {
                        format!("Bug {}: {}", bug, parsed.message)
                    } else {
                        parsed.message.clone()
                    };

                    // Check if this is the same commit as current range
                    if let Some(ref range) = current_range {
                        if range.commit_hash == short_hash {
                            // Extend current range
                            current_range = Some(CommitRange {
                                start_line: range.start_line,
                                end_line: line_num,
                                commit_hash: short_hash,
                                message,
                            });
                            continue;
                        } else {
                            // Different commit - flush current range
                            for output_line in &pending_output[..pending_output.len() - 1] {
                                println!("{}", output_line);
                            }
                            pending_output.clear();
                            pending_output.push(line.to_string());

                            // Print blame for previous range
                            if range.start_line == range.end_line {
                                println!(
                                    "         [{}] {} (line {})",
                                    range.commit_hash, range.message, range.start_line
                                );
                            } else {
                                println!(
                                    "         [{}] {} (lines {}-{})",
                                    range.commit_hash,
                                    range.message,
                                    range.start_line,
                                    range.end_line
                                );
                            }
                        }
                    }

                    // Start new range
                    current_range = Some(CommitRange {
                        start_line: line_num,
                        end_line: line_num,
                        commit_hash: short_hash,
                        message,
                    });
                }
            }
        }
    }

    // Flush remaining output
    for output_line in &pending_output {
        println!("{}", output_line);
    }

    // Print final range if exists
    if let Some(range) = current_range {
        if range.start_line == range.end_line {
            println!(
                "         [{}] {} (line {})",
                range.commit_hash, range.message, range.start_line
            );
        } else {
            println!(
                "         [{}] {} (lines {}-{})",
                range.commit_hash, range.message, range.start_line, range.end_line
            );
        }
    }
}

fn parse_path_line(s: &str) -> Result<(String, usize)> {
    let (path, line_str) = s
        .rsplit_once(':')
        .ok_or_else(|| anyhow::anyhow!("Expected path:line format, got '{}'", s))?;
    let line = line_str
        .parse::<usize>()
        .map_err(|_| anyhow::anyhow!("Invalid line number '{}' in '{}'", line_str, s))?;
    if line == 0 {
        anyhow::bail!("Line number must be >= 1");
    }
    Ok((path.to_string(), line))
}

fn print_nesting_contexts(path: &str, line: usize, contexts: &[NestingContext]) {
    println!("{}:{}", path, line);
    for (i, ctx) in contexts.iter().enumerate() {
        let indent = "  ".repeat(i);
        println!("{}in {}", indent, ctx.sym);
        if !ctx.pretty_line.is_empty() {
            println!("{}  {}", indent, ctx.pretty_line);
        }
    }
}

fn print_gc_info(info: &GcInfo) {
    if info.can_gc {
        println!("{}: can GC", info.pretty);
        if let Some(ref path) = info.gc_path {
            println!("  GC call path:");
            for line in path.lines() {
                println!("    > {}", line);
            }
        }
    } else {
        println!("{}: cannot GC", info.pretty);
    }
}
