//! Recall and Retrieval Handlers
//!
//! Memory retrieval endpoints including:
//! - Semantic recall (vector similarity)
//! - Hybrid recall (semantic + graph)
//! - Proactive context surfacing
//! - Tag-based and date-based recall
//! - Tracked retrieval with Hebbian feedback

use axum::{extract::State, response::Json};
use serde::{Deserialize, Serialize};
use tracing::info;

use super::state::MultiUserMemoryManager;
use super::types::{
    MemoryEvent, RecallExperience, RecallFact, RecallLineageEdge, RecallMemory, RecallRequest,
    RecallResponse, RecallTodo, ReinforceFeedbackRequest, RetrieveResponse, TrackedRetrieveRequest,
    TrackedRetrieveResponse,
};
use super::utils::{
    has_sufficient_alpha_ratio, is_bare_question, is_boilerplate_response,
    is_formatted_recall_output, is_tool_output_noise, strip_system_noise,
};
use crate::errors::{AppError, ValidationErrorExt};
use crate::memory::feedback;
use crate::similarity::cosine_similarity;
use dashmap::DashMap;
// Note: compute_relevance removed - using unified 5-layer pipeline scoring instead
use crate::memory::segmentation::{InputSource, SegmentationEngine};
use crate::memory::sessions::SessionEvent;
use crate::memory::storage::SearchCriteria;
use crate::memory::types::GeoFilter;
use crate::memory::types::MemoryId;
use crate::memory::{
    Experience, ExperienceType, Query as MemoryQuery, RetrievalMode, SharedMemory,
};
use crate::memory::{ProspectiveTrigger, TodoStatus};
use crate::metrics;
use crate::relevance;
use crate::validation;

/// Application state type alias
pub type AppState = std::sync::Arc<MultiUserMemoryManager>;

/// Map API mode string to RetrievalMode enum.
/// Defaults to Hybrid for unknown values (backward compat).
fn parse_retrieval_mode(mode: &str) -> RetrievalMode {
    match mode {
        "semantic" | "similarity" => RetrievalMode::Similarity,
        "associative" => RetrievalMode::Associative,
        "temporal" => RetrievalMode::Temporal,
        "causal" => RetrievalMode::Causal,
        "spatial" => RetrievalMode::Spatial,
        "mission" => RetrievalMode::Mission,
        "action_outcome" | "action-outcome" => RetrievalMode::ActionOutcome,
        _ => RetrievalMode::Hybrid,
    }
}

// =============================================================================
// CONTEXT SUMMARY TYPES
// =============================================================================

/// Context summary request
#[derive(Debug, Deserialize)]
pub struct ContextSummaryRequest {
    pub user_id: String,
    #[serde(default = "default_true")]
    pub include_decisions: bool,
    #[serde(default = "default_true")]
    pub include_learnings: bool,
    #[serde(default = "default_true")]
    pub include_context: bool,
    #[serde(default = "default_max_items")]
    pub max_items: usize,
}

fn default_true() -> bool {
    true
}

fn default_max_items() -> usize {
    5
}

/// Summary item - simplified memory for context
#[derive(Debug, Serialize)]
pub struct SummaryItem {
    pub id: String,
    pub content: String,
    pub importance: f32,
    pub created_at: String,
}

/// Context summary response - categorized memories for session bootstrap
#[derive(Debug, Serialize)]
pub struct ContextSummaryResponse {
    pub total_memories: usize,
    pub decisions: Vec<SummaryItem>,
    pub learnings: Vec<SummaryItem>,
    pub context: Vec<SummaryItem>,
    pub patterns: Vec<SummaryItem>,
    pub errors: Vec<SummaryItem>,
}

// =============================================================================
// PROACTIVE CONTEXT TYPES
// =============================================================================

/// Request for proactive context - returns relevant memories + triggered reminders
#[derive(Debug, Deserialize)]
pub struct ProactiveContextRequest {
    pub user_id: String,
    pub context: String,
    #[serde(default = "default_proactive_max_results")]
    pub max_results: usize,
    /// Minimum semantic similarity threshold (0.0-1.0)
    #[serde(default = "default_semantic_threshold")]
    pub semantic_threshold: f32,
    /// Weight for entity matching in relevance scoring
    #[serde(default = "default_entity_weight")]
    pub entity_match_weight: f32,
    /// Weight for recency boost
    #[serde(default = "default_recency_weight")]
    pub recency_weight: f32,
    /// Filter to specific memory types
    #[serde(default)]
    pub memory_types: Vec<String>,
    /// Whether to auto-ingest the context as a Conversation memory
    #[serde(default = "default_true")]
    pub auto_ingest: bool,
    /// Agent's previous response (for implicit feedback extraction)
    #[serde(default)]
    pub previous_response: Option<String>,
    /// User's followup message after agent response (for delayed signals)
    #[serde(default)]
    pub user_followup: Option<String>,
    /// Tool/actuator actions performed since last proactive_context call.
    /// Used for tool-aware feedback attribution: matches actions against
    /// previously surfaced memories to detect concrete usage.
    /// Claude Code: collected by hooks (Read, Edit, Bash calls).
    /// Robotics: constructed from action-outcome Experience fields.
    #[serde(default)]
    pub tool_actions: Vec<crate::memory::feedback::ToolAction>,
}

fn default_proactive_max_results() -> usize {
    5
}

fn default_semantic_threshold() -> f32 {
    0.05 // Minimum absolute score for quality gate on composite pipeline scores
}

fn default_entity_weight() -> f32 {
    0.4
}

fn default_recency_weight() -> f32 {
    0.2
}

/// Feedback processing results
#[derive(Debug, Serialize)]
pub struct FeedbackProcessed {
    pub memories_evaluated: usize,
    pub reinforced: Vec<String>,
    pub weakened: Vec<String>,
}

/// Surfaced memory in proactive context response
#[derive(Debug, Serialize)]
pub struct ProactiveSurfacedMemory {
    pub id: String,
    pub content: String,
    pub memory_type: String,
    pub score: f32,
    pub importance: f32,
    pub created_at: String,
    pub tags: Vec<String>,
    pub tier: String,
    /// Why this memory was surfaced ("semantic", "entity", "combined")
    pub relevance_reason: String,
    /// Entities from this memory that matched the query context
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub matched_entities: Vec<String>,
    /// Embedding for semantic feedback (not serialized to response)
    #[serde(skip)]
    pub embedding: Vec<f32>,
}

/// Entity detected in the query context
#[derive(Debug, Clone, Serialize)]
pub struct DetectedEntityInfo {
    pub name: String,
    pub entity_type: String,
}

/// Todo item in proactive context response
#[derive(Debug, Serialize)]
pub struct ProactiveTodoItem {
    pub id: String,
    pub short_id: String,
    pub content: String,
    pub status: String,
    pub priority: String,
    pub project: Option<String>,
    pub due_date: Option<String>,
    pub relevance_reason: String,
    /// Semantic similarity score (0.0 - 1.0)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub similarity_score: Option<f32>,
}

/// Individual reminder in response
#[derive(Debug, Serialize)]
pub struct ReminderItem {
    pub id: String,
    pub content: String,
    pub trigger_type: String,
    pub status: String,
    pub due_at: Option<chrono::DateTime<chrono::Utc>>,
    pub created_at: chrono::DateTime<chrono::Utc>,
    pub triggered_at: Option<chrono::DateTime<chrono::Utc>>,
    pub dismissed_at: Option<chrono::DateTime<chrono::Utc>>,
    pub priority: u8,
    pub tags: Vec<String>,
    pub overdue_seconds: Option<i64>,
}

/// Consolidated fact surfaced in proactive context
#[derive(Debug, Serialize)]
pub struct ProactiveFact {
    pub id: String,
    pub fact: String,
    pub confidence: f32,
    pub support_count: usize,
    pub related_entities: Vec<String>,
}

/// Response for proactive context
#[derive(Debug, Serialize)]
pub struct ProactiveContextResponse {
    /// Relevant memories based on context
    pub memories: Vec<ProactiveSurfacedMemory>,
    /// Due time-based reminders
    pub due_reminders: Vec<ReminderItem>,
    /// Context-triggered reminders (keyword match)
    pub context_reminders: Vec<ReminderItem>,
    /// Total counts
    pub memory_count: usize,
    pub reminder_count: usize,
    /// ID of auto-ingested memory (if auto_ingest=true)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ingested_memory_id: Option<String>,
    /// Feedback processing results (if previous_response was provided)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub feedback_processed: Option<FeedbackProcessed>,
    /// Relevant todos based on context
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub relevant_todos: Vec<ProactiveTodoItem>,
    /// Todo count
    #[serde(default)]
    pub todo_count: usize,
    /// Consolidated facts from knowledge graph
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub relevant_facts: Vec<ProactiveFact>,
    /// Processing latency in milliseconds
    pub latency_ms: f64,
    /// Entities detected in the query context
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub detected_entities: Vec<DetectedEntityInfo>,
    /// Number of temporal credits applied from multi-turn feedback window
    #[serde(skip_serializing_if = "Option::is_none")]
    pub temporal_credits_applied: Option<u32>,
}

// =============================================================================
// REINFORCE FEEDBACK TYPES
// =============================================================================

/// Response from reinforcement
#[derive(Debug, Serialize)]
pub struct ReinforceFeedbackResponse {
    pub memories_processed: usize,
    pub associations_strengthened: usize,
    pub importance_boosts: usize,
    pub importance_decays: usize,
}

// =============================================================================
// RECALL BY TAGS/DATE TYPES (local - not in shared types.rs)
// =============================================================================

/// Recall memories by tags
#[derive(Debug, Deserialize)]
pub struct RecallByTagsRequest {
    pub user_id: String,
    /// Tags to search for (returns memories matching ANY of these tags)
    pub tags: Vec<String>,
    /// Maximum number of results (default: 50)
    pub limit: Option<usize>,
}

/// Recall memories by date range
#[derive(Debug, Deserialize)]
pub struct RecallByDateRequest {
    pub user_id: String,
    /// Start of date range (inclusive) - ISO 8601 format
    pub start: chrono::DateTime<chrono::Utc>,
    /// End of date range (inclusive) - ISO 8601 format
    pub end: chrono::DateTime<chrono::Utc>,
    /// Maximum number of results (default: 50)
    pub limit: Option<usize>,
}

// =============================================================================
// MAIN RECALL HANDLER
// =============================================================================

