//! Graph RAG Retriever — Phase-86
//!
//! Implements `Retriever` trait for SocialGraphV2, enabling graph-enhanced
//! retrieval-augmented generation.
//!
//! # Features
//!
//! - **Vector search on graph entities**: Find semantically similar entities
//! - **Neighbor expansion**: Traverse edges 1-N hops to gather relationship context
//! - **Relation-type filtering**: Focus on specific relationship types
//! - **Edge-weight ranking**: Prioritize high-weight (SiFu) connections
//! - **Community summary injection**: Pre-generated community summaries as context
//! - **Three-way fusion**: Combine with Vector+BM25 results via RRF
//!
//! # Architecture
//!
//! ```text
//! Query embedding
//!       │
//!       ▼
//! ┌────────────────────┐
//! │  SocialGraphV2     │  ← vector search → seed entities
//! │  .search_by_vector │
//! └────────┬───────────┘
//!          │
//!          ▼
//! ┌────────────────────┐
//! │  Neighbor Expansion│  ← 1~N hop traversal along edges
//! │  (configurable)    │
//! └────────┬───────────┘
//!          │
//!          ▼
//! ┌────────────────────┐
//! │  Relation Context  │  ← Format: "A --type→ B (weight: 0.9)"
//! │  Formatting        │
//! └────────┬───────────┘
//!          │
//!          ▼
//!   Vec<RetrievalResult>
//! ```

use std::collections::{HashMap, HashSet};
use std::sync::Arc;

use super::error::{RagError, RagResult};
use super::traits::Retriever;
use super::types::{RagConfig, RetrievalResult};

use crate::social::store_v2::SocialStoreV2;
use crate::types::graph::{Entity, Relation};

// ============================================================================
// Configuration
// ============================================================================

/// Configuration for graph-based retrieval.
#[derive(Debug, Clone)]
pub struct GraphRetrieverConfig {
    /// Maximum number of seed entities from vector search
    pub seed_top_k: usize,

    /// Number of hops for neighbor expansion (1 = direct neighbors only)
    pub expansion_depth: usize,

    /// Maximum number of relations to include per seed entity
    pub max_relations_per_entity: usize,

    /// Maximum total relations in the result
    pub max_total_relations: usize,

    /// Optional: filter by relation types (empty = all types)
    pub relation_type_filter: Vec<String>,

    /// Optional: filter by entity types (empty = all types)
    pub entity_type_filter: Option<String>,

    /// Minimum edge weight to include (0.0 = all)
    pub min_edge_weight: f64,

    /// Whether to sort neighbors by edge weight (SiFu score)
    pub use_edge_weight_ranking: bool,

    /// Whether to include entity properties in the context text
    pub include_entity_properties: bool,

    /// Source label for score_details (used in RRF fusion)
    pub source_label: String,
}

impl Default for GraphRetrieverConfig {
    fn default() -> Self {
        Self {
            seed_top_k: 5,
            expansion_depth: 1,
            max_relations_per_entity: 10,
            max_total_relations: 50,
            relation_type_filter: Vec::new(),
            entity_type_filter: None,
            min_edge_weight: 0.0,
            use_edge_weight_ranking: true,
            include_entity_properties: true,
            source_label: "graph".to_string(),
        }
    }
}

// ============================================================================
// GraphRetriever
// ============================================================================

/// A `Retriever` that searches SocialGraphV2 for related entities and relations.
///
/// Pipeline:
/// 1. Vector search → seed entities
/// 2. Expand each seed along edges (1-N hops)
/// 3. Format entity + relation info as RetrievalResult
pub struct GraphRetriever {
    /// Reference to the graph store
    graph: Arc<SocialStoreV2>,

    /// Configuration
    config: GraphRetrieverConfig,
}

impl GraphRetriever {
    /// Create a new GraphRetriever.
    pub fn new(graph: Arc<SocialStoreV2>) -> Self {
        Self {
            graph,
            config: GraphRetrieverConfig::default(),
        }
    }

    /// Create with custom configuration.
    pub fn with_config(graph: Arc<SocialStoreV2>, config: GraphRetrieverConfig) -> Self {
        Self { graph, config }
    }

    /// Get a reference to the configuration.
    pub fn config(&self) -> &GraphRetrieverConfig {
        &self.config
    }

