//! Regexp query: match terms against a regular expression.
//!
//! Scans the term dictionary, matches each term against the compiled regex,
//! then rewrites to a disjunction of matching term queries.
//!
//! Uses the `regex` crate for correct, safe regex matching.
//!
//! See [[elasticsearch-parity]].

use regex::Regex;

use luci_core::{Result, ScoreMode, LuciError};

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

/// Regexp query on a field's term dictionary.
pub struct RegexpQuery {
    pub field: String,
    pub pattern: String,
}

impl Query for RegexpQuery {
    fn create_weight(
        &self,
        searcher: &Searcher,
        score_mode: ScoreMode,
    ) -> Result<Box<dyn Weight>> {
        // Compile regex — anchor to full term match (^...$)
        let anchored = if self.pattern.starts_with('^') && self.pattern.ends_with('$') {
            self.pattern.clone()
        } else {
            format!("^{}$", self.pattern)
        };

        let re = Regex::new(&anchored).map_err(|e| {
            LuciError::InvalidQuery(format!("invalid regexp: {e}"))
        })?;

        // Collect matching terms from all segments
        let mut matching_terms: Vec<String> = Vec::new();
        for segment in searcher.segments() {
            let field_id = match segment.header().fields.iter()
                .find(|f| f.field_name == self.field)
                .map(|f| f.field_id)
            {
                Some(id) => id,
                None => continue,
            };

            // Scan all terms (no prefix optimization for general regex)
            for (term, _) in segment.terms_with_prefix(field_id, "") {
                if re.is_match(&term) && !matching_terms.contains(&term) {
                    matching_terms.push(term);
                }
            }
        }

        if matching_terms.is_empty() {
            return crate::query::convert::MatchNoneQuery.create_weight(searcher, score_mode);
        }

        let should: Vec<Box<dyn Query>> = matching_terms.into_iter()
            .map(|term| -> Box<dyn Query> {
                Box::new(TermQuery {
                    field: self.field.clone(),
                    value: term,
                })
            })
            .collect();

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

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

    #[test]
    fn regexp_basic() {
        let schema = Mapping::builder().field("tag", FieldType::Keyword).build();
        let mut builder = SegmentBuilder::new(SegmentId::new(1), &schema);
        for tag in &["technology", "technical", "tennis", "science"] {
            builder.add_document(
                &[(FieldId::new(0), vec![Token::new(*tag, 0, tag.len(), 0)])],
                b"{}",
            );
        }
        let reader = SegmentReader::open(builder.build()).unwrap();
        let searcher = Searcher::new(vec![reader]);

        let results = searcher.search_query(&RegexpQuery {
            field: "tag".into(),
            pattern: "tech.*".into(),
        }, 10, 0, &crate::search::SourceFilter::Enabled, &None, false, &[]);
        assert_eq!(results.total_hits.value, 2); // technology, technical
    }

    #[test]
    fn regexp_character_class() {
        let schema = Mapping::builder().field("tag", FieldType::Keyword).build();
        let mut builder = SegmentBuilder::new(SegmentId::new(1), &schema);
        for tag in &["cat", "cut", "cot", "cart", "cit"] {
            builder.add_document(
                &[(FieldId::new(0), vec![Token::new(*tag, 0, tag.len(), 0)])],
                b"{}",
            );
        }
        let reader = SegmentReader::open(builder.build()).unwrap();
        let searcher = Searcher::new(vec![reader]);

        let results = searcher.search_query(&RegexpQuery {
            field: "tag".into(),
            pattern: "c[aou]t".into(),
        }, 10, 0, &crate::search::SourceFilter::Enabled, &None, false, &[]);
        assert_eq!(results.total_hits.value, 3); // cat, cut, cot
    }

    #[test]
    fn regexp_alternation() {
        let schema = Mapping::builder().field("tag", FieldType::Keyword).build();
        let mut builder = SegmentBuilder::new(SegmentId::new(1), &schema);
        for tag in &["red", "blue", "green"] {
            builder.add_document(
                &[(FieldId::new(0), vec![Token::new(*tag, 0, tag.len(), 0)])],
                b"{}",
            );
        }
        let reader = SegmentReader::open(builder.build()).unwrap();
        let searcher = Searcher::new(vec![reader]);

        let results = searcher.search_query(&RegexpQuery {
            field: "tag".into(),
            pattern: "red|blue".into(),
        }, 10, 0, &crate::search::SourceFilter::Enabled, &None, false, &[]);
        assert_eq!(results.total_hits.value, 2);
    }

    #[test]
    fn regexp_no_matches() {
        let schema = Mapping::builder().field("tag", FieldType::Keyword).build();
        let mut builder = SegmentBuilder::new(SegmentId::new(1), &schema);
        builder.add_document(
            &[(FieldId::new(0), vec![Token::new("hello", 0, 5, 0)])],
            b"{}",
        );
        let reader = SegmentReader::open(builder.build()).unwrap();
        let searcher = Searcher::new(vec![reader]);

        let results = searcher.search_query(&RegexpQuery {
            field: "tag".into(),
            pattern: "xyz.*".into(),
        }, 10, 0, &crate::search::SourceFilter::Enabled, &None, false, &[]);
        assert_eq!(results.total_hits.value, 0);
    }
}
