#![cfg_attr(coverage_nightly, coverage(off))]
// Hybrid Search Engine with RRF
// PMAT-SEARCH-005: Combine keyword (ripgrep) and vector search
//
// GREEN Phase: Full implementation

use super::search_engine::{SearchQuery, SearchResult, SemanticSearchEngine};
use std::collections::HashMap;
use std::path::Path;
use std::process::Command;
use std::sync::Arc;

/// Search mode for hybrid engine
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HybridSearchMode {
    KeywordOnly,
    VectorOnly,
    Hybrid,
}

/// Hybrid search query
#[derive(Debug, Clone)]
pub struct HybridSearchQuery {
    pub query: String,
    pub mode: HybridSearchMode,
    pub keyword_weight: f64,
    pub vector_weight: f64,
    pub language_filter: Option<String>,
    pub file_pattern: Option<String>,
    pub limit: usize,
}

/// Hybrid search result
#[derive(Debug, Clone)]
pub struct HybridSearchResult {
    pub file_path: String,
    pub chunk_name: String,
    pub chunk_type: String,
    pub language: String,
    pub start_line: usize,
    pub end_line: usize,
    pub keyword_score: f64,
    pub vector_score: f64,
    pub hybrid_score: f64,
    pub snippet: String,
}

/// Keyword match from ripgrep
#[derive(Debug, Clone)]
pub struct KeywordMatch {
    file_path: String,
    line_number: usize,
    content: String,
}

/// Hybrid search engine combining keyword and vector search
pub struct HybridSearchEngine {
    semantic_engine: Arc<SemanticSearchEngine>,
    search_root: std::path::PathBuf,
}

impl HybridSearchEngine {
    /// Create new hybrid search engine with local embeddings
    ///
    /// # Arguments
    /// * `db_path` - Vector database path
    /// * `search_root` - Root directory for keyword search
    ///
    /// # Note
    /// Uses pure Rust TF-IDF embeddings via aprender.
    /// No external API keys or internet connection required.
    pub async fn new(db_path: &str, search_root: &Path) -> Result<Self, String> {
        let semantic_engine = SemanticSearchEngine::new(db_path).await?;

        Ok(Self {
            semantic_engine: Arc::new(semantic_engine),
            search_root: search_root.to_path_buf(),
        })
    }

    /// Create new hybrid search engine (backward compatible - ignores api_key)
    #[deprecated(note = "Use new() without api_key - local embeddings don't require API keys")]
    pub async fn new_with_key(_api_key: &str, db_path: &str, search_root: &Path) -> Result<Self, String> {
        Self::new(db_path, search_root).await
    }

    /// Search using hybrid mode
    ///
    /// # Arguments
    /// * `query` - Search query
    ///
    /// # Returns
    /// Ranked hybrid search results
    pub async fn search(
        &self,
        query: &HybridSearchQuery,
    ) -> Result<Vec<HybridSearchResult>, String> {
        if query.query.trim().is_empty() {
            return Err("Query cannot be empty".to_string());
        }

        match query.mode {
            HybridSearchMode::KeywordOnly => self.keyword_only_search(query).await,
            HybridSearchMode::VectorOnly => self.vector_only_search(query).await,
            HybridSearchMode::Hybrid => self.hybrid_search(query).await,
        }
    }

    /// Keyword-only search using ripgrep
    async fn keyword_only_search(
        &self,
        query: &HybridSearchQuery,
    ) -> Result<Vec<HybridSearchResult>, String> {
        let matches = self.keyword_search(&query.query, query.limit * 2).await?;

        let mut results: Vec<HybridSearchResult> = matches
            .into_iter()
            .enumerate()
            .map(|(rank, m)| {
                let keyword_score = Self::compute_rrf_score(rank + 1, 60);

                HybridSearchResult {
                    file_path: m.file_path.clone(),
                    chunk_name: Self::extract_chunk_name(&m.content),
                    chunk_type: "file".to_string(),
                    language: Self::detect_language(&m.file_path),
                    start_line: m.line_number,
                    end_line: m.line_number,
                    keyword_score,
                    vector_score: 0.0,
                    hybrid_score: keyword_score,
                    snippet: Self::truncate(&m.content, 200),
                }
            })
            .collect();

        // Apply filters
        results = Self::apply_filters(results, query);
        results.truncate(query.limit);

        Ok(results)
    }

