mod cli;
mod commands;
mod display;
mod scoring;

use std::path::PathBuf;

use anyhow::Result;
use clap::{CommandFactory, Parser};
use rayon::ThreadPoolBuilder;

use colgrep::{
    acceleration::{apply_acceleration_mode, env_acceleration_mode, AccelerationMode},
    install_claude_code, install_codex, install_hermes, install_opencode, setup_signal_handler,
    uninstall_all, uninstall_claude_code, uninstall_codex, uninstall_hermes, uninstall_opencode,
};

use cli::{Cli, Commands};
use commands::search::{resolve_pool_factor, resolve_top_k};
use commands::{
    cmd_clear, cmd_config, cmd_init, cmd_reset_stats, cmd_search, cmd_session_hook, cmd_set_model,
    cmd_stats, cmd_status, cmd_task_hook, cmd_update, InitOptions,
};

fn main() -> Result<()> {
    // Set up Ctrl+C handler for graceful interruption during indexing
    // This is non-fatal if it fails (e.g., in environments without signal support)
    let _ = setup_signal_handler();

    init_global_rayon_pool();

    let cli = Cli::parse();
    let env_mode = env_acceleration_mode()?;
    let acceleration_mode = if cli.force_cpu {
        AccelerationMode::ForceCpu
    } else if cli.force_gpu {
        AccelerationMode::ForceGpu
    } else {
        env_mode
    };
    apply_acceleration_mode(acceleration_mode);

    // Handle global flags before subcommands
    if cli.install_claude_code {
        return install_claude_code();
    }

    if cli.uninstall_claude_code {
        return uninstall_claude_code();
    }

    if cli.install_opencode {
        return install_opencode();
    }

    if cli.uninstall_opencode {
        return uninstall_opencode();
    }

    if cli.install_codex {
        return install_codex();
    }

    if cli.uninstall_codex {
        return uninstall_codex();
    }

    if cli.install_hermes {
        return install_hermes();
    }

    if cli.uninstall_hermes {
        return uninstall_hermes();
    }

    if cli.uninstall {
        return uninstall_all();
    }

    if cli.session_hook {
        return cmd_session_hook();
    }

    if cli.task_hook {
        return cmd_task_hook();
    }

    if cli.stats {
        return cmd_stats();
    }

    if cli.reset_stats {
        return cmd_reset_stats();
    }

    // ONNX Runtime initialization is deferred to ensure_model_created() in index/mod.rs.
    // This lets us pick CPU vs GPU based on actual batch size (small batches < 300 units
    // use CPU to avoid ~25-30s CUDA library load). Commands that don't need the model
    // (Status, Clear, SetModel, Settings) skip ONNX entirely.
    let _ = &cli.command; // Suppress unused warning

    match cli.command {
        Some(Commands::Search {
            query,
            paths,
            top_k,
            model,
            json,
            recursive: _,
            include_patterns,
            files_only,
            show_content,
            context_lines,
            text_pattern,
            extended_regexp,
            fixed_strings,
            word_regexp,
            exclude_patterns,
            exclude_dirs,
            code_only,
            no_fts,
            alpha,
            no_pool,
            pool_factor,
            auto_confirm,
            static_batch,
        }) => {
            // If only -e pattern is given without a query, use the pattern as the query too
            let original_query = query.clone();
            let query = query.or_else(|| text_pattern.clone());
            if let Some(query) = query {
                // Helper to check if a string looks like a path
                let looks_like_path = |s: &str| {
                    s.starts_with('.')
                        || s.starts_with('/')
                        || s.starts_with('~')
                        || s.contains('/')
                        || s.contains('\\')
                };

                // Check if -e was provided and the "query" looks like a path
                // e.g., `colgrep search -e "pattern" ./src` parses as query="./src", paths=[]
                // We want: query="pattern", paths=["./src"]
                let (final_query, final_paths, final_text_pattern) = if text_pattern.is_some()
                    && original_query.is_some()
                    && looks_like_path(&query)
                {
                    // The "query" is actually a path - use text_pattern as query
                    let text_pattern_str = text_pattern.clone().unwrap();
                    let mut new_paths = paths;
                    new_paths.insert(0, PathBuf::from(&query)); // Add the misplaced "query" as first path
                    (text_pattern_str, new_paths, text_pattern)
                } else if text_pattern.is_none()
                    && !paths.is_empty()
                    && !paths[0].exists()
                    && !looks_like_path(&paths[0].to_string_lossy())
                {
                    // Check if first "path" is actually a semantic query
                    // e.g., `colgrep search "pattern" "semantic query"` should be interpreted as
                    // `colgrep search -e "pattern" "semantic query"`
                    // e.g., `colgrep search "pattern" "semantic query" ./src ./lib` should be interpreted as
                    // `colgrep search -e "pattern" "semantic query" ./src ./lib`
                    let path_str = paths[0].to_string_lossy().to_string();
                    let remaining_paths: Vec<PathBuf> = paths.into_iter().skip(1).collect();

                    // Use remaining paths if any exist, otherwise default to current directory
                    let actual_paths = if remaining_paths.is_empty() {
                        vec![PathBuf::from(".")]
                    } else {
                        remaining_paths
                    };

                    // Reinterpret: first arg becomes -e pattern, second becomes semantic query
                    (path_str, actual_paths, Some(query))
                } else if text_pattern.is_none()
                    && !paths.is_empty()
                    && !paths[0].exists()
                    && looks_like_path(&paths[0].to_string_lossy())
                {
                    // First path looks like a path but doesn't exist - keep as-is for error reporting
                    (query, paths, text_pattern)
                } else {
                    // Normal case: use paths as-is
                    let final_paths = if paths.is_empty() {
                        vec![PathBuf::from(".")]
                    } else {
                        paths
                    };
                    (query, final_paths, text_pattern)
                };

                // Default k: 10 if -n is provided, 15 otherwise
                let default_k = if context_lines.is_some() { 10 } else { 15 };

                cmd_search(
                    &final_query,
                    &final_paths,
                    resolve_top_k(top_k, default_k),
                    model.as_deref(),
                    json,
                    &include_patterns,
                    files_only,
                    show_content,
                    context_lines, // Pass raw Option to detect explicit -n flag
                    final_text_pattern.as_deref(),
                    extended_regexp,
                    fixed_strings,
                    word_regexp,
                    &exclude_patterns,
                    &exclude_dirs,
                    code_only,
                    no_fts,
                    alpha,
                    resolve_pool_factor(pool_factor, no_pool),
                    auto_confirm,
                    static_batch,
                )
            } else {
                // No query or text_pattern provided - show help
                Cli::command().print_help()?;
                println!();
                Ok(())
            }
        }
        Some(Commands::Init {
            path,
            model,
            no_pool,
            pool_factor,
            auto_confirm,
            batch_size,
            encode_batch_size,
            index_chunk_size,
            static_batch,
        }) => cmd_init(
            &path,
            InitOptions {
                cli_model: model.as_deref(),
                no_pool,
                pool_factor,
                auto_confirm,
                batch_size,
                encode_batch_size,
                index_chunk_size,
                static_batch,
            },
        ),
        Some(Commands::Update) => cmd_update(),
        Some(Commands::Status { path }) => cmd_status(&path),
        Some(Commands::Clear { path, all }) => cmd_clear(&path, all),
        Some(Commands::SetModel { model }) => cmd_set_model(&model),
        Some(Commands::Settings {
            default_k,
            default_n,
            fp32,
            int8,
            pool_factor,
            parallel_sessions,
            batch_size,
            max_recursion_depth,
            verbose,
            no_verbose,
            relative_paths,
            no_relative_paths,
            hybrid_search,
            no_hybrid_search,
            alpha,
            add_ignore,
            remove_ignore,
            add_force_include,
            remove_force_include,
            clear_ignore,
            clear_force_include,
        }) => cmd_config(
            default_k,
            default_n,
            fp32,
            int8,
            pool_factor,
            parallel_sessions,
            batch_size,
            max_recursion_depth,
            verbose,
            no_verbose,
            relative_paths,
            no_relative_paths,
            hybrid_search,
            no_hybrid_search,
            alpha,
            add_ignore,
            remove_ignore,
            add_force_include,
            remove_force_include,
            clear_ignore,
            clear_force_include,
        ),
        None => {
            // Default: run search if query is provided
            // If only -e pattern is given without a query, use the pattern as the query too
            let original_query = cli.query.clone();
            let query = cli.query.or_else(|| cli.text_pattern.clone());
            if let Some(query) = query {
                // Helper to check if a string looks like a path
                let looks_like_path = |s: &str| {
                    s.starts_with('.')
                        || s.starts_with('/')
                        || s.starts_with('~')
                        || s.contains('/')
                        || s.contains('\\')
                };

                // Check if -e was provided and the "query" looks like a path
                // e.g., `colgrep -e "pattern" ./src` parses as query="./src", paths=[]
                // We want: query="pattern", paths=["./src"]
                let (final_query, final_paths, final_text_pattern) = if cli.text_pattern.is_some()
                    && original_query.is_some()
                    && looks_like_path(&query)
                {
                    // The "query" is actually a path - use text_pattern as query
                    let text_pattern = cli.text_pattern.clone().unwrap();
                    let mut paths = cli.paths;
                    paths.insert(0, PathBuf::from(&query)); // Add the misplaced "query" as first path
                    (text_pattern, paths, cli.text_pattern)
                } else if cli.text_pattern.is_none()
                    && !cli.paths.is_empty()
                    && !cli.paths[0].exists()
                    && !looks_like_path(&cli.paths[0].to_string_lossy())
                {
                    // Check if first "path" is actually a semantic query
                    // e.g., `colgrep "pattern" "semantic query"` should be interpreted as
                    // `colgrep -e "pattern" "semantic query"`
                    // e.g., `colgrep "pattern" "semantic query" ./src ./lib` should be interpreted as
                    // `colgrep -e "pattern" "semantic query" ./src ./lib`
                    let path_str = cli.paths[0].to_string_lossy().to_string();
                    let remaining_paths: Vec<PathBuf> = cli.paths.into_iter().skip(1).collect();

                    // Use remaining paths if any exist, otherwise default to current directory
                    let actual_paths = if remaining_paths.is_empty() {
                        vec![PathBuf::from(".")]
                    } else {
                        remaining_paths
                    };

                    // Reinterpret: first arg becomes -e pattern, second becomes semantic query
                    (path_str, actual_paths, Some(query))
                } else if cli.text_pattern.is_none()
                    && !cli.paths.is_empty()
                    && !cli.paths[0].exists()
                    && looks_like_path(&cli.paths[0].to_string_lossy())
                {
                    // First path looks like a path but doesn't exist - keep as-is for error reporting
                    (query, cli.paths, cli.text_pattern)
                } else {
                    // Normal case: use paths as-is
                    let paths = if cli.paths.is_empty() {
                        vec![PathBuf::from(".")]
                    } else {
                        cli.paths
                    };
                    (query, paths, cli.text_pattern)
                };

                // Default k: 10 if -n is provided, 15 otherwise
                let default_k = if cli.context_lines.is_some() { 10 } else { 15 };

                cmd_search(
                    &final_query,
                    &final_paths,
                    resolve_top_k(cli.top_k, default_k),
                    cli.model.as_deref(),
                    cli.json,
                    &cli.include_patterns,
                    cli.files_only,
                    cli.show_content,
                    cli.context_lines, // Pass raw Option to detect explicit -n flag
                    final_text_pattern.as_deref(),
                    cli.extended_regexp,
                    cli.fixed_strings,
                    cli.word_regexp,
                    &cli.exclude_patterns,
                    &cli.exclude_dirs,
                    cli.code_only,
                    cli.no_fts,
                    cli.alpha,
                    resolve_pool_factor(cli.pool_factor, cli.no_pool),
                    cli.auto_confirm,
                    false,
                )
            } else {
                // No query provided - show help
                Cli::command().print_help()?;
                println!();
                Ok(())
            }
        }
    }
}

fn init_global_rayon_pool() {
    if std::env::var_os("RAYON_NUM_THREADS").is_some() {
        return;
    }

    let available = std::thread::available_parallelism()
        .map(|p| p.get())
        .unwrap_or(4);
    let configured = available.saturating_sub(4).max(1);

    let _ = ThreadPoolBuilder::new()
        .num_threads(configured)
        .thread_name(|idx| format!("next-plaid-rayon-{idx}"))
        .build_global();
}
