//! MatchPhraseQuery: terms must appear in order at consecutive positions.
//!
//! Uses position posting lists directly for both conjunction and position
//! verification, eliminating the overhead of maintaining separate posting
//! lists. The lead reader (fewest docs) drives iteration; other readers
//! advance() to each candidate doc, skipping intermediate positions.
//!
//! See [[query-dsl#Full-Text Queries]] and [[architecture-query-execution#Step 9]].

use luci_core::{DocId, FieldId, Result, ScoreMode, Scorer, TwoPhaseIterator, NO_MORE_DOCS};

use crate::query::{Query, ScorerSupplier, Weight};
use crate::query::term::TermQuery;
use crate::search::bm25::{bm25_idf, bm25_score};
use crate::search::searcher::Searcher;
use crate::segment::reader::SegmentReader;

pub struct MatchPhraseQuery {
    pub field: String,
    pub query_text: String,
    pub analyzer: Option<String>,
}

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

        if tokens.is_empty() {
            return Ok(Box::new(EmptyWeight));
        }

        if tokens.len() == 1 {
            let tq = TermQuery {
                field: self.field.clone(),
                value: tokens[0].text.clone(),
            };
            return tq.create_weight(searcher, score_mode);
        }

        let terms: Vec<String> = tokens.iter().map(|t| t.text.clone()).collect();

        Ok(Box::new(PhraseWeight {
            field: self.field.clone(),
            terms,
            total_docs: searcher.total_docs(),
            avg_field_length: searcher.avg_field_length(&self.field),
        }))
    }
}

struct PhraseWeight {
    field: String,
    terms: Vec<String>,
    total_docs: u32,
    avg_field_length: f32,
}

impl Weight for PhraseWeight {
    fn scorer_supplier(
        &self,
        reader: &SegmentReader,
    ) -> Result<Option<Box<dyn ScorerSupplier>>> {
        let field_id = match reader.header().fields.iter()
            .find(|f| f.field_name == self.field)
            .map(|f| f.field_id)
        {
            Some(id) => id,
            None => return Ok(None),
        };

        // All terms must exist for the phrase to match
        let mut term_doc_freqs = Vec::new();
        for term in &self.terms {
            let df = reader.doc_freq(field_id, term);
            if df == 0 {
                return Ok(None);
            }
            term_doc_freqs.push(df);
        }

        let cost = *term_doc_freqs.iter().min().unwrap() as u64;

        Ok(Some(Box::new(PhraseScorerSupplier {
            field_id,
            terms: self.terms.clone(),
            term_doc_freqs,
            total_docs: self.total_docs,
            avg_field_length: self.avg_field_length,
            cost,
            segment_data: reader as *const SegmentReader,
        })))
    }
}

struct PhraseScorerSupplier {
    field_id: FieldId,
    terms: Vec<String>,
    term_doc_freqs: Vec<u32>,
    total_docs: u32,
    avg_field_length: f32,
    cost: u64,
    segment_data: *const SegmentReader,
}

unsafe impl Send for PhraseScorerSupplier {}

impl ScorerSupplier for PhraseScorerSupplier {
    fn cost(&self) -> u64 {
        self.cost
    }

    fn scorer(self: Box<Self>) -> Result<Box<dyn Scorer>> {
        let reader = unsafe { &*self.segment_data };

        // Open position posting lists — used for BOTH conjunction and
        // position verification. This eliminates the separate simple posting
        // lists that the old approach maintained in parallel.
        let mut position_readers: Vec<crate::inverted::postings::PositionPostingListReader> = Vec::new();
        for term in &self.terms {
            match reader.postings_with_positions(self.field_id, term) {
                Some(r) => position_readers.push(r),
                None => return Ok(Box::new(SimpleIterScorer::empty())),
            }
        }

        // Sort by doc frequency (cheapest = fewest docs = lead iterator)
        // Use index mapping to track original term order for position offsets
        let mut term_order: Vec<usize> = (0..self.terms.len()).collect();
        term_order.sort_by_key(|&i| self.term_doc_freqs[i]);

        let sorted_readers: Vec<_> = term_order.iter()
            .map(|&i| std::mem::replace(&mut position_readers[i],
                crate::inverted::postings::PositionPostingListReader::new(&[])))
            .collect();
        let sorted_offsets: Vec<u32> = term_order.iter().map(|&i| i as u32).collect();

        // Compute phrase IDF as sum of individual IDFs
        let idf: f32 = self.term_doc_freqs.iter()
            .map(|&df| bm25_idf(self.total_docs, df))
            .sum();

        // Pre-read first entry from lead reader
        let mut reader_state: Vec<(u32, Vec<u32>)> = sorted_readers.iter()
            .map(|_| (u32::MAX, Vec::new()))
            .collect();

        let mut scorer = PhraseScorer {
            readers: sorted_readers,
            term_offsets: sorted_offsets,
            reader_state,
            current: NO_MORE_DOCS,
            idf,
            avg_field_length: self.avg_field_length,
            field_id: self.field_id,
            segment_data: self.segment_data,
            norms: reader.norms(self.field_id),
            constant_score: reader.norms(self.field_id)
                .and_then(|n| n.uniform_norm())
                .map(|dl| bm25_score(idf, 1.0, dl, self.avg_field_length)),
            ptrs_buf: Vec::new(),
        };

        // Position on the first phrase match
        scorer.advance_to_next_phrase();

        Ok(Box::new(scorer))
    }
}