    /// Vector-only search using semantic engine
    async fn vector_only_search(
        &self,
        query: &HybridSearchQuery,
    ) -> Result<Vec<HybridSearchResult>, String> {
        let semantic_query = SearchQuery {
            query: query.query.clone(),
            mode: super::SearchMode::SemanticOnly,
            language_filter: query.language_filter.clone(),
            file_pattern: query.file_pattern.clone(),
            chunk_type_filter: None,
            limit: query.limit,
        };

        let semantic_results = self.semantic_engine.search(&semantic_query).await?;

        let results = semantic_results
            .into_iter()
            .map(|r| HybridSearchResult {
                file_path: r.file_path,
                chunk_name: r.chunk_name,
                chunk_type: r.chunk_type,
                language: r.language,
                start_line: r.start_line,
                end_line: r.end_line,
                keyword_score: 0.0,
                vector_score: r.similarity_score,
                hybrid_score: r.similarity_score,
                snippet: r.snippet,
            })
            .collect();

        Ok(results)
    }

    /// Hybrid search combining keyword and vector results with RRF
    async fn hybrid_search(
        &self,
        query: &HybridSearchQuery,
    ) -> Result<Vec<HybridSearchResult>, String> {
        // Run both searches in parallel
        let keyword_matches = self.keyword_search(&query.query, query.limit * 2).await?;

        let semantic_query = SearchQuery {
            query: query.query.clone(),
            mode: super::SearchMode::SemanticOnly,
            language_filter: query.language_filter.clone(),
            file_pattern: query.file_pattern.clone(),
            chunk_type_filter: None,
            limit: query.limit * 2,
        };

        let semantic_results = self.semantic_engine.search(&semantic_query).await?;

        // Merge results using RRF
        let merged = self.merge_results(
            keyword_matches,
            semantic_results,
            (query.keyword_weight, query.vector_weight),
        );

        // Apply filters and limit
        let mut filtered = Self::apply_filters(merged, query);
        filtered.truncate(query.limit);

        Ok(filtered)
    }

    /// Keyword search using ripgrep
    async fn keyword_search(&self, query: &str, limit: usize) -> Result<Vec<KeywordMatch>, String> {
        let output = Command::new("rg")
            .arg("--line-number")
            .arg("--no-heading")
            .arg("--max-count")
            .arg(limit.to_string())
            .arg(query)
            .arg(&self.search_root)
            .output()
            .map_err(|e| format!("Failed to run ripgrep: {e}"))?;

        if !output.status.success() && !output.stdout.is_empty() {
            // ripgrep returns exit code 1 when no matches found, which is not an error
            if output.stdout.is_empty() {
                return Ok(Vec::new());
            }
        }

        let stdout = String::from_utf8_lossy(&output.stdout);
        let mut matches = Vec::new();

        for line in stdout.lines().take(limit) {
            // Format: path:line_number:content
            let parts: Vec<&str> = line.splitn(3, ':').collect();
            if parts.len() == 3 {
                if let Ok(line_num) = parts[1].parse::<usize>() {
                    matches.push(KeywordMatch {
                        file_path: parts[0].to_string(),
                        line_number: line_num,
                        content: parts[2].to_string(),
                    });
                }
            }
        }

        Ok(matches)
    }

    /// Merge keyword and vector results using RRF
    fn merge_results(
        &self,
        keyword_matches: Vec<KeywordMatch>,
        semantic_results: Vec<SearchResult>,
        weights: (f64, f64),
    ) -> Vec<HybridSearchResult> {
        let mut result_map: HashMap<String, HybridSearchResult> = HashMap::new();

        // Add keyword results with RRF scores
        for (rank, km) in keyword_matches.iter().enumerate() {
            let keyword_score = Self::compute_rrf_score(rank + 1, 60);
            let key = format!("{}:{}", km.file_path, km.line_number);

            result_map.insert(
                key,
                HybridSearchResult {
                    file_path: km.file_path.clone(),
                    chunk_name: Self::extract_chunk_name(&km.content),
                    chunk_type: "file".to_string(),
                    language: Self::detect_language(&km.file_path),
                    start_line: km.line_number,
                    end_line: km.line_number,
                    keyword_score,
                    vector_score: 0.0,
                    hybrid_score: weights.0 * keyword_score,
                    snippet: Self::truncate(&km.content, 200),
                },
            );
        }

        // Add/merge vector results with RRF scores
        for (rank, sr) in semantic_results.iter().enumerate() {
            let vector_score = Self::compute_rrf_score(rank + 1, 60);
            let key = format!("{}:{}", sr.file_path, sr.chunk_name);

            if let Some(existing) = result_map.get_mut(&key) {
                // Merge: update vector score and recalculate hybrid
                existing.vector_score = vector_score;
                existing.hybrid_score =
                    weights.0 * existing.keyword_score + weights.1 * vector_score;
            } else {
                // New entry from vector search
                result_map.insert(
                    key,
                    HybridSearchResult {
                        file_path: sr.file_path.clone(),
                        chunk_name: sr.chunk_name.clone(),
                        chunk_type: sr.chunk_type.clone(),
                        language: sr.language.clone(),
                        start_line: sr.start_line,
                        end_line: sr.end_line,
                        keyword_score: 0.0,
                        vector_score,
                        hybrid_score: weights.1 * vector_score,
                        snippet: sr.snippet.clone(),
                    },
                );
            }
        }

        // Convert to vec and sort by hybrid score
        let mut results: Vec<HybridSearchResult> = result_map.into_values().collect();
        results.sort_by(|a, b| {
            b.hybrid_score
                .partial_cmp(&a.hybrid_score)
                .expect("internal error")
        });

        results
    }