    /// Expand seed entities by traversing graph edges.
    ///
    /// Returns (expanded_entities, relations) where expanded_entities includes
    /// both seed entities and their neighbors.
    fn expand_neighbors(
        &self,
        seed_ids: &[String],
    ) -> (Vec<Entity>, Vec<Relation>) {
        let mut visited: HashSet<String> = HashSet::new();
        let mut all_entities: Vec<Entity> = Vec::new();
        let mut all_relations: Vec<Relation> = Vec::new();
        let mut frontier: Vec<String> = seed_ids.to_vec();

        // Mark seeds as visited and collect their entities
        for id in seed_ids {
            visited.insert(id.clone());
            if let Some(entity) = self.graph.get_entity_by_id(id) {
                all_entities.push(entity);
            }
        }

        // BFS expansion up to `expansion_depth` hops
        for _hop in 0..self.config.expansion_depth {
            let mut next_frontier: Vec<String> = Vec::new();

            for entity_id in &frontier {
                let relations = self.graph.get_entity_relations(entity_id);

                // Filter by relation type if specified
                let filtered: Vec<Relation> = relations
                    .into_iter()
                    .filter(|r| {
                        if !self.config.relation_type_filter.is_empty() {
                            self.config
                                .relation_type_filter
                                .iter()
                                .any(|t| t == &r.relation_type)
                        } else {
                            true
                        }
                    })
                    .filter(|r| r.weight >= self.config.min_edge_weight)
                    .collect();

                // Sort by weight if using edge-weight ranking
                let mut sorted = filtered;
                if self.config.use_edge_weight_ranking {
                    sorted.sort_by(|a, b| {
                        b.weight.partial_cmp(&a.weight).unwrap_or(std::cmp::Ordering::Equal)
                    });
                }

                // Limit relations per entity
                let truncated: Vec<Relation> = sorted
                    .into_iter()
                    .take(self.config.max_relations_per_entity)
                    .collect();

                for rel in truncated {
                    // Determine the neighbor ID (the other end of the edge)
                    let neighbor_id = if rel.source == *entity_id {
                        rel.target.clone()
                    } else {
                        rel.source.clone()
                    };

                    // Add relation
                    if all_relations.len() < self.config.max_total_relations {
                        all_relations.push(rel);
                    }

                    // Expand to unvisited neighbors
                    if !visited.contains(&neighbor_id) {
                        visited.insert(neighbor_id.clone());
                        if let Some(neighbor_entity) = self.graph.get_entity_by_id(&neighbor_id) {
                            all_entities.push(neighbor_entity);
                        }
                        next_frontier.push(neighbor_id);
                    }
                }
            }

            frontier = next_frontier;
            if frontier.is_empty() {
                break;
            }
        }

        (all_entities, all_relations)
    }

    /// Format entities and relations into a textual context for LLM consumption.
    fn format_graph_context(
        &self,
        entities: &[Entity],
        relations: &[Relation],
        seed_scores: &HashMap<String, f32>,
    ) -> Vec<RetrievalResult> {
        let entity_map: HashMap<&str, &Entity> = entities
            .iter()
            .map(|e| (e.id.as_str(), e))
            .collect();

        let mut results: Vec<RetrievalResult> = Vec::new();

        // 1) Create a RetrievalResult per seed entity with its relation context
        for entity in entities {
            let score = seed_scores
                .get(&entity.id)
                .copied()
                .unwrap_or(0.5) as f64;

            let mut content_parts: Vec<String> = Vec::new();

            // Entity header
            content_parts.push(format!(
                "[Entity] {} (type: {})",
                entity.name, entity.entity_type
            ));

            // Properties if enabled
            if self.config.include_entity_properties && !entity.properties.is_empty() {
                let props: Vec<String> = entity
                    .properties
                    .iter()
                    .filter(|(k, _)| *k != "embedding") // skip embedding vectors
                    .take(10) // limit property count
                    .map(|(k, v)| format!("  {}: {}", k, v))
                    .collect();
                if !props.is_empty() {
                    content_parts.push(format!("Properties:\n{}", props.join("\n")));
                }
            }

            // Outgoing/incoming relations for this entity
            let entity_relations: Vec<&Relation> = relations
                .iter()
                .filter(|r| r.source == entity.id || r.target == entity.id)
                .collect();

            if !entity_relations.is_empty() {
                let rel_lines: Vec<String> = entity_relations
                    .iter()
                    .map(|r| {
                        let source_name = entity_map
                            .get(r.source.as_str())
                            .map(|e| e.name.as_str())
                            .unwrap_or(&r.source);
                        let target_name = entity_map
                            .get(r.target.as_str())
                            .map(|e| e.name.as_str())
                            .unwrap_or(&r.target);
                        format!(
                            "  {} --[{}]--> {} (weight: {:.2})",
                            source_name, r.relation_type, target_name, r.weight
                        )
                    })
                    .collect();
                content_parts.push(format!("Relations:\n{}", rel_lines.join("\n")));
            }

            let content = content_parts.join("\n");

            let mut score_details = HashMap::new();
            score_details.insert(self.config.source_label.clone(), score);

            let mut metadata = HashMap::new();
            metadata.insert(
                "entity_type".to_string(),
                serde_json::json!(entity.entity_type),
            );
            metadata.insert(
                "entity_id".to_string(),
                serde_json::json!(entity.id),
            );
            metadata.insert(
                "source".to_string(),
                serde_json::json!("graph_retriever"),
            );
            metadata.insert(
                "relation_count".to_string(),
                serde_json::json!(entity_relations.len()),
            );

            results.push(RetrievalResult {
                id: entity.id.clone(),
                content,
                score,
                collection: Some("graph".to_string()),
                score_details,
                metadata,
            });
        }

        results
    }

    /// Text-based entity search fallback (when no embedding is available).
    fn text_search_entities(&self, query: &str, top_k: usize) -> Vec<(Entity, f32)> {
        // Use hybrid_search with text_query only
        match self.graph.hybrid_search(
            None,
            Some(query),
            self.config.entity_type_filter.as_deref(),
            top_k,
        ) {
            Ok(results) => results
                .into_iter()
                .map(|r| (r.entity, r.score))
                .collect(),
            Err(_) => Vec::new(),
        }
    }
}

