//! Segment merge: combine multiple segments into one, applying deletions.
//!
//! Re-reads source documents from each segment's doc store and re-indexes
//! them through the full indexing pipeline (text analysis, columnar values,
//! vectors, geo points, nested documents). This preserves all field types.
//!
//! Direct posting-list merge (merge-sort FSTs, re-delta-encode postings)
//! is a future optimization — see [[architecture-segment-merge]].
//!
//! See [[architecture-segment-merge]] and [[architecture-merge-policy]].

use luci_analysis::AnalyzerRegistry;
use luci_core::{DocId, SegmentId};
use luci_mapping::Mapping;

use crate::deletion::DeletionMap;
use crate::segment::reader::SegmentReader;

/// Merge multiple segments into one, applying deletions.
///
/// Re-reads source documents from each segment's doc store and runs them
/// through the full `IndexWriter::add()` pipeline, preserving all field
/// types: text, keyword, numeric, boolean, dense_vector, geo_point, and
/// nested documents.
///
/// Documents marked as deleted in `deletions` are excluded.
///
/// Returns the merged segment bytes.
pub fn merge_segments(
    new_segment_id: SegmentId,
    readers: &[&SegmentReader],
    deletions: &DeletionMap,
    schema: &Mapping,
    analyzers: &AnalyzerRegistry,
) -> Vec<u8> {
    use crate::segment::builder::SegmentBuilder;

    let mut builder = SegmentBuilder::new(new_segment_id, schema);

    // Build a temporary writer to reuse the full add() logic for field
    // extraction (text analysis, columnar, vectors, geo, nested).
    // We can't use IndexWriter directly (it owns storage), so we
    // replicate the document processing logic.
    for reader in readers {
        let seg_id = reader.segment_id();
        let doc_store = reader.doc_store();

        for doc_idx in 0..reader.doc_count() {
            let doc_id = DocId::new(doc_idx);

            // Skip deleted documents
            if deletions.is_deleted(seg_id, doc_id) {
                continue;
            }

            // Read source document
            let source_bytes = match doc_store.get(doc_idx) {
                Some(bytes) => bytes,
                None => continue,
            };

            let doc: serde_json::Value = match serde_json::from_slice(&source_bytes) {
                Ok(v) => v,
                Err(_) => continue,
            };

            // Run through full indexing pipeline
            index_document(&doc, &source_bytes, schema, analyzers, &mut builder);
        }
    }

    builder.build()
}

