use crate::store::SqliteStore;
use rmcp::{
    RoleServer,
    handler::server::wrapper::Parameters,
    model::{
        CallToolResult, ErrorData, GetPromptRequestParams, GetPromptResult, ListPromptsResult,
        PaginatedRequestParams, PromptMessage, PromptMessageRole,
    },
    prompt, prompt_handler, prompt_router, schemars,
    service::RequestContext,
    tool, tool_handler, tool_router,
};
use serde::Deserialize;
use serde_json::json;
use std::sync::Arc;

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct MemoryStoreInput {
    /// Short descriptive title for this memory
    pub title: String,
    /// Full content to store
    pub content: String,
    /// Tags for search and auto-linking
    #[serde(default)]
    pub tags: Vec<String>,
    /// Optional token count hint
    #[serde(default)]
    pub token_count: Option<i64>,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct MemoryRecallInput {
    /// Memory ID (mem_xxx) — use this or title, not both
    #[serde(default)]
    pub id: Option<String>,
    /// Exact title — use this or id, not both
    #[serde(default)]
    pub title: Option<String>,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct MemorySearchInput {
    /// Keywords to search memory titles and content
    pub query: String,
    /// Max results (default 5, capped at 10)
    #[serde(default)]
    pub limit: Option<i64>,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct MemoryUpdateInput {
    /// ID of the memory to update (mem_xxx)
    pub id: String,
    /// New content (omit to keep current)
    #[serde(default)]
    pub content: Option<String>,
    /// Replace all tags with these (omit to keep current)
    #[serde(default)]
    pub tags: Option<Vec<String>>,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct MemoryDeleteInput {
    /// ID of the memory to delete (mem_xxx)
    pub id: String,
    /// Must be true. Confirm with the user before deleting — deletion is permanent.
    pub confirm: bool,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct MemorySearchPromptInput {
    /// Keywords to search for in memory titles and content
    pub query: String,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct MemoryIdInput {
    /// Memory ID to target (e.g. mem_abc123)
    pub id: String,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct MemoryFlagInput {
    /// Memory ID to flag
    pub id: String,
    /// Reason for flagging: "incorrect", "outdated", "duplicate", "other"
    #[serde(default = "default_flag_reason")]
    pub reason: String,
    /// Optional note explaining the issue
    #[serde(default)]
    pub note: Option<String>,
}

fn default_flag_reason() -> String {
    "other".to_string()
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct ConflictIdInput {
    /// Conflict ID to merge (conflict_xxx)
    pub id: String,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct SessionStartInput {
    /// Absolute path to the project root where .hivemind.toml lives.
    pub project_path: String,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct MemoryStoreEdgeInput {
    /// Source memory ID (mem_xxx)
    pub source_id: String,
    /// Target memory ID (mem_xxx)
    pub target_id: String,
    /// Relationship type: "shares_tag" | "applies_to" | "pairs_with" | "used_in" | "related_to" | "custom"
    pub relationship: String,
}

#[derive(Clone)]
pub struct HiveMind {
    store: Arc<SqliteStore>,
    sync_trigger: Option<Arc<tokio::sync::Notify>>,
}

impl HiveMind {
    #[cfg(test)]
    pub fn new(store: SqliteStore) -> Self {
        Self {
            store: Arc::new(store),
            sync_trigger: None,
        }
    }

    pub fn with_store(store: Arc<SqliteStore>) -> Self {
        Self {
            store,
            sync_trigger: None,
        }
    }

    pub fn with_sync(store: Arc<SqliteStore>, trigger: Arc<tokio::sync::Notify>) -> Self {
        Self {
            store,
            sync_trigger: Some(trigger),
        }
    }

    pub async fn do_memory_store(&self, p: MemoryStoreInput) -> Result<CallToolResult, ErrorData> {
        let id = format!("mem_{}", uuid::Uuid::new_v4().simple());
        let title = p.title.clone();
        self.store
            .store(&id, &p.title, &p.content, &p.tags, p.token_count)
            .await
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
        if let Some(t) = &self.sync_trigger {
            t.notify_one();
        }
        Ok(CallToolResult::structured(json!({
            "id": id,
            "title": title,
        })))
    }

    pub async fn do_memory_recall(
        &self,
        p: MemoryRecallInput,
    ) -> Result<CallToolResult, ErrorData> {
        let entry = if let Some(ref id) = p.id {
            self.store.recall_by_id(id).await
        } else if let Some(ref title) = p.title {
            self.store.recall_by_title(title).await
        } else {
            return Err(ErrorData::invalid_params(
                "provide either 'id' or 'title'",
                None,
            ));
        }
        .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;

        match entry {
            None => Ok(CallToolResult::structured(json!({ "found": false }))),
            Some(e) => Ok(CallToolResult::structured(json!({
                "found": true,
                "id": e.id,
                "title": e.title,
                "content": e.content,
                "tags": e.tags,
                "created_at": e.created_at,
                "updated_at": e.updated_at,
            }))),
        }
    }

    pub async fn do_memory_search(
        &self,
        p: MemorySearchInput,
    ) -> Result<CallToolResult, ErrorData> {
        let limit = p.limit.unwrap_or(5).clamp(1, 10);
        let trimmed = p.query.trim();
        if trimmed.is_empty() {
            return Ok(CallToolResult::structured(json!({
                "count": 0,
                "results": [],
            })));
        }
        let hits = self
            .store
            .search(trimmed, limit)
            .await
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
        let results: Vec<_> = hits
            .iter()
            .map(|h| {
                let snippet: String = h.content.chars().take(200).collect();
                json!({
                    "id": h.id,
                    "title": h.title,
                    "snippet": snippet,
                    "tags": h.tags,
                })
            })
            .collect();
        Ok(CallToolResult::structured(json!({
            "count": results.len(),
            "results": results,
        })))
    }

    pub async fn do_memory_update(
        &self,
        p: MemoryUpdateInput,
    ) -> Result<CallToolResult, ErrorData> {
        // Fetch current state to fill in unchanged fields
        let current = self
            .store
            .recall_by_id(&p.id)
            .await
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
        let current = match current {
            None => {
                return Ok(CallToolResult::structured(json!({
                    "updated": false,
                    "id": p.id,
                })));
            }
            Some(c) => c,
        };
        let content = p.content.as_deref().unwrap_or(&current.content);
        let tags = p.tags.as_deref().unwrap_or(&current.tags);
        let updated = self
            .store
            .update(&p.id, content, tags)
            .await
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
        Ok(CallToolResult::structured(json!({
            "updated": updated,
            "id": p.id,
        })))
    }

    pub async fn do_memory_delete(
        &self,
        p: MemoryDeleteInput,
    ) -> Result<CallToolResult, ErrorData> {
        if !p.confirm {
            return Err(ErrorData::invalid_params(
                "Deletion is permanent and requires confirm: true. Confirm with the user first.",
                None,
            ));
        }
        let deleted = self
            .store
            .delete(&p.id)
            .await
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
        Ok(CallToolResult::structured(json!({
            "deleted": deleted,
            "id": p.id,
        })))
    }

    async fn do_memory_list_prompt(&self) -> Result<Vec<PromptMessage>, ErrorData> {
        let memories = self
            .store
            .list_memories(50, 0)
            .await
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
        let count = memories.len();
        let body = if memories.is_empty() {
            "No memories stored yet. Use memory_store to add some.".to_string()
        } else {
            let lines: Vec<String> = memories
                .iter()
                .map(|m| {
                    let tags = if m.tags.is_empty() {
                        String::new()
                    } else {
                        format!(" [{}]", m.tags.join(", "))
                    };
                    format!("• {} — {}{}", m.id, m.title, tags)
                })
                .collect();
            format!(
                "HiveMind Memory List ({count} memories):\n\n{}",
                lines.join("\n")
            )
        };
        Ok(vec![PromptMessage::new_text(PromptMessageRole::User, body)])
    }

    async fn do_memory_status_prompt(&self) -> Result<Vec<PromptMessage>, ErrorData> {
        let count = self
            .store
            .count()
            .await
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
        let recent = self
            .store
            .list_memories(5, 0)
            .await
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;

        let recent_lines: Vec<String> = recent
            .iter()
            .map(|m| format!("  \u{2022} {} \u{2014} {}", m.id, m.title))
            .collect();

        let mut parts = vec![
            "HiveMind Status".to_string(),
            "\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}".to_string(),
            format!("Total memories: {count}"),
        ];
        if !recent_lines.is_empty() {
            parts.push("\nRecent memories:".to_string());
            parts.extend(recent_lines);
        }
        parts.push("\nTip: Use /memory-list to browse all memories, or /memory-search <query> to find specific ones.".to_string());

        Ok(vec![PromptMessage::new_text(
            PromptMessageRole::User,
            parts.join("\n"),
        )])
    }

    async fn do_memory_search_prompt(
        &self,
        p: MemorySearchPromptInput,
    ) -> Result<Vec<PromptMessage>, ErrorData> {
        let trimmed = p.query.trim().to_string();
        let hits = if trimmed.is_empty() {
            vec![]
        } else {
            self.store
                .search(&trimmed, 10)
                .await
                .map_err(|e| ErrorData::internal_error(e.to_string(), None))?
        };
        let body = if hits.is_empty() {
            format!(
                "No memories found matching \"{}\".\n\nTip: Try broader keywords or use /memory-list to browse all memories.",
                p.query
            )
        } else {
            let lines: Vec<String> = hits
                .iter()
                .map(|h| {
                    let snippet: String = h.content.chars().take(200).collect();
                    format!("\u{2022} {} \u{2014} {}\n  {}", h.id, h.title, snippet)
                })
                .collect();
            format!(
                "Search results for \"{}\" ({} found):\n\n{}\n\nUse memory_recall with an ID for full content.",
                p.query,
                hits.len(),
                lines.join("\n\n")
            )
        };
        Ok(vec![PromptMessage::new_text(PromptMessageRole::User, body)])
    }

    async fn do_memory_edit_prompt(
        &self,
        p: MemoryIdInput,
    ) -> Result<Vec<PromptMessage>, ErrorData> {
        let mem = self
            .store
            .recall_by_id(&p.id)
            .await
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?
            .ok_or_else(|| ErrorData::invalid_params(format!("Memory {} not found", p.id), None))?;
        let tags = mem.tags.join(", ");
        let body = format!(
            "Memory to edit:\n\
             ━━━━━━━━━━━━━━\n\
             ID:      {}\n\
             Title:   {}\n\
             Tags:    {}\n\
             Content:\n{}\n\
             ━━━━━━━━━━━━━━\n\n\
             Ask the user what changes they want to make, then call memory_update with ID {} to save.\n\
             You can update content and/or tags. Omit fields you are not changing.",
            mem.id, mem.title, tags, mem.content, mem.id
        );
        Ok(vec![PromptMessage::new_text(PromptMessageRole::User, body)])
    }

    async fn do_memory_flag_prompt(
        &self,
        p: MemoryFlagInput,
    ) -> Result<Vec<PromptMessage>, ErrorData> {
        let mem = self
            .store
            .recall_by_id(&p.id)
            .await
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?
            .ok_or_else(|| ErrorData::invalid_params(format!("Memory {} not found", p.id), None))?;
        self.store
            .create_feedback(&p.id, &p.reason, p.note.as_deref())
            .await
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
        let body = format!(
            "Flagged memory \"{}\" ({}) as \"{}\".\n\
             A feedback record has been created and will appear in the dashboard under Feedback.\n\
             The memory has not been deleted — it remains available until a human reviews the flag.\n\
             {}",
            mem.title,
            mem.id,
            p.reason,
            p.note
                .as_ref()
                .map(|n| format!("Note: {n}"))
                .unwrap_or_default()
        );
        Ok(vec![PromptMessage::new_text(PromptMessageRole::User, body)])
    }

    async fn do_suggest_connections_prompt(&self) -> Result<Vec<PromptMessage>, ErrorData> {
        let memories = self
            .store
            .list_memories(100, 0)
            .await
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
        let edges = self
            .store
            .list_edges(None)
            .await
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;

        if memories.is_empty() {
            return Ok(vec![PromptMessage::new_text(
                PromptMessageRole::User,
                "No memories stored yet. Add some memories first with memory_store.".to_string(),
            )]);
        }

        let mem_lines: Vec<String> = memories
            .iter()
            .map(|m| {
                let tags = if m.tags.is_empty() {
                    String::new()
                } else {
                    format!(" [{}]", m.tags.join(", "))
                };
                let snippet: String = m.content.chars().take(80).collect();
                let ellipsis = if m.content.len() > 80 { "…" } else { "" };
                format!("{} | {} | {}{}", m.id, m.title, snippet, ellipsis) + &tags
            })
            .collect();

        let edge_lines: Vec<String> = edges
            .iter()
            .map(|e| {
                format!(
                    "{} --[{}]--> {} ({})",
                    e.source_id, e.relationship, e.target_id, e.status
                )
            })
            .collect();

        let edge_section = if edge_lines.is_empty() {
            "  (none yet)".to_string()
        } else {
            edge_lines.join("\n")
        };

        let body = format!(
            "HiveMind — Suggest Connections\n\
             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\
             You have {} memories and {} existing connections.\n\n\
             MEMORIES:\n\
             {}\n\n\
             EXISTING CONNECTIONS:\n\
             {}\n\n\
             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\
             Analyze the memories above and identify meaningful connections not yet captured.\n\
             For each suggested connection, call POST /api/v1/edges with a JSON body:\n\
               {{ \"source_id\": \"<id>\", \"target_id\": \"<id>\", \"relationship\": \"<type>\" }}\n\
             Relationship types: shares_tag | applies_to | pairs_with | used_in | related_to | custom\n\
             Suggest 3–7 connections. Focus on cross-domain insights.",
            memories.len(),
            edges.len(),
            mem_lines.join("\n"),
            edge_section,
        );
        Ok(vec![PromptMessage::new_text(PromptMessageRole::User, body)])
    }

    async fn do_review_feedback_prompt(&self) -> Result<Vec<PromptMessage>, ErrorData> {
        let items = self
            .store
            .list_feedback(None)
            .await
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;

        let open_items: Vec<_> = items.iter().filter(|i| i.status == "pending").collect();

        if open_items.is_empty() {
            return Ok(vec![PromptMessage::new_text(
                PromptMessageRole::User,
                "No open feedback items. All flagged memories have been reviewed.\n\
                 Tip: Use /memory-flag <id> to flag a memory that needs attention."
                    .to_string(),
            )]);
        }

        let mut lines = vec![
            format!("Open Feedback Items ({} total)", open_items.len()),
            "\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}".to_string(),
        ];

        for (i, item) in open_items.iter().enumerate() {
            let note = item.note.as_deref().unwrap_or("(no note)");
            lines.push(format!(
                "\n{}. [{}] Memory: {} | {}\n   Note: {}",
                i + 1,
                item.signal,
                item.memory_id,
                item.id,
                note
            ));
        }

        lines.push("\n\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}\u{2501}".to_string());
        lines.push("For each item, choose an action:".to_string());
        lines.push(
            "  \u{2022} Fix the memory \u{2014} call memory_update with the memory ID".to_string(),
        );
        lines.push(
            "  \u{2022} Delete if truly wrong \u{2014} call memory_delete with confirm:true"
                .to_string(),
        );
        lines.push("\nAsk the user how to handle each item before taking action.".to_string());

        Ok(vec![PromptMessage::new_text(
            PromptMessageRole::User,
            lines.join("\n"),
        )])
    }

    pub async fn do_session_start(
        &self,
        p: SessionStartInput,
    ) -> Result<CallToolResult, ErrorData> {
        let canon = std::fs::canonicalize(&p.project_path).map_err(|_| {
            ErrorData::invalid_params(
                format!("project_path does not exist: {}", p.project_path),
                None,
            )
        })?;
        if !canon.is_dir() {
            return Err(ErrorData::invalid_params(
                "project_path is not a directory",
                None,
            ));
        }

        let config = crate::config::load_config(&canon)
            .map_err(|e| ErrorData::invalid_params(e.to_string(), None))?;

        let result = crate::session::execute_session_start(&config, &self.store)
            .await
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;

        let context_loaded: Vec<_> = result
            .loaded
            .iter()
            .map(|l| {
                json!({
                    "id": l.entry.id,
                    "title": l.entry.title,
                    "content": l.entry.content,
                    "tags": l.entry.tags,
                })
            })
            .collect();

        let skipped: Vec<_> = result
            .skipped
            .iter()
            .map(|s| {
                json!({
                    "query": s.query,
                    "reason": s.reason.as_str(),
                })
            })
            .collect();

        Ok(CallToolResult::structured(json!({
            "project": result.project,
            "context_loaded": context_loaded,
            "budget": {
                "used_tokens": result.used_tokens,
                "max_tokens": result.max_tokens,
                "remaining": result.remaining(),
                "truncated": result.truncated(),
            },
            "skipped": skipped,
            "hint": "Session context loaded. Incorporate it silently and proceed with the user's request.",
        })))
    }
}

#[tool_router]
impl HiveMind {
    #[tool(
        description = "Store a memory, preference, or project context for future recall across sessions. Use when the user explicitly asks to remember something, or when important context should persist beyond this session."
    )]
    async fn memory_store(
        &self,
        Parameters(p): Parameters<MemoryStoreInput>,
    ) -> Result<CallToolResult, ErrorData> {
        self.do_memory_store(p).await
    }

    #[tool(
        description = "Recall a memory by exact title or ID. Returns full content. Use memory_search to find candidates first."
    )]
    async fn memory_recall(
        &self,
        Parameters(p): Parameters<MemoryRecallInput>,
    ) -> Result<CallToolResult, ErrorData> {
        self.do_memory_recall(p).await
    }

    #[tool(
        description = "Search stored memories by keyword (FTS). Returns ranked snippets to conserve context — use memory_recall with an id for full content. Default 5 results, max 10."
    )]
    async fn memory_search(
        &self,
        Parameters(p): Parameters<MemorySearchInput>,
    ) -> Result<CallToolResult, ErrorData> {
        self.do_memory_search(p).await
    }

    #[tool(
        description = "Update an existing memory's content or tags by id. Providing tags replaces all tags on the memory."
    )]
    async fn memory_update(
        &self,
        Parameters(p): Parameters<MemoryUpdateInput>,
    ) -> Result<CallToolResult, ErrorData> {
        self.do_memory_update(p).await
    }

    #[tool(
        description = "Permanently delete a memory by id. Requires confirm=true; always confirm with the user before calling."
    )]
    async fn memory_delete(
        &self,
        Parameters(p): Parameters<MemoryDeleteInput>,
    ) -> Result<CallToolResult, ErrorData> {
        self.do_memory_delete(p).await
    }

    #[tool(
        description = "Call this once at the start of every session when .hivemind.toml exists in the project root. Returns pre-configured memory context for this project."
    )]
    async fn hivemind_session_start(
        &self,
        Parameters(p): Parameters<SessionStartInput>,
    ) -> Result<CallToolResult, ErrorData> {
        self.do_session_start(p).await
    }

    #[tool(
        description = "Store a confirmed connection between two memories. Use after the user or Claude explicitly decides two memories are related. Valid relationships: shares_tag, applies_to, pairs_with, used_in, related_to, custom."
    )]
    async fn memory_store_edge(
        &self,
        Parameters(p): Parameters<MemoryStoreEdgeInput>,
    ) -> Result<CallToolResult, ErrorData> {
        self.store
            .create_edge(&p.source_id, &p.target_id, &p.relationship)
            .await
            .map(|edge| {
                CallToolResult::success(vec![rmcp::model::Content::text(format!(
                    "Edge created: {} --[{}]--> {}",
                    edge.source_id, edge.relationship, edge.target_id
                ))])
            })
            .map_err(|e| ErrorData::internal_error(e.to_string(), None))
    }
}

