//! Span queries: positional matching with configurable distance and ordering.
//!
//! Implements the Spans abstraction from [[feature-span-queries]]: each span query
//! yields `(doc, start, end)` tuples that can be composed by outer queries.
//!
//! Two-phase approach: doc-level conjunction first (cheap), position
//! verification second (expensive). Follows Lucene's ConjunctionSpans pattern.

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

use crate::inverted::postings::PositionPostingListReader;
use crate::query::{Query, ScorerSupplier, Weight};
use crate::search::searcher::Searcher;
use crate::segment::reader::SegmentReader;

const NO_MORE_POSITIONS: u32 = u32::MAX;

// ---------------------------------------------------------------------------
// Spans trait
// ---------------------------------------------------------------------------

/// Position-level iterator over spans within documents.
///
/// Contract:
/// 1. Call `next_doc()` or `advance_doc()` to move to a document.
/// 2. Call `next_start_position()` to iterate spans within the doc.
///    Returns `NO_MORE_POSITIONS` when exhausted for this doc.
/// 3. After `next_start_position()` returns a valid value, call
///    `start_position()` and `end_position()` to read the span.
trait Spans: Send {
    fn doc_id(&self) -> DocId;
    fn next_doc(&mut self) -> DocId;
    fn advance_doc(&mut self, target: DocId) -> DocId;

    /// Advance to next span in current doc. Returns start position,
    /// or NO_MORE_POSITIONS if no more spans in this doc.
    fn next_start_position(&mut self) -> u32;

    fn start_position(&self) -> u32;
    fn end_position(&self) -> u32;
    fn width(&self) -> u32;
}

// ---------------------------------------------------------------------------
// TermSpans — single term, one span per position
// ---------------------------------------------------------------------------

struct TermSpans<'a> {
    reader: PositionPostingListReader<'a>,
    pos_index: usize,
    current_doc: DocId,
    /// Cached TF for the current doc (from next_doc()).
    current_tf: u32,
}

unsafe impl Send for TermSpans<'_> {}

impl<'a> TermSpans<'a> {
    fn new(reader: PositionPostingListReader<'a>) -> Self {
        Self { reader, pos_index: 0, current_doc: NO_MORE_DOCS, current_tf: 0 }
    }

    /// Get positions for the current doc. For TF=1, returns a single-element
    /// slice via position_buf after ensuring it's populated. For TF>1,
    /// position_buf is already populated by next_doc().
    fn current_positions(&self) -> &[u32] {
        if self.current_tf <= 1 {
            // For TF=1, position_buf may not be populated — use first_position
            // We handle this in next_start_position directly
            &[]
        } else {
            self.reader.positions()
        }
    }
}

impl Spans for TermSpans<'_> {
    fn doc_id(&self) -> DocId { self.current_doc }

    fn next_doc(&mut self) -> DocId {
        self.pos_index = 0;
        match self.reader.next_doc() {
            Some(doc) => {
                self.current_doc = doc;
                self.current_tf = self.reader.current_tf();
                doc
            }
            None => {
                self.current_doc = NO_MORE_DOCS;
                self.current_tf = 0;
                NO_MORE_DOCS
            }
        }
    }

    fn advance_doc(&mut self, target: DocId) -> DocId {
        self.pos_index = 0;
        match self.reader.advance(target) {
            Some(doc) => {
                self.current_doc = doc;
                // advance() always fills position_buf
                self.current_tf = self.reader.positions().len() as u32;
                doc
            }
            None => {
                self.current_doc = NO_MORE_DOCS;
                self.current_tf = 0;
                NO_MORE_DOCS
            }
        }
    }

    fn next_start_position(&mut self) -> u32 {
        if self.current_doc == NO_MORE_DOCS { return NO_MORE_POSITIONS; }

        if self.current_tf == 1 {
            // TF=1: single position from cached first_position
            if self.pos_index == 0 {
                self.pos_index = 1;
                return self.reader.first_position();
            }
            return NO_MORE_POSITIONS;
        }

        // TF>1: positions in position_buf
        let positions = self.reader.positions();
        if self.pos_index < positions.len() {
            let pos = positions[self.pos_index];
            self.pos_index += 1;
            pos
        } else {
            NO_MORE_POSITIONS
        }
    }

    fn start_position(&self) -> u32 {
        if self.pos_index == 0 { return NO_MORE_POSITIONS; }
        if self.current_tf == 1 {
            self.reader.first_position()
        } else {
            self.reader.positions()[self.pos_index - 1]
        }
    }

    fn end_position(&self) -> u32 {
        if self.pos_index == 0 { return NO_MORE_POSITIONS; }
        self.start_position() + 1
    }

    fn width(&self) -> u32 { 0 }
}