/// POST /api/recall - Semantic + associative hybrid recall
///
/// Uses a hybrid retrieval strategy:
/// 1. Semantic search via vector similarity
/// 2. Graph traversal via spreading activation
/// 3. Hebbian boosting for frequently co-retrieved memories
#[tracing::instrument(skip(state), fields(user_id = %req.user_id, query = %req.query))]
pub async fn recall(
    State(state): State<AppState>,
    Json(req): Json<RecallRequest>,
) -> Result<Json<RecallResponse>, AppError> {
    let op_start = std::time::Instant::now();
    validation::validate_user_id(&req.user_id).map_validation_err("user_id")?;
    validation::validate_max_results(req.limit).map_validation_err("limit")?;

    // Validate and build geo_filter from lat/lon/radius triple
    let geo_filter = match (req.geo_lat, req.geo_lon, req.geo_radius_meters) {
        (Some(lat), Some(lon), Some(radius)) => {
            validation::validate_geo_filter(lat, lon, radius).map_validation_err("geo_filter")?;
            Some(GeoFilter::new(lat, lon, radius))
        }
        (None, None, None) => None,
        _ => {
            return Err(AppError::InvalidInput {
                field: "geo_filter".to_string(),
                reason: "geo_lat, geo_lon, and geo_radius_meters must all be provided together"
                    .to_string(),
            });
        }
    };

    // Build reward range from min/max pair
    let reward_range = match (req.reward_min, req.reward_max) {
        (Some(min), Some(max)) => Some((min, max)),
        (Some(min), None) => Some((min, 1.0)),
        (None, Some(max)) => Some((-1.0, max)),
        (None, None) => None,
    };

    let memory = state
        .get_user_memory(&req.user_id)
        .map_err(AppError::Internal)?;

    let _graph = state
        .get_user_graph(&req.user_id)
        .map_err(AppError::Internal)?;

    let limit = req.limit;
    let mode = req.mode.clone();
    let retrieval_mode_for_recall = parse_retrieval_mode(&mode);

    // SESSION-SCOPED RETRIEVAL: resolve session_id → time_range before spawn_blocking.
    // When session_id is provided, look up the session's time window and set
    // retrieval_mode to Temporal so temporal_search() uses the date range.
    let session_time_range = req.session_id.as_ref().and_then(|sid| {
        use crate::memory::sessions::SessionId;
        let session_id = SessionId(uuid::Uuid::parse_str(sid).ok()?);
        state.session_store().get_session_time_range(&session_id)
    });
    let session_id_for_recall = req.session_id.clone();

    // If session_id resolved to a time range, force Temporal mode
    let retrieval_mode_for_recall = if session_time_range.is_some() {
        RetrievalMode::Temporal
    } else {
        retrieval_mode_for_recall
    };

    // Clone robotics filter fields for the spawn_blocking move boundary
    let robot_id_for_recall = req.robot_id.clone();
    let mission_id_for_recall = req.mission_id.clone();
    let action_type_for_recall = req.action_type.clone();
    let outcome_type_for_recall = req.outcome_type.clone();
    let terrain_type_for_recall = req.terrain_type.clone();
    let tags_for_recall = req.tags.clone();
    let failures_only_for_recall = req.failures_only.unwrap_or(false);

    // PROSPECTIVE MEMORY + RECALL: Run inside a single spawn_blocking to share
    // the computed query embedding between prospective semantic matching and recall.
    // This fixes C5 (keyword-only → semantic) and sets up prospective_signals for boosting.
    let prospective_for_recall = state.prospective_store.clone();
    let memory_for_recall = memory.clone();
    let user_id_for_recall = req.user_id.clone();
    let query_for_recall = req.query.clone();

    let (mut memories, triggered_reminders, _prospective_signals) =
        tokio::task::spawn_blocking(move || {
            let memory_guard = memory_for_recall.read();

            // 1. Compute query embedding (reused for prospective + recall)
            let query_embedding_opt = memory_guard
                .compute_embedding(&query_for_recall)
                .ok();

            // 2. Semantic prospective matching (fixes C5: was keyword-only)
            // Skip semantic matching if embedding failed — keyword matching still works
            let matched_tasks = if let Some(ref query_embedding) = query_embedding_opt {
                let embed_fn =
                    |text: &str| -> Option<Vec<f32>> { memory_guard.compute_embedding(text).ok() };
                prospective_for_recall
                    .check_context_triggers_semantic(
                        &user_id_for_recall,
                        &query_for_recall,
                        query_embedding,
                    embed_fn,
                )
                .unwrap_or_default()
            } else {
                tracing::warn!("Embedding failed — skipping semantic prospective matching, recall proceeds via text");
                Vec::new()
            };

            // 3. Build signals and response reminders from matched tasks
            let mut signals: Vec<String> = Vec::new();
            let reminders: Vec<super::types::RecallReminder> = matched_tasks
                .into_iter()
                .map(|(task, score)| {
                    let keywords =
                        if let ProspectiveTrigger::OnContext { keywords, .. } = &task.trigger {
                            keywords.clone()
                        } else {
                            vec![]
                        };

                    signals.push(task.content.clone());
                    for kw in &keywords {
                        signals.push(kw.clone());
                    }

                    let match_type = if score >= 1.0 {
                        "keyword_match".to_string()
                    } else {
                        format!("semantic ({:.2})", score)
                    };

                    super::types::RecallReminder {
                        id: task.id.0.to_string(),
                        content: task.content,
                        keywords,
                        match_type,
                        priority: task.priority,
                        created_at: task.created_at.to_rfc3339(),
                    }
                })
                .collect();

            let prospective_signals = if signals.is_empty() {
                None
            } else {
                Some(signals)
            };

            // 4. Execute recall with prospective signals + robotics filters
            let query = MemoryQuery {
                user_id: Some(user_id_for_recall),
                query_text: Some(query_for_recall),
                max_results: limit,
                retrieval_mode: retrieval_mode_for_recall,
                prospective_signals: prospective_signals.clone(),
                session_id: session_id_for_recall,
                time_range: session_time_range,
                robot_id: robot_id_for_recall,
                mission_id: mission_id_for_recall,
                geo_filter,
                action_type: action_type_for_recall,
                reward_range,
                outcome_type: outcome_type_for_recall,
                failures_only: failures_only_for_recall,
                terrain_type: terrain_type_for_recall,
                tags: tags_for_recall,
                ..Default::default()
            };

            let memories = memory_guard.recall(&query).unwrap_or_default();

            (memories, reminders, prospective_signals)
        })
        .await
        .map_err(|e| AppError::Internal(anyhow::anyhow!("Blocking task panicked: {e}")))?;

    let triggered_reminder_count = triggered_reminders.len();
    if triggered_reminder_count > 0 {
        tracing::debug!(
            user_id = %req.user_id,
            count = triggered_reminder_count,
            "Context-triggered reminders found - future intentions will boost related memories"
        );
    }

    // Convert to response format with normalized scores.
    // Raw pipeline scores (RRF fusion × hebbian × recency × feedback) cluster in
    // 0.01-0.10 range, making percentage display useless (everything shows 1-5%).
    // Normalize relative to top score so results span 0-95% for meaningful display.
    let mut raw_scores: Vec<f32> = memories
        .iter()
        .map(|m| m.score.unwrap_or_else(|| m.salience_score_with_access()))
        .collect();

    // Lineage-aware retrieval boost: memories connected by causal chains to
    // other recalled memories receive a score boost proportional to edge confidence.
    // This makes causally related memories cluster together in results.
    if memories.len() >= 2 {
        let memory_for_lineage = memory.clone();
        let user_id_for_lineage = req.user_id.clone();
        let memory_ids: Vec<String> = memories.iter().map(|m| m.id.0.to_string()).collect();

        if let Ok(boosts) = tokio::task::spawn_blocking({
            let memory_ids = memory_ids.clone();
            move || -> Vec<f32> {
                let memory_guard = memory_for_lineage.read();
                let lineage = memory_guard.lineage_graph();
                let mut boosts = vec![0.0_f32; memory_ids.len()];
                let id_to_idx: std::collections::HashMap<&str, usize> = memory_ids
                    .iter()
                    .enumerate()
                    .map(|(i, id)| (id.as_str(), i))
                    .collect();

                for (idx, id_str) in memory_ids.iter().enumerate() {
                    let Ok(uuid) = uuid::Uuid::parse_str(id_str) else {
                        continue;
                    };
                    let mid = crate::memory::MemoryId(uuid);

                    // Check outgoing edges to other recalled memories
                    if let Ok(edges) = lineage.get_edges_from(&user_id_for_lineage, &mid) {
                        for edge in &edges {
                            if edge.confidence < crate::constants::LINEAGE_RETRIEVAL_MIN_CONFIDENCE
                            {
                                continue;
                            }
                            let to_str = edge.to.0.to_string();
                            if let Some(&target_idx) = id_to_idx.get(to_str.as_str()) {
                                if target_idx != idx {
                                    let boost = edge.confidence
                                        * crate::constants::LINEAGE_RETRIEVAL_BOOST_SCALE;
                                    boosts[target_idx] = (boosts[target_idx] + boost)
                                        .min(crate::constants::LINEAGE_RETRIEVAL_MAX_BOOST);
                                }
                            }
                        }
                    }

                    // Check incoming edges from other recalled memories
                    if let Ok(edges) = lineage.get_edges_to(&user_id_for_lineage, &mid) {
                        for edge in &edges {
                            if edge.confidence < crate::constants::LINEAGE_RETRIEVAL_MIN_CONFIDENCE
                            {
                                continue;
                            }
                            let from_str = edge.from.0.to_string();
                            if let Some(&source_idx) = id_to_idx.get(from_str.as_str()) {
                                if source_idx != idx {
                                    let boost = edge.confidence
                                        * crate::constants::LINEAGE_RETRIEVAL_BOOST_SCALE;
                                    boosts[source_idx] = (boosts[source_idx] + boost)
                                        .min(crate::constants::LINEAGE_RETRIEVAL_MAX_BOOST);
                                }
                            }
                        }
                    }
                }
                boosts
            }
        })
        .await
        {
            for (score, boost) in raw_scores.iter_mut().zip(boosts.iter()) {
                *score += boost;
            }
        }
    }

    // Lineage candidate expansion: inject causally-connected memories not in results.
    // Higher confidence bar (0.7) than boost (0.5) since we're adding new results.
    if !memories.is_empty() {
        let existing_ids: std::collections::HashSet<String> =
            memories.iter().map(|m| m.id.0.to_string()).collect();
        let memory_for_expansion = memory.clone();
        let user_id_for_expansion = req.user_id.clone();
        let existing_ids_clone = existing_ids.clone();
        let memory_ids_for_expansion: Vec<(String, f32)> = memories
            .iter()
            .zip(raw_scores.iter())
            .map(|(m, &s)| (m.id.0.to_string(), s))
            .collect();

        if let Ok(expanded) = tokio::task::spawn_blocking(move || {
            let memory_guard = memory_for_expansion.read();
            let lineage = memory_guard.lineage_graph();
            // Collect (memory, derived_score) pairs for connected memories not in results
            let mut candidates: Vec<(crate::memory::Memory, f32)> = Vec::new();

            for (id_str, source_score) in &memory_ids_for_expansion {
                let Ok(uuid) = uuid::Uuid::parse_str(id_str) else {
                    continue;
                };
                let mid = crate::memory::MemoryId(uuid);

                // Check both directions
                let mut edges = Vec::new();
                if let Ok(out) = lineage.get_edges_from(&user_id_for_expansion, &mid) {
                    edges.extend(out);
                }
                if let Ok(inc) = lineage.get_edges_to(&user_id_for_expansion, &mid) {
                    edges.extend(inc);
                }

                for edge in &edges {
                    if edge.confidence < crate::constants::LINEAGE_EXPANSION_MIN_CONFIDENCE {
                        continue;
                    }
                    // Get the other end of the edge
                    let other_id = if edge.from.0.to_string() == *id_str {
                        &edge.to
                    } else {
                        &edge.from
                    };
                    let other_str = other_id.0.to_string();
                    if existing_ids_clone.contains(&other_str) {
                        continue; // already in results
                    }
                    // Check we haven't already added this candidate
                    if candidates.iter().any(|(m, _)| m.id == *other_id) {
                        continue;
                    }
                    // Fetch from storage
                    if let Ok(mem) = memory_guard.get_memory(other_id) {
                        // Injected memories get 50% of source score scaled by edge confidence.
                        // A perfect edge (1.0) from the top result gives 50% of top score,
                        // ensuring injected memories sort below their source.
                        let derived_score = source_score * edge.confidence * 0.5;
                        candidates.push((mem, derived_score));
                        if candidates.len() >= crate::constants::LINEAGE_EXPANSION_MAX {
                            return candidates;
                        }
                    }
                }
                if candidates.len() >= crate::constants::LINEAGE_EXPANSION_MAX {
                    break;
                }
            }
            candidates
        })
        .await
        {
            for (mem, score) in expanded {
                raw_scores.push(score);
                memories.push(std::sync::Arc::new(mem));
            }
        }
    }

    let top_score = raw_scores.iter().cloned().fold(0.0_f32, f32::max);

    let recall_memories: Vec<RecallMemory> = memories
        .iter()
        .zip(raw_scores.iter())
        .map(|(m, &raw)| {
            let score = if top_score > 0.0 {
                (raw / top_score) * 0.95
            } else {
                0.0
            };
            RecallMemory {
                id: m.id.0.to_string(),
                experience: RecallExperience {
                    content: m.experience.content.clone(),
                    memory_type: Some(format!("{:?}", m.experience.experience_type)),
                    tags: m.experience.entities.clone(),
                },
                importance: m.importance(),
                created_at: m.created_at.to_rfc3339(),
                score,
                tier: format!("{:?}", m.tier),
            }
        })
        .collect();

    // Search todos semantically if query provided
    let todos: Vec<RecallTodo> = {
        // Compute embedding for todo search
        let query_for_embed = req.query.clone();
        let memory_for_embed = memory.clone();
        let embedding: Option<Vec<f32>> = tokio::task::spawn_blocking(move || {
            let guard = memory_for_embed.read();
            guard.compute_embedding(&query_for_embed).ok()
        })
        .await
        .ok()
        .flatten();

        if let Some(emb) = embedding {
            state
                .todo_store
                .search_similar(&req.user_id, &emb, 5)
                .unwrap_or_default()
                .into_iter()
                .filter(|(t, _)| {
                    matches!(
                        t.status,
                        TodoStatus::Todo | TodoStatus::InProgress | TodoStatus::Blocked
                    )
                })
                .map(|(t, score)| {
                    let project_name = t.project_id.as_ref().and_then(|pid| {
                        state
                            .todo_store
                            .get_project(&req.user_id, pid)
                            .ok()
                            .flatten()
                            .map(|p| p.name)
                    });
                    RecallTodo {
                        id: t.id.0.to_string(),
                        short_id: t.short_id(),
                        content: t.content.clone(),
                        status: format!("{:?}", t.status).to_lowercase(),
                        priority: t.priority.indicator().to_string(),
                        project: project_name,
                        due_date: t.due_date.map(|d| d.format("%Y-%m-%d").to_string()),
                        score,
                    }
                })
                .collect()
        } else {
            Vec::new()
        }
    };
    let todo_count = if todos.is_empty() {
        None
    } else {
        Some(todos.len())
    };

    // Fetch related semantic facts from entities mentioned in recalled memories
    let facts: Vec<RecallFact> = {
        // Collect all unique entities from recalled memories
        let mut all_entities: std::collections::HashSet<String> = std::collections::HashSet::new();
        for mem in &recall_memories {
            for tag in &mem.experience.tags {
                all_entities.insert(tag.to_lowercase());
            }
        }

        // Also extract simple entities from the query itself
        for word in req.query.split_whitespace() {
            let clean_word = word
                .trim_matches(|c: char| !c.is_alphanumeric())
                .to_lowercase();
            if clean_word.len() > 2 {
                all_entities.insert(clean_word);
            }
        }

        // Query the per-user fact store for facts related to these entities.
        // Uses MemorySystem's fact_store (same DB that list_facts/search_facts read from),
        // NOT state.fact_store which is a separate standalone DB.
        let entity_list: Vec<String> = all_entities.into_iter().take(10).collect();
        let found_facts = {
            let memory = memory.clone();
            let user_id = req.user_id.clone();
            tokio::task::spawn_blocking(move || {
                let memory_guard = memory.read();
                let mut facts = Vec::new();
                for entity in &entity_list {
                    if let Ok(entity_facts) = memory_guard.get_facts_by_entity(&user_id, entity, 5)
                    {
                        for fact in entity_facts {
                            facts.push(RecallFact {
                                id: fact.id.clone(),
                                fact: fact.fact.clone(),
                                confidence: fact.confidence,
                                support_count: fact.support_count,
                                related_entities: fact.related_entities.clone(),
                            });
                        }
                    }
                }
                facts
            })
            .await
            .unwrap_or_default()
        };

        // Deduplicate by fact ID and take top 5 by confidence
        let mut unique_facts: std::collections::HashMap<String, RecallFact> =
            std::collections::HashMap::new();
        for fact in found_facts {
            unique_facts.entry(fact.id.clone()).or_insert(fact);
        }
        let mut sorted_facts: Vec<RecallFact> = unique_facts.into_values().collect();
        sorted_facts.sort_by(|a, b| b.confidence.total_cmp(&a.confidence));
        sorted_facts.truncate(5);
        sorted_facts
    };
    let fact_count = if facts.is_empty() {
        None
    } else {
        Some(facts.len())
    };

    // Fetch lineage edges connecting recalled memories
    let lineage: Vec<RecallLineageEdge> = {
        let recalled_ids: std::collections::HashSet<String> =
            recall_memories.iter().map(|m| m.id.clone()).collect();
        if recalled_ids.len() >= 2 {
            let memory = memory.clone();
            let user_id = req.user_id.clone();
            let ids = recalled_ids.clone();
            tokio::task::spawn_blocking(move || {
                let memory_guard = memory.read();
                let lineage_graph = memory_guard.lineage_graph();
                let mut edges = Vec::new();
                let mut seen = std::collections::HashSet::new();
                for id in &ids {
                    let Ok(uuid) = uuid::Uuid::parse_str(id) else {
                        continue;
                    };
                    let mid = crate::memory::MemoryId(uuid);
                    if let Ok(from_edges) = lineage_graph.get_edges_from(&user_id, &mid) {
                        for edge in from_edges {
                            let to_str = edge.to.0.to_string();
                            if ids.contains(&to_str) && seen.insert(edge.id.clone()) {
                                edges.push(RecallLineageEdge {
                                    from: edge.from.0.to_string(),
                                    to: to_str,
                                    relation: format!("{:?}", edge.relation),
                                    confidence: edge.confidence,
                                });
                            }
                        }
                    }
                    if let Ok(to_edges) = lineage_graph.get_edges_to(&user_id, &mid) {
                        for edge in to_edges {
                            let from_str = edge.from.0.to_string();
                            if ids.contains(&from_str) && seen.insert(edge.id.clone()) {
                                edges.push(RecallLineageEdge {
                                    from: from_str,
                                    to: edge.to.0.to_string(),
                                    relation: format!("{:?}", edge.relation),
                                    confidence: edge.confidence,
                                });
                            }
                        }
                    }
                }
                edges
            })
            .await
            .unwrap_or_default()
        } else {
            Vec::new()
        }
    };
    let lineage_count = if lineage.is_empty() {
        None
    } else {
        Some(lineage.len())
    };

    let count = recall_memories.len();

    // Note: Coactivation for Hebbian learning is already recorded inside recall()
    // No need for explicit record_memory_coactivation call

    // Record metrics
    let duration = op_start.elapsed().as_secs_f64();
    metrics::MEMORY_RETRIEVE_DURATION
        .with_label_values(&[&mode])
        .observe(duration);
    metrics::MEMORY_RETRIEVE_TOTAL
        .with_label_values(&[&mode, &"success".to_string()])
        .inc();
    metrics::MEMORY_RETRIEVE_RESULTS
        .with_label_values(&[&mode])
        .observe(count as f64);

    // Broadcast RETRIEVE event for real-time dashboard with full results
    let results_json = serde_json::json!({
        "query": req.query,
        "mode": mode,
        "count": count,
        "latency_ms": duration * 1000.0,
        "memories": recall_memories.iter().map(|m| serde_json::json!({
            "id": m.id,
            "content": m.experience.content,
            "memory_type": m.experience.memory_type,
            "tags": m.experience.tags,
            "score": m.score,
            "importance": m.importance,
            "tier": m.tier,
            "created_at": m.created_at,
        })).collect::<Vec<_>>(),
        "facts": facts.iter().map(|f| serde_json::json!({
            "id": f.id,
            "fact": f.fact,
            "confidence": f.confidence,
            "support_count": f.support_count,
            "related_entities": f.related_entities,
        })).collect::<Vec<_>>(),
        "todos": todos.iter().map(|t| serde_json::json!({
            "short_id": t.short_id,
            "content": t.content,
            "status": t.status,
            "priority": t.priority,
            "project": t.project,
            "score": t.score,
        })).collect::<Vec<_>>(),
        "reminders": triggered_reminders.iter().map(|r| serde_json::json!({
            "id": r.id,
            "content": r.content,
            "keywords": r.keywords,
            "priority": r.priority,
        })).collect::<Vec<_>>(),
        "lineage": lineage.iter().map(|l| serde_json::json!({
            "from": l.from,
            "to": l.to,
            "relation": l.relation,
            "confidence": l.confidence,
        })).collect::<Vec<_>>(),
    });
    state.emit_event(MemoryEvent {
        event_type: "RETRIEVE".to_string(),
        timestamp: chrono::Utc::now(),
        user_id: req.user_id.clone(),
        memory_id: None,
        content_preview: Some(req.query.chars().take(50).collect()),
        memory_type: Some(mode),
        importance: None,
        count: Some(count),
        entities: None,
        results: Some(results_json),
    });

    // Track session event
    if count > 0 {
        let session_id = state.session_store.get_or_create_session(&req.user_id);
        let memory_ids: Vec<String> = recall_memories.iter().map(|m| m.id.clone()).collect();
        let avg_score = if !recall_memories.is_empty() {
            recall_memories.iter().map(|m| m.score).sum::<f32>() / recall_memories.len() as f32
        } else {
            0.0
        };
        state.session_store.add_event(
            &session_id,
            SessionEvent::MemoriesSurfaced {
                timestamp: chrono::Utc::now(),
                query_preview: req.query.chars().take(100).collect(),
                memory_count: count,
                memory_ids,
                avg_score,
            },
        );
    }

    // Build reminder count for response
    let reminder_count = if triggered_reminders.is_empty() {
        None
    } else {
        Some(triggered_reminders.len())
    };

    Ok(Json(RecallResponse {
        memories: recall_memories,
        count,
        retrieval_stats: None, // Retrieval stats not exposed in new API
        todos,
        todo_count,
        facts,
        fact_count,
        triggered_reminders,
        reminder_count,
        lineage,
        lineage_count,
    }))
}