impl Retriever for GraphRetriever {
    fn retrieve(
        &self,
        query: &str,
        embedding: Option<&[f32]>,
        config: &RagConfig,
    ) -> RagResult<Vec<RetrievalResult>> {
        let top_k = config.top_k.min(self.config.seed_top_k);

        // 1. Find seed entities via vector search or text fallback
        let seed_results: Vec<(String, f32, Entity)> = if let Some(emb) = embedding {
            // Vector search
            match self.graph.search_entities_by_vector(
                emb,
                top_k,
                self.config.entity_type_filter.as_deref(),
            ) {
                Ok(results) => results
                    .into_iter()
                    .map(|r| (r.entity_id, r.score, r.entity))
                    .collect(),
                Err(e) => {
                    // Fall back to text search if vector search fails
                    eprintln!("[GraphRetriever] Vector search failed: {}, falling back to text", e);
                    self.text_search_entities(query, top_k)
                        .into_iter()
                        .map(|(entity, score)| (entity.id.clone(), score, entity))
                        .collect()
                }
            }
        } else {
            // Text-only search
            self.text_search_entities(query, top_k)
                .into_iter()
                .map(|(entity, score)| (entity.id.clone(), score, entity))
                .collect()
        };

        if seed_results.is_empty() {
            return Ok(Vec::new());
        }

        // Build seed ID list and score map
        let seed_ids: Vec<String> = seed_results.iter().map(|(id, _, _)| id.clone()).collect();
        let seed_scores: HashMap<String, f32> = seed_results
            .iter()
            .map(|(id, score, _)| (id.clone(), *score))
            .collect();

        // 2. Expand neighbors
        let (entities, relations) = self.expand_neighbors(&seed_ids);

        // 3. Format as RetrievalResult
        let mut results = self.format_graph_context(&entities, &relations, &seed_scores);

        // 4. Apply min_score filter
        if let Some(min_score) = config.min_score {
            results.retain(|r| r.score >= min_score);
        }

        // 5. Sort by score descending and limit to top_k
        results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
        results.truncate(config.top_k);

        Ok(results)
    }
}

// ============================================================================
// Graph Context Expander — enrich existing retrieval results with graph context
// ============================================================================

/// Enriches existing `RetrievalResult`s with graph relationship context.
///
/// When documents are retrieved by Vector/BM25, this expander looks up
/// any entity IDs mentioned in the results and appends their graph
/// neighborhood as additional context.
pub struct GraphContextExpander {
    /// Reference to the graph store
    graph: Arc<SocialStoreV2>,

    /// Maximum expansion depth (hops)
    expansion_depth: usize,

    /// Maximum relations per entity
    max_relations: usize,

    /// Minimum edge weight
    min_weight: f64,
}

impl GraphContextExpander {
    /// Create a new expander.
    pub fn new(graph: Arc<SocialStoreV2>) -> Self {
        Self {
            graph,
            expansion_depth: 1,
            max_relations: 10,
            min_weight: 0.0,
        }
    }

    /// Set expansion depth.
    pub fn with_depth(mut self, depth: usize) -> Self {
        self.expansion_depth = depth;
        self
    }

    /// Set max relations per entity.
    pub fn with_max_relations(mut self, max: usize) -> Self {
        self.max_relations = max;
        self
    }

    /// Expand a set of retrieval results by looking up entity IDs in the graph.
    ///
    /// For each result whose `id` corresponds to a graph entity, appends
    /// relationship information to the content.
    pub fn expand(&self, results: &mut Vec<RetrievalResult>) {
        for result in results.iter_mut() {
            // Try to find entity by the result's ID
            if let Some(_entity) = self.graph.get_entity_by_id(&result.id) {
                let relations = self.get_weighted_relations(&result.id);
                if !relations.is_empty() {
                    let context = self.format_relations(&result.id, &relations);
                    result.content.push_str("\n\n[Graph Relations]\n");
                    result.content.push_str(&context);
                    result
                        .metadata
                        .insert("graph_expanded".into(), serde_json::json!(true));
                    result.metadata.insert(
                        "graph_relation_count".into(),
                        serde_json::json!(relations.len()),
                    );
                }
            }
        }
    }

    /// Get relations for an entity, filtered and sorted by weight.
    fn get_weighted_relations(&self, entity_id: &str) -> Vec<Relation> {
        let mut relations = self.graph.get_entity_relations(entity_id);

        // Filter by minimum weight
        relations.retain(|r| r.weight >= self.min_weight);

        // Sort by weight descending
        relations.sort_by(|a, b| {
            b.weight.partial_cmp(&a.weight).unwrap_or(std::cmp::Ordering::Equal)
        });

        // Limit
        relations.truncate(self.max_relations);
        relations
    }

    /// Format relations into human-readable text.
    fn format_relations(&self, entity_id: &str, relations: &[Relation]) -> String {
        let mut lines = Vec::new();
        for rel in relations {
            let other_id = if rel.source == entity_id {
                &rel.target
            } else {
                &rel.source
            };
            let other_name = self
                .graph
                .get_entity_by_id(other_id)
                .map(|e| e.name.clone())
                .unwrap_or_else(|| other_id.to_string());

            let direction = if rel.source == entity_id {
                "-->"
            } else {
                "<--"
            };

            lines.push(format!(
                "  {} [{}] {} (weight: {:.2})",
                direction, rel.relation_type, other_name, rel.weight
            ));
        }
        lines.join("\n")
    }