// ---------------------------------------------------------------------------
// NearSpansOrdered — ordered proximity matching
// ---------------------------------------------------------------------------

/// Finds documents where all sub-spans appear in order within `slop` gaps.
struct NearSpansOrdered<'a> {
    sub_spans: Vec<TermSpans<'a>>,
    slop: u32,
    current_doc: DocId,
    match_start: u32,
    match_end: u32,
    match_width: u32,
    /// Whether we've found a match in the current doc and need to
    /// find the next one on the next call to next_start_position().
    first_in_doc: bool,
}

unsafe impl Send for NearSpansOrdered<'_> {}

impl<'a> NearSpansOrdered<'a> {
    fn new(sub_spans: Vec<TermSpans<'a>>, slop: u32) -> Self {
        Self {
            sub_spans,
            slop,
            current_doc: NO_MORE_DOCS,
            match_start: NO_MORE_POSITIONS,
            match_end: NO_MORE_POSITIONS,
            match_width: 0,
            first_in_doc: false,
        }
    }

    /// Advance all sub-spans to the same document using conjunction.
    /// Returns the common doc ID, or NO_MORE_DOCS if no more common docs.
    fn advance_to_common_doc(&mut self) -> DocId {
        if self.sub_spans.is_empty() {
            return NO_MORE_DOCS;
        }

        // Start from the first sub-span's current doc
        let mut target = self.sub_spans[0].doc_id();
        if target == NO_MORE_DOCS {
            return NO_MORE_DOCS;
        }

        let mut i = 1;
        while i < self.sub_spans.len() {
            let doc = self.sub_spans[i].doc_id();
            if doc == target {
                i += 1;
                continue;
            }
            if doc == NO_MORE_DOCS {
                return NO_MORE_DOCS;
            }
            if doc < target {
                // Advance this sub-span to target
                let new_doc = self.sub_spans[i].advance_doc(target);
                if new_doc == NO_MORE_DOCS {
                    return NO_MORE_DOCS;
                }
                if new_doc > target {
                    // Overshot — restart from sub_spans[0]
                    target = new_doc;
                    // Advance sub_spans[0] to new target
                    let d0 = self.sub_spans[0].advance_doc(target);
                    if d0 == NO_MORE_DOCS {
                        return NO_MORE_DOCS;
                    }
                    target = d0;
                    i = 1; // restart alignment
                    continue;
                }
                i += 1;
            } else {
                // doc > target — advance sub_spans[0] and restart
                target = doc;
                let d0 = self.sub_spans[0].advance_doc(target);
                if d0 == NO_MORE_DOCS {
                    return NO_MORE_DOCS;
                }
                target = d0;
                i = 1;
            }
        }
        target
    }

    /// Try to form an ordered chain of sub-spans within slop.
    /// Returns true if a match is found, setting match_start/end/width.
    fn stretch_to_order(&mut self) -> bool {
        self.match_start = self.sub_spans[0].start_position();
        if self.match_start == NO_MORE_POSITIONS {
            return false;
        }
        self.match_width = 0;

        for i in 1..self.sub_spans.len() {
            let prev_end = self.sub_spans[i - 1].end_position();

            // Advance sub_span[i] so start >= prev_end (non-overlapping, ordered)
            while self.sub_spans[i].start_position() < prev_end {
                if self.sub_spans[i].next_start_position() == NO_MORE_POSITIONS {
                    return false;
                }
            }

            let gap = self.sub_spans[i].start_position() - prev_end;
            self.match_width += gap;
        }

        self.match_end = self.sub_spans.last().unwrap().end_position();
        self.match_width <= self.slop
    }

    /// Find the next position match in the current document.
    fn find_next_match_in_doc(&mut self) -> bool {
        loop {
            if !self.stretch_to_order() {
                return false;
            }
            if self.match_width <= self.slop {
                return true;
            }
            // Width too large — advance first sub-span and retry
            if self.sub_spans[0].next_start_position() == NO_MORE_POSITIONS {
                return false;
            }
            self.match_start = self.sub_spans[0].start_position();
        }
    }
}

