//! Conversational Memory — Three-Layer Memory Architecture (Phase-84)
//!
//! Provides long-term memory for conversations through a hierarchical
//! three-layer architecture:
//!
//! ```text
//! ┌─────────────────────────────────────────────────┐
//! │         MemoryManager (Phase-84)                │
//! │                                                 │
//! │  L1  Current Window (DashMap)     — ms latency  │
//! │  L2  Summary Store (DocumentStore)— s  latency  │
//! │  L3  Semantic Memory (VectorStore)— similarity   │
//! │                                                 │
//! │  ┌──────────────┐  ┌──────────────┐            │
//! │  │ Summarizer   │  │ ProfileEngine│            │
//! │  │ (LLM-based)  │  │ (LLM-based)  │            │
//! │  └──────────────┘  └──────────────┘            │
//! └─────────────────────────────────────────────────┘
//! ```
//!
//! # Memory Layers
//!
//! - **L1 (Window)**: The most recent N messages kept in-memory (DashMap).
//!   Sub-millisecond access. This is the "working memory".
//!
//! - **L2 (Summary)**: When messages exceed the window size, older messages
//!   are compressed into structured summaries via LLM. Stored in DocumentStore
//!   as JSON documents with key_points, entities, decisions.
//!
//! - **L3 (Semantic)**: Key QA pairs and summaries are embedded into vectors
//!   and stored in VectorStore. Enables semantic recall of distant memories
//!   by similarity search against the current query.
//!
//! # Usage
//!
//! ```ignore
//! use aifastdb_core::llm::memory::{MemoryManager, MemoryConfig};
//!
//! let memory = MemoryManager::new(config);
//!
//! // Before sending a message, retrieve relevant memories
//! let context = memory.recall(conv_id, user_id, "current question").await?;
//!
//! // After receiving a response, store the exchange
//! memory.memorize(conv_id, user_id, &user_msg, &assistant_msg).await?;
//! ```

use std::sync::Arc;

use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use tracing::{debug, warn};

use super::client::{LlmClient, LlmMessage, MessageRole};
use super::error::LlmResult;
use super::types::ChatConfig;

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

/// Configuration for the MemoryManager.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryConfig {
    /// Maximum number of messages in the L1 window before triggering summarization.
    /// Default: 20
    #[serde(default = "default_window_size")]
    pub window_size: usize,

    /// Number of messages to compress in each summarization batch.
    /// Default: 10
    #[serde(default = "default_summarize_batch")]
    pub summarize_batch_size: usize,

    /// Whether to enable L2 summary compression.
    /// Default: true
    #[serde(default = "default_true")]
    pub enable_summarization: bool,

    /// Whether to enable L3 semantic memory.
    /// Default: true
    #[serde(default = "default_true")]
    pub enable_semantic_memory: bool,

    /// Whether to enable user profile extraction.
    /// Default: true
    #[serde(default = "default_true")]
    pub enable_user_profile: bool,

    /// Extract user profile every N conversation rounds.
    /// Default: 10
    #[serde(default = "default_profile_interval")]
    pub profile_extraction_interval: usize,

    /// Maximum number of semantic memories to recall per query.
    /// Default: 5
    #[serde(default = "default_recall_limit")]
    pub semantic_recall_limit: usize,

    /// Minimum similarity score for semantic recall (0.0 - 1.0).
    /// Default: 0.3
    #[serde(default = "default_similarity_threshold")]
    pub similarity_threshold: f32,

    /// Token budget allocation for memory context (fraction of total budget).
    /// memory_budget + history_budget + user_budget should = 1.0
    #[serde(default = "default_memory_budget")]
    pub memory_budget: f32,

    /// Token budget allocation for recent history.
    #[serde(default = "default_history_budget")]
    pub history_budget: f32,

    /// Token budget allocation for user message.
    #[serde(default = "default_user_budget")]
    pub user_budget: f32,

    /// Vector dimension for semantic memory embeddings.
    /// Default: 384 (MiniLM)
    #[serde(default = "default_vector_dim")]
    pub vector_dimension: usize,
}

fn default_window_size() -> usize { 20 }
fn default_summarize_batch() -> usize { 10 }
fn default_true() -> bool { true }
fn default_profile_interval() -> usize { 10 }
fn default_recall_limit() -> usize { 5 }
fn default_similarity_threshold() -> f32 { 0.3 }
fn default_memory_budget() -> f32 { 0.2 }
fn default_history_budget() -> f32 { 0.5 }
fn default_user_budget() -> f32 { 0.3 }
fn default_vector_dim() -> usize { 384 }

impl Default for MemoryConfig {
    fn default() -> Self {
        Self {
            window_size: default_window_size(),
            summarize_batch_size: default_summarize_batch(),
            enable_summarization: true,
            enable_semantic_memory: true,
            enable_user_profile: true,
            profile_extraction_interval: default_profile_interval(),
            semantic_recall_limit: default_recall_limit(),
            similarity_threshold: default_similarity_threshold(),
            memory_budget: default_memory_budget(),
            history_budget: default_history_budget(),
            user_budget: default_user_budget(),
            vector_dimension: default_vector_dim(),
        }
    }
}

// ============================================================================
// Memory Types
// ============================================================================

/// A structured summary of a conversation segment.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversationSummary {
    /// Unique summary ID.
    pub id: String,

    /// Conversation ID this summary belongs to.
    pub conversation_id: String,

    /// User ID (for per-user memory isolation).
    pub user_id: String,

    /// Key points extracted from the conversation segment.
    pub key_points: Vec<String>,

    /// Named entities mentioned (people, products, concepts).
    pub entities: Vec<String>,

    /// Decisions or conclusions reached.
    pub decisions: Vec<String>,

    /// Compressed text summary.
    pub summary_text: String,

    /// Number of original messages that were summarized.
    pub message_count: usize,

    /// Timestamp range of the original messages.
    pub time_range: (i64, i64),

    /// Creation timestamp.
    pub created_at: i64,
}

/// A semantic memory entry stored as a vector.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SemanticMemory {
    /// Unique memory ID.
    pub id: String,

    /// Conversation ID this memory originated from.
    pub conversation_id: String,

    /// User ID (for per-user memory isolation).
    pub user_id: String,

    /// The memory content (question + answer pair, or summary excerpt).
    pub content: String,

    /// Memory type.
    pub memory_type: MemoryType,

    /// Relevance score when retrieved (populated during recall).
    #[serde(default)]
    pub relevance_score: f32,

    /// Creation timestamp.
    pub created_at: i64,
}