    /// Compute RRF score for a given rank
    ///
    /// # Arguments
    /// * `rank` - Position in result set (1-indexed)
    /// * `k` - Constant (typically 60)
    ///
    /// # Returns
    /// RRF score (higher is better)
    pub fn compute_rrf_score(rank: usize, k: usize) -> f64 {
        1.0 / (k as f64 + rank as f64)
    }

    /// Apply filters to results
    fn apply_filters(
        results: Vec<HybridSearchResult>,
        query: &HybridSearchQuery,
    ) -> Vec<HybridSearchResult> {
        results
            .into_iter()
            .filter(|r| {
                // Language filter
                if let Some(ref lang) = query.language_filter {
                    if &r.language != lang {
                        return false;
                    }
                }

                // File pattern filter
                if let Some(ref pattern) = query.file_pattern {
                    if !Self::matches_pattern(&r.file_path, pattern) {
                        return false;
                    }
                }

                true
            })
            .collect()
    }

    /// Index a directory
    pub async fn index_directory(&self, path: &Path) -> Result<(), String> {
        self.semantic_engine.index_directory(path).await?;
        Ok(())
    }

    /// Detect language from file path
    fn detect_language(path: &str) -> String {
        if path.ends_with(".rs") {
            "rust".to_string()
        } else if path.ends_with(".ts") || path.ends_with(".tsx") {
            "typescript".to_string()
        } else if path.ends_with(".py") {
            "python".to_string()
        } else if path.ends_with(".go") {
            "go".to_string()
        } else if path.ends_with(".c") || path.ends_with(".h") {
            "c".to_string()
        } else if path.ends_with(".cpp") || path.ends_with(".hpp") {
            "cpp".to_string()
        } else {
            "unknown".to_string()
        }
    }

    /// Extract chunk name from content
    fn extract_chunk_name(content: &str) -> String {
        // Simple heuristic: first word or identifier
        content
            .split_whitespace()
            .find(|s| s.chars().all(|c| c.is_alphanumeric() || c == '_'))
            .unwrap_or("unknown")
            .to_string()
    }

    /// Check if path matches pattern
    fn matches_pattern(path: &str, pattern: &str) -> bool {
        if let Some(suffix) = pattern.strip_prefix('*') {
            path.ends_with(suffix)
        } else {
            path.contains(pattern)
        }
    }

    /// Truncate string to max length
    fn truncate(s: &str, max_len: usize) -> String {
        if s.len() <= max_len {
            s.to_string()
        } else {
            format!("{}...", &s[..max_len])
        }
    }
}

// ============================================================================
// BM25 Search Engine (trueno-rag integration)
// TRUENO-RAG-1-BM25: Replace ripgrep+RRF with true BM25 scoring
// ============================================================================

use trueno_rag::index::BM25Index;
use trueno_rag::{Chunk, ChunkId, DocumentId, SparseIndex};

/// BM25-based keyword search engine using trueno-rag
/// Provides true BM25 scoring instead of rank-based RRF heuristics
pub struct Bm25SearchEngine {
    index: BM25Index,
    /// Maps ChunkId to file metadata
    chunk_metadata: HashMap<ChunkId, KeywordMatch>,
}

impl Bm25SearchEngine {
    /// Create a new BM25 search engine
    pub fn new() -> Self {
        Self {
            index: BM25Index::new(),
            chunk_metadata: HashMap::new(),
        }
    }

    /// Create with custom BM25 parameters
    ///
    /// # Arguments
    /// * `k1` - Term frequency saturation (default 1.2)
    /// * `b` - Length normalization (default 0.75)
    pub fn with_params(k1: f32, b: f32) -> Self {
        Self {
            index: BM25Index::with_params(k1, b),
            chunk_metadata: HashMap::new(),
        }
    }