impl Spans for NearSpansOrdered<'_> {
    fn doc_id(&self) -> DocId {
        self.current_doc
    }

    fn next_doc(&mut self) -> DocId {
        // Advance all sub-spans to next doc
        let next = self.sub_spans[0].next_doc();
        if next == NO_MORE_DOCS {
            self.current_doc = NO_MORE_DOCS;
            return NO_MORE_DOCS;
        }
        for i in 1..self.sub_spans.len() {
            self.sub_spans[i].next_doc();
        }
        self.current_doc = self.advance_to_common_doc();
        self.first_in_doc = true;
        self.current_doc
    }

    fn advance_doc(&mut self, target: DocId) -> DocId {
        for s in &mut self.sub_spans {
            s.advance_doc(target);
        }
        self.current_doc = self.advance_to_common_doc();
        self.first_in_doc = true;
        self.current_doc
    }

    fn next_start_position(&mut self) -> u32 {
        if self.current_doc == NO_MORE_DOCS {
            return NO_MORE_POSITIONS;
        }

        if self.first_in_doc {
            self.first_in_doc = false;
            // Initialize position iteration for all sub-spans
            for s in &mut self.sub_spans {
                if s.next_start_position() == NO_MORE_POSITIONS {
                    return NO_MORE_POSITIONS;
                }
            }
        } else {
            // Advance the first sub-span to find the next match
            if self.sub_spans[0].next_start_position() == NO_MORE_POSITIONS {
                return NO_MORE_POSITIONS;
            }
        }

        if self.find_next_match_in_doc() {
            self.match_start
        } else {
            NO_MORE_POSITIONS
        }
    }

    fn start_position(&self) -> u32 { self.match_start }
    fn end_position(&self) -> u32 { self.match_end }
    fn width(&self) -> u32 { self.match_width }
}

// ---------------------------------------------------------------------------
// NearSpansUnordered — unordered proximity matching via sliding window
// ---------------------------------------------------------------------------

/// Finds documents where all sub-spans appear within `slop` total gap
/// positions of each other, in any order. Uses a sliding window approach
/// over position arrays.
struct NearSpansUnordered<'a> {
    sub_spans: Vec<TermSpans<'a>>,
    slop: u32,
    current_doc: DocId,
    match_start: u32,
    match_end: u32,
    match_width: u32,
    /// Per-term position indices for the sliding window.
    indices: Vec<usize>,
    first_in_doc: bool,
}

unsafe impl Send for NearSpansUnordered<'_> {}

impl<'a> NearSpansUnordered<'a> {
    fn new(sub_spans: Vec<TermSpans<'a>>, slop: u32) -> Self {
        let n = sub_spans.len();
        Self {
            sub_spans,
            slop,
            current_doc: NO_MORE_DOCS,
            match_start: NO_MORE_POSITIONS,
            match_end: NO_MORE_POSITIONS,
            match_width: 0,
            indices: vec![0; n],
            first_in_doc: false,
        }
    }

    /// Reuse the same conjunction logic as ordered.
    fn advance_to_common_doc(&mut self) -> DocId {
        if self.sub_spans.is_empty() { return NO_MORE_DOCS; }
        let mut target = self.sub_spans[0].doc_id();
        if target == NO_MORE_DOCS { return NO_MORE_DOCS; }
        let mut i = 1;
        while i < self.sub_spans.len() {
            let doc = self.sub_spans[i].doc_id();
            if doc == target { i += 1; continue; }
            if doc == NO_MORE_DOCS { return NO_MORE_DOCS; }
            if doc < target {
                let new_doc = self.sub_spans[i].advance_doc(target);
                if new_doc == NO_MORE_DOCS { return NO_MORE_DOCS; }
                if new_doc > target {
                    target = new_doc;
                    let d0 = self.sub_spans[0].advance_doc(target);
                    if d0 == NO_MORE_DOCS { return NO_MORE_DOCS; }
                    target = d0; i = 1; continue;
                }
                i += 1;
            } else {
                target = doc;
                let d0 = self.sub_spans[0].advance_doc(target);
                if d0 == NO_MORE_DOCS { return NO_MORE_DOCS; }
                target = d0; i = 1;
            }
        }
        target
    }

    /// Get positions for sub-span i in the current doc.
    fn get_positions(&self, i: usize) -> Vec<u32> {
        let s = &self.sub_spans[i];
        if s.current_tf == 1 {
            vec![s.reader.first_position()]
        } else {
            s.reader.positions().to_vec()
        }
    }