/// Types of semantic memories.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum MemoryType {
    /// A question-answer pair from a conversation.
    QAPair,
    /// A summary of a conversation segment.
    SummaryExcerpt,
    /// A user profile insight.
    ProfileInsight,
}

/// User profile extracted from conversation history.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserProfile {
    /// User ID.
    pub user_id: String,

    /// User's preferences (e.g., "prefers concise answers").
    pub preferences: Vec<String>,

    /// Frequently used terms or jargon.
    pub terminology: Vec<String>,

    /// Topics/domains the user is interested in.
    pub interests: Vec<String>,

    /// Historical decisions and their context.
    pub decisions: Vec<String>,

    /// Last updated timestamp.
    pub updated_at: i64,

    /// Number of conversation rounds analyzed.
    pub rounds_analyzed: usize,
}

impl Default for UserProfile {
    fn default() -> Self {
        Self {
            user_id: String::new(),
            preferences: Vec::new(),
            terminology: Vec::new(),
            interests: Vec::new(),
            decisions: Vec::new(),
            updated_at: 0,
            rounds_analyzed: 0,
        }
    }
}

/// Memory context assembled for injection into a conversation.
///
/// This is the output of `MemoryManager::recall()` — a collection of
/// relevant memories from all three layers, ready to be prepended to
/// the conversation context.
#[derive(Debug, Clone, Default)]
pub struct MemoryContext {
    /// L2 summaries of older conversation segments.
    pub summaries: Vec<ConversationSummary>,

    /// L3 semantically-recalled memories.
    pub semantic_memories: Vec<SemanticMemory>,

    /// User profile (if available).
    pub user_profile: Option<UserProfile>,

    /// Approximate token count of the assembled memory context.
    pub estimated_tokens: usize,
}

impl MemoryContext {
    /// Check if this context has any useful content.
    pub fn is_empty(&self) -> bool {
        self.summaries.is_empty()
            && self.semantic_memories.is_empty()
            && self.user_profile.is_none()
    }

    /// Convert the memory context to LLM messages for injection.
    ///
    /// Returns a system-level message summarizing the memory context.
    pub fn to_system_message(&self) -> Option<String> {
        if self.is_empty() {
            return None;
        }

        let mut parts = Vec::new();

        // User profile section
        if let Some(ref profile) = self.user_profile {
            let mut profile_parts = Vec::new();
            if !profile.preferences.is_empty() {
                profile_parts.push(format!(
                    "Preferences: {}",
                    profile.preferences.join(", ")
                ));
            }
            if !profile.interests.is_empty() {
                profile_parts.push(format!(
                    "Interests: {}",
                    profile.interests.join(", ")
                ));
            }
            if !profile.terminology.is_empty() {
                profile_parts.push(format!(
                    "Familiar terms: {}",
                    profile.terminology.join(", ")
                ));
            }
            if !profile_parts.is_empty() {
                parts.push(format!(
                    "[User Profile]\n{}",
                    profile_parts.join("\n")
                ));
            }
        }

        // Summaries section
        if !self.summaries.is_empty() {
            let summary_texts: Vec<String> = self
                .summaries
                .iter()
                .map(|s| s.summary_text.clone())
                .collect();
            parts.push(format!(
                "[Conversation History Summary]\n{}",
                summary_texts.join("\n---\n")
            ));
        }

        // Semantic memories section
        if !self.semantic_memories.is_empty() {
            let memory_texts: Vec<String> = self
                .semantic_memories
                .iter()
                .map(|m| format!("- {}", m.content))
                .collect();
            parts.push(format!(
                "[Related Past Context]\n{}",
                memory_texts.join("\n")
            ));
        }

        if parts.is_empty() {
            None
        } else {
            Some(parts.join("\n\n"))
        }
    }
}

// ============================================================================
// Memory Layer Trait
// ============================================================================

/// Abstract interface for a memory storage layer.
///
/// Each layer (L2 summary, L3 semantic) implements this trait to
/// provide a uniform interface for the MemoryManager.
pub trait MemoryLayer: Send + Sync {
    /// Store a conversation summary (L2).
    fn store_summary(&self, summary: &ConversationSummary) -> LlmResult<()>;

    /// Retrieve all summaries for a conversation, ordered by time.
    fn get_summaries(&self, conversation_id: &str) -> LlmResult<Vec<ConversationSummary>>;

    /// Store a semantic memory entry (L3).
    fn store_semantic_memory(
        &self,
        memory: &SemanticMemory,
        embedding: &[f32],
    ) -> LlmResult<()>;

    /// Search semantic memories by embedding similarity.
    fn search_semantic_memories(
        &self,
        query_embedding: &[f32],
        user_id: &str,
        limit: usize,
        min_score: f32,
    ) -> LlmResult<Vec<(SemanticMemory, f32)>>;

    /// Store/update a user profile.
    fn store_user_profile(&self, profile: &UserProfile) -> LlmResult<()>;

    /// Retrieve a user profile.
    fn get_user_profile(&self, user_id: &str) -> LlmResult<Option<UserProfile>>;

    /// Delete all memories for a conversation.
    fn delete_conversation_memories(&self, conversation_id: &str) -> LlmResult<()>;

    /// Get memory statistics.
    fn stats(&self) -> MemoryStats;
}

/// Memory storage statistics.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MemoryStats {
    /// Number of conversation summaries stored.
    pub summary_count: usize,
    /// Number of semantic memory entries.
    pub semantic_memory_count: usize,
    /// Number of user profiles.
    pub profile_count: usize,
}

// ============================================================================
// In-Memory Layer (Default implementation)
// ============================================================================

/// In-memory implementation of the MemoryLayer trait.
///
/// Suitable for development, testing, and single-instance deployments.
/// Data is lost on restart. For persistence, use the DocumentStore-backed
/// implementation (Phase-84 T84.3).
pub struct InMemoryLayer {
    summaries: DashMap<String, Vec<ConversationSummary>>,
    semantic_memories: DashMap<String, (SemanticMemory, Vec<f32>)>,
    user_profiles: DashMap<String, UserProfile>,
}