    /// Multi-hop expansion: given entity IDs from retrieval results,
    /// gather entities and relations within `expansion_depth` hops.
    pub fn expand_multi_hop(
        &self,
        entity_ids: &[String],
    ) -> Vec<RetrievalResult> {
        let mut visited: HashSet<String> = HashSet::new();
        let mut frontier: Vec<String> = entity_ids.to_vec();
        let mut all_relations: Vec<Relation> = Vec::new();

        for id in entity_ids {
            visited.insert(id.clone());
        }

        for _hop in 0..self.expansion_depth {
            let mut next: Vec<String> = Vec::new();
            for id in &frontier {
                let rels = self.get_weighted_relations(id);
                for rel in rels {
                    let other = if rel.source == *id {
                        &rel.target
                    } else {
                        &rel.source
                    };
                    if !visited.contains(other) {
                        visited.insert(other.clone());
                        next.push(other.clone());
                    }
                    all_relations.push(rel);
                }
            }
            frontier = next;
            if frontier.is_empty() {
                break;
            }
        }

        // Deduplicate relations by ID
        let mut seen: HashSet<String> = HashSet::new();
        all_relations.retain(|r| seen.insert(r.id.clone()));

        // Build a single "graph context" RetrievalResult
        if all_relations.is_empty() {
            return Vec::new();
        }

        let context_lines: Vec<String> = all_relations
            .iter()
            .map(|r| {
                let src_name = self.graph.get_entity_by_id(&r.source)
                    .map(|e| e.name.clone())
                    .unwrap_or_else(|| r.source.clone());
                let tgt_name = self.graph.get_entity_by_id(&r.target)
                    .map(|e| e.name.clone())
                    .unwrap_or_else(|| r.target.clone());
                format!(
                    "{} --[{}]--> {} (weight: {:.2})",
                    src_name, r.relation_type, tgt_name, r.weight
                )
            })
            .collect();

        let content = format!(
            "[Graph Context — {} relations across {} entities]\n{}",
            all_relations.len(),
            visited.len(),
            context_lines.join("\n")
        );

        let mut metadata = HashMap::new();
        metadata.insert("source".into(), serde_json::json!("graph_context_expander"));
        metadata.insert("entity_count".into(), serde_json::json!(visited.len()));
        metadata.insert("relation_count".into(), serde_json::json!(all_relations.len()));

        let mut score_details = HashMap::new();
        score_details.insert("graph".to_string(), 0.8); // Default graph context score

        vec![RetrievalResult {
            id: "graph-context".to_string(),
            content,
            score: 0.8,
            collection: Some("graph".to_string()),
            score_details,
            metadata,
        }]
    }
}

// ============================================================================
// Community Summary Cache
// ============================================================================

/// Pre-generated community summaries for Graph RAG.
///
/// Stores LLM-generated summaries of graph communities/sub-graphs.
/// During retrieval, when a seed entity belongs to a community with a cached
/// summary, the summary is injected directly — avoiding real-time LLM calls.
#[derive(Debug, Clone)]
pub struct CommunitySummaryCache {
    /// Map: community_id → summary text
    summaries: HashMap<String, CommunitySummary>,

    /// Map: entity_id → community_id (membership)
    entity_community: HashMap<String, String>,
}

/// A single community summary.
#[derive(Debug, Clone)]
pub struct CommunitySummary {
    /// Community identifier
    pub community_id: String,

    /// Human-readable name
    pub name: String,

    /// LLM-generated summary text
    pub summary: String,

    /// Entity IDs in this community
    pub member_ids: Vec<String>,

    /// When the summary was generated (Unix millis)
    pub generated_at: u64,
}

impl CommunitySummaryCache {
    /// Create an empty cache.
    pub fn new() -> Self {
        Self {
            summaries: HashMap::new(),
            entity_community: HashMap::new(),
        }
    }

    /// Add a community summary.
    pub fn add_summary(&mut self, summary: CommunitySummary) {
        for member_id in &summary.member_ids {
            self.entity_community
                .insert(member_id.clone(), summary.community_id.clone());
        }
        self.summaries
            .insert(summary.community_id.clone(), summary);
    }

    /// Look up a summary for a given entity.
    pub fn get_summary_for_entity(&self, entity_id: &str) -> Option<&CommunitySummary> {
        self.entity_community
            .get(entity_id)
            .and_then(|cid| self.summaries.get(cid))
    }

    /// Get all summaries.
    pub fn all_summaries(&self) -> Vec<&CommunitySummary> {
        self.summaries.values().collect()
    }

    /// Number of cached summaries.
    pub fn len(&self) -> usize {
        self.summaries.len()
    }

    /// Whether the cache is empty.
    pub fn is_empty(&self) -> bool {
        self.summaries.is_empty()
    }

    /// Look up summaries relevant to a set of entity IDs.
    /// Returns unique summaries (deduped by community_id).
    pub fn get_summaries_for_entities(
        &self,
        entity_ids: &[String],
    ) -> Vec<&CommunitySummary> {
        let mut seen: HashSet<&str> = HashSet::new();
        let mut results = Vec::new();

        for eid in entity_ids {
            if let Some(cid) = self.entity_community.get(eid.as_str()) {
                if seen.insert(cid.as_str()) {
                    if let Some(summary) = self.summaries.get(cid.as_str()) {
                        results.push(summary);
                    }
                }
            }
        }

        results
    }