// =============================================================================
// CONTEXT SUMMARY HANDLER
// =============================================================================

/// POST /api/context_summary - Get categorized memories for session bootstrap
///
/// Returns memories grouped by type (decisions, learnings, context, patterns, errors)
/// for quick session initialization.
#[tracing::instrument(skip(state), fields(user_id = %req.user_id))]
pub async fn context_summary(
    State(state): State<AppState>,
    Json(req): Json<ContextSummaryRequest>,
) -> Result<Json<ContextSummaryResponse>, AppError> {
    validation::validate_user_id(&req.user_id).map_validation_err("user_id")?;
    validation::validate_max_results(req.max_items).map_validation_err("max_items")?;

    let memory = state
        .get_user_memory(&req.user_id)
        .map_err(AppError::Internal)?;

    let max_items = req.max_items;
    let include_decisions = req.include_decisions;
    let include_learnings = req.include_learnings;
    let include_context = req.include_context;

    let response = {
        let memory = memory.clone();
        tokio::task::spawn_blocking(move || {
            let memory_guard = memory.read();

            let stats = memory_guard.stats();
            let total_memories = stats.total_memories;

            // Helper to search by type using advanced_search
            let search_by_type = |exp_type: ExperienceType, limit: usize| -> Vec<SummaryItem> {
                memory_guard
                    .advanced_search(SearchCriteria::ByType(exp_type))
                    .unwrap_or_default()
                    .into_iter()
                    .take(limit)
                    .map(|m| SummaryItem {
                        id: m.id.0.to_string(),
                        content: m.experience.content.clone(),
                        importance: m.importance(),
                        created_at: m.created_at.to_rfc3339(),
                    })
                    .collect()
            };

            // Get memories by type using advanced_search
            let decisions = if include_decisions {
                search_by_type(ExperienceType::Decision, max_items)
            } else {
                Vec::new()
            };

            let learnings = if include_learnings {
                search_by_type(ExperienceType::Learning, max_items)
            } else {
                Vec::new()
            };

            let context = if include_context {
                search_by_type(ExperienceType::Context, max_items)
            } else {
                Vec::new()
            };

            let patterns = search_by_type(ExperienceType::Pattern, max_items);
            let errors = search_by_type(ExperienceType::Error, max_items);

            ContextSummaryResponse {
                total_memories,
                decisions,
                learnings,
                context,
                patterns,
                errors,
            }
        })
        .await
        .map_err(|e| AppError::Internal(anyhow::anyhow!("Blocking task panicked: {e}")))?
    };

    Ok(Json(response))
}

// =============================================================================
// PROACTIVE CONTEXT HANDLER
// =============================================================================

