//! Fuzzy query: match terms within edit distance (Levenshtein).
//!
//! Scans the term dictionary, computes edit distance for each term,
//! rewrites matching terms to a disjunction.
//!
//! See [[elasticsearch-parity]].

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;

/// Fuzzy query: find terms within Levenshtein edit distance.
pub struct FuzzyQuery {
    pub field: String,
    pub value: String,
    pub fuzziness: u32,
}

/// Compute Levenshtein edit distance between two strings.
fn levenshtein(a: &str, b: &str) -> u32 {
    let a = a.as_bytes();
    let b = b.as_bytes();
    let m = a.len();
    let n = b.len();

    // Quick reject: if length difference exceeds max possible edit distance
    // this is checked by caller, but we include it for safety
    if m.abs_diff(n) > 255 {
        return u32::MAX;
    }

    // Single-row DP (space optimization)
    let mut prev: Vec<u32> = (0..=n as u32).collect();
    let mut curr = vec![0u32; n + 1];

    for i in 1..=m {
        curr[0] = i as u32;
        for j in 1..=n {
            let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
            curr[j] = (prev[j] + 1)          // deletion
                .min(curr[j - 1] + 1)        // insertion
                .min(prev[j - 1] + cost);    // substitution
        }
        std::mem::swap(&mut prev, &mut curr);
    }

    prev[n]
}

impl Query for FuzzyQuery {
    fn create_weight(
        &self,
        searcher: &Searcher,
        score_mode: ScoreMode,
    ) -> Result<Box<dyn Weight>> {
        let max_dist = self.fuzziness;

        // 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 — fuzzy matching can't use prefix optimization
            // in general (a typo in the first character would miss the prefix)
            for (term, _) in segment.terms_with_prefix(field_id, "") {
                // Quick reject: length difference > fuzziness
                if term.len().abs_diff(self.value.len()) > max_dist as usize {
                    continue;
                }
                if levenshtein(&self.value, &term) <= max_dist
                    && !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 levenshtein_identical() {
        assert_eq!(levenshtein("hello", "hello"), 0);
    }

    #[test]
    fn levenshtein_one_edit() {
        assert_eq!(levenshtein("hello", "hallo"), 1); // substitution
        assert_eq!(levenshtein("hello", "hell"), 1);   // deletion
        assert_eq!(levenshtein("hello", "hellos"), 1); // insertion
    }

    #[test]
    fn levenshtein_two_edits() {
        assert_eq!(levenshtein("hello", "hxllo"), 1);
        assert_eq!(levenshtein("kitten", "sitting"), 3);
    }

    #[test]
    fn fuzzy_query_basic() {
        let schema = Mapping::builder().field("tag", FieldType::Keyword).build();
        let mut builder = SegmentBuilder::new(SegmentId::new(1), &schema);
        for tag in &["technology", "technlogy", "techology", "science", "tennis"] {
            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(&FuzzyQuery {
            field: "tag".into(),
            value: "technology".into(),
            fuzziness: 1,
        }, 10, 0, &crate::search::SourceFilter::Enabled, &None, false, &[]);
        assert_eq!(results.total_hits.value, 3); // technology (0), technlogy (1), techology (1)
    }

    #[test]
    fn fuzzy_query_fuzziness_2() {
        let schema = Mapping::builder().field("tag", FieldType::Keyword).build();
        let mut builder = SegmentBuilder::new(SegmentId::new(1), &schema);
        for tag in &["apple", "aple", "apples", "apply", "orange"] {
            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(&FuzzyQuery {
            field: "tag".into(),
            value: "apple".into(),
            fuzziness: 2,
        }, 10, 0, &crate::search::SourceFilter::Enabled, &None, false, &[]);
        // apple(0), aple(1), apples(1), apply(2)
        assert_eq!(results.total_hits.value, 4);
    }

    #[test]
    fn fuzzy_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(&FuzzyQuery {
            field: "tag".into(),
            value: "xyz".into(),
            fuzziness: 1,
        }, 10, 0, &crate::search::SourceFilter::Enabled, &None, false, &[]);
        assert_eq!(results.total_hits.value, 0);
    }
}