    /// Convert community summaries into RetrievalResults for context injection.
    pub fn to_retrieval_results(
        &self,
        entity_ids: &[String],
    ) -> Vec<RetrievalResult> {
        self.get_summaries_for_entities(entity_ids)
            .into_iter()
            .map(|cs| {
                let mut metadata = HashMap::new();
                metadata.insert("source".into(), serde_json::json!("community_summary"));
                metadata.insert("community_id".into(), serde_json::json!(cs.community_id));
                metadata.insert("member_count".into(), serde_json::json!(cs.member_ids.len()));

                let mut score_details = HashMap::new();
                score_details.insert("community".to_string(), 0.85);

                RetrievalResult {
                    id: format!("community-{}", cs.community_id),
                    content: format!(
                        "[Community: {}]\n{}",
                        cs.name, cs.summary
                    ),
                    score: 0.85,
                    collection: Some("graph_community".to_string()),
                    score_details,
                    metadata,
                }
            })
            .collect()
    }
}

impl Default for CommunitySummaryCache {
    fn default() -> Self {
        Self::new()
    }
}

// ============================================================================
// Three-way Fusion Retriever (Vector + BM25 + Graph)
// ============================================================================

/// Three-way hybrid retriever: Vector + BM25 (via HybridRetriever) + Graph.
///
/// Combines results from a traditional `Retriever` and a `GraphRetriever`
/// using Reciprocal Rank Fusion (RRF).
pub struct ThreeWayFusionRetriever {
    /// Traditional hybrid retriever (Vector + BM25)
    hybrid: Box<dyn Retriever>,

    /// Graph-based retriever
    graph: GraphRetriever,

    /// Optional community summary cache
    community_cache: Option<CommunitySummaryCache>,

    /// Optional graph context expander (for enriching hybrid results)
    expander: Option<GraphContextExpander>,

    /// Weight for hybrid results in final score (0.0-1.0)
    hybrid_weight: f64,

    /// Weight for graph results in final score (0.0-1.0)
    graph_weight: f64,

    /// Weight for community summary results (0.0-1.0)
    community_weight: f64,

    /// RRF k parameter (default 60)
    rrf_k: f64,
}

impl ThreeWayFusionRetriever {
    /// Create a new fusion retriever.
    pub fn new(
        hybrid: Box<dyn Retriever>,
        graph: GraphRetriever,
    ) -> Self {
        Self {
            hybrid,
            graph,
            community_cache: None,
            expander: None,
            hybrid_weight: 0.5,
            graph_weight: 0.4,
            community_weight: 0.1,
            rrf_k: 60.0,
        }
    }

    /// Set community summary cache.
    pub fn with_community_cache(mut self, cache: CommunitySummaryCache) -> Self {
        self.community_cache = Some(cache);
        self
    }

    /// Set graph context expander for enriching hybrid results.
    pub fn with_expander(mut self, expander: GraphContextExpander) -> Self {
        self.expander = Some(expander);
        self
    }

    /// Set weights (hybrid, graph, community). Automatically normalized.
    pub fn with_weights(mut self, hybrid: f64, graph: f64, community: f64) -> Self {
        let total = hybrid + graph + community;
        if total > 0.0 {
            self.hybrid_weight = hybrid / total;
            self.graph_weight = graph / total;
            self.community_weight = community / total;
        }
        self
    }

    /// Set RRF k parameter.
    pub fn with_rrf_k(mut self, k: f64) -> Self {
        self.rrf_k = k;
        self
    }

    /// Weighted RRF fusion: merge multiple ranked lists.
    ///
    /// For each document, RRF score = Σ (weight_i / (k + rank_i))
    fn rrf_fuse(
        &self,
        lists: Vec<(Vec<RetrievalResult>, f64)>, // (results, weight)
        top_k: usize,
    ) -> Vec<RetrievalResult> {
        // Map: doc_id → (rrf_score, best RetrievalResult)
        let mut scores: HashMap<String, (f64, RetrievalResult)> = HashMap::new();

        for (results, weight) in lists {
            for (rank, result) in results.into_iter().enumerate() {
                let rrf_score = weight / (self.rrf_k + rank as f64 + 1.0);
                let entry = scores.entry(result.id.clone()).or_insert_with(|| {
                    (0.0, result.clone())
                });
                entry.0 += rrf_score;

                // Merge score_details
                for (key, val) in &result.score_details {
                    entry.1.score_details.insert(key.clone(), *val);
                }
                // Merge metadata
                for (key, val) in &result.metadata {
                    entry.1.metadata.insert(key.clone(), val.clone());
                }
            }
        }

        // Build final results sorted by RRF score
        let mut fused: Vec<RetrievalResult> = scores
            .into_iter()
            .map(|(_, (rrf_score, mut result))| {
                result.score = rrf_score;
                result.score_details.insert("rrf".to_string(), rrf_score);
                result
            })
            .collect();

        fused.sort_by(|a, b| {
            b.score
                .partial_cmp(&a.score)
                .unwrap_or(std::cmp::Ordering::Equal)
        });
        fused.truncate(top_k);
        fused
    }
}