    /// Index a code file
    ///
    /// # Arguments
    /// * `file_path` - Path to the file
    /// * `content` - File content
    /// * `language` - Programming language
    pub fn index_file(&mut self, file_path: &str, content: &str, _language: &str) {
        // Split content into lines and index each
        for (line_num, line) in content.lines().enumerate() {
            if line.trim().is_empty() {
                continue;
            }

            let doc_id = DocumentId::new();
            let chunk = Chunk::new(doc_id, line.to_string(), line_num, line_num + 1);
            let chunk_id = chunk.id;

            self.index.add(&chunk);
            self.chunk_metadata.insert(
                chunk_id,
                KeywordMatch {
                    file_path: file_path.to_string(),
                    line_number: line_num + 1,
                    content: line.to_string(),
                },
            );
        }
    }

    /// Search using BM25 scoring
    ///
    /// # Arguments
    /// * `query` - Search query
    /// * `limit` - Maximum results
    ///
    /// # Returns
    /// Results with true BM25 scores (not rank-based)
    pub fn search(&self, query: &str, limit: usize) -> Vec<(KeywordMatch, f32)> {
        let results = self.index.search(query, limit);

        results
            .into_iter()
            .filter_map(|(chunk_id, score)| {
                self.chunk_metadata
                    .get(&chunk_id)
                    .map(|meta| (meta.clone(), score))
            })
            .collect()
    }

    /// Get the number of indexed documents
    pub fn len(&self) -> usize {
        self.index.len()
    }

    /// Check if the index is empty
    pub fn is_empty(&self) -> bool {
        self.index.is_empty()
    }
}

impl Default for Bm25SearchEngine {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
    use super::*;

    // ============================================================================
    // TRUENO-RAG-1-BM25: RED Phase Tests
    // These tests define the expected BM25 behavior
    // ============================================================================

    #[test]
    fn test_bm25_engine_creation() {
        let engine = Bm25SearchEngine::new();
        assert!(engine.is_empty());
        assert_eq!(engine.len(), 0);
    }

    #[test]
    fn test_bm25_engine_with_params() {
        let engine = Bm25SearchEngine::with_params(1.5, 0.5);
        assert!(engine.is_empty());
    }

    #[test]
    fn test_bm25_index_single_file() {
        let mut engine = Bm25SearchEngine::new();

        let content = r#"
fn main() {
    println!("Hello, world!");
}

fn calculate_sum(a: i32, b: i32) -> i32 {
    a + b
}
"#;

        engine.index_file("src/main.rs", content, "rust");

        // Should have indexed non-empty lines
        assert!(engine.len() > 0);
    }

    #[test]
    fn test_bm25_search_returns_results() {
        let mut engine = Bm25SearchEngine::new();

        engine.index_file(
            "src/lib.rs",
            "fn process_data() { /* process */ }\nfn handle_request() { /* handle */ }",
            "rust",
        );

        let results = engine.search("process", 10);
        assert!(!results.is_empty(), "BM25 should find 'process'");
    }

    #[test]
    fn test_bm25_score_is_not_rank_based() {
        // BM25 scores should reflect actual term relevance, not just position
        let mut engine = Bm25SearchEngine::new();

        // Document with term appearing multiple times should score higher
        engine.index_file("a.rs", "fn test_foo() { foo(); foo(); foo(); }", "rust");
        engine.index_file("b.rs", "fn bar() { foo(); }", "rust");

        let results = engine.search("foo", 10);
        assert!(results.len() >= 2);

        // File with more "foo" occurrences should have higher score
        let a_score = results.iter().find(|(m, _)| m.file_path == "a.rs");
        let b_score = results.iter().find(|(m, _)| m.file_path == "b.rs");

        if let (Some((_, score_a)), Some((_, score_b))) = (a_score, b_score) {
            assert!(
                score_a > score_b,
                "BM25: More term occurrences should score higher (a={}, b={})",
                score_a,
                score_b
            );
        }
    }