/// Phrase scorer using position posting lists directly for both conjunction
/// and position verification. Eliminates the separate simple posting lists
/// that the old two-phase approach maintained in parallel.
///
/// The lead reader (fewest docs) drives iteration. For each lead doc, all
/// other readers advance to that doc. If all terms are present, positions
/// are checked for consecutive sequence.
struct PhraseScorer<'a> {
    /// Position readers sorted by doc frequency (lead = index 0).
    readers: Vec<crate::inverted::postings::PositionPostingListReader<'a>>,
    /// Original term index for each reader (for position offset calculation).
    /// term_offsets[i] = the position offset of the i-th sorted reader's
    /// term in the original phrase.
    term_offsets: Vec<u32>,
    /// Per-reader: (current_doc_id, current_positions). Reused across docs.
    reader_state: Vec<(u32, Vec<u32>)>,
    /// Current matched doc.
    current: DocId,
    idf: f32,
    avg_field_length: f32,
    field_id: FieldId,
    segment_data: *const SegmentReader,
    /// Cached norms reader for scoring (avoids per-doc lookup).
    norms: Option<crate::inverted::norms::FieldNormsReader<'a>>,
    /// Precomputed constant score when norms are uniform.
    constant_score: Option<f32>,
    /// Reusable pointer array for two-pointer merge.
    ptrs_buf: Vec<usize>,
}

// SAFETY: segment_data is only dereferenced within the search call that
// holds the SegmentReader, so the pointer remains valid.
unsafe impl Send for PhraseScorer<'_> {}