impl InMemoryLayer {
    pub fn new() -> Self {
        Self {
            summaries: DashMap::new(),
            semantic_memories: DashMap::new(),
            user_profiles: DashMap::new(),
        }
    }
}

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

impl MemoryLayer for InMemoryLayer {
    fn store_summary(&self, summary: &ConversationSummary) -> LlmResult<()> {
        self.summaries
            .entry(summary.conversation_id.clone())
            .or_default()
            .push(summary.clone());
        Ok(())
    }

    fn get_summaries(&self, conversation_id: &str) -> LlmResult<Vec<ConversationSummary>> {
        Ok(self
            .summaries
            .get(conversation_id)
            .map(|r| r.value().clone())
            .unwrap_or_default())
    }

    fn store_semantic_memory(
        &self,
        memory: &SemanticMemory,
        embedding: &[f32],
    ) -> LlmResult<()> {
        self.semantic_memories
            .insert(memory.id.clone(), (memory.clone(), embedding.to_vec()));
        Ok(())
    }

    fn search_semantic_memories(
        &self,
        query_embedding: &[f32],
        user_id: &str,
        limit: usize,
        min_score: f32,
    ) -> LlmResult<Vec<(SemanticMemory, f32)>> {
        let mut results: Vec<(SemanticMemory, f32)> = self
            .semantic_memories
            .iter()
            .filter(|entry| entry.value().0.user_id == user_id)
            .map(|entry| {
                let (mem, vec) = entry.value();
                let score = cosine_similarity(query_embedding, vec);
                (mem.clone(), score)
            })
            .filter(|(_, score)| *score >= min_score)
            .collect();

        results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
        results.truncate(limit);
        Ok(results)
    }

    fn store_user_profile(&self, profile: &UserProfile) -> LlmResult<()> {
        self.user_profiles
            .insert(profile.user_id.clone(), profile.clone());
        Ok(())
    }

    fn get_user_profile(&self, user_id: &str) -> LlmResult<Option<UserProfile>> {
        Ok(self.user_profiles.get(user_id).map(|r| r.value().clone()))
    }

    fn delete_conversation_memories(&self, conversation_id: &str) -> LlmResult<()> {
        self.summaries.remove(conversation_id);
        // Remove semantic memories belonging to this conversation
        let to_remove: Vec<String> = self
            .semantic_memories
            .iter()
            .filter(|e| e.value().0.conversation_id == conversation_id)
            .map(|e| e.key().clone())
            .collect();
        for key in to_remove {
            self.semantic_memories.remove(&key);
        }
        Ok(())
    }

    fn stats(&self) -> MemoryStats {
        MemoryStats {
            summary_count: self.summaries.iter().map(|e| e.value().len()).sum(),
            semantic_memory_count: self.semantic_memories.len(),
            profile_count: self.user_profiles.len(),
        }
    }
}

// ============================================================================
// MemoryManager
// ============================================================================

/// Orchestrates the three-layer memory architecture.
///
/// The `MemoryManager` coordinates reading and writing across L1 (window),
/// L2 (summaries), and L3 (semantic vectors). It uses an LLM client for
/// summarization and profile extraction.
pub struct MemoryManager {
    /// Configuration.
    config: MemoryConfig,

    /// Storage layer (L2 + L3).
    layer: Arc<dyn MemoryLayer>,

    /// Per-conversation message round counter (for profile extraction timing).
    round_counters: DashMap<String, usize>,
}

impl MemoryManager {
    /// Create a new MemoryManager with the given configuration and storage layer.
    pub fn new(config: MemoryConfig, layer: Arc<dyn MemoryLayer>) -> Self {
        Self {
            config,
            layer,
            round_counters: DashMap::new(),
        }
    }

    /// Create a MemoryManager with default config and in-memory storage.
    pub fn in_memory() -> Self {
        Self::new(MemoryConfig::default(), Arc::new(InMemoryLayer::new()))
    }

    /// Get the current configuration.
    pub fn config(&self) -> &MemoryConfig {
        &self.config
    }

    /// Get memory statistics.
    pub fn stats(&self) -> MemoryStats {
        self.layer.stats()
    }

    // ========================================================================
    // Recall — Retrieve relevant memories for context assembly
    // ========================================================================

    /// Recall relevant memories for the current conversation context.
    ///
    /// This is called before sending a message to the LLM. It assembles
    /// a `MemoryContext` from:
    /// - L2 summaries for this conversation
    /// - L3 semantic memories matching the current query
    /// - User profile (if available)
    ///
    /// The `query_embedding` is optional — if not provided, L3 search is skipped.
    pub async fn recall(
        &self,
        conversation_id: &str,
        user_id: &str,
        query_embedding: Option<&[f32]>,
    ) -> LlmResult<MemoryContext> {
        let mut ctx = MemoryContext::default();

        // L2: Get conversation summaries
        if self.config.enable_summarization {
            ctx.summaries = self.layer.get_summaries(conversation_id)?;
            debug!(
                conv_id = %conversation_id,
                summary_count = ctx.summaries.len(),
                "Recalled L2 summaries"
            );
        }

        // L3: Semantic memory search
        if self.config.enable_semantic_memory {
            if let Some(embedding) = query_embedding {
                let results = self.layer.search_semantic_memories(
                    embedding,
                    user_id,
                    self.config.semantic_recall_limit,
                    self.config.similarity_threshold,
                )?;
                ctx.semantic_memories = results
                    .into_iter()
                    .map(|(mut mem, score)| {
                        mem.relevance_score = score;
                        mem
                    })
                    .collect();
                debug!(
                    conv_id = %conversation_id,
                    memory_count = ctx.semantic_memories.len(),
                    "Recalled L3 semantic memories"
                );
            }
        }

        // User profile
        if self.config.enable_user_profile {
            ctx.user_profile = self.layer.get_user_profile(user_id)?;
        }

        // Estimate tokens (rough: ~4 chars per token)
        ctx.estimated_tokens = estimate_context_tokens(&ctx);

        Ok(ctx)
    }

    // ========================================================================
    // Memorize — Store conversation exchanges into memory layers
    // ========================================================================