    #[test]
    fn test_bm25_idf_affects_scoring() {
        // Rare terms should have higher IDF and thus higher scores
        let mut engine = Bm25SearchEngine::new();

        // "common" appears in many documents, "rare" in only one
        engine.index_file("1.rs", "common word common", "rust");
        engine.index_file("2.rs", "common word common", "rust");
        engine.index_file("3.rs", "common word common", "rust");
        engine.index_file("4.rs", "rare unique term", "rust");

        let common_results = engine.search("common", 10);
        let rare_results = engine.search("rare", 10);

        // Both should find results
        assert!(!common_results.is_empty());
        assert!(!rare_results.is_empty());

        // Rare term should have higher score due to higher IDF
        let common_max_score = common_results
            .iter()
            .map(|(_, s)| *s)
            .fold(0.0f32, f32::max);
        let rare_max_score = rare_results.iter().map(|(_, s)| *s).fold(0.0f32, f32::max);

        assert!(
            rare_max_score > common_max_score,
            "BM25: Rare terms should have higher IDF score (rare={}, common={})",
            rare_max_score,
            common_max_score
        );
    }

    #[test]
    fn test_bm25_empty_query_returns_empty() {
        let mut engine = Bm25SearchEngine::new();
        engine.index_file("test.rs", "fn test() {}", "rust");

        let results = engine.search("", 10);
        assert!(results.is_empty());
    }

    #[test]
    fn test_bm25_stopwords_filtered() {
        let mut engine = Bm25SearchEngine::new();
        engine.index_file("test.rs", "the quick brown fox", "rust");

        // Search for stopword should return empty (BM25Index filters stopwords)
        let results = engine.search("the", 10);
        assert!(results.is_empty(), "Stopwords should be filtered");

        // Non-stopword should work
        let results = engine.search("quick", 10);
        assert!(!results.is_empty());
    }

    #[test]
    fn test_bm25_no_match_returns_empty() {
        let mut engine = Bm25SearchEngine::new();
        engine.index_file("test.rs", "fn hello() {}", "rust");

        let results = engine.search("nonexistent_term_xyz", 10);
        assert!(results.is_empty());
    }

    #[test]
    fn test_bm25_respects_limit() {
        let mut engine = Bm25SearchEngine::new();

        for i in 0..20 {
            engine.index_file(
                &format!("file{}.rs", i),
                &format!("fn test{i}() {{}}", i = i),
                "rust",
            );
        }

        let results = engine.search("fn", 5);
        assert!(results.len() <= 5);
    }

    #[test]
    fn test_bm25_returns_file_metadata() {
        let mut engine = Bm25SearchEngine::new();
        engine.index_file("src/utils/helper.rs", "fn helper_function() {}", "rust");

        let results = engine.search("helper", 10);
        assert!(!results.is_empty());

        let (meta, _score) = &results[0];
        assert_eq!(meta.file_path, "src/utils/helper.rs");
        assert!(meta.line_number > 0);
        assert!(meta.content.contains("helper"));
    }

    // ============================================================================
    // Original RRF tests (kept for backward compatibility)
    // ============================================================================

    #[test]
    fn test_rrf_score_calculation() {
        let score1 = HybridSearchEngine::compute_rrf_score(1, 60);
        let score2 = HybridSearchEngine::compute_rrf_score(2, 60);
        let score10 = HybridSearchEngine::compute_rrf_score(10, 60);

        assert!((score1 - 1.0 / 61.0).abs() < 0.001);
        assert!((score2 - 1.0 / 62.0).abs() < 0.001);
        assert!((score10 - 1.0 / 70.0).abs() < 0.001);

        assert!(score1 > score2);
        assert!(score2 > score10);
    }

    #[test]
    fn test_detect_language() {
        assert_eq!(HybridSearchEngine::detect_language("src/main.rs"), "rust");
        assert_eq!(HybridSearchEngine::detect_language("app.ts"), "typescript");
        assert_eq!(HybridSearchEngine::detect_language("script.py"), "python");
        assert_eq!(HybridSearchEngine::detect_language("main.go"), "go");
        assert_eq!(HybridSearchEngine::detect_language("test.c"), "c");
        assert_eq!(HybridSearchEngine::detect_language("test.cpp"), "cpp");
    }

    #[test]
    fn test_matches_pattern() {
        assert!(HybridSearchEngine::matches_pattern("src/main.rs", "*.rs"));
        assert!(!HybridSearchEngine::matches_pattern("src/main.rs", "*.py"));
        assert!(HybridSearchEngine::matches_pattern(
            "src/utils/math.rs",
            "utils"
        ));
    }

    #[test]
    fn test_truncate() {
        let short = "hello";
        assert_eq!(HybridSearchEngine::truncate(short, 10), "hello");

        let long = "a".repeat(300);
        let truncated = HybridSearchEngine::truncate(&long, 200);
        assert_eq!(truncated.len(), 203); // 200 + "..."
        assert!(truncated.ends_with("..."));
    }
}
