//! Query command for cqs
//!
//! Executes semantic search queries.

use anyhow::{bail, Context, Result};

use cqs::parser::ChunkType;
use cqs::store::UnifiedResult;
use cqs::structural::Pattern;
use cqs::{reference, Embedder, HnswIndex, SearchFilter, Store};

use crate::cli::{display, find_project_root, signal, Cli};

/// Execute a semantic search query and display results
pub(crate) fn cmd_query(cli: &Cli, query: &str) -> Result<()> {
    let _span = tracing::info_span!("cmd_query", query_len = query.len()).entered();

    let root = find_project_root();
    let cq_dir = root.join(".cq");
    let index_path = cq_dir.join("index.db");

    if !index_path.exists() {
        bail!("Index not found. Run 'cq init && cq index' first.");
    }

    let store = Store::open(&index_path)?;
    let embedder = Embedder::new()?;

    let query_embedding = embedder.embed_query(query)?;

    let languages = match &cli.lang {
        Some(l) => Some(vec![l.parse().context(
            "Invalid language. Valid: rust, python, typescript, javascript, go, c, java",
        )?]),
        None => None,
    };

    let chunk_types = match &cli.chunk_type {
        Some(types) => {
            let parsed: Result<Vec<ChunkType>, _> = types.iter().map(|t| t.parse()).collect();
            Some(parsed.context(
                "Invalid chunk type. Valid: function, method, class, struct, enum, trait, interface, constant",
            )?)
        }
        None => None,
    };

    let filter = SearchFilter {
        languages,
        chunk_types,
        path_pattern: cli.path.clone(),
        name_boost: cli.name_boost,
        query_text: query.to_string(),
        enable_rrf: true, // Enable RRF hybrid search by default
        note_weight: cli.note_weight,
    };

    // Load vector index for O(log n) search
    let index: Option<Box<dyn cqs::index::VectorIndex>> = {
        #[cfg(feature = "gpu-search")]
        {
            // Priority: CAGRA (GPU, large indexes) > HNSW (CPU) > brute-force
            //
            // CAGRA rebuilds index each CLI invocation (~1s for 474 vectors).
            // Only worth it when search time savings exceed rebuild cost.
            // Threshold: 5000 vectors (where CAGRA search is ~10x faster than HNSW)
            const CAGRA_THRESHOLD: u64 = 5000;
            let chunk_count = store.chunk_count().unwrap_or(0);
            if chunk_count >= CAGRA_THRESHOLD && cqs::cagra::CagraIndex::gpu_available() {
                match cqs::cagra::CagraIndex::build_from_store(&store) {
                    Ok(idx) => {
                        tracing::info!("Using CAGRA GPU index ({} vectors)", idx.len());
                        Some(Box::new(idx) as Box<dyn cqs::index::VectorIndex>)
                    }
                    Err(e) => {
                        tracing::warn!("Failed to build CAGRA index, falling back to HNSW: {}", e);
                        HnswIndex::try_load(&cq_dir)
                    }
                }
            } else {
                if chunk_count < CAGRA_THRESHOLD {
                    tracing::debug!(
                        "Index too small for CAGRA ({} < {}), using HNSW",
                        chunk_count,
                        CAGRA_THRESHOLD
                    );
                } else {
                    tracing::debug!("GPU not available, using HNSW");
                }
                HnswIndex::try_load(&cq_dir)
            }
        }
        #[cfg(not(feature = "gpu-search"))]
        {
            HnswIndex::try_load(&cq_dir)
        }
    };

    // Load references for multi-index search
    let config = cqs::config::Config::load(&root);
    let references = reference::load_references(&config.references);

    // Parse pattern filter if specified
    let pattern: Option<Pattern> = cli
        .pattern
        .as_ref()
        .map(|p| p.parse())
        .transpose()
        .context("Invalid pattern")?;

    // Use unified search with vector index if available
    let results = store.search_unified_with_index(
        &query_embedding,
        &filter,
        // Fetch more results if pattern filter will reduce them
        if pattern.is_some() {
            cli.limit * 3
        } else {
            cli.limit
        },
        cli.threshold,
        index.as_deref(),
    )?;

    // Apply structural pattern filter if specified
    let results = if let Some(ref pat) = pattern {
        let mut filtered: Vec<UnifiedResult> = results
            .into_iter()
            .filter(|r| match r {
                UnifiedResult::Code(sr) => {
                    pat.matches(&sr.chunk.content, &sr.chunk.name, Some(sr.chunk.language))
                }
                UnifiedResult::Note(_) => false, // Pattern filter only applies to code
            })
            .collect();
        filtered.truncate(cli.limit);
        filtered
    } else {
        results
    };

    // Fast path: no references configured
    if references.is_empty() {
        if results.is_empty() {
            if cli.json {
                println!(r#"{{"results":[],"query":"{}","total":0}}"#, query);
            } else {
                println!("No results found.");
            }
            std::process::exit(signal::ExitCode::NoResults as i32);
        }

        if cli.json {
            display::display_unified_results_json(&results, query)?;
        } else {
            display::display_unified_results(&results, &root, cli.no_content, cli.context)?;
        }
        return Ok(());
    }

    // Multi-index search: search each reference
    let mut ref_results = Vec::new();
    for ref_idx in &references {
        let r = reference::search_reference(
            ref_idx,
            &query_embedding,
            &filter,
            cli.limit,
            cli.threshold,
        );
        if !r.is_empty() {
            ref_results.push((ref_idx.name.clone(), r));
        }
    }

    let tagged = reference::merge_results(results, ref_results, cli.limit);

    if tagged.is_empty() {
        if cli.json {
            println!(r#"{{"results":[],"query":"{}","total":0}}"#, query);
        } else {
            println!("No results found.");
        }
        std::process::exit(signal::ExitCode::NoResults as i32);
    }

    if cli.json {
        display::display_tagged_results_json(&tagged, query)?;
    } else {
        display::display_tagged_results(&tagged, &root, cli.no_content, cli.context)?;
    }

    Ok(())
}