impl Retriever for ThreeWayFusionRetriever {
    fn retrieve(
        &self,
        query: &str,
        embedding: Option<&[f32]>,
        config: &RagConfig,
    ) -> RagResult<Vec<RetrievalResult>> {
        let mut lists: Vec<(Vec<RetrievalResult>, f64)> = Vec::new();

        // 1. Hybrid retrieval (Vector + BM25)
        let mut hybrid_results = self.hybrid.retrieve(query, embedding, config)?;

        // Optionally expand hybrid results with graph context
        if let Some(ref expander) = self.expander {
            expander.expand(&mut hybrid_results);
        }

        if !hybrid_results.is_empty() {
            lists.push((hybrid_results, self.hybrid_weight));
        }

        // 2. Graph retrieval
        let graph_results = self.graph.retrieve(query, embedding, config)?;
        if !graph_results.is_empty() {
            // Collect entity IDs for community lookup
            let entity_ids: Vec<String> = graph_results
                .iter()
                .filter_map(|r| r.metadata.get("entity_id"))
                .filter_map(|v| v.as_str())
                .map(|s| s.to_string())
                .collect();

            lists.push((graph_results, self.graph_weight));

            // 3. Community summaries (if available)
            if let Some(ref cache) = self.community_cache {
                let community_results = cache.to_retrieval_results(&entity_ids);
                if !community_results.is_empty() {
                    lists.push((community_results, self.community_weight));
                }
            }
        }

        if lists.is_empty() {
            return Ok(Vec::new());
        }

        // 4. RRF fusion
        Ok(self.rrf_fuse(lists, config.top_k))
    }
}

// ============================================================================
// Four-way Fusion Retriever (Vector + BM25 + Graph + Tree) — Phase-91 T91.5
// ============================================================================

/// Four-way hybrid retriever: Vector + BM25 + Graph + Tree.
///
/// Extends `ThreeWayFusionRetriever` by adding tree-indexed document retrieval.
/// All four sources are fused via weighted Reciprocal Rank Fusion (RRF).
///
/// When a `TreeRetriever` is not provided, this behaves identically to
/// `ThreeWayFusionRetriever`.
pub struct FourWayFusionRetriever {
    /// Traditional hybrid retriever (Vector + BM25)
    hybrid: Box<dyn Retriever>,

    /// Graph-based retriever
    graph: GraphRetriever,

    /// Optional tree-based retriever (Phase-91)
    tree: Option<super::tree_retriever::TreeRetriever>,

    /// Optional community summary cache
    community_cache: Option<CommunitySummaryCache>,

    /// Optional graph context expander
    expander: Option<GraphContextExpander>,

    /// Weight for hybrid results (0.0-1.0)
    hybrid_weight: f64,

    /// Weight for graph results (0.0-1.0)
    graph_weight: f64,

    /// Weight for community summary results (0.0-1.0)
    community_weight: f64,

    /// Weight for tree results (0.0-1.0)
    tree_weight: f64,

    /// RRF k parameter (default 60)
    rrf_k: f64,
}

impl FourWayFusionRetriever {
    /// Create a new four-way fusion retriever.
    pub fn new(
        hybrid: Box<dyn Retriever>,
        graph: GraphRetriever,
    ) -> Self {
        Self {
            hybrid,
            graph,
            tree: None,
            community_cache: None,
            expander: None,
            hybrid_weight: 0.4,
            graph_weight: 0.3,
            community_weight: 0.05,
            tree_weight: 0.25,
            rrf_k: 60.0,
        }
    }

    /// Add a tree retriever for structure-aware document retrieval.
    pub fn with_tree(mut self, tree: super::tree_retriever::TreeRetriever) -> Self {
        self.tree = Some(tree);
        self
    }

    /// Set community summary cache.
    pub fn with_community_cache(mut self, cache: CommunitySummaryCache) -> Self {
        self.community_cache = Some(cache);
        self
    }

    /// Set graph context expander.
    pub fn with_expander(mut self, expander: GraphContextExpander) -> Self {
        self.expander = Some(expander);
        self
    }

    /// Set weights (hybrid, graph, community, tree). Automatically normalized.
    pub fn with_weights(mut self, hybrid: f64, graph: f64, community: f64, tree: f64) -> Self {
        let total = hybrid + graph + community + tree;
        if total > 0.0 {
            self.hybrid_weight = hybrid / total;
            self.graph_weight = graph / total;
            self.community_weight = community / total;
            self.tree_weight = tree / total;
        }
        self
    }

    /// Set RRF k parameter.
    pub fn with_rrf_k(mut self, k: f64) -> Self {
        self.rrf_k = k;
        self
    }