    /// Store a conversation exchange into the memory layers.
    ///
    /// Called after a successful LLM response. This:
    /// 1. Increments the round counter
    /// 2. Optionally creates a semantic memory for the QA pair
    ///
    /// Summarization and profile extraction are triggered separately
    /// via `maybe_summarize()` and `maybe_extract_profile()`.
    pub async fn memorize(
        &self,
        conversation_id: &str,
        user_id: &str,
        user_content: &str,
        assistant_content: &str,
        qa_embedding: Option<Vec<f32>>,
    ) -> LlmResult<()> {
        // Increment round counter
        let round = {
            let mut counter = self.round_counters.entry(conversation_id.to_string()).or_insert(0);
            *counter += 1;
            *counter
        };

        // Store as semantic memory (L3) if embedding is provided
        if self.config.enable_semantic_memory {
            if let Some(embedding) = qa_embedding {
                let memory = SemanticMemory {
                    id: format!("{}-qa-{}", conversation_id, round),
                    conversation_id: conversation_id.to_string(),
                    user_id: user_id.to_string(),
                    content: format!("Q: {}\nA: {}", truncate(user_content, 200), truncate(assistant_content, 300)),
                    memory_type: MemoryType::QAPair,
                    relevance_score: 0.0,
                    created_at: now_millis(),
                };
                self.layer.store_semantic_memory(&memory, &embedding)?;
                debug!(
                    conv_id = %conversation_id,
                    round = round,
                    "Stored L3 semantic memory (QA pair)"
                );
            }
        }

        Ok(())
    }

    // ========================================================================
    // Summarization — Compress old messages into summaries (L2)
    // ========================================================================

    /// Check if summarization should be triggered and execute it.
    ///
    /// Returns `true` if summarization was performed.
    ///
    /// The `messages` parameter should be the full message list for the
    /// conversation. The `client` is used to call the LLM for summarization.
    pub async fn maybe_summarize(
        &self,
        conversation_id: &str,
        user_id: &str,
        messages: &[super::conversation::StoredMessage],
        client: &dyn LlmClient,
    ) -> LlmResult<bool> {
        if !self.config.enable_summarization {
            return Ok(false);
        }

        // Check if we have enough messages to warrant summarization
        let existing_summaries = self.layer.get_summaries(conversation_id)?;
        let summarized_count: usize = existing_summaries.iter().map(|s| s.message_count).sum();

        // Only summarize if there are enough un-summarized messages beyond the window
        let unsummarized = messages.len().saturating_sub(summarized_count);
        if unsummarized <= self.config.window_size {
            return Ok(false);
        }

        // Calculate how many messages to summarize
        let to_summarize = unsummarized - self.config.window_size;
        let batch_start = summarized_count;
        let batch_end = (batch_start + to_summarize).min(messages.len());

        if batch_end <= batch_start {
            return Ok(false);
        }

        let batch = &messages[batch_start..batch_end];
        debug!(
            conv_id = %conversation_id,
            batch_start = batch_start,
            batch_end = batch_end,
            batch_size = batch.len(),
            "Triggering L2 summarization"
        );

        // Build summarization prompt
        let summary = self.summarize_messages(conversation_id, user_id, batch, client).await?;
        self.layer.store_summary(&summary)?;

        // Also store summary as semantic memory for cross-conversation recall
        // (embedding will be provided by the caller if semantic memory is enabled)

        Ok(true)
    }

    /// Summarize a batch of messages using the LLM.
    async fn summarize_messages(
        &self,
        conversation_id: &str,
        user_id: &str,
        messages: &[super::conversation::StoredMessage],
        client: &dyn LlmClient,
    ) -> LlmResult<ConversationSummary> {
        // Build the conversation text for summarization
        let conversation_text: String = messages
            .iter()
            .map(|m| format!("{}: {}", m.role, m.content))
            .collect::<Vec<_>>()
            .join("\n");

        let system_prompt = "You are a conversation summarizer. Given a conversation segment, extract:\n\
            1. key_points: A list of important points discussed\n\
            2. entities: Named entities mentioned (people, products, concepts)\n\
            3. decisions: Any decisions or conclusions reached\n\
            4. summary: A concise paragraph summarizing the exchange\n\n\
            Respond in JSON format:\n\
            {\"key_points\": [...], \"entities\": [...], \"decisions\": [...], \"summary\": \"...\"}";

        let llm_messages = vec![
            LlmMessage::system(system_prompt.to_string()),
            LlmMessage::user(format!("Summarize this conversation segment:\n\n{}", conversation_text)),
        ];

        let config = ChatConfig {
            temperature: Some(0.3),
            max_tokens: Some(500),
            ..Default::default()
        };

        let response = client.chat(&llm_messages, &config).await?;

        // Parse the JSON response
        let (key_points, entities, decisions, summary_text) =
            parse_summary_response(&response.content);

        let time_range = if messages.is_empty() {
            (0, 0)
        } else {
            (
                messages.first().map(|m| m.created_at).unwrap_or(0),
                messages.last().map(|m| m.created_at).unwrap_or(0),
            )
        };

        Ok(ConversationSummary {
            id: format!("{}-summary-{}", conversation_id, now_millis()),
            conversation_id: conversation_id.to_string(),
            user_id: user_id.to_string(),
            key_points,
            entities,
            decisions,
            summary_text,
            message_count: messages.len(),
            time_range,
            created_at: now_millis(),
        })
    }

    // ========================================================================
    // User Profile Extraction
    // ========================================================================

    /// Check if profile extraction should be triggered and execute it.
    ///
    /// Returns `true` if a profile update was performed.
    pub async fn maybe_extract_profile(
        &self,
        conversation_id: &str,
        user_id: &str,
        messages: &[super::conversation::StoredMessage],
        client: &dyn LlmClient,
    ) -> LlmResult<bool> {
        if !self.config.enable_user_profile {
            return Ok(false);
        }

        let round = self
            .round_counters
            .get(conversation_id)
            .map(|r| *r)
            .unwrap_or(0);

        if round == 0 || round % self.config.profile_extraction_interval != 0 {
            return Ok(false);
        }

        debug!(
            conv_id = %conversation_id,
            user_id = %user_id,
            round = round,
            "Triggering user profile extraction"
        );

        let profile = self.extract_profile(user_id, messages, client).await?;
        self.layer.store_user_profile(&profile)?;

        Ok(true)
    }