#[prompt_router]
impl HiveMind {
    /// List all memories with titles and tags
    #[prompt(
        name = "memory-list",
        description = "List all stored memories with titles and tags. Use to browse what HiveMind knows before searching or editing."
    )]
    async fn memory_list_prompt(&self) -> Result<Vec<PromptMessage>, ErrorData> {
        self.do_memory_list_prompt().await
    }

    /// Show the current memory count and recent activity
    #[prompt(
        name = "memory-status",
        description = "Show total memory count and recent memories. Use at the start of a session to understand what context is available."
    )]
    async fn memory_status_prompt(&self) -> Result<Vec<PromptMessage>, ErrorData> {
        self.do_memory_status_prompt().await
    }

    /// Search memories by keyword and present results
    #[prompt(
        name = "memory-search",
        description = "Search HiveMind memories by keyword. Returns matching memories with content snippets. Follow up with memory_recall for full content."
    )]
    async fn memory_search_prompt(
        &self,
        Parameters(p): Parameters<MemorySearchPromptInput>,
    ) -> Result<Vec<PromptMessage>, ErrorData> {
        self.do_memory_search_prompt(p).await
    }

    /// Fetch a memory and return a prompt for editing its content or tags
    #[prompt(
        name = "memory-edit",
        description = "Fetch a specific memory by ID and present its current content for editing. After reviewing, call memory_update to save changes."
    )]
    async fn memory_edit_prompt(
        &self,
        Parameters(p): Parameters<MemoryIdInput>,
    ) -> Result<Vec<PromptMessage>, ErrorData> {
        self.do_memory_edit_prompt(p).await
    }

    /// Flag a memory as incorrect, outdated, or duplicate
    #[prompt(
        name = "memory-flag",
        description = "Flag a memory for review. Creates a feedback record with the specified reason."
    )]
    async fn memory_flag_prompt(
        &self,
        Parameters(p): Parameters<MemoryFlagInput>,
    ) -> Result<Vec<PromptMessage>, ErrorData> {
        self.do_memory_flag_prompt(p).await
    }

    /// Analyze the memory graph and suggest new connections between related memories
    #[prompt(
        name = "suggest-connections",
        description = "Fetch all memories and existing connections, then analyze them to suggest new edges."
    )]
    async fn suggest_connections_prompt(&self) -> Result<Vec<PromptMessage>, ErrorData> {
        self.do_suggest_connections_prompt().await
    }

    /// Surface all open feedback items for interactive review and resolution
    #[prompt(
        name = "review-feedback",
        description = "Fetch open feedback items (flagged memories) and present them for interactive resolution."
    )]
    async fn review_feedback_prompt(&self) -> Result<Vec<PromptMessage>, ErrorData> {
        self.do_review_feedback_prompt().await
    }
}