    /// Sliding window: find a combination of positions (one per term) where
    /// the total gap fits within slop. Uses the min-advance approach.
    fn find_match_unordered(&mut self) -> bool {
        let n = self.sub_spans.len();
        let all_positions: Vec<Vec<u32>> = (0..n).map(|i| self.get_positions(i)).collect();

        // Check all position lists are non-empty
        for positions in &all_positions {
            if positions.is_empty() { return false; }
        }

        // Initialize indices to 0
        for idx in &mut self.indices { *idx = 0; }

        let max_span = self.slop + n as u32 - 1;

        loop {
            let mut min_pos = u32::MAX;
            let mut max_pos = 0u32;
            let mut min_idx = 0;

            for (i, &idx) in self.indices.iter().enumerate() {
                if idx >= all_positions[i].len() { return false; }
                let pos = all_positions[i][idx];
                if pos < min_pos { min_pos = pos; min_idx = i; }
                if pos > max_pos { max_pos = pos; }
            }

            let window = max_pos - min_pos;
            if window <= max_span {
                self.match_start = min_pos;
                self.match_end = max_pos + 1;
                self.match_width = window - (n as u32 - 1); // total gap
                return true;
            }

            // Advance the minimum
            self.indices[min_idx] += 1;
            if self.indices[min_idx] >= all_positions[min_idx].len() {
                return false;
            }
        }
    }
}

impl Spans for NearSpansUnordered<'_> {
    fn doc_id(&self) -> DocId { self.current_doc }

    fn next_doc(&mut self) -> DocId {
        let next = self.sub_spans[0].next_doc();
        if next == NO_MORE_DOCS { self.current_doc = NO_MORE_DOCS; return NO_MORE_DOCS; }
        for i in 1..self.sub_spans.len() { self.sub_spans[i].next_doc(); }
        self.current_doc = self.advance_to_common_doc();
        self.first_in_doc = true;
        self.current_doc
    }

    fn advance_doc(&mut self, target: DocId) -> DocId {
        for s in &mut self.sub_spans { s.advance_doc(target); }
        self.current_doc = self.advance_to_common_doc();
        self.first_in_doc = true;
        self.current_doc
    }

    fn next_start_position(&mut self) -> u32 {
        if self.current_doc == NO_MORE_DOCS { return NO_MORE_POSITIONS; }
        if self.first_in_doc {
            self.first_in_doc = false;
            if self.find_match_unordered() {
                return self.match_start;
            }
            return NO_MORE_POSITIONS;
        }
        // For subsequent matches in same doc, advance past current match
        // This is simplified — we only return the first match per doc.
        NO_MORE_POSITIONS
    }

    fn start_position(&self) -> u32 { self.match_start }
    fn end_position(&self) -> u32 { self.match_end }
    fn width(&self) -> u32 { self.match_width }
}

// ---------------------------------------------------------------------------
// SpanNotQuery — exclude overlapping spans
// ---------------------------------------------------------------------------

pub struct SpanNotQuery {
    pub include: Box<dyn Query>,
    pub exclude: Box<dyn Query>,
}

impl Query for SpanNotQuery {
    fn create_weight(&self, searcher: &Searcher, score_mode: ScoreMode) -> Result<Box<dyn Weight>> {
        let include_weight = self.include.create_weight(searcher, score_mode)?;
        let exclude_weight = self.exclude.create_weight(searcher, score_mode)?;
        Ok(Box::new(SpanNotWeight { include_weight, exclude_weight }))
    }
}

struct SpanNotWeight {
    include_weight: Box<dyn Weight>,
    exclude_weight: Box<dyn Weight>,
}

impl Weight for SpanNotWeight {
    fn scorer_supplier(&self, reader: &SegmentReader) -> Result<Option<Box<dyn ScorerSupplier>>> {
        let include = match self.include_weight.scorer_supplier(reader)? {
            Some(s) => s,
            None => return Ok(None),
        };
        let exclude = self.exclude_weight.scorer_supplier(reader)?;
        Ok(Some(Box::new(SpanNotScorerSupplier { include, exclude })))
    }
}

struct SpanNotScorerSupplier {
    include: Box<dyn ScorerSupplier>,
    exclude: Option<Box<dyn ScorerSupplier>>,
}

impl ScorerSupplier for SpanNotScorerSupplier {
    fn cost(&self) -> u64 { self.include.cost() }
    fn scorer(self: Box<Self>) -> Result<Box<dyn Scorer>> {
        let include = self.include.scorer()?;
        let exclude = match self.exclude {
            Some(e) => Some(e.scorer()?),
            None => None,
        };
        let mut scorer = SpanNotScorer { include, exclude };
        scorer.find_next_non_excluded();
        Ok(Box::new(scorer))
    }
}

/// Wraps an include scorer, filtering out docs that also match the exclude.
struct SpanNotScorer {
    include: Box<dyn Scorer>,
    exclude: Option<Box<dyn Scorer>>,
}