    /// Extract a user profile from conversation messages using the LLM.
    async fn extract_profile(
        &self,
        user_id: &str,
        messages: &[super::conversation::StoredMessage],
        client: &dyn LlmClient,
    ) -> LlmResult<UserProfile> {
        // Use only user messages for profile extraction
        let user_messages: String = messages
            .iter()
            .filter(|m| m.role == MessageRole::User)
            .map(|m| m.content.clone())
            .collect::<Vec<_>>()
            .join("\n");

        let system_prompt = "You are a user profile analyzer. Given a user's messages from a conversation, extract:\n\
            1. preferences: Communication/response preferences (e.g., \"prefers concise answers\")\n\
            2. terminology: Technical terms or jargon the user frequently uses\n\
            3. interests: Topics/domains the user is interested in\n\
            4. decisions: Key decisions or conclusions the user has made\n\n\
            Respond in JSON format:\n\
            {\"preferences\": [...], \"terminology\": [...], \"interests\": [...], \"decisions\": [...]}";

        let llm_messages = vec![
            LlmMessage::system(system_prompt.to_string()),
            LlmMessage::user(format!("Analyze this user's messages:\n\n{}", truncate(&user_messages, 2000))),
        ];

        let config = ChatConfig {
            temperature: Some(0.3),
            max_tokens: Some(400),
            ..Default::default()
        };

        let response = client.chat(&llm_messages, &config).await?;

        // Parse JSON response
        let existing = self.layer.get_user_profile(user_id)?.unwrap_or_default();
        let profile = parse_profile_response(user_id, &response.content, &existing);

        Ok(profile)
    }

    // ========================================================================
    // Cleanup
    // ========================================================================

    /// Delete all memories for a conversation.
    pub fn delete_conversation_memories(&self, conversation_id: &str) -> LlmResult<()> {
        self.round_counters.remove(conversation_id);
        self.layer.delete_conversation_memories(conversation_id)
    }

    /// Get the current round counter for a conversation.
    pub fn get_round_count(&self, conversation_id: &str) -> usize {
        self.round_counters
            .get(conversation_id)
            .map(|r| *r)
            .unwrap_or(0)
    }
}

// ============================================================================
// Helper Functions
// ============================================================================

/// Cosine similarity between two vectors.
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
    if a.len() != b.len() || a.is_empty() {
        return 0.0;
    }

    let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
    let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
    let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();

    if norm_a == 0.0 || norm_b == 0.0 {
        return 0.0;
    }

    dot / (norm_a * norm_b)
}

/// Estimate token count for a MemoryContext (~4 chars per token).
fn estimate_context_tokens(ctx: &MemoryContext) -> usize {
    let mut chars = 0usize;

    for s in &ctx.summaries {
        chars += s.summary_text.len();
        for kp in &s.key_points {
            chars += kp.len();
        }
    }

    for m in &ctx.semantic_memories {
        chars += m.content.len();
    }

    if let Some(ref p) = ctx.user_profile {
        for pref in &p.preferences {
            chars += pref.len();
        }
        for term in &p.terminology {
            chars += term.len();
        }
        for interest in &p.interests {
            chars += interest.len();
        }
    }

    chars / 4
}

/// Truncate a string to a maximum number of characters.
fn truncate(s: &str, max_chars: usize) -> &str {
    if s.len() <= max_chars {
        s
    } else {
        // Find a valid UTF-8 boundary
        let mut end = max_chars;
        while end > 0 && !s.is_char_boundary(end) {
            end -= 1;
        }
        &s[..end]
    }
}

/// Current time in milliseconds.
fn now_millis() -> i64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as i64
}

/// Parse the LLM's JSON response for conversation summarization.
fn parse_summary_response(content: &str) -> (Vec<String>, Vec<String>, Vec<String>, String) {
    // Try to parse as JSON
    if let Ok(value) = serde_json::from_str::<serde_json::Value>(content) {
        let key_points = extract_string_array(&value, "key_points");
        let entities = extract_string_array(&value, "entities");
        let decisions = extract_string_array(&value, "decisions");
        let summary = value
            .get("summary")
            .and_then(|v| v.as_str())
            .unwrap_or(content)
            .to_string();
        return (key_points, entities, decisions, summary);
    }

    // Try to extract JSON from markdown code block
    if let Some(json_str) = extract_json_from_markdown(content) {
        if let Ok(value) = serde_json::from_str::<serde_json::Value>(&json_str) {
            let key_points = extract_string_array(&value, "key_points");
            let entities = extract_string_array(&value, "entities");
            let decisions = extract_string_array(&value, "decisions");
            let summary = value
                .get("summary")
                .and_then(|v| v.as_str())
                .unwrap_or(content)
                .to_string();
            return (key_points, entities, decisions, summary);
        }
    }

    // Fallback: use the entire response as summary
    warn!("Failed to parse summary JSON, using raw response");
    (Vec::new(), Vec::new(), Vec::new(), content.to_string())
}

/// Parse the LLM's JSON response for user profile extraction.
fn parse_profile_response(user_id: &str, content: &str, existing: &UserProfile) -> UserProfile {
    let value = serde_json::from_str::<serde_json::Value>(content)
        .or_else(|_| {
            extract_json_from_markdown(content)
                .ok_or_else(|| serde_json::Error::io(std::io::Error::new(
                    std::io::ErrorKind::InvalidData,
                    "No JSON found",
                )))
                .and_then(|s| serde_json::from_str(&s))
        })
        .unwrap_or_default();

    let mut preferences = extract_string_array(&value, "preferences");
    let mut terminology = extract_string_array(&value, "terminology");
    let mut interests = extract_string_array(&value, "interests");
    let mut decisions = extract_string_array(&value, "decisions");

    // Merge with existing profile (deduplicate)
    merge_vec(&mut preferences, &existing.preferences);
    merge_vec(&mut terminology, &existing.terminology);
    merge_vec(&mut interests, &existing.interests);
    merge_vec(&mut decisions, &existing.decisions);

    UserProfile {
        user_id: user_id.to_string(),
        preferences,
        terminology,
        interests,
        decisions,
        updated_at: now_millis(),
        rounds_analyzed: existing.rounds_analyzed + 1,
    }
}

/// Extract a string array from a JSON value by key.
fn extract_string_array(value: &serde_json::Value, key: &str) -> Vec<String> {
    value
        .get(key)
        .and_then(|v| v.as_array())
        .map(|arr| {
            arr.iter()
                .filter_map(|v| v.as_str().map(|s| s.to_string()))
                .collect()
        })
        .unwrap_or_default()
}