#[tool_handler]
#[prompt_handler]
impl rmcp::ServerHandler for HiveMind {
    fn get_info(&self) -> rmcp::model::ServerInfo {
        rmcp::model::ServerInfo::new(
            rmcp::model::ServerCapabilities::builder()
                .enable_tools()
                .enable_prompts()
                .build(),
        )
        .with_server_info(rmcp::model::Implementation::new(
            "hivemind",
            env!("CARGO_PKG_VERSION"),
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{config::SyncSettings, db, store::SqliteStore};
    use tempfile::TempDir;

    async fn test_hivemind() -> (HiveMind, TempDir) {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("test.db");
        let sync = SyncSettings::default();
        let database = db::open_database(&sync, path.to_str().unwrap())
            .await
            .unwrap();
        let conn = database.connect().unwrap();
        db::run_migrations(&conn).await.unwrap();
        (HiveMind::new(SqliteStore::new(conn)), dir)
    }

    #[tokio::test]
    async fn get_info_advertises_name_and_tools_capability() {
        use rmcp::ServerHandler;
        let (hm, _dir) = test_hivemind().await;
        let info = hm.get_info();
        assert_eq!(info.server_info.name, "hivemind");
        assert!(
            info.capabilities.tools.is_some(),
            "tools capability must be advertised"
        );
    }

    #[tokio::test]
    async fn get_info_advertises_prompts_capability() {
        use rmcp::ServerHandler;
        let (hm, _dir) = test_hivemind().await;
        let info = hm.get_info();
        assert!(
            info.capabilities.prompts.is_some(),
            "prompts capability must be advertised"
        );
    }

    #[test]
    fn list_prompts_returns_memory_list() {
        let prompts = HiveMind::prompt_router().list_all();
        let names: Vec<&str> = prompts.iter().map(|p| p.name.as_str()).collect();
        assert!(
            names.contains(&"memory-list"),
            "memory-list prompt must be listed"
        );
    }

    #[tokio::test]
    async fn memory_store_tool_returns_mem_id() {
        let (hm, _dir) = test_hivemind().await;
        let result = hm
            .do_memory_store(MemoryStoreInput {
                title: "my preference".to_string(),
                content: "prefer tabs over spaces".to_string(),
                tags: vec!["style".to_string()],
                token_count: None,
            })
            .await
            .unwrap();
        let val = result.structured_content.unwrap();
        assert!(val["id"].as_str().unwrap().starts_with("mem_"));
    }

    #[tokio::test]
    async fn memory_recall_by_id_returns_content() {
        let (hm, _dir) = test_hivemind().await;
        let stored = hm
            .do_memory_store(MemoryStoreInput {
                title: "rust style".to_string(),
                content: "use clippy, rustfmt, and deny warnings".to_string(),
                tags: vec!["rust".to_string()],
                token_count: None,
            })
            .await
            .unwrap();
        let id = stored.structured_content.unwrap()["id"]
            .as_str()
            .unwrap()
            .to_string();

        let result = hm
            .do_memory_recall(MemoryRecallInput {
                id: Some(id),
                title: None,
            })
            .await
            .unwrap();
        let val = result.structured_content.unwrap();
        assert_eq!(val["found"], true);
        assert_eq!(val["title"], "rust style");
        assert!(val["content"].as_str().unwrap().contains("clippy"));
    }

    #[tokio::test]
    async fn memory_recall_by_title_returns_content() {
        let (hm, _dir) = test_hivemind().await;
        hm.do_memory_store(MemoryStoreInput {
            title: "clean arch".to_string(),
            content: "domain at center, infra at edge".to_string(),
            tags: vec!["architecture".to_string()],
            token_count: None,
        })
        .await
        .unwrap();

        let result = hm
            .do_memory_recall(MemoryRecallInput {
                id: None,
                title: Some("clean arch".to_string()),
            })
            .await
            .unwrap();
        let val = result.structured_content.unwrap();
        assert_eq!(val["found"], true);
        assert_eq!(val["content"], "domain at center, infra at edge");
    }

    #[tokio::test]
    async fn memory_recall_returns_not_found_for_missing_id() {
        let (hm, _dir) = test_hivemind().await;
        let result = hm
            .do_memory_recall(MemoryRecallInput {
                id: Some("mem_doesnotexist".to_string()),
                title: None,
            })
            .await
            .unwrap();
        assert_eq!(result.structured_content.unwrap()["found"], false);
    }

    #[tokio::test]
    async fn memory_recall_errors_without_id_or_title() {
        let (hm, _dir) = test_hivemind().await;
        let err = hm
            .do_memory_recall(MemoryRecallInput {
                id: None,
                title: None,
            })
            .await;
        assert!(err.is_err());
    }

    #[tokio::test]
    async fn memory_search_returns_snippets() {
        let (hm, _dir) = test_hivemind().await;
        hm.do_memory_store(MemoryStoreInput {
            title: "db driver choice".to_string(),
            content: "we standardized on pgx v5 for postgres".to_string(),
            tags: vec!["golang".to_string(), "database".to_string()],
            token_count: None,
        })
        .await
        .unwrap();

        let result = hm
            .do_memory_search(MemorySearchInput {
                query: "pgx".to_string(),
                limit: None,
            })
            .await
            .unwrap();
        let val = result.structured_content.unwrap();
        assert_eq!(val["count"], 1);
        assert_eq!(val["results"][0]["title"], "db driver choice");
        assert!(
            val["results"][0]["snippet"]
                .as_str()
                .unwrap()
                .to_lowercase()
                .contains("pgx")
        );
        assert!(
            val["results"][0].get("content").is_none(),
            "search returns snippets, not full content"
        );
    }

    #[tokio::test]
    async fn memory_search_empty_query_returns_zero() {
        let (hm, _dir) = test_hivemind().await;
        let result = hm
            .do_memory_search(MemorySearchInput {
                query: "  ".to_string(),
                limit: None,
            })
            .await
            .unwrap();
        assert_eq!(result.structured_content.unwrap()["count"], 0);
    }

    #[tokio::test]
    async fn memory_update_changes_content() {
        let (hm, _dir) = test_hivemind().await;
        let stored = hm
            .do_memory_store(MemoryStoreInput {
                title: "deploy notes".to_string(),
                content: "uses docker swarm".to_string(),
                tags: vec!["devops".to_string()],
                token_count: None,
            })
            .await
            .unwrap();
        let id = stored.structured_content.unwrap()["id"]
            .as_str()
            .unwrap()
            .to_string();

        let result = hm
            .do_memory_update(MemoryUpdateInput {
                id: id.clone(),
                content: Some("migrated to kubernetes".to_string()),
                tags: None,
            })
            .await
            .unwrap();
        assert_eq!(result.structured_content.unwrap()["updated"], true);

        let recalled = hm
            .do_memory_recall(MemoryRecallInput {
                id: Some(id),
                title: None,
            })
            .await
            .unwrap();
        assert_eq!(
            recalled.structured_content.unwrap()["content"],
            "migrated to kubernetes"
        );
    }

    #[tokio::test]
    async fn memory_update_returns_updated_false_for_missing() {
        let (hm, _dir) = test_hivemind().await;
        let result = hm
            .do_memory_update(MemoryUpdateInput {
                id: "mem_nope".to_string(),
                content: None,
                tags: None,
            })
            .await
            .unwrap();
        assert_eq!(result.structured_content.unwrap()["updated"], false);
    }

    #[tokio::test]
    async fn session_start_loads_configured_recalls() {
        let (hm, _dir) = test_hivemind().await;
        hm.do_memory_store(MemoryStoreInput {
            title: "golang preferences".to_string(),
            content: "use uber/zap, sqlc, pgx v5".to_string(),
            tags: vec!["golang".to_string()],
            token_count: None,
        })
        .await
        .unwrap();

        let tmp = tempfile::tempdir().unwrap();
        std::fs::write(
            tmp.path().join(".hivemind.toml"),
            "[project]\nname=\"demo\"\n[hooks.on_session_start]\nmax_tokens=2000\nrecalls=[\"golang preferences\"]\n",
        ).unwrap();

        let result = hm
            .do_session_start(SessionStartInput {
                project_path: tmp.path().to_string_lossy().into_owned(),
            })
            .await
            .unwrap();
        let val = result.structured_content.unwrap();
        assert_eq!(val["project"], "demo");
        assert_eq!(val["context_loaded"].as_array().unwrap().len(), 1);
        assert_eq!(val["context_loaded"][0]["title"], "golang preferences");
        assert_eq!(val["budget"]["truncated"], false);
        assert!(val["budget"]["used_tokens"].as_u64().unwrap() > 0);
    }

    #[tokio::test]
    async fn session_start_rejects_nonexistent_path() {
        let (hm, _dir) = test_hivemind().await;
        let err = hm
            .do_session_start(SessionStartInput {
                project_path: "/no/such/dir/anywhere".to_string(),
            })
            .await;
        assert!(err.is_err());
    }

    #[tokio::test]
    async fn session_start_errors_without_config() {
        let (hm, _dir) = test_hivemind().await;
        let tmp = tempfile::tempdir().unwrap();
        let err = hm
            .do_session_start(SessionStartInput {
                project_path: tmp.path().to_string_lossy().into_owned(),
            })
            .await;
        assert!(err.is_err());
    }

    fn prompt_text(msg: &PromptMessage) -> &str {
        match &msg.content {
            rmcp::model::PromptMessageContent::Text { text } => text.as_str(),
            _ => panic!("expected text content"),
        }
    }

    #[tokio::test]
    async fn memory_list_prompt_returns_no_memories_message() {
        let (hm, _dir) = test_hivemind().await;
        let result = hm.do_memory_list_prompt().await.unwrap();
        assert_eq!(result.len(), 1);
        assert!(prompt_text(&result[0]).contains("No memories"));
    }

    #[tokio::test]
    async fn memory_status_prompt_includes_count() {
        let (hm, _dir) = test_hivemind().await;
        hm.do_memory_store(MemoryStoreInput {
            title: "test".to_string(),
            content: "c".to_string(),
            tags: vec![],
            token_count: None,
        })
        .await
        .unwrap();
        let result = hm.do_memory_status_prompt().await.unwrap();
        let text = prompt_text(&result[0]);
        assert!(text.contains("1"), "status should show count of 1");
    }

    #[tokio::test]
    async fn memory_search_prompt_returns_results() {
        let (hm, _dir) = test_hivemind().await;
        hm.do_memory_store(MemoryStoreInput {
            title: "golang preferences".to_string(),
            content: "use uber/zap and chi router".to_string(),
            tags: vec!["golang".to_string()],
            token_count: None,
        })
        .await
        .unwrap();
        let result = hm
            .do_memory_search_prompt(MemorySearchPromptInput {
                query: "uber".to_string(),
            })
            .await
            .unwrap();
        let text = prompt_text(&result[0]);
        assert!(text.contains("uber") || text.contains("golang"));
    }

    #[tokio::test]
    async fn memory_edit_prompt_returns_formatted_content() {
        let (hm, _dir) = test_hivemind().await;
        let stored = hm
            .do_memory_store(MemoryStoreInput {
                title: "rust style".to_string(),
                content: "use clippy and rustfmt".to_string(),
                tags: vec!["rust".to_string()],
                token_count: None,
            })
            .await
            .unwrap();
        let id = stored.structured_content.unwrap()["id"]
            .as_str()
            .unwrap()
            .to_string();

        let result = hm
            .do_memory_edit_prompt(MemoryIdInput { id: id.clone() })
            .await
            .unwrap();
        let text = prompt_text(&result[0]);
        assert!(text.contains("rust style"), "should include memory title");
        assert!(text.contains("clippy"), "should include memory content");
        assert!(text.contains(&id), "should include the ID");
    }

    #[tokio::test]
    async fn memory_edit_prompt_returns_error_for_missing_id() {
        let (hm, _dir) = test_hivemind().await;
        let result = hm
            .do_memory_edit_prompt(MemoryIdInput {
                id: "mem_nonexistent".to_string(),
            })
            .await;
        assert!(result.is_err(), "should error when memory not found");
    }

    #[tokio::test]
    async fn memory_flag_prompt_creates_feedback_record() {
        let (hm, _dir) = test_hivemind().await;
        let stored = hm
            .do_memory_store(MemoryStoreInput {
                title: "test".to_string(),
                content: "c".to_string(),
                tags: vec![],
                token_count: None,
            })
            .await
            .unwrap();
        let id = stored.structured_content.unwrap()["id"]
            .as_str()
            .unwrap()
            .to_string();

        let result = hm
            .do_memory_flag_prompt(MemoryFlagInput {
                id: id.clone(),
                reason: "outdated".to_string(),
                note: None,
            })
            .await
            .unwrap();
        let text = prompt_text(&result[0]);
        assert!(
            text.to_lowercase().contains("flagged"),
            "should confirm the flag"
        );

        let feedback = hm.store.list_feedback(None).await.unwrap();
        assert_eq!(feedback.len(), 1, "feedback record should be created");
    }

    #[tokio::test]
    async fn suggest_connections_prompt_lists_memories_and_edges() {
        let (hm, _dir) = test_hivemind().await;
        hm.do_memory_store(MemoryStoreInput {
            title: "golang preferences".to_string(),
            content: "use uber/zap and chi router".to_string(),
            tags: vec!["golang".to_string()],
            token_count: None,
        })
        .await
        .unwrap();
        hm.do_memory_store(MemoryStoreInput {
            title: "observability stack".to_string(),
            content: "prometheus, grafana, loki".to_string(),
            tags: vec!["observability".to_string()],
            token_count: None,
        })
        .await
        .unwrap();
        let result = hm.do_suggest_connections_prompt().await.unwrap();
        let text = prompt_text(&result[0]);
        assert!(
            text.contains("golang preferences"),
            "should include memory titles"
        );
        assert!(
            text.contains("/api/v1/edges"),
            "should instruct Claude to use the REST API to create edges"
        );
    }

    #[tokio::test]
    async fn review_feedback_prompt_shows_open_items() {
        let (hm, _dir) = test_hivemind().await;
        let stored = hm
            .do_memory_store(MemoryStoreInput {
                title: "old pref".to_string(),
                content: "stale content".to_string(),
                tags: vec![],
                token_count: None,
            })
            .await
            .unwrap();
        let mem_id = stored.structured_content.unwrap()["id"]
            .as_str()
            .unwrap()
            .to_string();
        hm.store
            .create_feedback(&mem_id, "outdated", Some("This is outdated"))
            .await
            .unwrap();

        let result = hm.do_review_feedback_prompt().await.unwrap();
        let text = prompt_text(&result[0]);
        assert!(
            text.contains("outdated") || text.contains("old pref"),
            "should show feedback items"
        );
    }

    #[tokio::test]
    async fn review_feedback_prompt_empty_when_no_open_items() {
        let (hm, _dir) = test_hivemind().await;
        let result = hm.do_review_feedback_prompt().await.unwrap();
        let text = prompt_text(&result[0]);
        assert!(
            text.to_lowercase().contains("no open"),
            "should indicate no items"
        );
    }

    #[test]
    fn all_seven_prompts_are_registered() {
        let prompts = HiveMind::prompt_router().list_all();
        let names: Vec<&str> = prompts.iter().map(|p| p.name.as_str()).collect();
        let expected = [
            "memory-list",
            "memory-status",
            "memory-search",
            "memory-edit",
            "memory-flag",
            "suggest-connections",
            "review-feedback",
        ];
        for name in &expected {
            assert!(
                names.contains(name),
                "prompt {name} must be registered; got: {names:?}"
            );
        }
        assert_eq!(prompts.len(), 7, "exactly 7 prompts expected");
    }

    #[tokio::test]
    async fn memory_delete_requires_confirm() {
        let (hm, _dir) = test_hivemind().await;
        let stored = hm
            .do_memory_store(MemoryStoreInput {
                title: "temp".to_string(),
                content: "delete me".to_string(),
                tags: vec!["tmp".to_string()],
                token_count: None,
            })
            .await
            .unwrap();
        let id = stored.structured_content.unwrap()["id"]
            .as_str()
            .unwrap()
            .to_string();

        let err = hm
            .do_memory_delete(MemoryDeleteInput {
                id: id.clone(),
                confirm: false,
            })
            .await;
        assert!(err.is_err());
        assert!(
            hm.do_memory_recall(MemoryRecallInput {
                id: Some(id.clone()),
                title: None
            })
            .await
            .unwrap()
            .structured_content
            .unwrap()["found"]
                == true
        );

        let ok = hm
            .do_memory_delete(MemoryDeleteInput {
                id: id.clone(),
                confirm: true,
            })
            .await
            .unwrap();
        assert_eq!(ok.structured_content.unwrap()["deleted"], true);
        assert_eq!(
            hm.do_memory_recall(MemoryRecallInput {
                id: Some(id),
                title: None
            })
            .await
            .unwrap()
            .structured_content
            .unwrap()["found"],
            false
        );
    }
}