impl PhraseScorer<'_> {
    /// Advance lead reader and find the next document where all terms are
    /// present with consecutive positions.
    fn advance_to_next_phrase(&mut self) {
        let num_readers = self.readers.len();
        if num_readers == 0 {
            self.current = NO_MORE_DOCS;
            return;
        }

        // Fast path for 2-term phrases (most common case)
        if num_readers == 2 {
            self.advance_two_term_phrase();
            return;
        }

        'outer: loop {
            let lead_doc = match self.readers[0].advance(DocId::new(
                if self.current == NO_MORE_DOCS { 0 } else { self.current.as_u32() + 1 }
            )) {
                Some(id) => id,
                None => { self.current = NO_MORE_DOCS; return; }
            };
            let target = lead_doc.as_u32();

            self.reader_state[0].0 = target;
            self.reader_state[0].1.clear();
            self.reader_state[0].1.extend_from_slice(self.readers[0].positions());

            for i in 1..num_readers {
                match self.readers[i].advance(lead_doc) {
                    Some(id) if id.as_u32() == target => {
                        self.reader_state[i].0 = target;
                        self.reader_state[i].1.clear();
                        self.reader_state[i].1.extend_from_slice(self.readers[i].positions());
                    }
                    Some(id) => {
                        match self.readers[0].advance(id) {
                            Some(new_lead) => {
                                self.current = DocId::new(new_lead.as_u32().saturating_sub(1));
                                continue 'outer;
                            }
                            None => { self.current = NO_MORE_DOCS; return; }
                        }
                    }
                    None => { self.current = NO_MORE_DOCS; return; }
                }
            }

            if num_readers == 1 || self.check_positions() {
                self.current = lead_doc;
                return;
            }
            self.current = lead_doc;
        }
    }

    /// Specialized hot path for 2-term phrase queries.
    /// Avoids Vec copy for TF=1, uses next_doc() for sequential access,
    /// and inlines the position check.
    fn advance_two_term_phrase(&mut self) {
        let off0 = self.term_offsets[0];
        let off1 = self.term_offsets[1];

        'outer: loop {
            // Advance lead reader
            let lead_doc = if self.current == NO_MORE_DOCS {
                match self.readers[0].advance(DocId::new(0)) {
                    Some(id) => id,
                    None => { self.current = NO_MORE_DOCS; return; }
                }
            } else {
                match self.readers[0].next_doc() {
                    Some(id) => id,
                    None => { self.current = NO_MORE_DOCS; return; }
                }
            };

            // Advance other reader to same doc
            match self.readers[1].advance(lead_doc) {
                Some(id) if id == lead_doc => {}
                Some(id) => {
                    // Other reader jumped ahead — advance lead to catch up
                    match self.readers[0].advance(id) {
                        Some(new_lead) => {
                            self.current = DocId::new(new_lead.as_u32().saturating_sub(1));
                            continue 'outer;
                        }
                        None => { self.current = NO_MORE_DOCS; return; }
                    }
                }
                None => { self.current = NO_MORE_DOCS; return; }
            }

            // Both readers at same doc. Check positions.
            let tf0 = self.readers[0].current_tf();
            let tf1 = self.readers[1].current_tf();

            if tf0 == 1 && tf1 == 1 {
                // Fast path: single position per term
                let pos0 = self.readers[0].first_position();
                let pos1 = self.readers[1].first_position();
                // Check if positions are consecutive in phrase order
                if pos0.wrapping_sub(off0) == pos1.wrapping_sub(off1) {
                    self.current = lead_doc;
                    return;
                }
            } else {
                // General path: copy positions and use two-pointer merge
                self.reader_state[0].0 = lead_doc.as_u32();
                self.reader_state[0].1.clear();
                self.reader_state[0].1.extend_from_slice(self.readers[0].positions());
                self.reader_state[1].0 = lead_doc.as_u32();
                self.reader_state[1].1.clear();
                self.reader_state[1].1.extend_from_slice(self.readers[1].positions());

                if self.check_positions() {
                    self.current = lead_doc;
                    return;
                }
            }

            self.current = lead_doc;
        }
    }

    /// Check if positions form a consecutive phrase across all terms.
    /// Uses term_offsets to account for the original phrase order
    /// (readers are sorted by doc frequency, not phrase order).
    fn check_positions(&mut self) -> bool {
        let num = self.readers.len();
        self.ptrs_buf.clear();
        self.ptrs_buf.resize(num, 0);

        // Find the reader with the smallest term_offset to use as anchor
        let anchor = self.term_offsets.iter()
            .enumerate()
            .min_by_key(|(_, off)| *off)
            .map(|(i, _)| i)
            .unwrap();
        let anchor_offset = self.term_offsets[anchor];
        let anchor_positions = &self.reader_state[anchor].1;

        for &anchor_pos in anchor_positions.iter() {
            let start = anchor_pos - anchor_offset; // phrase start position
            let mut matched = true;

            for i in 0..num {
                if i == anchor { continue; }
                let expected = start + self.term_offsets[i];
                let positions = &self.reader_state[i].1;

                while self.ptrs_buf[i] < positions.len() && positions[self.ptrs_buf[i]] < expected {
                    self.ptrs_buf[i] += 1;
                }

                if self.ptrs_buf[i] >= positions.len() || positions[self.ptrs_buf[i]] != expected {
                    matched = false;
                    break;
                }
            }

            if matched {
                return true;
            }
        }
        false
    }
}

impl Scorer for PhraseScorer<'_> {
    fn doc_id(&self) -> DocId {
        self.current
    }

    fn next(&mut self) -> DocId {
        self.advance_to_next_phrase();
        self.current
    }

    fn advance(&mut self, target: DocId) -> DocId {
        if self.current < target {
            self.current = DocId::new(target.as_u32().saturating_sub(1));
        }
        self.advance_to_next_phrase();
        self.current
    }

    fn score(&mut self) -> f32 {
        if let Some(cs) = self.constant_score {
            return cs;
        }
        let dl = self.norms.as_ref()
            .map(|n| n.norm(self.doc_id()))
            .unwrap_or(1.0);
        bm25_score(self.idf, 1.0, dl, self.avg_field_length)
    }

    fn two_phase(&mut self) -> Option<&mut dyn TwoPhaseIterator> {
        None
    }
}

struct SimpleIterScorer<'a> {
    postings: crate::inverted::postings::PostingListReader<'a>,
    current: DocId,
}

impl<'a> SimpleIterScorer<'a> {
    fn new(mut postings: crate::inverted::postings::PostingListReader<'a>) -> Self {
        let current = match postings.next() {
            Some((id, _)) => id,
            None => NO_MORE_DOCS,
        };
        Self { postings, current }
    }

    fn empty() -> Self {
        Self {
            postings: crate::inverted::postings::PostingListReader::new(&[0, 0, 0, 0, 0]),
            current: NO_MORE_DOCS,
        }
    }
}