impl SpanNotScorer {
    fn is_excluded(&mut self) -> bool {
        let Some(ref mut exc) = self.exclude else { return false };
        let doc = self.include.doc_id();
        if exc.doc_id() < doc {
            exc.advance(doc);
        }
        exc.doc_id() == doc
    }

    fn find_next_non_excluded(&mut self) -> DocId {
        loop {
            let doc = self.include.doc_id();
            if doc == NO_MORE_DOCS { return NO_MORE_DOCS; }
            if !self.is_excluded() { return doc; }
            self.include.next();
        }
    }
}

impl Scorer for SpanNotScorer {
    fn doc_id(&self) -> DocId { self.include.doc_id() }
    fn next(&mut self) -> DocId {
        self.include.next();
        self.find_next_non_excluded()
    }
    fn advance(&mut self, target: DocId) -> DocId {
        self.include.advance(target);
        self.find_next_non_excluded()
    }
    fn score(&mut self) -> f32 { 1.0 }
    fn two_phase(&mut self) -> Option<&mut dyn TwoPhaseIterator> { None }
}

// ---------------------------------------------------------------------------
// SpanFirstQuery — match spans starting within first N positions
// ---------------------------------------------------------------------------

pub struct SpanFirstQuery {
    pub inner: Box<dyn Query>,
    pub end: u32,
}

impl Query for SpanFirstQuery {
    fn create_weight(&self, searcher: &Searcher, score_mode: ScoreMode) -> Result<Box<dyn Weight>> {
        let inner_weight = self.inner.create_weight(searcher, score_mode)?;
        Ok(Box::new(SpanFirstWeight { inner_weight, end: self.end }))
    }
}

struct SpanFirstWeight {
    inner_weight: Box<dyn Weight>,
    end: u32,
}

impl Weight for SpanFirstWeight {
    fn scorer_supplier(&self, reader: &SegmentReader) -> Result<Option<Box<dyn ScorerSupplier>>> {
        let inner = match self.inner_weight.scorer_supplier(reader)? {
            Some(s) => s,
            None => return Ok(None),
        };
        Ok(Some(Box::new(SpanFirstScorerSupplier { inner, end: self.end })))
    }
}

struct SpanFirstScorerSupplier {
    inner: Box<dyn ScorerSupplier>,
    end: u32,
}

impl ScorerSupplier for SpanFirstScorerSupplier {
    fn cost(&self) -> u64 { self.inner.cost() }
    fn scorer(self: Box<Self>) -> Result<Box<dyn Scorer>> {
        let inner = self.inner.scorer()?;
        // SpanFirst filters docs from the inner scorer based on position.
        // Since our span_term scorer doesn't expose positions through the
        // Scorer trait, we implement this as a doc-level filter that
        // re-checks position via TwoPhaseIterator pattern.
        // For now, we use span_term's position info implicitly —
        // the inner scorer already matches docs with the term, and we
        // need to filter to those where the term appears before position `end`.
        //
        // This simplified implementation accepts all docs from the inner
        // scorer. A full implementation would need the Spans interface
        // exposed through the scorer. TODO: wire Spans into SpanFirst properly.
        Ok(inner)
    }
}

// ---------------------------------------------------------------------------
// SpanTermQuery

// ---------------------------------------------------------------------------

pub struct SpanTermQuery {
    pub field: String,
    pub value: String,
}

impl Query for SpanTermQuery {
    fn create_weight(&self, _: &Searcher, _: ScoreMode) -> Result<Box<dyn Weight>> {
        Ok(Box::new(SpanTermWeight {
            field: self.field.clone(),
            value: self.value.clone(),
        }))
    }
}

struct SpanTermWeight { field: String, value: String }

impl Weight for SpanTermWeight {
    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),
        };
        if reader.postings_with_positions(field_id, &self.value).is_none() {
            return Ok(None);
        }
        Ok(Some(Box::new(SpanTermScorerSupplier {
            segment: reader as *const SegmentReader,
            field_id,
            value: self.value.clone(),
        })))
    }
}

struct SpanTermScorerSupplier { segment: *const SegmentReader, field_id: FieldId, value: String }
unsafe impl Send for SpanTermScorerSupplier {}

impl ScorerSupplier for SpanTermScorerSupplier {
    fn cost(&self) -> u64 { 1000 }
    fn scorer(self: Box<Self>) -> Result<Box<dyn Scorer>> {
        let reader = unsafe { &*self.segment };
        let pos_reader = reader.postings_with_positions(self.field_id, &self.value).unwrap();
        let mut spans = TermSpans::new(pos_reader);
        spans.next_doc(); // position on first doc
        Ok(Box::new(SimpleSpanScorer { spans }))
    }
}

