//! MatchQuery: analyze query text, build bool of term queries.
//!
//! Analyzes the query string through the field's analyzer, then:
//! - 1 token → TermQuery
//! - N tokens → BoolQuery { should: [TermQuery * N] }
//!
//! See [[query-dsl#Full-Text Queries]] and [[architecture-query-execution#Step 8]].

use luci_core::{Result, ScoreMode};

use crate::query::{Query, Weight};
use crate::query::boolean::BoolQuery;
use crate::query::term::TermQuery;
use crate::search::searcher::Searcher;

/// Full-text match query: analyze and search.
pub struct MatchQuery {
    pub field: String,
    pub query_text: String,
    pub analyzer: Option<String>,
}

impl Query for MatchQuery {
    fn create_weight(
        &self,
        searcher: &Searcher,
        score_mode: ScoreMode,
    ) -> Result<Box<dyn Weight>> {
        // Resolve analyzer
        let analyzer_name = self.analyzer.as_deref().unwrap_or("standard");
        let analyzers = searcher.analyzers();
        let analyzer = analyzers.get(analyzer_name);

        // Analyze query text
        let tokens = analyzer.analyze(&self.query_text);

        if tokens.is_empty() {
            // No terms after analysis → match nothing
            return MatchNoneQuery.create_weight(searcher, score_mode);
        }

        if tokens.len() == 1 {
            // Single term → rewrite to TermQuery
            let term_query = TermQuery {
                field: self.field.clone(),
                value: tokens[0].text.clone(),
            };
            return term_query.create_weight(searcher, score_mode);
        }

        // Multiple terms → bool { should: [TermQuery * N] }
        let should: Vec<Box<dyn Query>> = tokens.iter()
            .map(|t| -> Box<dyn Query> {
                Box::new(TermQuery {
                    field: self.field.clone(),
                    value: t.text.clone(),
                })
            })
            .collect();

        let bool_query = BoolQuery {
            must: vec![],
            should,
            must_not: vec![],
            filter: vec![],
        };
        bool_query.create_weight(searcher, score_mode)
    }
}

/// Match-nothing query.
struct MatchNoneQuery;

impl Query for MatchNoneQuery {
    fn create_weight(
        &self,
        _searcher: &Searcher,
        _score_mode: ScoreMode,
    ) -> Result<Box<dyn Weight>> {
        Ok(Box::new(MatchNoneWeight))
    }
}

struct MatchNoneWeight;

impl Weight for MatchNoneWeight {
    fn scorer_supplier(
        &self,
        _reader: &crate::segment::reader::SegmentReader,
    ) -> Result<Option<Box<dyn crate::query::ScorerSupplier>>> {
        Ok(None)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use luci_core::{FieldId, SegmentId, NO_MORE_DOCS};
    use luci_analysis::AnalyzerRegistry;
    use crate::segment::builder::SegmentBuilder;
    use luci_analysis::Token;
    use luci_mapping::{FieldType,Mapping};

    fn make_tokens(terms: &[&str]) -> Vec<Token> {
        terms.iter().enumerate()
            .map(|(i, t)| Token::new(*t, 0, t.len(), i as u32))
            .collect()
    }

    fn build_test_searcher() -> Searcher {
        let schema = Mapping::builder()
            .field("body", FieldType::Text)
            .build();
        let mut builder = SegmentBuilder::new(SegmentId::new(1), &schema);

        builder.add_document(&[
            (FieldId::new(0), make_tokens(&["the", "quick", "brown", "fox"])),
        ], b"{}");
        builder.add_document(&[
            (FieldId::new(0), make_tokens(&["the", "lazy", "dog"])),
        ], b"{}");
        builder.add_document(&[
            (FieldId::new(0), make_tokens(&["quick", "search", "engine"])),
        ], b"{}");

        let reader = crate::segment::reader::SegmentReader::open(builder.build()).unwrap();
        Searcher::with_analyzers(vec![reader], AnalyzerRegistry::new())
    }

    fn collect_doc_ids(scorer: &mut dyn luci_core::Scorer) -> Vec<u32> {
        let mut ids = Vec::new();
        while scorer.doc_id() != NO_MORE_DOCS {
            ids.push(scorer.doc_id().as_u32());
            scorer.next();
        }
        ids
    }

    #[test]
    fn match_single_term() {
        let searcher = build_test_searcher();
        let query = MatchQuery {
            field: "body".into(),
            query_text: "Fox".into(), // standard analyzer lowercases
            analyzer: None,
        };

        let weight = query.create_weight(&searcher, ScoreMode::Complete).unwrap();
        let supplier = weight.scorer_supplier(&searcher.segments()[0]).unwrap().unwrap();
        let mut scorer = supplier.scorer().unwrap();

        let ids = collect_doc_ids(scorer.as_mut());
        assert_eq!(ids, vec![0]); // "fox" only in doc 0
    }

    #[test]
    fn match_multi_term() {
        let searcher = build_test_searcher();
        let query = MatchQuery {
            field: "body".into(),
            query_text: "quick lazy".into(),
            analyzer: None,
        };

        let weight = query.create_weight(&searcher, ScoreMode::Complete).unwrap();
        let supplier = weight.scorer_supplier(&searcher.segments()[0]).unwrap().unwrap();
        let mut scorer = supplier.scorer().unwrap();

        // "quick" in docs 0,2 and "lazy" in doc 1 → should match all three
        let ids = collect_doc_ids(scorer.as_mut());
        assert_eq!(ids, vec![0, 1, 2]);
    }

    #[test]
    fn match_empty_query() {
        let searcher = build_test_searcher();
        let query = MatchQuery {
            field: "body".into(),
            query_text: "".into(),
            analyzer: None,
        };

        let weight = query.create_weight(&searcher, ScoreMode::Complete).unwrap();
        let supplier = weight.scorer_supplier(&searcher.segments()[0]).unwrap();
        assert!(supplier.is_none()); // no terms → no matches
    }

    #[test]
    fn match_case_normalization() {
        let searcher = build_test_searcher();
        let query = MatchQuery {
            field: "body".into(),
            query_text: "THE".into(),
            analyzer: None,
        };

        let weight = query.create_weight(&searcher, ScoreMode::Complete).unwrap();
        let supplier = weight.scorer_supplier(&searcher.segments()[0]).unwrap().unwrap();
        let mut scorer = supplier.scorer().unwrap();

        // "the" in docs 0, 1
        let ids = collect_doc_ids(scorer.as_mut());
        assert_eq!(ids, vec![0, 1]);
    }
}