    /// Weighted RRF fusion: merge multiple ranked lists.
    fn rrf_fuse(
        &self,
        lists: Vec<(Vec<RetrievalResult>, f64)>,
        top_k: usize,
    ) -> Vec<RetrievalResult> {
        let mut scores: HashMap<String, (f64, RetrievalResult)> = HashMap::new();

        for (results, weight) in lists {
            for (rank, result) in results.into_iter().enumerate() {
                let rrf_score = weight / (self.rrf_k + rank as f64 + 1.0);
                let entry = scores.entry(result.id.clone()).or_insert_with(|| {
                    (0.0, result.clone())
                });
                entry.0 += rrf_score;

                // Merge score_details
                for (key, val) in &result.score_details {
                    entry.1.score_details.insert(key.clone(), *val);
                }
                // Merge metadata
                for (key, val) in &result.metadata {
                    entry.1.metadata.insert(key.clone(), val.clone());
                }
            }
        }

        let mut fused: Vec<RetrievalResult> = scores
            .into_iter()
            .map(|(_, (rrf_score, mut result))| {
                result.score = rrf_score;
                result.score_details.insert("rrf".to_string(), rrf_score);
                result
            })
            .collect();

        fused.sort_by(|a, b| {
            b.score
                .partial_cmp(&a.score)
                .unwrap_or(std::cmp::Ordering::Equal)
        });
        fused.truncate(top_k);
        fused
    }
}

impl Retriever for FourWayFusionRetriever {
    fn retrieve(
        &self,
        query: &str,
        embedding: Option<&[f32]>,
        config: &RagConfig,
    ) -> RagResult<Vec<RetrievalResult>> {
        let mut lists: Vec<(Vec<RetrievalResult>, f64)> = Vec::new();

        // 1. Hybrid retrieval (Vector + BM25)
        let mut hybrid_results = self.hybrid.retrieve(query, embedding, config)?;

        if let Some(ref expander) = self.expander {
            expander.expand(&mut hybrid_results);
        }

        if !hybrid_results.is_empty() {
            lists.push((hybrid_results, self.hybrid_weight));
        }

        // 2. Graph retrieval
        let graph_results = self.graph.retrieve(query, embedding, config)?;
        if !graph_results.is_empty() {
            let entity_ids: Vec<String> = graph_results
                .iter()
                .filter_map(|r| r.metadata.get("entity_id"))
                .filter_map(|v| v.as_str())
                .map(|s| s.to_string())
                .collect();

            lists.push((graph_results, self.graph_weight));

            // 3. Community summaries (if available)
            if let Some(ref cache) = self.community_cache {
                let community_results = cache.to_retrieval_results(&entity_ids);
                if !community_results.is_empty() {
                    lists.push((community_results, self.community_weight));
                }
            }
        }

        // 4. Tree retrieval (Phase-91)
        if let Some(ref tree) = self.tree {
            let tree_results = tree.retrieve(query, embedding, config)?;
            if !tree_results.is_empty() {
                lists.push((tree_results, self.tree_weight));
            }
        }

        if lists.is_empty() {
            return Ok(Vec::new());
        }

        // 5. RRF fusion
        Ok(self.rrf_fuse(lists, config.top_k))
    }
}

