//! Search module providing lexical, semantic, and hybrid search.

mod lexical;
mod query;
mod semantic;

pub use lexical::LexicalIndex;
pub use query::{SearchFilter, SearchQuery};
pub use semantic::SemanticIndex;

use crate::types::{MatchType, SearchResult};

/// Merge and rank results from multiple search methods.
pub fn merge_results(
    results: Vec<Vec<SearchResult>>,
    weights: &[f32],
    limit: usize,
) -> Vec<SearchResult> {
    use std::collections::HashMap;

    assert_eq!(results.len(), weights.len());

    let mut score_map: HashMap<String, (f32, SearchResult)> = HashMap::new();

    for (result_set, &weight) in results.into_iter().zip(weights.iter()) {
        for result in result_set {
            let chunk_id = result.chunk.id.0.to_string();
            let weighted_score = result.score * weight;

            match score_map.get_mut(&chunk_id) {
                Some((total_score, existing)) => {
                    *total_score += weighted_score;
                    existing.highlights.extend(result.highlights);
                    if existing.match_type != result.match_type {
                        existing.match_type = MatchType::Hybrid;
                    }
                }
                None => {
                    score_map.insert(chunk_id, (weighted_score, result));
                }
            }
        }
    }

    let mut merged: Vec<_> = score_map
        .into_values()
        .map(|(score, mut result)| {
            result.score = score;
            result
        })
        .collect();

    merged.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
    merged.truncate(limit);
    merged
}

/// Normalize scores to 0-1 range.
pub fn normalize_scores(results: &mut [SearchResult]) {
    if results.is_empty() {
        return;
    }

    let max_score = results
        .iter()
        .map(|r| r.score)
        .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
        .unwrap_or(1.0);

    if max_score > 0.0 {
        for result in results {
            result.score /= max_score;
        }
    }
}

/// Create highlights for matching text.
pub fn create_highlights(content: &str, query_terms: &[String]) -> Vec<crate::types::Highlight> {
    let mut highlights = Vec::new();
    let content_lower = content.to_lowercase();

    for term in query_terms {
        let term_lower = term.to_lowercase();
        let mut start = 0;

        while let Some(pos) = content_lower[start..].find(&term_lower) {
            let abs_pos = start + pos;
            highlights.push(crate::types::Highlight {
                start: abs_pos,
                end: abs_pos + term.len(),
                text: content[abs_pos..abs_pos + term.len()].to_string(),
            });
            start = abs_pos + 1;
        }
    }

    highlights.sort_by_key(|h| h.start);
    highlights
}