/// Try to extract a JSON object from a markdown code block.
fn extract_json_from_markdown(content: &str) -> Option<String> {
    // Look for ```json ... ``` or ``` ... ```
    let start = content.find("```json")
        .map(|i| i + 7)
        .or_else(|| content.find("```").map(|i| i + 3))?;
    let rest = &content[start..];
    let end = rest.find("```")?;
    Some(rest[..end].trim().to_string())
}

/// Merge new items into existing vec, avoiding duplicates.
fn merge_vec(new: &mut Vec<String>, existing: &[String]) {
    for item in existing {
        let lower = item.to_lowercase();
        if !new.iter().any(|s| s.to_lowercase() == lower) {
            new.push(item.clone());
        }
    }
}

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

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

    #[test]
    fn test_memory_config_defaults() {
        let config = MemoryConfig::default();
        assert_eq!(config.window_size, 20);
        assert_eq!(config.summarize_batch_size, 10);
        assert!(config.enable_summarization);
        assert!(config.enable_semantic_memory);
        assert!(config.enable_user_profile);
        assert_eq!(config.profile_extraction_interval, 10);
        assert_eq!(config.semantic_recall_limit, 5);
        assert!((config.similarity_threshold - 0.3).abs() < f32::EPSILON);
    }

    #[test]
    fn test_cosine_similarity() {
        let a = vec![1.0, 0.0, 0.0];
        let b = vec![1.0, 0.0, 0.0];
        assert!((cosine_similarity(&a, &b) - 1.0).abs() < 1e-6);

        let c = vec![0.0, 1.0, 0.0];
        assert!(cosine_similarity(&a, &c).abs() < 1e-6);

        let d = vec![0.707, 0.707, 0.0];
        let sim = cosine_similarity(&a, &d);
        assert!(sim > 0.7 && sim < 0.71);
    }

    #[test]
    fn test_cosine_similarity_edge_cases() {
        assert_eq!(cosine_similarity(&[], &[]), 0.0);
        assert_eq!(cosine_similarity(&[1.0], &[1.0, 2.0]), 0.0);
        assert_eq!(cosine_similarity(&[0.0, 0.0], &[1.0, 2.0]), 0.0);
    }

    #[test]
    fn test_truncate() {
        assert_eq!(truncate("hello", 10), "hello");
        assert_eq!(truncate("hello world", 5), "hello");
        assert_eq!(truncate("", 5), "");
    }

    #[test]
    fn test_memory_context_empty() {
        let ctx = MemoryContext::default();
        assert!(ctx.is_empty());
        assert!(ctx.to_system_message().is_none());
    }

    #[test]
    fn test_memory_context_with_profile() {
        let mut ctx = MemoryContext::default();
        ctx.user_profile = Some(UserProfile {
            user_id: "user1".to_string(),
            preferences: vec!["concise answers".to_string()],
            terminology: vec!["Rust".to_string()],
            interests: vec!["databases".to_string()],
            ..Default::default()
        });

        assert!(!ctx.is_empty());
        let msg = ctx.to_system_message().unwrap();
        assert!(msg.contains("[User Profile]"));
        assert!(msg.contains("concise answers"));
        assert!(msg.contains("databases"));
    }

    #[test]
    fn test_memory_context_with_semantic_memories() {
        let mut ctx = MemoryContext::default();
        ctx.semantic_memories.push(SemanticMemory {
            id: "m1".to_string(),
            conversation_id: "conv1".to_string(),
            user_id: "u1".to_string(),
            content: "The server runs on port 8080".to_string(),
            memory_type: MemoryType::QAPair,
            relevance_score: 0.85,
            created_at: 0,
        });

        let msg = ctx.to_system_message().unwrap();
        assert!(msg.contains("[Related Past Context]"));
        assert!(msg.contains("port 8080"));
    }

    #[test]
    fn test_in_memory_layer_summaries() {
        let layer = InMemoryLayer::new();

        let summary = ConversationSummary {
            id: "s1".to_string(),
            conversation_id: "conv1".to_string(),
            user_id: "u1".to_string(),
            key_points: vec!["Point 1".to_string()],
            entities: vec!["Entity A".to_string()],
            decisions: vec!["Decision X".to_string()],
            summary_text: "This is a summary".to_string(),
            message_count: 10,
            time_range: (100, 200),
            created_at: 300,
        };

        layer.store_summary(&summary).unwrap();
        let summaries = layer.get_summaries("conv1").unwrap();
        assert_eq!(summaries.len(), 1);
        assert_eq!(summaries[0].summary_text, "This is a summary");

        // Different conversation should be empty
        let other = layer.get_summaries("conv2").unwrap();
        assert!(other.is_empty());
    }

    #[test]
    fn test_in_memory_layer_semantic_search() {
        let layer = InMemoryLayer::new();

        let mem1 = SemanticMemory {
            id: "m1".to_string(),
            conversation_id: "conv1".to_string(),
            user_id: "u1".to_string(),
            content: "Rust is great for performance".to_string(),
            memory_type: MemoryType::QAPair,
            relevance_score: 0.0,
            created_at: 100,
        };
        let embedding1 = vec![1.0, 0.0, 0.0];
        layer.store_semantic_memory(&mem1, &embedding1).unwrap();

        let mem2 = SemanticMemory {
            id: "m2".to_string(),
            conversation_id: "conv1".to_string(),
            user_id: "u1".to_string(),
            content: "Python is good for ML".to_string(),
            memory_type: MemoryType::QAPair,
            relevance_score: 0.0,
            created_at: 200,
        };
        let embedding2 = vec![0.0, 1.0, 0.0];
        layer.store_semantic_memory(&mem2, &embedding2).unwrap();

        // Search with embedding close to mem1
        let query = vec![0.9, 0.1, 0.0];
        let results = layer.search_semantic_memories(&query, "u1", 5, 0.0).unwrap();
        assert_eq!(results.len(), 2);
        assert_eq!(results[0].0.id, "m1"); // Closer to the query

        // Different user should get no results
        let results = layer.search_semantic_memories(&query, "u2", 5, 0.0).unwrap();
        assert!(results.is_empty());
    }

    #[test]
    fn test_in_memory_layer_user_profile() {
        let layer = InMemoryLayer::new();

        let profile = UserProfile {
            user_id: "u1".to_string(),
            preferences: vec!["concise".to_string()],
            ..Default::default()
        };

        layer.store_user_profile(&profile).unwrap();
        let loaded = layer.get_user_profile("u1").unwrap().unwrap();
        assert_eq!(loaded.preferences, vec!["concise"]);

        // Non-existent user
        assert!(layer.get_user_profile("u2").unwrap().is_none());
    }

    #[test]
    fn test_in_memory_layer_delete() {
        let layer = InMemoryLayer::new();

        let summary = ConversationSummary {
            id: "s1".to_string(),
            conversation_id: "conv1".to_string(),
            user_id: "u1".to_string(),
            key_points: vec![],
            entities: vec![],
            decisions: vec![],
            summary_text: "summary".to_string(),
            message_count: 5,
            time_range: (0, 100),
            created_at: 100,
        };
        layer.store_summary(&summary).unwrap();

        let mem = SemanticMemory {
            id: "m1".to_string(),
            conversation_id: "conv1".to_string(),
            user_id: "u1".to_string(),
            content: "test".to_string(),
            memory_type: MemoryType::QAPair,
            relevance_score: 0.0,
            created_at: 100,
        };
        layer.store_semantic_memory(&mem, &[1.0, 0.0]).unwrap();

        // Verify they exist
        assert_eq!(layer.get_summaries("conv1").unwrap().len(), 1);
        assert_eq!(layer.stats().semantic_memory_count, 1);

        // Delete
        layer.delete_conversation_memories("conv1").unwrap();
        assert!(layer.get_summaries("conv1").unwrap().is_empty());
        assert_eq!(layer.stats().semantic_memory_count, 0);
    }

    #[test]
    fn test_memory_manager_creation() {
        let mgr = MemoryManager::in_memory();
        assert_eq!(mgr.config().window_size, 20);
        assert_eq!(mgr.get_round_count("conv1"), 0);
    }

    #[test]
    fn test_parse_summary_response_json() {
        let json = r#"{"key_points": ["point1"], "entities": ["ent1"], "decisions": ["dec1"], "summary": "A summary"}"#;
        let (kp, ent, dec, sum) = parse_summary_response(json);
        assert_eq!(kp, vec!["point1"]);
        assert_eq!(ent, vec!["ent1"]);
        assert_eq!(dec, vec!["dec1"]);
        assert_eq!(sum, "A summary");
    }

    #[test]
    fn test_parse_summary_response_markdown() {
        let content = "Here is the summary:\n```json\n{\"key_points\": [\"p1\"], \"entities\": [], \"decisions\": [], \"summary\": \"test\"}\n```";
        let (kp, _, _, sum) = parse_summary_response(content);
        assert_eq!(kp, vec!["p1"]);
        assert_eq!(sum, "test");
    }

    #[test]
    fn test_parse_summary_response_fallback() {
        let content = "This is not JSON at all, just a summary.";
        let (kp, ent, dec, sum) = parse_summary_response(content);
        assert!(kp.is_empty());
        assert!(ent.is_empty());
        assert!(dec.is_empty());
        assert_eq!(sum, content);
    }

    #[test]
    fn test_merge_vec() {
        let mut new = vec!["A".to_string(), "b".to_string()];
        let existing = vec!["B".to_string(), "c".to_string()];
        merge_vec(&mut new, &existing);
        // "B" should not be added because "b" already exists (case-insensitive)
        assert_eq!(new, vec!["A", "b", "c"]);
    }

    #[test]
    fn test_extract_json_from_markdown() {
        let content = "Some text\n```json\n{\"a\": 1}\n```\nMore text";
        assert_eq!(
            extract_json_from_markdown(content),
            Some("{\"a\": 1}".to_string())
        );

        let no_json = "No code blocks here";
        assert_eq!(extract_json_from_markdown(no_json), None);
    }

    #[test]
    fn test_memory_stats() {
        let layer = InMemoryLayer::new();
        let stats = layer.stats();
        assert_eq!(stats.summary_count, 0);
        assert_eq!(stats.semantic_memory_count, 0);
        assert_eq!(stats.profile_count, 0);
    }

    #[tokio::test]
    async fn test_recall_empty() {
        let mgr = MemoryManager::in_memory();
        let ctx = mgr.recall("conv1", "u1", None).await.unwrap();
        assert!(ctx.is_empty());
        assert_eq!(ctx.estimated_tokens, 0);
    }

    #[tokio::test]
    async fn test_memorize_increments_round() {
        let mgr = MemoryManager::in_memory();
        assert_eq!(mgr.get_round_count("conv1"), 0);

        mgr.memorize("conv1", "u1", "Hello", "Hi there", None)
            .await
            .unwrap();
        assert_eq!(mgr.get_round_count("conv1"), 1);

        mgr.memorize("conv1", "u1", "How?", "Like this", None)
            .await
            .unwrap();
        assert_eq!(mgr.get_round_count("conv1"), 2);
    }

    #[tokio::test]
    async fn test_memorize_with_embedding() {
        let mgr = MemoryManager::in_memory();
        let embedding = vec![1.0, 0.0, 0.0];

        mgr.memorize("conv1", "u1", "What is Rust?", "A systems programming language", Some(embedding.clone()))
            .await
            .unwrap();

        // Should be searchable via recall
        let ctx = mgr.recall("conv1", "u1", Some(&embedding)).await.unwrap();
        assert_eq!(ctx.semantic_memories.len(), 1);
        assert!(ctx.semantic_memories[0].content.contains("What is Rust?"));
    }

    #[tokio::test]
    async fn test_delete_conversation_memories() {
        let mgr = MemoryManager::in_memory();
        let embedding = vec![1.0, 0.0];

        mgr.memorize("conv1", "u1", "Q", "A", Some(embedding.clone()))
            .await
            .unwrap();
        assert_eq!(mgr.get_round_count("conv1"), 1);

        mgr.delete_conversation_memories("conv1").unwrap();
        assert_eq!(mgr.get_round_count("conv1"), 0);

        let ctx = mgr.recall("conv1", "u1", Some(&embedding)).await.unwrap();
        assert!(ctx.semantic_memories.is_empty());
    }

    #[test]
    fn test_estimate_context_tokens() {
        let mut ctx = MemoryContext::default();
        assert_eq!(estimate_context_tokens(&ctx), 0);

        ctx.semantic_memories.push(SemanticMemory {
            id: "m".to_string(),
            conversation_id: "c".to_string(),
            user_id: "u".to_string(),
            content: "1234567890123456".to_string(), // 16 chars = ~4 tokens
            memory_type: MemoryType::QAPair,
            relevance_score: 0.0,
            created_at: 0,
        });
        assert_eq!(estimate_context_tokens(&ctx), 4);
    }

    #[tokio::test]
    async fn test_recall_with_all_layers() {
        let layer = Arc::new(InMemoryLayer::new());

        // Pre-populate L2 summary
        layer.store_summary(&ConversationSummary {
            id: "s1".into(),
            conversation_id: "conv1".into(),
            user_id: "u1".into(),
            key_points: vec!["We discussed database design".into()],
            entities: vec!["PostgreSQL".into()],
            decisions: vec!["Use PostgreSQL for main data".into()],
            summary_text: "Conversation about database design choices.".into(),
            message_count: 10,
            time_range: (100, 200),
            created_at: 300,
        }).unwrap();

        // Pre-populate L3 semantic memory
        layer.store_semantic_memory(&SemanticMemory {
            id: "m1".into(),
            conversation_id: "conv1".into(),
            user_id: "u1".into(),
            content: "Q: What DB? A: Use PostgreSQL".into(),
            memory_type: MemoryType::QAPair,
            relevance_score: 0.0,
            created_at: 100,
        }, &[1.0, 0.0, 0.0]).unwrap();

        // Pre-populate user profile
        layer.store_user_profile(&UserProfile {
            user_id: "u1".into(),
            preferences: vec!["prefers Rust".into()],
            interests: vec!["databases".into()],
            ..Default::default()
        }).unwrap();

        let mgr = MemoryManager::new(MemoryConfig::default(), layer);

        // Recall with query embedding
        let ctx = mgr.recall("conv1", "u1", Some(&[0.9, 0.1, 0.0])).await.unwrap();

        // Verify all layers are present
        assert!(!ctx.is_empty());
        assert_eq!(ctx.summaries.len(), 1);
        assert_eq!(ctx.semantic_memories.len(), 1);
        assert!(ctx.user_profile.is_some());

        // Verify system message contains all sections
        let msg = ctx.to_system_message().unwrap();
        assert!(msg.contains("[User Profile]"));
        assert!(msg.contains("[Conversation History Summary]"));
        assert!(msg.contains("[Related Past Context]"));
        assert!(msg.contains("prefers Rust"));
        assert!(msg.contains("database design"));
    }

    #[tokio::test]
    async fn test_memorize_multi_round_with_embeddings() {
        let mgr = MemoryManager::in_memory();

        // Simulate 5 rounds of conversation
        for i in 0..5 {
            let embedding = vec![i as f32 / 5.0, 1.0 - i as f32 / 5.0, 0.0];
            mgr.memorize(
                "conv1",
                "u1",
                &format!("Question {}", i),
                &format!("Answer {}", i),
                Some(embedding),
            )
            .await
            .unwrap();
        }

        assert_eq!(mgr.get_round_count("conv1"), 5);

        // Recall should find semantically similar memories
        let query = vec![0.0, 1.0, 0.0]; // Close to round 0
        let ctx = mgr.recall("conv1", "u1", Some(&query)).await.unwrap();
        assert!(!ctx.semantic_memories.is_empty());
        // First memory should be the one with the highest cosine similarity to [0, 1, 0]
        assert!(ctx.semantic_memories[0].content.contains("Question 0"));
    }

    #[test]
    fn test_memory_context_full_message() {
        let ctx = MemoryContext {
            summaries: vec![ConversationSummary {
                id: "s1".into(),
                conversation_id: "c1".into(),
                user_id: "u1".into(),
                key_points: vec![],
                entities: vec![],
                decisions: vec![],
                summary_text: "User wanted to build a REST API.".into(),
                message_count: 5,
                time_range: (0, 100),
                created_at: 0,
            }],
            semantic_memories: vec![SemanticMemory {
                id: "m1".into(),
                conversation_id: "c1".into(),
                user_id: "u1".into(),
                content: "Uses Rust with Axum framework".into(),
                memory_type: MemoryType::QAPair,
                relevance_score: 0.9,
                created_at: 0,
            }],
            user_profile: Some(UserProfile {
                user_id: "u1".into(),
                preferences: vec!["concise answers".into()],
                terminology: vec!["Axum".into(), "Tokio".into()],
                interests: vec!["web development".into()],
                ..Default::default()
            }),
            estimated_tokens: 50,
        };

        let msg = ctx.to_system_message().unwrap();
        // Should contain all 3 sections
        assert!(msg.contains("[User Profile]"));
        assert!(msg.contains("concise answers"));
        assert!(msg.contains("Axum"));
        assert!(msg.contains("[Conversation History Summary]"));
        assert!(msg.contains("REST API"));
        assert!(msg.contains("[Related Past Context]"));
        assert!(msg.contains("Rust with Axum"));
    }

    #[test]
    fn test_parse_profile_response_json() {
        let content = r#"{"preferences": ["brief"], "terminology": ["API"], "interests": ["Rust"], "decisions": ["Use axum"]}"#;
        let existing = UserProfile::default();
        let profile = parse_profile_response("u1", content, &existing);
        assert_eq!(profile.preferences, vec!["brief"]);
        assert_eq!(profile.terminology, vec!["API"]);
        assert_eq!(profile.interests, vec!["Rust"]);
        assert_eq!(profile.decisions, vec!["Use axum"]);
        assert_eq!(profile.rounds_analyzed, 1);
    }

    #[test]
    fn test_parse_profile_response_merge() {
        let content = r#"{"preferences": ["verbose"], "terminology": [], "interests": ["Python"], "decisions": []}"#;
        let existing = UserProfile {
            user_id: "u1".into(),
            preferences: vec!["concise".into()],
            interests: vec!["Rust".into()],
            rounds_analyzed: 2,
            ..Default::default()
        };
        let profile = parse_profile_response("u1", content, &existing);
        // "verbose" is new, "concise" is merged from existing
        assert!(profile.preferences.contains(&"verbose".to_string()));
        assert!(profile.preferences.contains(&"concise".to_string()));
        // "Python" is new, "Rust" is merged from existing
        assert!(profile.interests.contains(&"Python".to_string()));
        assert!(profile.interests.contains(&"Rust".to_string()));
        assert_eq!(profile.rounds_analyzed, 3);
    }
}