impl Scorer for SimpleIterScorer<'_> {
    fn doc_id(&self) -> DocId { self.current }
    fn next(&mut self) -> DocId {
        self.current = match self.postings.next() {
            Some((id, _)) => id,
            None => NO_MORE_DOCS,
        };
        self.current
    }
    fn advance(&mut self, target: DocId) -> DocId {
        while self.current < target && self.current != NO_MORE_DOCS {
            self.next();
        }
        self.current
    }
    fn score(&mut self) -> f32 { 1.0 }
    fn two_phase(&mut self) -> Option<&mut dyn TwoPhaseIterator> { None }
}

struct EmptyWeight;
impl Weight for EmptyWeight {
    fn scorer_supplier(&self, _: &SegmentReader) -> Result<Option<Box<dyn ScorerSupplier>>> {
        Ok(None)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use luci_analysis::{AnalyzerRegistry, Token};
    use luci_core::SegmentId;
    use crate::segment::builder::SegmentBuilder;
    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_phrase_searcher() -> Searcher {
        let schema = Mapping::builder()
            .field("body", FieldType::Text)
            .build();
        let mut builder = SegmentBuilder::new(SegmentId::new(1), &schema);

        // Doc 0: "the quick brown fox" — positions [0,1,2,3]
        builder.add_document(&[
            (FieldId::new(0), make_tokens(&["the", "quick", "brown", "fox"])),
        ], br#"{"body":"the quick brown fox"}"#);

        // Doc 1: "the brown quick fox" — positions [0,1,2,3] but different order
        builder.add_document(&[
            (FieldId::new(0), make_tokens(&["the", "brown", "quick", "fox"])),
        ], br#"{"body":"the brown quick fox"}"#);

        // Doc 2: "quick fox brown" — no "the", different order
        builder.add_document(&[
            (FieldId::new(0), make_tokens(&["quick", "fox", "brown"])),
        ], br#"{"body":"quick fox brown"}"#);

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

    #[test]
    fn phrase_exact_match() {
        let searcher = build_phrase_searcher();
        let query = MatchPhraseQuery {
            field: "body".into(),
            query_text: "quick brown".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 brown" at consecutive positions only in doc 0
        assert_eq!(scorer.doc_id(), DocId::new(0));
        assert_eq!(scorer.next(), NO_MORE_DOCS);
    }

    #[test]
    fn phrase_wrong_order_no_match() {
        let searcher = build_phrase_searcher();
        let query = MatchPhraseQuery {
            field: "body".into(),
            query_text: "brown quick".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();

        // "brown quick" is consecutive in doc 1 (positions 1,2)
        assert_eq!(scorer.doc_id(), DocId::new(1));
        assert_eq!(scorer.next(), NO_MORE_DOCS);
    }

    #[test]
    fn phrase_no_match() {
        let searcher = build_phrase_searcher();
        let query = MatchPhraseQuery {
            field: "body".into(),
            query_text: "fox quick".into(), // never consecutive
            analyzer: None,
        };

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

        // "fox" is always after "quick" in all docs, never before
        // Doc 0: fox@3, quick@1 → not consecutive
        // Doc 1: fox@3, quick@2 → not consecutive
        // Doc 2: fox@1, quick@0 → consecutive! "quick fox" but query is "fox quick"
        // Wait — "fox quick" means fox at position X, quick at X+1. Doc 2 has quick@0, fox@1.
        // So "fox quick" would need fox before quick at consecutive positions.
        // Doc 2: fox@1, quick@0 → fox@1 then quick@1+1=2, but quick is at 0, not 2 → no match

        assert_eq!(scorer.doc_id(), NO_MORE_DOCS);
    }

    #[test]
    fn phrase_single_term_degenerates() {
        let searcher = build_phrase_searcher();
        let query = MatchPhraseQuery {
            field: "body".into(),
            query_text: "quick".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();

        // Single term → just a term query. "quick" in docs 0, 1, 2
        let mut ids = Vec::new();
        while scorer.doc_id() != NO_MORE_DOCS {
            ids.push(scorer.doc_id().as_u32());
            scorer.next();
        }
        assert_eq!(ids, vec![0, 1, 2]);
    }

    #[test]
    fn phrase_three_terms() {
        let searcher = build_phrase_searcher();
        let query = MatchPhraseQuery {
            field: "body".into(),
            query_text: "the quick brown".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 quick brown" at positions 0,1,2 only in doc 0
        assert_eq!(scorer.doc_id(), DocId::new(0));
        assert_eq!(scorer.next(), NO_MORE_DOCS);
    }

    #[test]
    fn phrase_has_positive_score() {
        let searcher = build_phrase_searcher();
        let query = MatchPhraseQuery {
            field: "body".into(),
            query_text: "quick brown".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();

        assert_eq!(scorer.doc_id(), DocId::new(0));
        let score = scorer.score();
        assert!(score > 0.0, "phrase score should be positive, got {score}");
    }
}