// ---------------------------------------------------------------------------
// SpanNearQuery (ordered)
// ---------------------------------------------------------------------------

pub struct SpanNearQuery {
    pub field: String,
    pub terms: Vec<String>,
    pub slop: u32,
    pub in_order: bool,
}

impl Query for SpanNearQuery {
    fn create_weight(&self, _: &Searcher, _: ScoreMode) -> Result<Box<dyn Weight>> {
        Ok(Box::new(SpanNearWeight {
            field: self.field.clone(),
            terms: self.terms.clone(),
            slop: self.slop,
            in_order: self.in_order,
        }))
    }
}

struct SpanNearWeight { field: String, terms: Vec<String>, slop: u32, in_order: bool }

impl Weight for SpanNearWeight {
    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 term in &self.terms {
            if reader.postings_with_positions(field_id, term).is_none() {
                return Ok(None);
            }
        }
        Ok(Some(Box::new(SpanNearScorerSupplier {
            segment: reader as *const SegmentReader,
            field_id,
            terms: self.terms.clone(),
            slop: self.slop,
            in_order: self.in_order,
        })))
    }
}

struct SpanNearScorerSupplier {
    segment: *const SegmentReader, field_id: FieldId,
    terms: Vec<String>, slop: u32, in_order: bool,
}
unsafe impl Send for SpanNearScorerSupplier {}

impl ScorerSupplier for SpanNearScorerSupplier {
    fn cost(&self) -> u64 { 1000 }
    fn scorer(self: Box<Self>) -> Result<Box<dyn Scorer>> {
        let reader = unsafe { &*self.segment };
        let sub_spans: Vec<TermSpans> = self.terms.iter()
            .map(|t| TermSpans::new(reader.postings_with_positions(self.field_id, t).unwrap()))
            .collect();

        if self.in_order {
            let mut spans = NearSpansOrdered::new(sub_spans, self.slop);
            spans.next_doc();
            let mut scorer = TwoPhaseSpanScorer { spans };
            scorer.find_next_matching_doc();
            Ok(Box::new(scorer))
        } else {
            let mut spans = NearSpansUnordered::new(sub_spans, self.slop);
            spans.next_doc();
            let mut scorer = TwoPhaseSpanScorerUnordered { spans };
            scorer.find_next_matching_doc();
            Ok(Box::new(scorer))
        }
    }
}

// ---------------------------------------------------------------------------
// Scorer implementations that wrap Spans
// ---------------------------------------------------------------------------

/// Simple span scorer for span_term: every doc with the term matches.
struct SimpleSpanScorer<'a> {
    spans: TermSpans<'a>,
}

unsafe impl Send for SimpleSpanScorer<'_> {}

impl Scorer for SimpleSpanScorer<'_> {
    fn doc_id(&self) -> DocId { self.spans.doc_id() }
    fn next(&mut self) -> DocId { self.spans.next_doc() }
    fn advance(&mut self, target: DocId) -> DocId { self.spans.advance_doc(target) }
    fn score(&mut self) -> f32 {
        1.0 // Span term scores 1.0 (positions don't affect score)
    }
    fn two_phase(&mut self) -> Option<&mut dyn TwoPhaseIterator> { None }
}

/// Two-phase span scorer for span_near: doc conjunction + position check.
/// Iterates through documents where all terms co-occur, then checks positions.
struct TwoPhaseSpanScorer<'a> {
    spans: NearSpansOrdered<'a>,
}

unsafe impl Send for TwoPhaseSpanScorer<'_> {}

impl TwoPhaseSpanScorer<'_> {
    /// Find next doc where positions satisfy the near constraint.
    fn find_next_matching_doc(&mut self) -> DocId {
        loop {
            if self.spans.current_doc == NO_MORE_DOCS {
                return NO_MORE_DOCS;
            }
            if self.spans.next_start_position() != NO_MORE_POSITIONS {
                return self.spans.current_doc;
            }
            self.spans.next_doc();
        }
    }
}

impl Scorer for TwoPhaseSpanScorer<'_> {
    fn doc_id(&self) -> DocId { self.spans.doc_id() }
    fn next(&mut self) -> DocId { self.spans.next_doc(); self.find_next_matching_doc() }
    fn advance(&mut self, target: DocId) -> DocId { self.spans.advance_doc(target); self.find_next_matching_doc() }
    fn score(&mut self) -> f32 { 1.0 }
    fn two_phase(&mut self) -> Option<&mut dyn TwoPhaseIterator> { None }
}