// ============================================================================
// Tests
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;

    // ── Config tests ──────────────────────────────────────────────────

    #[test]
    fn test_graph_retriever_config_defaults() {
        let config = GraphRetrieverConfig::default();
        assert_eq!(config.seed_top_k, 5);
        assert_eq!(config.expansion_depth, 1);
        assert_eq!(config.max_relations_per_entity, 10);
        assert_eq!(config.max_total_relations, 50);
        assert!(config.relation_type_filter.is_empty());
        assert!(config.entity_type_filter.is_none());
        assert_eq!(config.min_edge_weight, 0.0);
        assert!(config.use_edge_weight_ranking);
        assert!(config.include_entity_properties);
        assert_eq!(config.source_label, "graph");
    }

    // ── Community summary cache tests ─────────────────────────────────

    #[test]
    fn test_community_cache_empty() {
        let cache = CommunitySummaryCache::new();
        assert!(cache.is_empty());
        assert_eq!(cache.len(), 0);
        assert!(cache.get_summary_for_entity("entity-1").is_none());
    }

    #[test]
    fn test_community_cache_add_and_lookup() {
        let mut cache = CommunitySummaryCache::new();
        cache.add_summary(CommunitySummary {
            community_id: "comm-1".into(),
            name: "Tech Community".into(),
            summary: "A community of tech entities".into(),
            member_ids: vec!["e1".into(), "e2".into(), "e3".into()],
            generated_at: 1000,
        });

        assert_eq!(cache.len(), 1);
        assert!(!cache.is_empty());

        // Lookup by member entity
        let s = cache.get_summary_for_entity("e1").unwrap();
        assert_eq!(s.community_id, "comm-1");
        assert_eq!(s.name, "Tech Community");

        let s2 = cache.get_summary_for_entity("e2").unwrap();
        assert_eq!(s2.community_id, "comm-1");

        // Non-member returns None
        assert!(cache.get_summary_for_entity("e99").is_none());
    }

    #[test]
    fn test_community_cache_multiple_communities() {
        let mut cache = CommunitySummaryCache::new();
        cache.add_summary(CommunitySummary {
            community_id: "c1".into(),
            name: "Community 1".into(),
            summary: "Summary 1".into(),
            member_ids: vec!["a".into(), "b".into()],
            generated_at: 1000,
        });
        cache.add_summary(CommunitySummary {
            community_id: "c2".into(),
            name: "Community 2".into(),
            summary: "Summary 2".into(),
            member_ids: vec!["c".into(), "d".into()],
            generated_at: 2000,
        });

        assert_eq!(cache.len(), 2);

        let summaries = cache.get_summaries_for_entities(&["a".into(), "c".into(), "x".into()]);
        assert_eq!(summaries.len(), 2);
    }

    #[test]
    fn test_community_to_retrieval_results() {
        let mut cache = CommunitySummaryCache::new();
        cache.add_summary(CommunitySummary {
            community_id: "c1".into(),
            name: "AI Research".into(),
            summary: "Entities related to AI research topics".into(),
            member_ids: vec!["e1".into(), "e2".into()],
            generated_at: 1000,
        });

        let results = cache.to_retrieval_results(&["e1".into()]);
        assert_eq!(results.len(), 1);
        assert!(results[0].content.contains("AI Research"));
        assert!(results[0].content.contains("Entities related to AI research"));
        assert_eq!(results[0].collection.as_deref(), Some("graph_community"));
    }

    #[test]
    fn test_community_dedup() {
        let mut cache = CommunitySummaryCache::new();
        cache.add_summary(CommunitySummary {
            community_id: "c1".into(),
            name: "Comm".into(),
            summary: "Test".into(),
            member_ids: vec!["e1".into(), "e2".into()],
            generated_at: 0,
        });

        // Both e1 and e2 belong to same community — should return only 1 summary
        let results = cache.get_summaries_for_entities(&["e1".into(), "e2".into()]);
        assert_eq!(results.len(), 1);
    }

    // ── RRF fusion tests ──────────────────────────────────────────────

    #[test]
    fn test_rrf_single_list() {
        let retriever = ThreeWayFusionRetriever {
            hybrid: Box::new(DummyRetriever(Vec::new())),
            graph: create_dummy_graph_retriever(),
            community_cache: None,
            expander: None,
            hybrid_weight: 0.5,
            graph_weight: 0.5,
            community_weight: 0.0,
            rrf_k: 60.0,
        };

        let results = vec![
            RetrievalResult::new("a", "Content A", 0.9),
            RetrievalResult::new("b", "Content B", 0.8),
        ];

        let fused = retriever.rrf_fuse(vec![(results, 1.0)], 10);
        assert_eq!(fused.len(), 2);
        assert!(fused[0].score > fused[1].score);
    }

    #[test]
    fn test_rrf_two_lists_overlap() {
        let retriever = ThreeWayFusionRetriever {
            hybrid: Box::new(DummyRetriever(Vec::new())),
            graph: create_dummy_graph_retriever(),
            community_cache: None,
            expander: None,
            hybrid_weight: 0.5,
            graph_weight: 0.5,
            community_weight: 0.0,
            rrf_k: 60.0,
        };

        let list1 = vec![
            RetrievalResult::new("a", "Content A", 0.9),
            RetrievalResult::new("b", "Content B", 0.8),
        ];
        let list2 = vec![
            RetrievalResult::new("b", "Content B from graph", 0.95),
            RetrievalResult::new("c", "Content C", 0.7),
        ];

        let fused = retriever.rrf_fuse(
            vec![(list1, 0.5), (list2, 0.5)],
            10,
        );

        // "b" appears in both lists, should have highest fused score
        assert_eq!(fused[0].id, "b");
        assert_eq!(fused.len(), 3);
    }

    #[test]
    fn test_rrf_truncate() {
        let retriever = ThreeWayFusionRetriever {
            hybrid: Box::new(DummyRetriever(Vec::new())),
            graph: create_dummy_graph_retriever(),
            community_cache: None,
            expander: None,
            hybrid_weight: 0.5,
            graph_weight: 0.5,
            community_weight: 0.0,
            rrf_k: 60.0,
        };

        let results = (0..20)
            .map(|i| RetrievalResult::new(format!("doc-{}", i), format!("Content {}", i), 1.0 - i as f64 * 0.01))
            .collect();

        let fused = retriever.rrf_fuse(vec![(results, 1.0)], 5);
        assert_eq!(fused.len(), 5);
    }

    // ── GraphContextExpander tests ────────────────────────────────────

    #[test]
    fn test_graph_context_expander_creation() {
        // Just verify the expander can be created (no live graph)
        // Full integration tests require a running SocialStoreV2
        let config = GraphRetrieverConfig::default();
        assert_eq!(config.expansion_depth, 1);
    }

    // ── Test helpers ──────────────────────────────────────────────────

    /// Dummy retriever for testing RRF fusion.
    struct DummyRetriever(Vec<RetrievalResult>);

    impl Retriever for DummyRetriever {
        fn retrieve(
            &self,
            _query: &str,
            _embedding: Option<&[f32]>,
            _config: &RagConfig,
        ) -> RagResult<Vec<RetrievalResult>> {
            Ok(self.0.clone())
        }
    }

    /// Create a dummy GraphRetriever for testing RRF (won't actually search).
    fn create_dummy_graph_retriever() -> GraphRetriever {
        use crate::social::store_v2::{SocialStoreV2, SocialStoreV2Config};
        let dir = std::env::temp_dir().join(format!("graph_rag_test_{}", std::process::id()));
        let _ = std::fs::create_dir_all(&dir);
        let config = SocialStoreV2Config::new(&dir);
        let graph = Arc::new(SocialStoreV2::new(config));
        GraphRetriever::new(graph)
    }
}