/// Index a single document into a SegmentBuilder, handling all field types.
///
/// This replicates the field processing logic from `IndexWriter::add()` so
/// that merge preserves all data (text, columnar, vectors, geo, nested).
fn index_document(
    doc: &serde_json::Value,
    source_bytes: &[u8],
    schema: &Mapping,
    analyzers: &AnalyzerRegistry,
    builder: &mut crate::segment::builder::SegmentBuilder,
) {
    use luci_analysis::Token;
    use luci_core::FieldId;
    use luci_mapping::FieldType;
    use crate::columnar::writer::ColumnValue;
    use crate::spatial::geo::GeoPoint;

    let obj = match doc.as_object() {
        Some(o) => o,
        None => return,
    };

    let mut analyzed_fields: Vec<(FieldId, Vec<Token>)> = Vec::new();
    let mut column_values: Vec<(FieldId, ColumnValue)> = Vec::new();
    let mut vector_fields: Vec<(FieldId, Vec<f32>)> = Vec::new();
    let mut geo_points: Vec<(FieldId, GeoPoint)> = Vec::new();
    let mut geo_shapes: Vec<(FieldId, ::geo::Geometry<f64>)> = Vec::new();

    for (field_name, value) in obj {
        let field_id = match schema.field_id(field_name) {
            Some(id) => id,
            None => continue,
        };

        let mapping = schema.field(field_id);

        // Build tokens for inverted index (skipped if index: false)
        let tokens = match &mapping.field_type {
            FieldType::Text => {
                let text = value.as_str().unwrap_or_default();
                let analyzer_name = mapping.analyzer.as_deref().unwrap_or("standard");
                let analyzer = analyzers.get(analyzer_name);
                analyzer.analyze(text)
            }
            FieldType::Keyword => {
                let text = match value {
                    serde_json::Value::String(s) => s.clone(),
                    other => other.to_string(),
                };
                vec![Token::new(text, 0, 0, 0)]
            }
            FieldType::Ip => {
                let text = value.as_str().unwrap_or_default();
                let normalized = crate::ip::normalize_ip(text);
                if normalized.is_empty() { Vec::new() }
                else { vec![Token::new(normalized, 0, 0, 0)] }
            }
            _ => Vec::new(),
        };

        if !tokens.is_empty() && mapping.indexed {
            analyzed_fields.push((field_id, tokens));
        }

        // Geo points
        if matches!(mapping.field_type, FieldType::GeoPoint) {
            if let Some(point) = GeoPoint::from_json(value) {
                geo_points.push((field_id, point));
            }
        }

        // Geo shapes
        if matches!(mapping.field_type, FieldType::GeoShape) {
            if let Some(geom) = crate::spatial::shape::parse_geojson(value) {
                geo_shapes.push((field_id, geom));
            }
        }

        // Vectors
        if mapping.field_type.is_dense_vector() {
            if let serde_json::Value::Array(arr) = value {
                let vec: Vec<f32> = arr.iter()
                    .filter_map(|v| v.as_f64().map(|f| f as f32))
                    .collect();
                if !vec.is_empty() {
                    vector_fields.push((field_id, vec));
                }
            }
        }

        // Columnar values
        if mapping.doc_values {
            let col_val = match &mapping.field_type {
                FieldType::Keyword => match value {
                    serde_json::Value::String(s) => ColumnValue::Keyword(s.clone()),
                    serde_json::Value::Null => ColumnValue::Null,
                    other => ColumnValue::Keyword(other.to_string()),
                },
                FieldType::Integer | FieldType::Long => match value {
                    serde_json::Value::Number(n) => ColumnValue::I64(n.as_i64().unwrap_or(0)),
                    _ => ColumnValue::Null,
                },
                FieldType::Float | FieldType::Double => match value {
                    serde_json::Value::Number(n) => ColumnValue::F64(n.as_f64().unwrap_or(0.0)),
                    _ => ColumnValue::Null,
                },
                FieldType::Boolean => match value {
                    serde_json::Value::Bool(b) => ColumnValue::Bool(*b),
                    _ => ColumnValue::Null,
                },
                FieldType::TokenCount => {
                    let text = value.as_str().unwrap_or_default();
                    let analyzer_name = mapping.analyzer.as_deref().unwrap_or("standard");
                    let analyzer = analyzers.get(analyzer_name);
                    ColumnValue::I64(analyzer.analyze(text).len() as i64)
                },
                FieldType::Ip => {
                    let text = value.as_str().unwrap_or_default();
                    match crate::ip::ip_to_i64(text) {
                        Some(v) => ColumnValue::I64(v),
                        None => ColumnValue::Null,
                    }
                },
                _ => ColumnValue::Null,
            };
            column_values.push((field_id, col_val));
        }
    }

    // Add the document
    let has_nested = schema.fields().iter()
        .any(|f| matches!(f.field_type, FieldType::Nested));

    builder.add_document(&analyzed_fields, source_bytes);

    if has_nested {
        builder.mark_parent();
    }

    for (field_id, col_val) in column_values {
        builder.add_column_value(field_id, col_val);
    }

    for (field_id, vec) in vector_fields {
        builder.add_vector(field_id, vec);
    }

    for (field_id, point) in geo_points {
        builder.add_geo_point(field_id, point);
    }

    for (field_id, geom) in &geo_shapes {
        builder.add_geo_shape(*field_id, geom);
    }

    // Nested documents
    for mapping in schema.fields() {
        if !matches!(mapping.field_type, FieldType::Nested) {
            continue;
        }
        let field_name = &mapping.name;
        if let Some(serde_json::Value::Array(nested_arr)) = obj.get(field_name) {
            for nested_obj in nested_arr {
                if let Some(nested_map) = nested_obj.as_object() {
                    let mut nested_fields: Vec<(FieldId, Vec<Token>)> = Vec::new();
                    for (nested_key, nested_val) in nested_map {
                        let prefixed = format!("{field_name}.{nested_key}");
                        if let Some(fid) = schema.field_id(&prefixed) {
                            let m = schema.field(fid);
                            let tokens = match &m.field_type {
                                FieldType::Text => {
                                    let text = nested_val.as_str().unwrap_or_default();
                                    let analyzer = analyzers.get(
                                        m.analyzer.as_deref().unwrap_or("standard"),
                                    );
                                    analyzer.analyze(text)
                                }
                                FieldType::Keyword => {
                                    let text = match nested_val {
                                        serde_json::Value::String(s) => s.clone(),
                                        other => other.to_string(),
                                    };
                                    vec![Token::new(text, 0, 0, 0)]
                                }
                                _ => continue,
                            };
                            if !tokens.is_empty() {
                                nested_fields.push((fid, tokens));
                            }
                        }
                    }
                    builder.add_document(&nested_fields, b"{}");
                    builder.mark_nested();
                }
            }
        }
    }
}

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

    fn test_schema() -> Mapping {
        Mapping::builder()
            .field("body", FieldType::Text)
            .field("tag", FieldType::Keyword)
            .build()
    }

    fn build_segment(id: u64, docs: &[serde_json::Value]) -> Vec<u8> {
        let schema = test_schema();
        let analyzers = AnalyzerRegistry::new();
        let mut builder = SegmentBuilder::new(SegmentId::new(id), &schema);

        for doc in docs {
            let source = serde_json::to_vec(doc).unwrap();
            index_document(doc, &source, &schema, &analyzers, &mut builder);
        }

        builder.build()
    }

    #[test]
    fn merge_two_segments_no_deletions() {
        let seg1_data = build_segment(
            1,
            &[
                serde_json::json!({"body": "hello world", "tag": "a"}),
                serde_json::json!({"body": "foo bar", "tag": "b"}),
            ],
        );
        let seg2_data = build_segment(
            2,
            &[
                serde_json::json!({"body": "hello luci", "tag": "c"}),
            ],
        );

        let reader1 = SegmentReader::open(seg1_data).unwrap();
        let reader2 = SegmentReader::open(seg2_data).unwrap();
        let deletions = DeletionMap::new();

        let merged_data = merge_segments(
            SegmentId::new(10),
            &[&reader1, &reader2],
            &deletions,
            &test_schema(),
            &AnalyzerRegistry::new(),
        );

        let merged = SegmentReader::open(merged_data).unwrap();
        assert_eq!(merged.doc_count(), 3);

        // "hello" should appear in 2 docs
        assert_eq!(merged.doc_freq(FieldId::new(0), "hello"), 2);
    }

    #[test]
    fn merge_with_some_deletions() {
        let seg1_data = build_segment(
            1,
            &[
                serde_json::json!({"body": "doc zero"}),
                serde_json::json!({"body": "doc one"}),
                serde_json::json!({"body": "doc two"}),
            ],
        );

        let reader1 = SegmentReader::open(seg1_data).unwrap();
        let mut deletions = DeletionMap::new();
        deletions.mark_deleted(SegmentId::new(1), DocId::new(1)); // delete "doc one"

        let merged_data = merge_segments(
            SegmentId::new(10),
            &[&reader1],
            &deletions,
            &test_schema(),
            &AnalyzerRegistry::new(),
        );

        let merged = SegmentReader::open(merged_data).unwrap();
        assert_eq!(merged.doc_count(), 2);

        // "one" should not be found (deleted)
        assert_eq!(merged.doc_freq(FieldId::new(0), "one"), 0);
        // "zero" and "two" should still be present
        assert_eq!(merged.doc_freq(FieldId::new(0), "zero"), 1);
        assert_eq!(merged.doc_freq(FieldId::new(0), "two"), 1);
    }

    #[test]
    fn merge_with_all_deletions() {
        let seg1_data = build_segment(
            1,
            &[
                serde_json::json!({"body": "hello"}),
                serde_json::json!({"body": "world"}),
            ],
        );

        let reader1 = SegmentReader::open(seg1_data).unwrap();
        let mut deletions = DeletionMap::new();
        deletions.mark_deleted(SegmentId::new(1), DocId::new(0));
        deletions.mark_deleted(SegmentId::new(1), DocId::new(1));

        let merged_data = merge_segments(
            SegmentId::new(10),
            &[&reader1],
            &deletions,
            &test_schema(),
            &AnalyzerRegistry::new(),
        );

        let merged = SegmentReader::open(merged_data).unwrap();
        assert_eq!(merged.doc_count(), 0);
    }

    #[test]
    fn merged_doc_store_works() {
        let seg1_data = build_segment(
            1,
            &[serde_json::json!({"body": "alpha"})],
        );
        let seg2_data = build_segment(
            2,
            &[serde_json::json!({"body": "beta"})],
        );

        let reader1 = SegmentReader::open(seg1_data).unwrap();
        let reader2 = SegmentReader::open(seg2_data).unwrap();

        let merged_data = merge_segments(
            SegmentId::new(10),
            &[&reader1, &reader2],
            &DeletionMap::new(),
            &test_schema(),
            &AnalyzerRegistry::new(),
        );

        let merged = SegmentReader::open(merged_data).unwrap();
        let store = merged.doc_store();
        let doc0: serde_json::Value = serde_json::from_slice(&store.get(0).unwrap()).unwrap();
        let doc1: serde_json::Value = serde_json::from_slice(&store.get(1).unwrap()).unwrap();
        assert_eq!(doc0["body"], "alpha");
        assert_eq!(doc1["body"], "beta");
    }

    #[test]
    fn merged_norms_correct() {
        let seg1_data = build_segment(
            1,
            &[serde_json::json!({"body": "one two three"})],
        );
        let seg2_data = build_segment(
            2,
            &[serde_json::json!({"body": "four five"})],
        );

        let reader1 = SegmentReader::open(seg1_data).unwrap();
        let reader2 = SegmentReader::open(seg2_data).unwrap();

        let merged_data = merge_segments(
            SegmentId::new(10),
            &[&reader1, &reader2],
            &DeletionMap::new(),
            &test_schema(),
            &AnalyzerRegistry::new(),
        );

        let merged = SegmentReader::open(merged_data).unwrap();
        let norms = merged.norms(FieldId::new(0)).unwrap();
        assert_eq!(norms.norm(DocId::new(0)), 3.0); // "one two three"
        assert_eq!(norms.norm(DocId::new(1)), 2.0); // "four five"
    }

    #[test]
    fn merged_term_queries_work() {
        let seg1_data = build_segment(
            1,
            &[
                serde_json::json!({"body": "search engine"}),
                serde_json::json!({"body": "search luci"}),
            ],
        );
        let seg2_data = build_segment(
            2,
            &[
                serde_json::json!({"body": "search index"}),
            ],
        );

        let reader1 = SegmentReader::open(seg1_data).unwrap();
        let reader2 = SegmentReader::open(seg2_data).unwrap();
        let mut deletions = DeletionMap::new();
        deletions.mark_deleted(SegmentId::new(1), DocId::new(0)); // delete "search engine"

        let merged_data = merge_segments(
            SegmentId::new(10),
            &[&reader1, &reader2],
            &deletions,
            &test_schema(),
            &AnalyzerRegistry::new(),
        );

        let merged = SegmentReader::open(merged_data).unwrap();
        assert_eq!(merged.doc_count(), 2); // "search luci" + "search index"
        assert_eq!(merged.doc_freq(FieldId::new(0), "search"), 2);
        assert_eq!(merged.doc_freq(FieldId::new(0), "engine"), 0);
        assert_eq!(merged.doc_freq(FieldId::new(0), "luci"), 1);
        assert_eq!(merged.doc_freq(FieldId::new(0), "index"), 1);
    }

    #[test]
    fn contiguous_doc_ids_after_merge() {
        let seg1_data = build_segment(
            1,
            &[
                serde_json::json!({"body": "doc zero"}),
                serde_json::json!({"body": "doc one"}),
                serde_json::json!({"body": "doc two"}),
            ],
        );

        let reader1 = SegmentReader::open(seg1_data).unwrap();
        let mut deletions = DeletionMap::new();
        deletions.mark_deleted(SegmentId::new(1), DocId::new(1));

        let merged_data = merge_segments(
            SegmentId::new(10),
            &[&reader1],
            &deletions,
            &test_schema(),
            &AnalyzerRegistry::new(),
        );

        let merged = SegmentReader::open(merged_data).unwrap();
        assert_eq!(merged.doc_count(), 2);

        // Doc IDs should be contiguous 0..1
        let mut postings = merged.postings(FieldId::new(0), "doc").unwrap();
        assert_eq!(postings.next().unwrap().0, DocId::new(0));
        assert_eq!(postings.next().unwrap().0, DocId::new(1));
        assert!(postings.next().is_none());
    }

    #[test]
    fn bm25_rankings_preserved_after_merge() {
        // Build two segments, merge, verify that BM25 search on the merged
        // segment produces the expected ranking.
        let seg1_data = build_segment(
            1,
            &[
                serde_json::json!({"body": "search"}),
                serde_json::json!({"body": "search search search"}),
            ],
        );
        let seg2_data = build_segment(
            2,
            &[
                serde_json::json!({"body": "search other terms here"}),
            ],
        );

        let reader1 = SegmentReader::open(seg1_data).unwrap();
        let reader2 = SegmentReader::open(seg2_data).unwrap();

        let merged_data = merge_segments(
            SegmentId::new(10),
            &[&reader1, &reader2],
            &DeletionMap::new(),
            &test_schema(),
            &AnalyzerRegistry::new(),
        );

        let merged = SegmentReader::open(merged_data).unwrap();

        // Search the merged segment
        use crate::search::searcher::Searcher;
        let searcher = Searcher::new(vec![merged]);
        let results = searcher.search_term(FieldId::new(0), "search", 10);

        assert_eq!(results.total_hits.value, 3);
        // Doc with highest TF ("search search search") should rank first
        assert_eq!(results.hits[0].doc_id, DocId::new(1));
    }
}