/// Two-phase span scorer for unordered span_near.
struct TwoPhaseSpanScorerUnordered<'a> {
    spans: NearSpansUnordered<'a>,
}

unsafe impl Send for TwoPhaseSpanScorerUnordered<'_> {}

impl TwoPhaseSpanScorerUnordered<'_> {
    fn find_next_matching_doc(&mut self) -> DocId {
        loop {
            if self.spans.current_doc == NO_MORE_DOCS { return NO_MORE_DOCS; }
            if self.spans.next_start_position() != NO_MORE_POSITIONS {
                return self.spans.current_doc;
            }
            self.spans.next_doc();
        }
    }
}

impl Scorer for TwoPhaseSpanScorerUnordered<'_> {
    fn doc_id(&self) -> DocId { self.spans.doc_id() }
    fn next(&mut self) -> DocId { self.spans.next_doc(); self.find_next_matching_doc() }
    fn advance(&mut self, target: DocId) -> DocId { self.spans.advance_doc(target); self.find_next_matching_doc() }
    fn score(&mut self) -> f32 { 1.0 }

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

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::segment::builder::SegmentBuilder;
    use luci_analysis::Token;
    use luci_core::SegmentId;
    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_searcher(docs: &[&[&str]]) -> Searcher {
        let schema = Mapping::builder().field("text", FieldType::Text).build();
        let mut builder = SegmentBuilder::new(SegmentId::new(1), &schema);
        for terms in docs {
            builder.add_document(&[(FieldId::new(0), make_tokens(terms))], b"{}");
        }
        Searcher::new(vec![SegmentReader::open(builder.build()).unwrap()])
    }

    #[test]
    fn span_term_basic() {
        let searcher = build_searcher(&[
            &["the", "quick", "brown", "fox"],
            &["the", "lazy", "dog"],
            &["quick", "fox"],
        ]);
        let results = searcher.search_query(&SpanTermQuery {
            field: "text".into(), value: "quick".into(),
        }, 10, 0, &crate::search::SourceFilter::Enabled, &None, false, &[]);
        assert_eq!(results.total_hits.value, 2); // docs 0 and 2
    }

    #[test]
    fn span_term_missing() {
        let searcher = build_searcher(&[&["the", "quick"]]);
        let results = searcher.search_query(&SpanTermQuery {
            field: "text".into(), value: "nonexistent".into(),
        }, 10, 0, &crate::search::SourceFilter::Enabled, &None, false, &[]);
        assert_eq!(results.total_hits.value, 0);
    }

    #[test]
    fn span_near_exact_phrase() {
        // slop=0, in_order=true → exact phrase match
        let searcher = build_searcher(&[
            &["the", "quick", "brown", "fox"],   // "quick brown" at [1,2]
            &["brown", "quick", "fox"],            // "quick" at [1], "brown" at [0] — wrong order
            &["quick", "brown"],                   // exact match at [0,1]
        ]);
        let results = searcher.search_query(&SpanNearQuery {
            field: "text".into(),
            terms: vec!["quick".into(), "brown".into()],
            slop: 0,
            in_order: true,
        }, 10, 0, &crate::search::SourceFilter::Enabled, &None, false, &[]);
        assert_eq!(results.total_hits.value, 2); // docs 0 and 2
    }

    #[test]
    fn span_near_with_slop() {
        let searcher = build_searcher(&[
            &["quick", "brown", "fox"],   // quick(0) fox(2): gap=1
            &["quick", "fox"],             // quick(0) fox(1): gap=0
            &["quick", "a", "b", "fox"],  // quick(0) fox(3): gap=2
        ]);
        let results = searcher.search_query(&SpanNearQuery {
            field: "text".into(),
            terms: vec!["quick".into(), "fox".into()],
            slop: 1,
            in_order: true,
        }, 10, 0, &crate::search::SourceFilter::Enabled, &None, false, &[]);
        assert_eq!(results.total_hits.value, 2); // docs 0 (gap=1) and 1 (gap=0)
    }

    #[test]
    fn span_near_no_match() {
        let searcher = build_searcher(&[
            &["quick", "a", "b", "c", "fox"],  // gap=3, too far for slop=1
        ]);
        let results = searcher.search_query(&SpanNearQuery {
            field: "text".into(),
            terms: vec!["quick".into(), "fox".into()],
            slop: 1,
            in_order: true,
        }, 10, 0, &crate::search::SourceFilter::Enabled, &None, false, &[]);
        assert_eq!(results.total_hits.value, 0);
    }

    #[test]
    fn span_near_three_terms() {
        let searcher = build_searcher(&[
            &["the", "quick", "brown", "fox"],  // quick(1) brown(2) fox(3): consecutive
            &["quick", "fox", "brown"],          // wrong order for brown/fox
        ]);
        let results = searcher.search_query(&SpanNearQuery {
            field: "text".into(),
            terms: vec!["quick".into(), "brown".into(), "fox".into()],
            slop: 0,
            in_order: true,
        }, 10, 0, &crate::search::SourceFilter::Enabled, &None, false, &[]);
        assert_eq!(results.total_hits.value, 1); // only doc 0
    }

    #[test]
    fn span_near_wrong_order() {
        let searcher = build_searcher(&[
            &["fox", "quick"],  // fox before quick — doesn't match ordered
        ]);
        let results = searcher.search_query(&SpanNearQuery {
            field: "text".into(),
            terms: vec!["quick".into(), "fox".into()],
            slop: 5,
            in_order: true,
        }, 10, 0, &crate::search::SourceFilter::Enabled, &None, false, &[]);
        assert_eq!(results.total_hits.value, 0);
    }

    #[test]
    fn span_near_one_term_missing() {
        let searcher = build_searcher(&[&["quick", "brown"]]);
        let results = searcher.search_query(&SpanNearQuery {
            field: "text".into(),
            terms: vec!["quick".into(), "nonexistent".into()],
            slop: 10,
            in_order: true,
        }, 10, 0, &crate::search::SourceFilter::Enabled, &None, false, &[]);
        assert_eq!(results.total_hits.value, 0);
    }

    // --- Unordered tests ---

    #[test]
    fn span_near_unordered_basic() {
        let searcher = build_searcher(&[
            &["the", "fox", "quick"],  // fox(1) quick(2): distance 1
            &["quick", "a", "b", "c", "fox"],  // quick(0) fox(4): distance 3
        ]);
        let results = searcher.search_query(&SpanNearQuery {
            field: "text".into(),
            terms: vec!["quick".into(), "fox".into()],
            slop: 1,
            in_order: false,
        }, 10, 0, &crate::search::SourceFilter::Enabled, &None, false, &[]);
        assert_eq!(results.total_hits.value, 1); // only doc 0 (window=1)
    }

    #[test]
    fn span_near_unordered_reversed() {
        // "fox" before "quick" — should match unordered but not ordered
        let searcher = build_searcher(&[
            &["fox", "quick"],
        ]);
        let ordered = searcher.search_query(&SpanNearQuery {
            field: "text".into(),
            terms: vec!["quick".into(), "fox".into()],
            slop: 1,
            in_order: true,
        }, 10, 0, &crate::search::SourceFilter::Enabled, &None, false, &[]);
        assert_eq!(ordered.total_hits.value, 0); // ordered: doesn't match

        let unordered = searcher.search_query(&SpanNearQuery {
            field: "text".into(),
            terms: vec!["quick".into(), "fox".into()],
            slop: 0,
            in_order: false,
        }, 10, 0, &crate::search::SourceFilter::Enabled, &None, false, &[]);
        assert_eq!(unordered.total_hits.value, 1); // unordered: matches
    }

    // --- SpanNot tests ---

    #[test]
    fn span_not_basic() {
        let searcher = build_searcher(&[
            &["quick", "fox"],       // has "quick" and "fox"
            &["quick", "brown"],     // has "quick" but not "fox"
            &["slow", "dog"],        // doesn't have "quick"
        ]);
        let results = searcher.search_query(&SpanNotQuery {
            include: Box::new(SpanTermQuery {
                field: "text".into(), value: "quick".into(),
            }),
            exclude: Box::new(SpanTermQuery {
                field: "text".into(), value: "fox".into(),
            }),
        }, 10, 0, &crate::search::SourceFilter::Enabled, &None, false, &[]);
        assert_eq!(results.total_hits.value, 1); // only doc 1 (has quick, no fox)
    }

    #[test]
    fn span_not_no_exclusions() {
        let searcher = build_searcher(&[
            &["quick", "fox"],
            &["quick", "brown"],
        ]);
        let results = searcher.search_query(&SpanNotQuery {
            include: Box::new(SpanTermQuery {
                field: "text".into(), value: "quick".into(),
            }),
            exclude: Box::new(SpanTermQuery {
                field: "text".into(), value: "nonexistent".into(),
            }),
        }, 10, 0, &crate::search::SourceFilter::Enabled, &None, false, &[]);
        assert_eq!(results.total_hits.value, 2); // nothing excluded
    }
}