/// POST /api/proactive_context - Combined recall + reminders for AI agents
///
/// Returns relevant memories based on semantic similarity and entity matching,
/// plus any due or context-triggered reminders. Optionally stores the context
/// as a Conversation memory for future recall.
#[tracing::instrument(skip(state), fields(user_id = %req.user_id))]
pub async fn proactive_context(
    State(state): State<AppState>,
    Json(mut req): Json<ProactiveContextRequest>,
) -> Result<Json<ProactiveContextResponse>, AppError> {
    validation::validate_user_id(&req.user_id).map_validation_err("user_id")?;
    validation::validate_max_results(req.max_results).map_validation_err("max_results")?;
    validation::validate_weight("semantic_threshold", req.semantic_threshold)
        .map_validation_err("semantic_threshold")?;
    validation::validate_weight("entity_match_weight", req.entity_match_weight)
        .map_validation_err("entity_match_weight")?;
    validation::validate_weight("recency_weight", req.recency_weight)
        .map_validation_err("recency_weight")?;

    // Strip system noise BEFORE any processing — <task-notification>, <system-reminder>,
    // <shodh-context>, code blocks, file contents, etc. This ensures embedding, NER, BM25,
    // and auto-ingest all operate on meaningful user content, not XML scaffolding.
    let raw_len = req.context.len();
    req.context = strip_system_noise(&req.context);
    if raw_len > 0 && req.context.len() < raw_len {
        tracing::debug!(
            "proactive_context: stripped system noise from context ({} -> {} bytes)",
            raw_len,
            req.context.len()
        );
    }

    // Validate context: must be non-empty and have meaningful content
    let trimmed_context = req.context.trim();
    if trimmed_context.is_empty()
        || trimmed_context
            .chars()
            .filter(|c| c.is_alphabetic())
            .count()
            < 3
    {
        tracing::debug!(
            "proactive_context: empty or meaningless context, returning empty response"
        );
        return Ok(Json(ProactiveContextResponse {
            memories: Vec::new(),
            due_reminders: Vec::new(),
            context_reminders: Vec::new(),
            memory_count: 0,
            reminder_count: 0,
            ingested_memory_id: None,
            feedback_processed: None,
            relevant_todos: Vec::new(),
            todo_count: 0,
            relevant_facts: Vec::new(),
            latency_ms: 0.0,
            detected_entities: Vec::new(),
            temporal_credits_applied: None,
        }));
    }

    let op_start = std::time::Instant::now();

    let memory_system = state
        .get_user_memory(&req.user_id)
        .map_err(AppError::Internal)?;

    let graph_memory = state
        .get_user_graph(&req.user_id)
        .map_err(AppError::Internal)?;

    let t_init = op_start.elapsed();
    tracing::info!(
        user_id = %req.user_id,
        init_ms = format!("{:.2}", t_init.as_secs_f64() * 1000.0),
        "proactive_context [phase:init] user memory + graph acquired"
    );

    // 0. Process pending feedback if previous_response is provided
    let mut temporal_credits_total: u32 = 0;
    let feedback_processed = if let Some(ref prev_response) = req.previous_response {
        let feedback_store = state.feedback_store.clone();
        let user_id_for_feedback = req.user_id.clone();
        let response_text = super::utils::strip_mcp_response_noise(prev_response);
        let followup = req.user_followup.clone();
        let memory_for_embed = memory_system.clone();
        let tool_actions_for_feedback = std::mem::take(&mut req.tool_actions);

        // Process feedback and collect memory IDs for reinforcement.
        // Split into 3 phases to minimize write-lock hold time on the shared FeedbackStore:
        //   Phase 1: Take pending + detect context pattern (write lock, <1ms)
        //   Phase 2: Compute embedding + signals (NO lock, 10-30ms)
        //   Phase 3: Apply momentum updates + flush (write lock, <5ms)
        let (result, helpful_ids, misleading_ids, temporal_credits_count) = tokio::task::spawn_blocking(move || {
            // Phase 1: Extract pending data under brief write lock.
            // Also push the taken pending into the FeedbackWindow as a historical
            // entry for multi-turn temporal credit assignment (Issue #125).
            let (pending, context_pattern, window_entries, current_turn) = {
                let mut store = feedback_store.write();
                let pending = store.take_pending(&user_id_for_feedback).map(|mut p| {
                    // Attach tool actions from current request to previous pending.
                    // These actions happened AFTER memories were surfaced (previous call)
                    // and BEFORE this call — exactly the attribution window.
                    if !tool_actions_for_feedback.is_empty() {
                        p.tool_actions = tool_actions_for_feedback;
                    }
                    p
                });
                let context_pattern = pending.as_ref().and_then(|p| {
                    if p.context_embedding.is_empty() {
                        return None;
                    }
                    store.detect_context_pattern(&user_id_for_feedback, &p.context_embedding)
                });

                // Push the consumed pending into the window as a historical entry.
                // This entry is now eligible for multi-turn credit from future turns.
                if let Some(ref p) = pending {
                    let window = store.get_or_create_window(&user_id_for_feedback);
                    let turn_number = window.turn_counter;
                    let entry = feedback::WindowEntry {
                        turn_number,
                        surfaced_memories: p.surfaced_memories.clone(),
                        surfaced_at: p.surfaced_at,
                        context_embedding: p.context_embedding.clone(),
                        context_preview: p.context.chars().take(200).collect(),
                        tool_actions: p.tool_actions.clone(),
                    };
                    store.push_window_entry(&user_id_for_feedback, entry);
                }

                // Snapshot window entries for Phase 2 (computed without lock)
                let window_entries = store.snapshot_window_entries(&user_id_for_feedback);
                let current_turn = store.window_turn_counter(&user_id_for_feedback);

                (pending, context_pattern, window_entries, current_turn)
                // write lock released here
            };

            if let Some(pending) = pending {
                // Phase 2: Pure computation — no lock held
                let response_embedding: Option<Vec<f32>> = {
                    let memory_guard = memory_for_embed.read();
                    memory_guard.compute_embedding(&response_text).ok()
                };

                let mut signals = feedback::process_implicit_feedback_with_semantics(
                    &pending,
                    &response_text,
                    followup.as_deref(),
                    response_embedding.as_deref(),
                );

                // Apply context pattern adjustments (computed in Phase 1)
                if let Some((is_repetition, is_topic_change, similarity)) = context_pattern {
                    if is_repetition || is_topic_change {
                        feedback::apply_context_pattern_signals(
                            &mut signals,
                            is_repetition,
                            is_topic_change,
                            similarity,
                        );
                        tracing::debug!(
                            user_id = %user_id_for_feedback,
                            is_repetition,
                            is_topic_change,
                            similarity,
                            "Applied context pattern signals to feedback"
                        );
                    }
                }

                let context_entities: Vec<String> =
                    feedback::extract_entities_simple(&pending.context)
                        .into_iter()
                        .collect();
                let context_embedding = pending.context_embedding.clone();

                // Phase 2b: Compute temporally discounted signals for older window entries.
                // The most recent window entry was just pushed in Phase 1 (it IS the taken
                // pending), so we skip it — it was already processed above as the immediate
                // signal. We process entries 0..N-1 (older turns).
                let mut deferred_credits: Vec<(MemoryId, feedback::DeferredCredit)> = Vec::new();
                let gamma = crate::constants::TEMPORAL_DISCOUNT_GAMMA;

                if window_entries.len() > 1 {
                    // Iterate all entries except the last one (which is the just-pushed pending)
                    for entry in window_entries.iter().take(window_entries.len().saturating_sub(1)) {
                        let turns_elapsed = current_turn.saturating_sub(entry.turn_number);
                        if turns_elapsed == 0 {
                            continue;
                        }

                        let discount = gamma.powi(turns_elapsed as i32);
                        if discount < 0.05 {
                            continue; // Below meaningful contribution
                        }

                        // Build a synthetic PendingFeedback from the window entry
                        // to reuse existing signal computation
                        let synthetic_pending = feedback::PendingFeedback::new(
                            user_id_for_feedback.clone(),
                            entry.context_preview.clone(),
                            entry.context_embedding.clone(),
                            entry.surfaced_memories.clone(),
                        );

                        let window_signals = feedback::process_implicit_feedback_with_semantics(
                            &synthetic_pending,
                            &response_text,
                            None, // No followup for historical entries
                            response_embedding.as_deref(),
                        );

                        for (memory_id, signal) in window_signals {
                            let discounted_value = signal.value * discount;
                            if discounted_value.abs() >= crate::constants::TEMPORAL_CREDIT_MIN_THRESHOLD {
                                deferred_credits.push((
                                    memory_id,
                                    feedback::DeferredCredit {
                                        raw_signal: signal.value,
                                        confidence: signal.confidence,
                                        trigger: signal.trigger,
                                        turns_elapsed,
                                        discounted_value,
                                        computed_at: chrono::Utc::now(),
                                    },
                                ));
                            }
                        }
                    }
                }

                // Detect session-level outcomes from the window
                let session_outcome = if window_entries.len() >= 2 {
                    // Build a temporary window to detect outcomes
                    let mut temp_window = feedback::FeedbackWindow::new(user_id_for_feedback.clone());
                    temp_window.entries = window_entries.into_iter().collect();
                    temp_window.detect_session_outcome()
                } else {
                    None
                };

                // Classify signals before acquiring lock
                let mut reinforced = Vec::new();
                let mut weakened = Vec::new();
                let mut helpful_ids: Vec<MemoryId> = Vec::new();
                let mut misleading_ids: Vec<MemoryId> = Vec::new();

                // Collect signal classifications for Phase 3
                let classified: Vec<_> = signals
                    .into_iter()
                    .map(|(memory_id, signal)| {
                        let is_helpful = signal.value > 0.3;
                        let is_misleading = signal.value < -0.3;
                        (memory_id, signal, is_helpful, is_misleading)
                    })
                    .collect();

                // Phase 3: Apply momentum updates under brief write lock
                let temporal_credits_count;
                {
                    let mut store = feedback_store.write();

                    // 3a: Apply immediate signals (same as before)
                    for (memory_id, signal, is_helpful, is_misleading) in &classified {
                        let momentum = store.get_or_create_momentum(
                            memory_id.clone(),
                            crate::memory::types::ExperienceType::Context,
                        );

                        let old_ema = momentum.ema;
                        let new_ema = {
                            momentum.update(signal.clone());
                            momentum.ema
                        };

                        if *is_helpful || *is_misleading {
                            let fingerprint = feedback::ContextFingerprint::new(
                                context_entities.clone(),
                                &context_embedding,
                                *is_helpful,
                            );
                            momentum.add_context(fingerprint);
                        }

                        if *is_helpful || new_ema > old_ema + 0.05 {
                            reinforced.push(memory_id.0.to_string());
                            helpful_ids.push(memory_id.clone());
                        } else if *is_misleading || new_ema < old_ema - 0.05 {
                            weakened.push(memory_id.0.to_string());
                            misleading_ids.push(memory_id.clone());
                        }

                        store.mark_dirty(memory_id);
                    }

                    // 3b: Accumulate deferred credits from multi-turn window
                    temporal_credits_count = deferred_credits.len();
                    for (memory_id, credit) in deferred_credits {
                        store.accumulate_deferred_credit(
                            &user_id_for_feedback,
                            &memory_id,
                            credit,
                        );
                    }

                    // 3c: Apply session-level outcome signals
                    if let Some(ref outcome) = session_outcome {
                        match outcome {
                            feedback::SessionOutcome::TaskCompletion { turns_engaged, .. } => {
                                // Boost all memories in window
                                let window = store.get_or_create_window(&user_id_for_feedback);
                                let all_ids = window.all_memory_ids();
                                for id in all_ids {
                                    let credit = feedback::DeferredCredit {
                                        raw_signal: crate::constants::SESSION_COMPLETION_BOOST,
                                        confidence: 0.7,
                                        trigger: feedback::SignalTrigger::TopicChange {
                                            similarity: 0.0,
                                        },
                                        turns_elapsed: 0,
                                        discounted_value: crate::constants::SESSION_COMPLETION_BOOST,
                                        computed_at: chrono::Utc::now(),
                                    };
                                    store.accumulate_deferred_credit(
                                        &user_id_for_feedback,
                                        &id,
                                        credit,
                                    );
                                }
                                tracing::info!(
                                    user_id = %user_id_for_feedback,
                                    turns = turns_engaged,
                                    "Session outcome: task completion detected, boosting window memories"
                                );
                            }
                            feedback::SessionOutcome::Abandonment { gap_seconds, frustration_detected } => {
                                // Penalize last 2 entries
                                let window = store.get_or_create_window(&user_id_for_feedback);
                                let recent_ids: Vec<MemoryId> = window.entries
                                    .iter()
                                    .rev()
                                    .take(2)
                                    .flat_map(|e| e.surfaced_memories.iter().map(|m| m.id.clone()))
                                    .collect();
                                for id in recent_ids {
                                    let credit = feedback::DeferredCredit {
                                        raw_signal: crate::constants::SESSION_ABANDONMENT_PENALTY,
                                        confidence: 0.5,
                                        trigger: feedback::SignalTrigger::Ignored {
                                            overlap_ratio: 0.0,
                                        },
                                        turns_elapsed: 0,
                                        discounted_value: crate::constants::SESSION_ABANDONMENT_PENALTY,
                                        computed_at: chrono::Utc::now(),
                                    };
                                    store.accumulate_deferred_credit(
                                        &user_id_for_feedback,
                                        &id,
                                        credit,
                                    );
                                }
                                tracing::info!(
                                    user_id = %user_id_for_feedback,
                                    gap_seconds,
                                    frustration_detected,
                                    "Session outcome: abandonment detected, penalizing recent memories"
                                );
                            }
                            feedback::SessionOutcome::ReEngagement { gap_turns, topic_similarity } => {
                                // Boost memories from the first entry (re-engaged topic)
                                let window = store.get_or_create_window(&user_id_for_feedback);
                                if let Some(first) = window.entries.front() {
                                    let topic_ids: Vec<MemoryId> = first
                                        .surfaced_memories
                                        .iter()
                                        .map(|m| m.id.clone())
                                        .collect();
                                    for id in topic_ids {
                                        let credit = feedback::DeferredCredit {
                                            raw_signal: crate::constants::SESSION_REENGAGEMENT_BOOST,
                                            confidence: 0.75,
                                            trigger: feedback::SignalTrigger::TopicChange {
                                                similarity: *topic_similarity,
                                            },
                                            turns_elapsed: *gap_turns,
                                            discounted_value: crate::constants::SESSION_REENGAGEMENT_BOOST,
                                            computed_at: chrono::Utc::now(),
                                        };
                                        store.accumulate_deferred_credit(
                                            &user_id_for_feedback,
                                            &id,
                                            credit,
                                        );
                                    }
                                }
                                tracing::info!(
                                    user_id = %user_id_for_feedback,
                                    gap_turns,
                                    topic_similarity,
                                    "Session outcome: re-engagement detected, boosting original topic memories"
                                );
                            }
                            feedback::SessionOutcome::NaturalEnd => {}
                        }
                    }

                    if temporal_credits_count > 0 {
                        tracing::debug!(
                            user_id = %user_id_for_feedback,
                            credits = temporal_credits_count,
                            "Accumulated temporal deferred credits from multi-turn window"
                        );
                    }

                    if let Err(e) = store.flush() {
                        tracing::warn!("Failed to flush feedback store: {}", e);
                    }
                    // write lock released here
                }

                let result = FeedbackProcessed {
                    memories_evaluated: pending.surfaced_memories.len(),
                    reinforced,
                    weakened,
                };
                (Some(result), helpful_ids, misleading_ids, temporal_credits_count)
            } else {
                (None, Vec::new(), Vec::new(), 0usize)
            }
        })
        .await
        .map_err(|e| AppError::Internal(anyhow::anyhow!("Feedback task panicked: {e}")))?;

        temporal_credits_total = temporal_credits_count as u32;

        // Apply reinforcement to memory system, graph, AND retrieval weights
        if !helpful_ids.is_empty() || !misleading_ids.is_empty() {
            let memory_sys_for_reinforce = memory_system.clone();
            let graph_for_reinforce = graph_memory.clone();
            let helpful_ids_for_graph = helpful_ids.clone();
            let relevance_engine = state.relevance_engine.clone();
            let helpful_count = helpful_ids.len();
            let misleading_count = misleading_ids.len();
            let user_id = req.user_id.clone();
            tokio::task::spawn_blocking(move || {
                let memory_guard = memory_sys_for_reinforce.read();

                // Reinforce helpful memories (importance boost)
                if !helpful_ids.is_empty() {
                    if let Err(e) = memory_guard
                        .reinforce_recall(&helpful_ids, crate::memory::RetrievalOutcome::Helpful)
                    {
                        tracing::warn!("Failed to reinforce helpful memories: {}", e);
                    }
                }

                // Weaken misleading memories (importance decay)
                if !misleading_ids.is_empty() {
                    if let Err(e) = memory_guard.reinforce_recall(
                        &misleading_ids,
                        crate::memory::RetrievalOutcome::Misleading,
                    ) {
                        tracing::warn!("Failed to weaken misleading memories: {}", e);
                    }
                }

                // Strengthen graph edges for helpful memories (Hebbian: "what fires together, wires together")
                // When feedback confirms a memory was useful, strengthen its episode's entity edges
                // so the knowledge graph learns which associations are valuable.
                if !helpful_ids_for_graph.is_empty() {
                    let graph_guard = graph_for_reinforce.read();
                    for memory_id in &helpful_ids_for_graph {
                        match graph_guard.strengthen_episode_entity_edges(&memory_id.0) {
                            Ok(count) if count > 0 => {
                                tracing::debug!(
                                    memory_id = %memory_id.0,
                                    edges = count,
                                    "Feedback-driven edge strengthening applied"
                                );
                            }
                            Err(e) => {
                                tracing::debug!(
                                    memory_id = %memory_id.0,
                                    "Feedback edge strengthening failed: {}",
                                    e
                                );
                            }
                            _ => {}
                        }
                    }
                }

                // Reinforce/weaken lineage edges connected to helpful/misleading memories.
                // This is the selection pressure that makes causal chains adaptive:
                // chains that participate in useful recalls get stronger, chains connected
                // to misleading memories weaken. Without this, all inferred edges remain
                // at their initial confidence with no learning signal.
                {
                    let lineage = memory_guard.lineage_graph();
                    let mut reinforced_edge_ids = std::collections::HashSet::new();

                    // Reinforce edges for helpful memories
                    for memory_id in &helpful_ids_for_graph {
                        for edges in [
                            lineage.get_edges_from(&user_id, memory_id),
                            lineage.get_edges_to(&user_id, memory_id),
                        ] {
                            if let Ok(edges) = edges {
                                for mut edge in edges {
                                    if reinforced_edge_ids.insert(edge.id.clone()) {
                                        edge.reinforce();
                                        let _ = lineage.store_edge(&user_id, &edge);
                                    }
                                }
                            }
                        }
                    }

                    // Weaken edges for misleading memories, pruning zombie edges
                    for memory_id in &misleading_ids {
                        for edges in [
                            lineage.get_edges_from(&user_id, memory_id),
                            lineage.get_edges_to(&user_id, memory_id),
                        ] {
                            if let Ok(edges) = edges {
                                for mut edge in edges {
                                    if reinforced_edge_ids.insert(edge.id.clone()) {
                                        let should_prune = edge.weaken();
                                        if should_prune {
                                            let _ = lineage.delete_edge(&user_id, &edge.id);
                                        } else {
                                            let _ = lineage.store_edge(&user_id, &edge);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }

                // Update adaptive retrieval weights via gradient descent (Rescorla-Wagner, 1972).
                // proactive_context always uses semantic retrieval; entity matching contributes
                // when entities were extracted. This closes the loop: feedback now adjusts
                // how much weight semantic vs entity vs tag signals get in future retrievals.
                let entity_contributed = helpful_count > 0; // entities always extracted in proactive_context
                for _ in 0..helpful_count {
                    relevance_engine.apply_feedback(true, entity_contributed, false, true);
                }
                for _ in 0..misleading_count {
                    relevance_engine.apply_feedback(true, entity_contributed, false, false);
                }
            })
            .await
            .map_err(|e| AppError::Internal(anyhow::anyhow!("Reinforce task panicked: {e}")))?;

            // Emit SSE event for feedback processing
            if let Some(ref feedback) = result {
                state.emit_event(MemoryEvent {
                    event_type: "FEEDBACK_PROCESSED".to_string(),
                    timestamp: chrono::Utc::now(),
                    user_id: req.user_id.clone(),
                    memory_id: None,
                    content_preview: Some(format!(
                        "Evaluated {} memories: {} reinforced, {} weakened",
                        feedback.memories_evaluated,
                        feedback.reinforced.len(),
                        feedback.weakened.len()
                    )),
                    memory_type: Some("feedback".to_string()),
                    importance: None,
                    count: Some(feedback.memories_evaluated),
                    entities: None,
                    results: None,
                });

                // Dishabituation: reset habituation counters for memories that received
                // positive feedback. This models the biological dishabituation response —
                // when a habituated stimulus produces a novel outcome, the response recovers.
                if !feedback.reinforced.is_empty() {
                    if let Some(user_map) = state.habituation_tracker.get(&req.user_id) {
                        let now = chrono::Utc::now();
                        for mem_id_str in &feedback.reinforced {
                            if let Some(mut entry) = user_map.get_mut(mem_id_str) {
                                entry.surfacings_without_utility = 0;
                                entry.last_utility = Some(now);
                            }
                        }
                    }
                }
            }
        }

        result
    } else {
        None
    };

    let t_feedback = op_start.elapsed();
    tracing::info!(
        user_id = %req.user_id,
        feedback_ms = format!("{:.2}", (t_feedback - t_init).as_secs_f64() * 1000.0),
        cumulative_ms = format!("{:.2}", t_feedback.as_secs_f64() * 1000.0),
        had_feedback = req.previous_response.is_some(),
        "proactive_context [phase:feedback] feedback processing complete"
    );

    // 1 + 1.5: Compute embedding and extract NER entities in parallel
    // Both are independent blocking tasks (~10ms + ~5ms → ~10ms parallel)
    let context_for_embedding = req.context.clone();
    let memory_for_embedding = memory_system.clone();
    let embedding_task = tokio::task::spawn_blocking(move || {
        let memory_guard = memory_for_embedding.read();
        match memory_guard.compute_embedding(&context_for_embedding) {
            Ok(emb) => (emb, true),
            Err(e) => {
                tracing::warn!("proactive_context: embedding computation failed: {e}, skipping embedding-dependent operations");
                (Vec::new(), false)
            }
        }
    });

    let ner = state.get_neural_ner();
    let context_for_ner = req.context.clone();
    let ner_task = {
        let ner = ner.clone();
        tokio::task::spawn_blocking(move || match ner.extract(&context_for_ner) {
            Ok(entities) => {
                // Build word set from context for validating NER extractions
                let context_words: std::collections::HashSet<&str> = context_for_ner
                    .split(|c: char| !c.is_alphanumeric() && c != '-' && c != '_')
                    .filter(|w| w.len() >= 3)
                    .collect();
                let context_lower = context_for_ner.to_lowercase();

                let filtered: Vec<_> = entities
                    .into_iter()
                    .filter(|e| {
                        let t = e.text.trim();
                        // Length: at least 3 chars
                        if t.len() < 3 {
                            return false;
                        }
                        // Filter known NER artifacts
                        let lower = t.to_lowercase();
                        if matches!(
                            lower.as_str(),
                            "undefined" | "null" | "none" | "true" | "false"
                        ) {
                            return false;
                        }
                        // Validate: entity must appear in context.
                        // Single-word: check word set (prevents subword fragments like "ian" from "Hebbian")
                        // Multi-word: check case-insensitive substring of original context
                        if !t.contains(' ') {
                            context_words.contains(t)
                                || context_words.iter().any(|w| w.eq_ignore_ascii_case(t))
                        } else {
                            context_lower.contains(&lower)
                        }
                    })
                    .collect();
                let infos: Vec<DetectedEntityInfo> = filtered
                    .iter()
                    .map(|e| DetectedEntityInfo {
                        name: e.text.clone(),
                        entity_type: format!("{:?}", e.entity_type),
                    })
                    .collect();
                let names: Vec<String> = filtered.iter().map(|e| e.text.to_lowercase()).collect();
                (infos, names)
            }
            Err(_) => (Vec::new(), Vec::new()),
        })
    };

    let (embedding_result, ner_result) = tokio::join!(embedding_task, ner_task);
    let (context_embedding, embedding_valid): (Vec<f32>, bool) = embedding_result
        .map_err(|e| AppError::Internal(anyhow::anyhow!("Embedding task panicked: {e}")))?;
    let (detected_entities, context_entity_names): (Vec<DetectedEntityInfo>, Vec<String>) =
        ner_result.map_err(|e| AppError::Internal(anyhow::anyhow!("NER task panicked: {e}")))?;

    let t_embed_ner = op_start.elapsed();
    tracing::info!(
        user_id = %req.user_id,
        embed_ner_ms = format!("{:.2}", (t_embed_ner - t_feedback).as_secs_f64() * 1000.0),
        cumulative_ms = format!("{:.2}", t_embed_ner.as_secs_f64() * 1000.0),
        embedding_valid,
        entity_count = detected_entities.len(),
        "proactive_context [phase:embed+ner] embedding + NER complete"
    );

    // 1.8: Check context-triggered prospective tasks — builds signals for recall boost
    // This runs before recall so that "future informs present" can influence retrieval.
    // Fast operation: scans pending tasks for this user (typically < 10 tasks).
    let ctx_trigger_uid = req.user_id.clone();
    let ctx_trigger_context = req.context.clone();
    let ctx_trigger_emb = context_embedding.clone();
    let ctx_trigger_memory = memory_system.clone();
    let ctx_trigger_prosp = state.prospective_store.clone();

    let (prospective_signals, cached_context_triggers) = tokio::task::spawn_blocking(move || {
        let embed_fn = |text: &str| -> Option<Vec<f32>> {
            let memory_guard = ctx_trigger_memory.read();
            memory_guard.compute_embedding(text).ok()
        };
        let matched = ctx_trigger_prosp
            .check_context_triggers_semantic(
                &ctx_trigger_uid,
                &ctx_trigger_context,
                &ctx_trigger_emb,
                embed_fn,
            )
            .unwrap_or_default();

        if matched.is_empty() {
            (None, Vec::new())
        } else {
            let mut signals = Vec::new();
            for (task, _score) in &matched {
                signals.push(task.content.clone());
                if let ProspectiveTrigger::OnContext { keywords, .. } = &task.trigger {
                    for kw in keywords {
                        signals.push(kw.clone());
                    }
                }
            }
            (Some(signals), matched)
        }
    })
    .await
    .map_err(|e| AppError::Internal(anyhow::anyhow!("Prospective check panicked: {e}")))?;

    let t_prospective = op_start.elapsed();
    tracing::info!(
        user_id = %req.user_id,
        prospective_ms = format!("{:.2}", (t_prospective - t_embed_ner).as_secs_f64() * 1000.0),
        cumulative_ms = format!("{:.2}", t_prospective.as_secs_f64() * 1000.0),
        triggers_found = cached_context_triggers.len(),
        "proactive_context [phase:prospective] context trigger check complete"
    );

    if !cached_context_triggers.is_empty() {
        tracing::debug!(
            user_id = %req.user_id,
            count = cached_context_triggers.len(),
            "Context-triggered prospective tasks found — will boost related memories"
        );
    }

    // 2. Retrieve memories using unified 5-layer pipeline
    // The pipeline already applies: RRF fusion + hebbian + recency + feedback (PIPE-9)
    // No double-scoring needed - just use the scores from recall() directly
    //
    // Post-pipeline: adaptive involuntary memory constraints (Berntsen 2009)
    // Applied here (not in semantic_retrieve) to keep voluntary recall untouched.
    let context_clone = req.context.clone();
    let context_lower_for_tags = req.context.to_lowercase();
    let max_results = req.max_results;
    let user_id_for_query = req.user_id.clone();
    let entity_names_for_recall = context_entity_names.clone();
    let entity_match_weight = req.entity_match_weight;
    let recency_weight = req.recency_weight;
    let semantic_threshold = req.semantic_threshold;
    let embedding_for_query = context_embedding.clone();
    let habituation_tracker = state.habituation_tracker.clone();
    let user_id_for_habituation = req.user_id.clone();
    let graph_for_habituation = state.get_user_graph(&req.user_id).ok();
    let memory_type_filter: Vec<ExperienceType> = req
        .memory_types
        .iter()
        .map(|s| super::remember::parse_experience_type(Some(s)))
        .collect();
    let memories: Vec<ProactiveSurfacedMemory> = {
        let memory = memory_system.clone();
        tokio::task::spawn_blocking(move || {
            let memory_guard = memory.read();

            // Build word set from query for anti-echo detection (borrows context_clone)
            let query_words: std::collections::HashSet<String> = context_clone
                .split_whitespace()
                .map(|w| {
                    w.trim_matches(|c: char| !c.is_alphanumeric())
                        .to_lowercase()
                })
                .filter(|w| w.len() >= 3)
                .collect();

            let query = MemoryQuery {
                user_id: Some(user_id_for_query),
                query_text: Some(context_clone),
                query_embedding: if embedding_valid {
                    Some(embedding_for_query)
                } else {
                    None
                },
                max_results,
                recency_weight: Some(recency_weight),
                prospective_signals,
                experience_types: if memory_type_filter.is_empty() {
                    None
                } else {
                    Some(memory_type_filter)
                },
                ..Default::default()
            };
            let results = memory_guard.recall(&query).unwrap_or_default();

            let candidates: Vec<(SharedMemory, f32)> = results
                .into_iter()
                .filter(|m| {
                    // Quality gate: skip garbage/truncated memories
                    let content = m.experience.content.trim();
                    if content.len() < 30 {
                        return false;
                    }
                    // Skip content that's mostly non-alphabetic (binary noise, IDs, etc.)
                    let alpha_count = content.chars().filter(|c| c.is_alphabetic()).count();
                    if alpha_count < 10 {
                        return false;
                    }
                    // Anti-echo: skip memories that are just our own context echoed back
                    // (auto-ingest stores context, which then gets retrieved for itself)
                    // Uses inline comparison to avoid allocating a HashSet<String> per candidate
                    if !query_words.is_empty() {
                        let mut mem_word_count = 0usize;
                        let mut overlap = 0usize;
                        for w in content.split_whitespace() {
                            let w = w.trim_matches(|c: char| !c.is_alphanumeric());
                            if w.len() >= 3 {
                                mem_word_count += 1;
                                if query_words.iter().any(|qw| qw.eq_ignore_ascii_case(w)) {
                                    overlap += 1;
                                }
                            }
                        }
                        let smaller = query_words.len().min(mem_word_count);
                        if smaller > 0 && overlap * 100 / smaller >= 70 {
                            return false;
                        }
                    }
                    true
                })
                .map(|m| {
                    let score = m.get_score().unwrap_or(0.0);
                    (m, score)
                })
                .collect();

            let context_lower = &context_lower_for_tags;

            // Compute entity matches and boost scores BEFORE quality gate
            let context_entity_count = entity_names_for_recall.len().max(1);
            let mut enriched: Vec<(
                std::sync::Arc<crate::memory::types::Memory>,
                f32,
                Vec<String>,
            )> = candidates
                .into_iter()
                .map(|(m, mut score)| {
                    let mut memory_terms: Vec<String> = m
                        .experience
                        .entities
                        .iter()
                        .map(|e| e.to_lowercase())
                        .collect();
                    for tag in &m.experience.tags {
                        let lower = tag.to_lowercase();
                        if !memory_terms.contains(&lower) {
                            memory_terms.push(lower);
                        }
                    }

                    let matched: Vec<String> = entity_names_for_recall
                        .iter()
                        .filter(|ctx_ent| {
                            ctx_ent.len() >= 3
                                && memory_terms.iter().any(|me| {
                                    if me.len() < 3 {
                                        return false;
                                    }
                                    if me == ctx_ent.as_str() {
                                        return true;
                                    }
                                    let (shorter, longer) = if me.len() <= ctx_ent.len() {
                                        (me.as_str(), ctx_ent.as_str())
                                    } else {
                                        (ctx_ent.as_str(), me.as_str())
                                    };
                                    shorter.len() * 100 / longer.len() >= 60
                                        && longer.contains(shorter)
                                })
                        })
                        .cloned()
                        .collect();

                    // Apply entity match boost with diminishing returns (log scaling).
                    // Linear scaling over-rewards memories with many entity matches;
                    // log scaling reflects cue distinctiveness (Berntsen 2009).
                    if !matched.is_empty() {
                        let match_ratio =
                            (matched.len() as f32 / context_entity_count as f32).min(1.0);
                        let diminishing = (1.0 + matched.len() as f32).ln() / (1.0_f32 + 3.0).ln();
                        let entity_boost = entity_match_weight * match_ratio * diminishing.min(1.0);
                        score *= 1.0 + entity_boost;
                    }

                    // Structured tag matching — boost memories whose hook-written
                    // tags (tool:*, file:*, error) align with context signals.
                    {
                        let mut tag_matches: u32 = 0;
                        for tag in &m.experience.tags {
                            let lower_tag = tag.to_lowercase();
                            if let Some(tool_name) = lower_tag.strip_prefix("tool:") {
                                if entity_names_for_recall
                                    .iter()
                                    .any(|e| e.eq_ignore_ascii_case(tool_name))
                                {
                                    tag_matches += 1;
                                }
                            } else if let Some(file_path) = lower_tag.strip_prefix("file:") {
                                let file_name = file_path
                                    .rsplit('/')
                                    .next()
                                    .or_else(|| file_path.rsplit('\\').next())
                                    .unwrap_or(file_path);
                                if file_name.len() >= 3
                                    && entity_names_for_recall.iter().any(|e| {
                                        e.contains(file_name) || file_name.contains(e.as_str())
                                    })
                                {
                                    tag_matches += 1;
                                }
                            } else if lower_tag == "error"
                                && (context_lower.contains("error")
                                    || context_lower.contains("fail")
                                    || context_lower.contains("bug")
                                    || context_lower.contains("fix"))
                            {
                                tag_matches += 1;
                            }
                        }
                        if tag_matches > 0 {
                            let capped = tag_matches.min(3) as f32;
                            score *= 1.0 + crate::constants::TAG_RELEVANCE_BOOST * capped;
                        }
                    }

                    (m, score, matched)
                })
                .collect();

            // Sort by boosted score (highest first)
            enriched.sort_by(|a, b| b.1.total_cmp(&a.1));

            // --- Adaptive involuntary memory constraints (Berntsen 2009) ---
            // These operate ONLY on proactive_context, not voluntary recall.

            // (A) Elaboration quality gate — rich memories outrank fragments.
            // Quality factor: content_len/200 scaled by structural richness.
            // Multiplicative so fragments score lower, not zero.
            {
                use crate::constants::ELABORATION_QUALITY_MIN;
                for (m, score, _) in enriched.iter_mut() {
                    let content_len = m.experience.content.len() as f32;
                    let length_factor = (content_len / 200.0).min(1.0);
                    let has_entities = if m.experience.entities.is_empty() {
                        0.0
                    } else {
                        0.1
                    };
                    let has_context = if m.experience.context.is_some() {
                        0.1
                    } else {
                        0.0
                    };
                    let quality = (length_factor * (1.0 + has_entities + has_context))
                        .max(ELABORATION_QUALITY_MIN);
                    *score *= quality;
                }
                enriched.sort_by(|a, b| b.1.total_cmp(&a.1));
            }

            // (B) Steeper proactive recency — involuntary memories favor recent events.
            // Applies an additional recency adjustment on top of Layer 5's recency boost.
            // Uses PROACTIVE_RECENCY_DECAY_RATE (0.03/h) vs voluntary's 0.01/h.
            {
                use crate::constants::PROACTIVE_RECENCY_DECAY_RATE;
                let now = chrono::Utc::now();
                // Layer 5 already applied exp(-0.01*h)*recency_weight.
                // We apply the *differential*: exp(-(0.03-0.01)*h) = exp(-0.02*h)
                let differential_rate = PROACTIVE_RECENCY_DECAY_RATE - 0.01;
                for (m, score, _) in enriched.iter_mut() {
                    let hours_old = (now - m.created_at).num_hours().max(0) as f32;
                    let proactive_recency_factor = (-differential_rate * hours_old).exp();
                    *score *= proactive_recency_factor;
                }
                enriched.sort_by(|a, b| b.1.total_cmp(&a.1));
            }

            // (C) Habituation — penalize memories surfaced repeatedly without utility.
            // Thompson & Spencer (1966): repeated stimulation without reinforcement
            // diminishes response. Logarithmic decay prevents permanent suppression.
            {
                use crate::constants::{HABITUATION_DECAY_FACTOR, HABITUATION_MAX_PENALTY};
                let user_map = habituation_tracker
                    .entry(user_id_for_habituation.clone())
                    .or_insert_with(DashMap::new);
                for (m, score, _) in enriched.iter_mut() {
                    let mem_id = m.id.0.to_string();
                    if let Some(entry) = user_map.get(&mem_id) {
                        if entry.surfacings_without_utility > 0 {
                            let penalty = (HABITUATION_DECAY_FACTOR
                                * (1.0 + entry.surfacings_without_utility as f32).ln())
                            .min(HABITUATION_MAX_PENALTY);
                            *score *= 1.0 - penalty;
                        }
                    }
                }
                enriched.sort_by(|a, b| b.1.total_cmp(&a.1));
            }

            // (D) Lateral inhibition — similar candidates suppress each other.
            // O'Reilly & McClelland (1994): pattern separation via competition.
            // Greedy top-down: each selected memory inhibits similar remaining candidates.
            {
                use crate::constants::{LATERAL_INHIBITION_STRENGTH, LATERAL_INHIBITION_THRESHOLD};
                let mut selected_embeddings: Vec<Vec<f32>> = Vec::new();
                for (m, score, _) in enriched.iter_mut() {
                    if let Some(ref emb) = m.experience.embeddings {
                        if !emb.is_empty() {
                            // Check against all already-selected memories
                            let mut max_sim: f32 = 0.0;
                            for sel_emb in &selected_embeddings {
                                if sel_emb.len() == emb.len() {
                                    let sim = cosine_similarity(emb, sel_emb);
                                    if sim > max_sim {
                                        max_sim = sim;
                                    }
                                }
                            }
                            if max_sim > LATERAL_INHIBITION_THRESHOLD {
                                // Suppress: the more similar, the stronger the inhibition
                                *score *= 1.0 - LATERAL_INHIBITION_STRENGTH * max_sim;
                            }
                            selected_embeddings.push(emb.clone());
                        }
                    }
                }
                // Final sort after all biological constraints applied
                enriched.sort_by(|a, b| b.1.total_cmp(&a.1));
            }

            // Drop results below minimum absolute score — don't pad with irrelevant filler
            // Also drop results that are < 30% of the top score (too weak relative to best)
            let top_score = enriched.first().map(|(_, s, _)| *s).unwrap_or(0.0);
            let abs_min = semantic_threshold;
            let relative_min = top_score * 0.30;
            let effective_min = abs_min.max(relative_min);
            enriched.retain(|(_, s, _)| *s >= effective_min);

            // Normalize scores for display: scale relative to top result
            if top_score > 0.0 {
                for (_, score, _) in enriched.iter_mut() {
                    *score = (*score / top_score) * 0.95;
                }
            }

            // (E) Update habituation state — record that these memories were surfaced.
            // Positive feedback resets count in Phase 0 of the next call.
            // Also penalize entities when memories exceed habituation threshold.
            {
                use crate::constants::{
                    ENTITY_SALIENCE_HABITUATION_PENALTY, ENTITY_SALIENCE_HABITUATION_THRESHOLD,
                };
                let user_map = habituation_tracker
                    .entry(user_id_for_habituation)
                    .or_insert_with(DashMap::new);
                let now = chrono::Utc::now();
                for (m, score, _) in enriched.iter() {
                    if *score >= effective_min {
                        let mem_id = m.id.0.to_string();
                        let mem_uuid = m.id.0;
                        let mut entry = user_map.entry(mem_id).or_insert_with(|| {
                            super::state::HabituationEntry {
                                surfacings_without_utility: 0,
                                last_surfaced: now,
                                last_utility: None,
                            }
                        });
                        entry.surfacings_without_utility += 1;
                        entry.last_surfaced = now;

                        // Penalize entities when memory repeatedly ignored
                        if entry.surfacings_without_utility > ENTITY_SALIENCE_HABITUATION_THRESHOLD
                        {
                            if let Some(ref graph) = graph_for_habituation {
                                let graph_guard = graph.read();
                                let _ = graph_guard.reinforce_entity_salience(
                                    &[mem_uuid],
                                    ENTITY_SALIENCE_HABITUATION_PENALTY,
                                );
                            }
                        }
                    }
                }
            }

            // Return top results with entity overlap annotation
            enriched
                .into_iter()
                .take(max_results)
                .map(|(m, score, matched)| {
                    let has_entity_match = !matched.is_empty();
                    let has_semantic_match = score > 0.0;
                    let relevance_reason = if has_entity_match && has_semantic_match {
                        "combined"
                    } else if has_entity_match {
                        "entity"
                    } else {
                        "semantic"
                    }
                    .to_string();

                    ProactiveSurfacedMemory {
                        id: m.id.0.to_string(),
                        content: m.experience.content.clone(),
                        memory_type: format!("{:?}", m.experience.experience_type),
                        score,
                        importance: m.importance(),
                        created_at: m.created_at.to_rfc3339(),
                        tags: m.experience.tags.clone(),
                        tier: format!("{:?}", m.tier),
                        relevance_reason,
                        matched_entities: matched,
                        embedding: m.experience.embeddings.clone().unwrap_or_default(),
                    }
                })
                .collect()
        })
        .await
        .map_err(|e| AppError::Internal(anyhow::anyhow!("Blocking task panicked: {e}")))?
    };

    let t_recall = op_start.elapsed();
    tracing::info!(
        user_id = %req.user_id,
        recall_ms = format!("{:.2}", (t_recall - t_prospective).as_secs_f64() * 1000.0),
        cumulative_ms = format!("{:.2}", t_recall.as_secs_f64() * 1000.0),
        memories_found = memories.len(),
        "proactive_context [phase:recall] memory retrieval complete"
    );

    // 2.5. Coactivation already recorded inside semantic_retrieve() — no duplicate call.
    // Previously this fired a second record_memory_coactivation() on the filtered subset,
    // inflating Hebbian edge weights 2x per proactive_context call.

    // 3. Store pending feedback (fast, in-memory — do before parallel block)
    if embedding_valid {
        let surfaced_infos: Vec<feedback::SurfacedMemoryInfo> = memories
            .iter()
            .map(|m| {
                let id = uuid::Uuid::parse_str(&m.id).unwrap_or_else(|_| uuid::Uuid::new_v4());
                feedback::SurfacedMemoryInfo {
                    id: MemoryId(id),
                    entities: feedback::extract_entities_simple(&m.content),
                    content_preview: m.content.chars().take(100).collect(),
                    score: m.score,
                    embedding: m.embedding.clone(),
                }
            })
            .collect();

        let surfaced_memory_ids: Vec<MemoryId> = memories
            .iter()
            .filter_map(|m| uuid::Uuid::parse_str(&m.id).ok())
            .map(MemoryId)
            .collect();

        let pending = feedback::PendingFeedback::new(
            req.user_id.clone(),
            req.context.clone(),
            context_embedding.clone(),
            surfaced_infos,
        );
        let feedback_store = state.feedback_store.clone();
        {
            let mut store = feedback_store.write();
            store.set_pending(pending);

            // Track this context for repetition/topic-change detection on the next call.
            // detect_context_pattern() compares the next call's embedding against this one.
            store.set_previous_context(
                &req.user_id,
                req.context.clone(),
                context_embedding.clone(),
                surfaced_memory_ids,
            );
        }
    }

    let t_feedback_store = op_start.elapsed();
    tracing::info!(
        user_id = %req.user_id,
        feedback_store_ms = format!("{:.2}", (t_feedback_store - t_recall).as_secs_f64() * 1000.0),
        cumulative_ms = format!("{:.2}", t_feedback_store.as_secs_f64() * 1000.0),
        "proactive_context [phase:coactivation+feedback_store] coactivation + feedback store complete"
    );

    // 4. Auto-ingest previous assistant response — fire-and-forget
    if req.auto_ingest {
        if let Some(ref prev_response) = req.previous_response {
            let response_text = prev_response.trim();
            let is_meaningful = response_text.len() > 100
                && response_text.len() < 3000
                && !response_text.starts_with("```")
                && !is_boilerplate_response(response_text);

            if is_meaningful {
                let response_text_owned = response_text.to_string();
                let memory = memory_system.clone();
                tokio::task::spawn(async move {
                    let _ = tokio::task::spawn_blocking(move || {
                        let memory_guard = memory.read();
                        let segmenter = SegmentationEngine::new();
                        let segments =
                            segmenter.segment(&response_text_owned, InputSource::AutoIngest);
                        for segment in segments {
                            let content = format!(
                                "[Assistant: {:?}] {}",
                                segment.experience_type, segment.content
                            );
                            let experience = Experience {
                                content,
                                experience_type: segment.experience_type,
                                entities: segment.entities,
                                tags: vec![
                                    "assistant-response".to_string(),
                                    "auto-captured".to_string(),
                                ],
                                context: super::remember::build_rich_context(
                                    None,
                                    None,
                                    None,
                                    Some("ai_generated".to_string()),
                                    Some(0.6),
                                    None,
                                    None,
                                    None,
                                ),
                                ..Default::default()
                            };
                            let _ = memory_guard.remember(experience, None);
                        }
                    })
                    .await;
                });
            }
        }
    }

    // 5. Auto-ingest user context — fire-and-forget with ID capture
    // Dedup: skip if identical content was ingested within the last 5 seconds
    // (prevents hook + MCP tool from double-ingesting the same user message)
    static INGEST_DEDUP: std::sync::LazyLock<
        parking_lot::Mutex<std::collections::HashMap<u64, std::time::Instant>>,
    > = std::sync::LazyLock::new(|| parking_lot::Mutex::new(std::collections::HashMap::new()));

    // req.context is already cleaned at handler entry — no redundant strip needed
    let clean_context = req.context.clone();
    let content_hash = {
        use std::hash::{Hash, Hasher};
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        req.user_id.hash(&mut hasher);
        clean_context.hash(&mut hasher);
        hasher.finish()
    };
    let is_duplicate = {
        let mut dedup = INGEST_DEDUP.lock();
        dedup.retain(|_, t| t.elapsed().as_secs() < 10);
        match dedup.entry(content_hash) {
            std::collections::hash_map::Entry::Occupied(_) => true,
            std::collections::hash_map::Entry::Vacant(e) => {
                e.insert(std::time::Instant::now());
                false
            }
        }
    };
    let should_ingest = req.auto_ingest
        && !is_duplicate
        && clean_context.len() > 50
        && clean_context.len() < 5000
        && !is_bare_question(&clean_context)
        && !is_tool_output_noise(&clean_context)
        && has_sufficient_alpha_ratio(&clean_context)
        && !is_formatted_recall_output(&clean_context);

    let ingested_memory_id = if should_ingest {
        let context = clean_context;
        let memory = memory_system.clone();
        let (tx, rx) = tokio::sync::oneshot::channel();
        tokio::task::spawn(async move {
            let result = tokio::task::spawn_blocking(move || {
                let memory_guard = memory.read();
                let segmenter = SegmentationEngine::new();
                let segments = segmenter.segment(&context, InputSource::AutoIngest);
                let mut first_id = None;
                for segment in segments {
                    let experience = Experience {
                        content: segment.content,
                        experience_type: segment.experience_type,
                        entities: segment.entities,
                        tags: vec!["auto-captured".to_string()],
                        context: super::remember::build_rich_context(
                            None,
                            None,
                            None,
                            Some("user".to_string()),
                            Some(0.9),
                            None,
                            None,
                            None,
                        ),
                        ..Default::default()
                    };
                    if let Ok(id) = memory_guard.remember(experience, None) {
                        if first_id.is_none() {
                            first_id = Some(id);
                        }
                    }
                }
                first_id
            })
            .await;
            let _ = tx.send(result);
        });
        match tokio::time::timeout(std::time::Duration::from_millis(50), rx).await {
            Ok(Ok(Ok(Some(id)))) => Some(id.0.to_string()),
            _ => None,
        }
    } else {
        None
    };

    let t_auto_ingest = op_start.elapsed();
    tracing::info!(
        user_id = %req.user_id,
        auto_ingest_ms = format!("{:.2}", (t_auto_ingest - t_feedback_store).as_secs_f64() * 1000.0),
        cumulative_ms = format!("{:.2}", t_auto_ingest.as_secs_f64() * 1000.0),
        ingested = ingested_memory_id.is_some(),
        "proactive_context [phase:auto_ingest] context + response ingest complete"
    );

    // 6. Collect fact entities synchronously (fast iteration, needed before parallel block)
    let fact_entity_list: Vec<String> = {
        let mut all_entities: std::collections::HashSet<String> = std::collections::HashSet::new();
        for name in &context_entity_names {
            all_entities.insert(name.clone());
        }
        for mem in &memories {
            for tag in &mem.tags {
                let lower = tag.to_lowercase();
                if lower.len() > 2 {
                    all_entities.insert(lower);
                }
            }
        }
        for word in req.context.split_whitespace() {
            let clean = word
                .trim_matches(|c: char| !c.is_alphanumeric())
                .to_lowercase();
            if clean.len() > 3 {
                all_entities.insert(clean);
            }
        }
        all_entities.into_iter().take(15).collect()
    };

    // 7. Parallel block: due reminders + context reminders + todos + facts
    // All 4 are independent blocking I/O operations — run concurrently via tokio::join!

    // Prepare clones for parallel tasks
    let due_uid = req.user_id.clone();
    let due_prospective = state.prospective_store.clone();

    let todo_uid = req.user_id.clone();
    let todo_emb = context_embedding.clone();
    let todo_store = state.todo_store.clone();

    let fact_uid = req.user_id.clone();
    let fact_memory = memory_system.clone();

    // B: Context-triggered reminders — reuse cached results from step 1.8
    // (already checked before recall to build prospective_signals for the boost)
    let context_reminders_from_cache: Vec<ReminderItem> = cached_context_triggers
        .into_iter()
        .map(|(t, score)| {
            let overdue = t.overdue_seconds();
            ReminderItem {
                id: t.id.0.to_string(),
                content: t.content,
                trigger_type: format!("context (score: {score:.2})"),
                status: format!("{:?}", t.status).to_lowercase(),
                due_at: t.trigger.due_at(),
                created_at: t.created_at,
                triggered_at: t.triggered_at,
                dismissed_at: t.dismissed_at,
                priority: t.priority,
                tags: t.tags,
                overdue_seconds: overdue,
            }
        })
        .collect();

    let (due_result, todo_result, fact_result) = tokio::join!(
        // A: Due reminders
        tokio::task::spawn_blocking(move || {
            due_prospective
                .get_due_tasks(&due_uid)
                .unwrap_or_default()
                .into_iter()
                .map(|t| {
                    let overdue = t.overdue_seconds();
                    let trigger_type = match &t.trigger {
                        ProspectiveTrigger::AtTime { .. } => "time".to_string(),
                        ProspectiveTrigger::AfterDuration { .. } => "duration".to_string(),
                        ProspectiveTrigger::OnContext { .. } => "context".to_string(),
                    };
                    ReminderItem {
                        id: t.id.0.to_string(),
                        content: t.content,
                        trigger_type,
                        status: format!("{:?}", t.status).to_lowercase(),
                        due_at: t.trigger.due_at(),
                        created_at: t.created_at,
                        triggered_at: t.triggered_at,
                        dismissed_at: t.dismissed_at,
                        priority: t.priority,
                        tags: t.tags,
                        overdue_seconds: overdue,
                    }
                })
                .collect::<Vec<ReminderItem>>()
        }),
        // C: Todo search (was inline blocking — now properly in spawn_blocking)
        tokio::task::spawn_blocking(move || {
            let semantic_results = todo_store
                .search_similar(&todo_uid, &todo_emb, 10)
                .unwrap_or_default();

            let mut todos_with_scores: Vec<ProactiveTodoItem> = semantic_results
                .into_iter()
                .filter(|(t, _score)| {
                    matches!(
                        t.status,
                        TodoStatus::Todo | TodoStatus::InProgress | TodoStatus::Blocked
                    )
                })
                .map(|(t, score)| {
                    let project_name = t.project_id.as_ref().and_then(|pid| {
                        todo_store
                            .get_project(&todo_uid, pid)
                            .ok()
                            .flatten()
                            .map(|p| p.name)
                    });
                    ProactiveTodoItem {
                        id: t.id.0.to_string(),
                        short_id: t.short_id(),
                        content: t.content.clone(),
                        status: format!("{:?}", t.status).to_lowercase(),
                        priority: t.priority.indicator().to_string(),
                        project: project_name,
                        due_date: t.due_date.map(|d| d.format("%Y-%m-%d").to_string()),
                        relevance_reason: format!("semantic: {:.0}%", score * 100.0),
                        similarity_score: Some(score),
                    }
                })
                .collect();

            // Also include in_progress todos for work continuity
            let in_progress_candidates = todo_store
                .list_todos_for_user(&todo_uid, None)
                .unwrap_or_default()
                .into_iter()
                .filter(|t| t.status == TodoStatus::InProgress)
                .collect::<Vec<_>>();

            let in_progress_todos: Vec<ProactiveTodoItem> = in_progress_candidates
                .into_iter()
                .filter(|t| !todos_with_scores.iter().any(|s| s.id == t.id.0.to_string()))
                .map(|t| {
                    let project_name = t.project_id.as_ref().and_then(|pid| {
                        todo_store
                            .get_project(&todo_uid, pid)
                            .ok()
                            .flatten()
                            .map(|p| p.name)
                    });
                    ProactiveTodoItem {
                        id: t.id.0.to_string(),
                        short_id: t.short_id(),
                        content: t.content.clone(),
                        status: "in_progress".to_string(),
                        priority: t.priority.indicator().to_string(),
                        project: project_name,
                        due_date: t.due_date.map(|d| d.format("%Y-%m-%d").to_string()),
                        relevance_reason: "active work".to_string(),
                        similarity_score: None,
                    }
                })
                .collect();

            todos_with_scores.extend(in_progress_todos);
            todos_with_scores.sort_by(|a, b| {
                let a_ip = a.status == "in_progress";
                let b_ip = b.status == "in_progress";
                match (a_ip, b_ip) {
                    (true, false) => std::cmp::Ordering::Less,
                    (false, true) => std::cmp::Ordering::Greater,
                    _ => b
                        .similarity_score
                        .unwrap_or(0.0)
                        .total_cmp(&a.similarity_score.unwrap_or(0.0)),
                }
            });
            todos_with_scores
                .into_iter()
                .take(5)
                .collect::<Vec<ProactiveTodoItem>>()
        }),
        // D: Fact surfacing with quality gates
        async {
            if fact_entity_list.is_empty() {
                return Ok(Vec::new());
            }
            tokio::task::spawn_blocking(move || {
                let memory_guard = fact_memory.read();
                let mut found: std::collections::HashMap<String, ProactiveFact> =
                    std::collections::HashMap::new();
                for entity in &fact_entity_list {
                    if let Ok(entity_facts) = memory_guard.get_facts_by_entity(&fact_uid, entity, 5)
                    {
                        for fact in entity_facts {
                            // Quality gate: skip low-confidence facts
                            if fact.confidence < 0.35 {
                                continue;
                            }
                            // Quality gate: skip single-source facts (unreliable)
                            if fact.support_count < 2 {
                                continue;
                            }
                            // Quality gate: skip ALL "relates to" entity-pair facts
                            // These are auto-generated from entity co-occurrence and are
                            // uniformly low-value noise (e.g., "X relates to Y")
                            if fact.fact.contains(" relates to ") {
                                continue;
                            }
                            // Skip facts that are too short to be meaningful
                            if fact.fact.trim().len() < 15 {
                                continue;
                            }
                            found.entry(fact.id.clone()).or_insert(ProactiveFact {
                                id: fact.id.clone(),
                                fact: fact.fact.clone(),
                                confidence: fact.confidence,
                                support_count: fact.support_count,
                                related_entities: fact.related_entities.clone(),
                            });
                        }
                    }
                }
                // Deduplicate by text similarity: if two facts share >80% words, keep higher confidence
                let mut sorted: Vec<ProactiveFact> = found.into_values().collect();
                sorted.sort_by(|a, b| b.confidence.total_cmp(&a.confidence));
                let mut deduped: Vec<ProactiveFact> = Vec::new();
                for fact in sorted {
                    let fact_words: std::collections::HashSet<&str> =
                        fact.fact.split_whitespace().collect();
                    let is_dup = deduped.iter().any(|existing| {
                        let existing_words: std::collections::HashSet<&str> =
                            existing.fact.split_whitespace().collect();
                        if fact_words.is_empty() || existing_words.is_empty() {
                            return false;
                        }
                        let intersection = fact_words.intersection(&existing_words).count();
                        let smaller = fact_words.len().min(existing_words.len());
                        intersection * 100 / smaller >= 80
                    });
                    if !is_dup {
                        deduped.push(fact);
                    }
                }
                deduped.truncate(5);
                deduped
            })
            .await
        }
    );

    let due_reminders: Vec<ReminderItem> = due_result
        .map_err(|e| AppError::Internal(anyhow::anyhow!("Due reminders task panicked: {e}")))?;
    let context_reminders: Vec<ReminderItem> = context_reminders_from_cache;
    let relevant_todos: Vec<ProactiveTodoItem> = todo_result
        .map_err(|e| AppError::Internal(anyhow::anyhow!("Todo search task panicked: {e}")))?;
    let relevant_facts: Vec<ProactiveFact> = fact_result
        .map_err(|e| AppError::Internal(anyhow::anyhow!("Fact surfacing task panicked: {e}")))?;
    let todo_count = relevant_todos.len();

    let t_parallel = op_start.elapsed();
    tracing::info!(
        user_id = %req.user_id,
        parallel_ms = format!("{:.2}", (t_parallel - t_auto_ingest).as_secs_f64() * 1000.0),
        cumulative_ms = format!("{:.2}", t_parallel.as_secs_f64() * 1000.0),
        reminders = due_reminders.len(),
        ctx_reminders = context_reminders.len(),
        todos = todo_count,
        facts = relevant_facts.len(),
        "proactive_context [phase:parallel] reminders + todos + facts complete"
    );

    let memory_count = memories.len();
    let reminder_count = due_reminders.len() + context_reminders.len();

    // Emit event for dashboard with full results
    let proactive_latency = op_start.elapsed().as_secs_f64() * 1000.0;
    let proactive_results = serde_json::json!({
        "context": req.context,
        "memory_count": memory_count,
        "reminder_count": reminder_count,
        "todo_count": todo_count,
        "latency_ms": proactive_latency,
        "memories": memories.iter().map(|m| serde_json::json!({
            "id": m.id,
            "content": m.content,
            "memory_type": m.memory_type,
            "tags": m.tags,
            "score": m.score,
            "importance": m.importance,
            "tier": m.tier,
            "created_at": m.created_at,
            "relevance_reason": m.relevance_reason,
        })).collect::<Vec<_>>(),
        "facts": relevant_facts.iter().map(|f| serde_json::json!({
            "id": f.id,
            "fact": f.fact,
            "confidence": f.confidence,
            "support_count": f.support_count,
            "related_entities": f.related_entities,
        })).collect::<Vec<_>>(),
        "todos": relevant_todos.iter().map(|t| serde_json::json!({
            "short_id": t.short_id,
            "content": t.content,
            "status": t.status,
            "priority": t.priority,
            "project": t.project,
        })).collect::<Vec<_>>(),
        "due_reminders": due_reminders.iter().map(|r| serde_json::json!({
            "id": r.id,
            "content": r.content,
            "trigger_type": r.trigger_type,
            "priority": r.priority,
        })).collect::<Vec<_>>(),
        "context_reminders": context_reminders.iter().map(|r| serde_json::json!({
            "id": r.id,
            "content": r.content,
            "trigger_type": r.trigger_type,
            "priority": r.priority,
        })).collect::<Vec<_>>(),
        "detected_entities": detected_entities.iter().map(|e| serde_json::json!({
            "name": e.name,
            "type": e.entity_type,
        })).collect::<Vec<_>>(),
    });
    state.emit_event(MemoryEvent {
        event_type: "PROACTIVE_CONTEXT".to_string(),
        timestamp: chrono::Utc::now(),
        user_id: req.user_id.clone(),
        memory_id: ingested_memory_id.clone(),
        content_preview: Some(req.context.chars().take(50).collect()),
        memory_type: Some("proactive".to_string()),
        importance: None,
        count: Some(memory_count + reminder_count),
        entities: None,
        results: Some(proactive_results),
    });

    // Audit log for proactive context operations
    state.log_event(
        &req.user_id,
        "PROACTIVE_CONTEXT",
        ingested_memory_id.as_deref().unwrap_or("none"),
        &format!(
            "Context='{}' surfaced {} memories, {} reminders, {} todos (auto_ingest={})",
            req.context.chars().take(50).collect::<String>(),
            memory_count,
            reminder_count,
            todo_count,
            req.auto_ingest
        ),
    );

    // Track session event for memories surfaced
    if memory_count > 0 {
        let session_id = state.session_store.get_or_create_session(&req.user_id);
        let memory_ids: Vec<String> = memories.iter().map(|m| m.id.clone()).collect();
        let avg_score = if !memories.is_empty() {
            memories.iter().map(|m| m.score).sum::<f32>() / memories.len() as f32
        } else {
            0.0
        };
        state.session_store.add_event(
            &session_id,
            SessionEvent::MemoriesSurfaced {
                timestamp: chrono::Utc::now(),
                query_preview: req.context.chars().take(100).collect(),
                memory_count,
                memory_ids,
                avg_score,
            },
        );
    }

    let latency_ms = op_start.elapsed().as_secs_f64() * 1000.0;

    tracing::info!(
        user_id = %req.user_id,
        response_assembly_ms = format!("{:.2}", (op_start.elapsed() - t_parallel).as_secs_f64() * 1000.0),
        total_ms = format!("{:.2}", latency_ms),
        memories = memory_count,
        reminders = reminder_count,
        todos = todo_count,
        facts = relevant_facts.len(),
        "proactive_context [phase:total] === COMPLETE ==="
    );

    Ok(Json(ProactiveContextResponse {
        memories,
        due_reminders,
        context_reminders,
        memory_count,
        reminder_count,
        ingested_memory_id,
        feedback_processed,
        relevant_todos,
        todo_count,
        relevant_facts,
        latency_ms,
        detected_entities,
        temporal_credits_applied: if temporal_credits_total > 0 {
            Some(temporal_credits_total)
        } else {
            None
        },
    }))
}

// =============================================================================
// SURFACE RELEVANT HANDLER
// =============================================================================

/// POST /api/relevant - Proactive memory surfacing
/// Returns relevant memories based on current context using entity matching
/// and semantic similarity. Target latency: <30ms
#[tracing::instrument(skip(state), fields(user_id = %req.user_id))]
pub async fn surface_relevant(
    State(state): State<AppState>,
    Json(req): Json<relevance::RelevanceRequest>,
) -> Result<Json<relevance::RelevanceResponse>, AppError> {
    validation::validate_user_id(&req.user_id).map_validation_err("user_id")?;
    validation::validate_max_results(req.config.max_results).map_validation_err("max_results")?;

    let memory_sys = state
        .get_user_memory(&req.user_id)
        .map_err(AppError::Internal)?;
    let graph_memory = state
        .get_user_graph(&req.user_id)
        .map_err(AppError::Internal)?;
    let engine = state.relevance_engine.clone();
    let feedback_store = state.feedback_store.clone();

    let response = {
        let memory_sys = memory_sys.clone();
        let graph_memory = graph_memory.clone();
        let context = req.context.clone();
        let config = req.config.clone();

        tokio::task::spawn_blocking(move || {
            let memory_guard = memory_sys.read();
            let graph_guard = graph_memory.read();
            engine.surface_relevant(
                &context,
                &memory_guard,
                Some(&*graph_guard),
                &config,
                Some(&feedback_store),
            )
        })
        .await
        .map_err(|e| AppError::Internal(anyhow::anyhow!("Blocking task panicked: {e}")))?
        .map_err(AppError::Internal)?
    };

    // Broadcast RETRIEVE event for real-time dashboard
    state.emit_event(MemoryEvent {
        event_type: "RETRIEVE".to_string(),
        timestamp: chrono::Utc::now(),
        user_id: req.user_id.clone(),
        memory_id: None,
        content_preview: Some(req.context.chars().take(50).collect()),
        memory_type: Some("proactive".to_string()),
        importance: None,
        count: Some(response.memories.len()),
        entities: None,
        results: None,
    });

    Ok(Json(response))
}

// =============================================================================
// TRACKED RECALL HANDLER
// =============================================================================

/// POST /api/recall/tracked - Retrieval with tracking for later feedback
///
/// Use this when you want to provide feedback later on whether memories were helpful.
/// Returns memory_ids that can be passed to /api/reinforce for Hebbian strengthening.
#[tracing::instrument(skip(state), fields(user_id = %req.user_id, query = %req.query))]
pub async fn recall_tracked(
    State(state): State<AppState>,
    Json(req): Json<TrackedRetrieveRequest>,
) -> Result<Json<TrackedRetrieveResponse>, AppError> {
    let op_start = std::time::Instant::now();
    validation::validate_user_id(&req.user_id).map_validation_err("user_id")?;
    validation::validate_max_results(req.limit).map_validation_err("limit")?;

    let memory = state
        .get_user_memory(&req.user_id)
        .map_err(AppError::Internal)?;

    let query_text = req.query.clone();
    let limit = req.limit;
    let user_id = req.user_id.clone();
    let retrieval_mode = parse_retrieval_mode(&req.mode);

    let memories = {
        let memory = memory.clone();
        tokio::task::spawn_blocking(move || {
            let memory_guard = memory.read();
            let query = MemoryQuery {
                user_id: Some(user_id),
                query_text: Some(query_text),
                max_results: limit,
                retrieval_mode,
                ..Default::default()
            };
            memory_guard.recall(&query).unwrap_or_default()
        })
        .await
        .map_err(|e| AppError::Internal(anyhow::anyhow!("Blocking task panicked: {e}")))?
    };

    // Extract memory IDs for tracking
    let memory_ids: Vec<String> = memories.iter().map(|m| m.id.0.to_string()).collect();

    // Generate tracking ID (could be stored for audit, but for now just a UUID)
    let tracking_id = uuid::Uuid::new_v4().to_string();

    // Normalize scores relative to top result (same as recall handler)
    let raw_scores: Vec<f32> = memories
        .iter()
        .map(|m| m.score.unwrap_or_else(|| m.salience_score_with_access()))
        .collect();
    let top_score = raw_scores.iter().cloned().fold(0.0_f32, f32::max);

    let recall_memories: Vec<RecallMemory> = memories
        .iter()
        .zip(raw_scores.iter())
        .map(|(m, &raw)| {
            let score = if top_score > 0.0 {
                (raw / top_score) * 0.95
            } else {
                0.0
            };
            RecallMemory {
                id: m.id.0.to_string(),
                experience: RecallExperience {
                    content: m.experience.content.clone(),
                    memory_type: Some(format!("{:?}", m.experience.experience_type)),
                    tags: m.experience.entities.clone(),
                },
                importance: m.importance(),
                created_at: m.created_at.to_rfc3339(),
                score,
                tier: format!("{:?}", m.tier),
            }
        })
        .collect();

    let count = recall_memories.len();

    // Record metrics
    let duration = op_start.elapsed().as_secs_f64();
    metrics::MEMORY_RETRIEVE_DURATION
        .with_label_values(&["tracked"])
        .observe(duration);
    metrics::MEMORY_RETRIEVE_TOTAL
        .with_label_values(&["tracked", "success"])
        .inc();
    metrics::MEMORY_RETRIEVE_RESULTS
        .with_label_values(&["tracked"])
        .observe(count as f64);

    Ok(Json(TrackedRetrieveResponse {
        tracking_id,
        ids: memory_ids,
        memories: recall_memories,
    }))
}

// =============================================================================
// REINFORCE FEEDBACK HANDLER
// =============================================================================

/// POST /api/reinforce - Hebbian reinforcement based on task outcome
///
/// Call this after using memories to complete a task:
/// - "helpful": Memories that helped → boost importance, strengthen associations
/// - "misleading": Memories that misled → reduce importance, don't strengthen
/// - "neutral": Just record access, mild strengthening
#[tracing::instrument(skip(state), fields(user_id = %req.user_id, outcome = %req.outcome, count = req.ids.len()))]
pub async fn reinforce_feedback(
    State(state): State<AppState>,
    Json(req): Json<ReinforceFeedbackRequest>,
) -> Result<Json<ReinforceFeedbackResponse>, AppError> {
    let op_start = std::time::Instant::now();

    validation::validate_user_id(&req.user_id).map_validation_err("user_id")?;

    if req.ids.is_empty() {
        return Ok(Json(ReinforceFeedbackResponse {
            memories_processed: 0,
            associations_strengthened: 0,
            importance_boosts: 0,
            importance_decays: 0,
        }));
    }

    // Parse outcome
    let outcome_label = req.outcome.to_lowercase();
    let outcome = match outcome_label.as_str() {
        "helpful" => crate::memory::RetrievalOutcome::Helpful,
        "misleading" => crate::memory::RetrievalOutcome::Misleading,
        _ => crate::memory::RetrievalOutcome::Neutral,
    };

    // Convert string IDs to MemoryId
    let memory_ids: Vec<MemoryId> = req
        .ids
        .iter()
        .filter_map(|id| uuid::Uuid::parse_str(id).ok())
        .map(MemoryId)
        .collect();

    if memory_ids.is_empty() {
        return Err(AppError::InvalidInput {
            field: "ids".to_string(),
            reason: "No valid UUIDs provided".to_string(),
        });
    }

    let memory = state
        .get_user_memory(&req.user_id)
        .map_err(AppError::Internal)?;

    // Run reinforcement in blocking task (involves RocksDB writes)
    let graph = state.get_user_graph(&req.user_id).ok();

    let memory_uuids: Vec<uuid::Uuid> = memory_ids.iter().map(|m| m.0).collect();
    let mut stats = {
        let memory = memory.clone();
        tokio::task::spawn_blocking(move || {
            let memory_guard = memory.read();
            memory_guard.reinforce_recall(&memory_ids, outcome)
        })
        .await
        .map_err(|e| AppError::Internal(anyhow::anyhow!("Blocking task panicked: {e}")))?
        .map_err(AppError::Internal)?
    };

    // Propagate feedback to entity salience in the knowledge graph
    if let Some(graph) = graph {
        let boost = match outcome {
            crate::memory::RetrievalOutcome::Helpful => {
                crate::constants::ENTITY_SALIENCE_HELPFUL_BOOST
            }
            crate::memory::RetrievalOutcome::Misleading => {
                crate::constants::ENTITY_SALIENCE_MISLEADING_PENALTY
            }
            crate::memory::RetrievalOutcome::Neutral => 0.0,
        };
        if boost != 0.0 {
            let graph_clone = graph.clone();
            let uuids = memory_uuids.clone();
            match tokio::task::spawn_blocking(move || {
                let graph_guard = graph_clone.read();
                graph_guard.reinforce_entity_salience(&uuids, boost)
            })
            .await
            {
                Ok(Ok(count)) => {
                    stats.entity_edges_reinforced = count;
                    tracing::info!(
                        entities_reinforced = count,
                        boost = boost,
                        "Entity salience reinforcement applied"
                    );
                }
                Ok(Err(e)) => tracing::warn!("Entity reinforcement failed: {}", e),
                Err(e) => tracing::warn!("Entity reinforcement task panicked: {}", e),
            }
        }
    }

    tracing::info!(
        user_id = %req.user_id,
        processed = stats.memories_processed,
        strengthened = stats.associations_strengthened,
        boosts = stats.importance_boosts,
        decays = stats.importance_decays,
        entity_salience_reinforced = stats.entity_edges_reinforced,
        "Hebbian reinforcement applied"
    );

    // Record metrics
    let duration = op_start.elapsed().as_secs_f64();
    metrics::HEBBIAN_REINFORCE_DURATION
        .with_label_values(&[&outcome_label])
        .observe(duration);
    metrics::HEBBIAN_REINFORCE_TOTAL
        .with_label_values(&[&outcome_label, &String::from("success")])
        .inc();

    Ok(Json(ReinforceFeedbackResponse {
        memories_processed: stats.memories_processed,
        associations_strengthened: stats.associations_strengthened,
        importance_boosts: stats.importance_boosts,
        importance_decays: stats.importance_decays,
    }))
}

// =============================================================================
// RECALL BY TAGS HANDLER
// =============================================================================

/// POST /api/recall/tags - Recall memories by tags
///
/// Returns memories matching ANY of the provided tags.
#[tracing::instrument(skip(state), fields(user_id = %req.user_id))]
pub async fn recall_by_tags(
    State(state): State<AppState>,
    Json(req): Json<RecallByTagsRequest>,
) -> Result<Json<RetrieveResponse>, AppError> {
    validation::validate_user_id(&req.user_id).map_validation_err("user_id")?;

    if let Some(limit) = req.limit {
        validation::validate_max_results(limit).map_validation_err("limit")?;
    }

    if req.tags.is_empty() {
        return Err(AppError::InvalidInput {
            field: "tags".to_string(),
            reason: "At least one tag must be provided".to_string(),
        });
    }

    let memory_sys = state
        .get_user_memory(&req.user_id)
        .map_err(AppError::Internal)?;

    let memory_guard = memory_sys.read();
    let limit = req.limit.unwrap_or(50);

    // Use recall_by_tags which increments the retrieval counter
    let raw_memories = memory_guard
        .recall_by_tags(&req.tags, limit)
        .map_err(AppError::Internal)?;
    let count = raw_memories.len();

    // Serialize memories to JSON for response
    let memories: Vec<serde_json::Value> = raw_memories
        .into_iter()
        .filter_map(|m| serde_json::to_value(&m).ok())
        .collect();

    info!(
        "📋 Recall by tags: user={}, tags={:?}, found={}",
        req.user_id, req.tags, count
    );

    // Broadcast RETRIEVE event for real-time dashboard
    state.emit_event(MemoryEvent {
        event_type: "RETRIEVE".to_string(),
        timestamp: chrono::Utc::now(),
        user_id: req.user_id.clone(),
        memory_id: None,
        content_preview: Some(format!("tags: {}", req.tags.join(", "))),
        memory_type: Some("by_tags".to_string()),
        importance: None,
        count: Some(count),
        entities: None,
        results: None,
    });

    Ok(Json(RetrieveResponse { memories, count }))
}

// =============================================================================
// RECALL BY DATE HANDLER
// =============================================================================

/// POST /api/recall/date - Recall memories by date range
///
/// Returns memories created within the specified date range.
#[tracing::instrument(skip(state), fields(user_id = %req.user_id))]
pub async fn recall_by_date(
    State(state): State<AppState>,
    Json(req): Json<RecallByDateRequest>,
) -> Result<Json<RetrieveResponse>, AppError> {
    validation::validate_user_id(&req.user_id).map_validation_err("user_id")?;

    if let Some(limit) = req.limit {
        validation::validate_max_results(limit).map_validation_err("limit")?;
    }

    if req.end < req.start {
        return Err(AppError::InvalidInput {
            field: "end".to_string(),
            reason: "End date must be after start date".to_string(),
        });
    }

    let memory_sys = state
        .get_user_memory(&req.user_id)
        .map_err(AppError::Internal)?;

    let memory_guard = memory_sys.read();
    let limit = req.limit.unwrap_or(50);

    // Use recall_by_date which increments the retrieval counter
    let raw_memories = memory_guard
        .recall_by_date(req.start, req.end, limit)
        .map_err(AppError::Internal)?;
    let count = raw_memories.len();

    // Serialize memories to JSON for response
    let memories: Vec<serde_json::Value> = raw_memories
        .into_iter()
        .filter_map(|m| serde_json::to_value(&m).ok())
        .collect();

    info!(
        "📅 Recall by date: user={}, start={}, end={}, found={}",
        req.user_id, req.start, req.end, count
    );

    // Broadcast RETRIEVE event for real-time dashboard
    state.emit_event(MemoryEvent {
        event_type: "RETRIEVE".to_string(),
        timestamp: chrono::Utc::now(),
        user_id: req.user_id.clone(),
        memory_id: None,
        content_preview: Some(format!(
            "{} to {}",
            req.start.format("%Y-%m-%d"),
            req.end.format("%Y-%m-%d")
        )),
        memory_type: Some("by_date".to_string()),
        importance: None,
        count: Some(count),
        entities: None,
        results: None,
    });

    Ok(Json(RetrieveResponse { memories, count }))
}
