//! Built-in Tool: db_vector_search — Phase-83 T83.2
//!
//! Performs vector similarity search on a collection's HNSW index.
//! Supports both direct vector input and text-based search
//! (when the collection has a text field to match against).

use std::sync::Arc;

use async_trait::async_trait;
use serde::{Deserialize, Serialize};

use crate::storage::collection::CollectionStore;
use crate::llm::error::{LlmError, LlmResult};
use crate::llm::tool_executor::{ToolExecutor, ToolMetadata, ToolResult};

/// Tool for performing vector similarity search on collections.
pub struct VectorSearchTool {
    collections: Arc<CollectionStore>,
}

impl VectorSearchTool {
    /// Create a new vector search tool.
    pub fn new(collections: Arc<CollectionStore>) -> Self {
        Self { collections }
    }
}

/// Arguments for the vector search tool.
#[derive(Debug, Deserialize)]
struct VectorSearchArgs {
    /// Collection name to search in.
    collection: String,

    /// Text query to search for (matched against document content).
    #[serde(default)]
    query: Option<String>,

    /// Direct vector for similarity search (alternative to text query).
    #[serde(default)]
    vector: Option<Vec<f32>>,

    /// Number of results to return (default: 5).
    #[serde(default = "default_top_k")]
    top_k: usize,
}

fn default_top_k() -> usize {
    5
}

/// A single search result returned to the LLM.
#[derive(Debug, Serialize)]
struct VectorSearchResult {
    id: String,
    score: f32,
    #[serde(skip_serializing_if = "Option::is_none")]
    content: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    metadata: Option<serde_json::Value>,
}

#[async_trait]
impl ToolExecutor for VectorSearchTool {
    fn metadata(&self) -> ToolMetadata {
        ToolMetadata::new(
            "db_vector_search",
            "Search for similar documents in a collection using vector similarity. \
             You can provide either a text query (matched against document content) \
             or a direct vector. Returns the top-K most similar documents with scores.",
            serde_json::json!({
                "type": "object",
                "properties": {
                    "collection": {
                        "type": "string",
                        "description": "Name of the collection to search in"
                    },
                    "query": {
                        "type": "string",
                        "description": "Text query to search for (uses text-based matching)"
                    },
                    "vector": {
                        "type": "array",
                        "items": { "type": "number" },
                        "description": "Direct vector for KNN similarity search (alternative to query)"
                    },
                    "top_k": {
                        "type": "integer",
                        "description": "Number of results to return (default: 5)",
                        "default": 5,
                        "minimum": 1,
                        "maximum": 100
                    }
                },
                "required": ["collection"]
            }),
        )
        .with_category("search")
    }

    async fn execute(&self, arguments: &str) -> LlmResult<ToolResult> {
        let args: VectorSearchArgs = serde_json::from_str(arguments)
            .map_err(|e| LlmError::ResponseParseError(format!("Invalid arguments: {}", e)))?;

        // Validate: need at least query or vector
        if args.query.is_none() && args.vector.is_none() {
            return Ok(ToolResult::err(
                "Either 'query' (text) or 'vector' (float array) must be provided",
            ));
        }

        // Get the collection
        let collection = self
            .collections
            .get_collection(&args.collection)
            .map_err(|e| LlmError::Internal(format!("Failed to access collection: {}", e)))?
            .ok_or_else(|| {
                LlmError::NotFound(format!("Collection '{}' not found", args.collection))
            })?;

        let top_k = args.top_k.min(100).max(1);

        // Perform vector search if a vector is provided
        if let Some(ref vector) = args.vector {
            let search_results = collection.vector_engine.search(vector, top_k)
                .map_err(|e| LlmError::Internal(format!("Vector search failed: {}", e)))?;

            let results: Vec<VectorSearchResult> = search_results
                .into_iter()
                .map(|r| {
                    // Try to get the document content
                    let doc = collection.docs.get(&r.id).ok().flatten();
                    VectorSearchResult {
                        id: r.id,
                        score: r.distance,
                        content: doc.as_ref().map(|d| truncate_content(&d.content, 500)),
                        metadata: doc.and_then(|d| {
                            if d.metadata.is_empty() {
                                None
                            } else {
                                Some(serde_json::json!(d.metadata))
                            }
                        }),
                    }
                })
                .collect();

            return Ok(ToolResult::ok_json(&serde_json::json!({
                "collection": args.collection,
                "result_count": results.len(),
                "results": results
            })));
        }

        // Text-based search: search documents by content matching
        if let Some(ref query_text) = args.query {
            // Use the collection's document store to find matching documents
            let all_docs = collection.docs.all()
                .map_err(|e| LlmError::Internal(format!("Failed to read documents: {}", e)))?;

            let query_lower = query_text.to_lowercase();
            let mut scored: Vec<VectorSearchResult> = all_docs
                .into_iter()
                .filter_map(|doc| {
                    let content_lower = doc.content.to_lowercase();
                    if content_lower.contains(&query_lower) {
                        // Simple relevance score based on occurrence density
                        let count = content_lower.matches(&query_lower).count();
                        let score = count as f32 / doc.content.len().max(1) as f32 * 1000.0;
                        Some(VectorSearchResult {
                            id: doc.id.clone(),
                            score,
                            content: Some(truncate_content(&doc.content, 500)),
                            metadata: if doc.metadata.is_empty() {
                                None
                            } else {
                                Some(serde_json::json!(doc.metadata))
                            },
                        })
                    } else {
                        None
                    }
                })
                .collect();

            // Sort by score descending
            scored.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
            scored.truncate(top_k);

            return Ok(ToolResult::ok_json(&serde_json::json!({
                "collection": args.collection,
                "result_count": scored.len(),
                "search_type": "text_match",
                "results": scored
            })));
        }

        Ok(ToolResult::err("No search criteria provided"))
    }
}

/// Truncate content to a maximum number of characters.
fn truncate_content(content: &str, max_chars: usize) -> String {
    if content.len() <= max_chars {
        content.to_string()
    } else {
        let mut end = max_chars;
        // Don't split in the middle of a multi-byte char
        while !content.is_char_boundary(end) && end > 0 {
            end -= 1;
        }
        format!("{}...", &content[..end])
    }
}

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

    #[test]
    fn test_truncate_content() {
        assert_eq!(truncate_content("hello", 10), "hello");
        assert_eq!(truncate_content("hello world foo bar", 11), "hello world...");
    }

    #[test]
    fn test_vector_search_metadata() {
        let tool = VectorSearchTool::new(Arc::new(
            CollectionStore::open_or_create(std::env::temp_dir().join("test_vs_meta")).unwrap(),
        ));
        let meta = tool.metadata();
        assert_eq!(meta.name, "db_vector_search");
        assert_eq!(meta.category, "search");
        assert!(meta.is_read_only);

        let def = meta.to_tool_definition();
        assert_eq!(def.function.name, "db_vector_search");
        assert!(def.function.description.unwrap().contains("vector similarity"));
    }
}
