//! Built-in Tool: db_fts_search — Phase-83 T83.5
//!
//! Performs full-text search on a collection's document store using Tantivy BM25.

use std::sync::Arc;

use async_trait::async_trait;
use serde::Deserialize;

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

/// Tool for full-text search on collections via Tantivy/BM25.
pub struct FtsSearchTool {
    collections: Arc<CollectionStore>,
}

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

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

    /// Search query text.
    query: String,

    /// Maximum number of results (default: 10).
    #[serde(default = "default_limit")]
    limit: usize,
}

fn default_limit() -> usize {
    10
}

#[async_trait]
impl ToolExecutor for FtsSearchTool {
    fn metadata(&self) -> ToolMetadata {
        ToolMetadata::new(
            "db_fts_search",
            "Perform full-text search on a collection's documents using BM25 ranking. \
             Returns documents matching the query text, ranked by relevance score. \
             Supports keyword, phrase, and boolean queries.",
            serde_json::json!({
                "type": "object",
                "properties": {
                    "collection": {
                        "type": "string",
                        "description": "Name of the collection to search in"
                    },
                    "query": {
                        "type": "string",
                        "description": "Full-text search query"
                    },
                    "limit": {
                        "type": "integer",
                        "description": "Maximum number of results (default: 10)",
                        "default": 10,
                        "minimum": 1,
                        "maximum": 100
                    }
                },
                "required": ["collection", "query"]
            }),
        )
        .with_category("search")
    }

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

        if args.query.trim().is_empty() {
            return Ok(ToolResult::err("Search query cannot be empty"));
        }

        // 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 limit = args.limit.min(100).max(1);

        // Try full-text search via the storage backend
        if collection.docs.supports_fts() {
            match collection.docs.full_text_search(&args.query, limit) {
                Ok(results) => {
                    let items: Vec<serde_json::Value> = results
                        .into_iter()
                        .map(|(id, score)| {
                            // Try to fetch document content
                            let content = collection
                                .docs
                                .get(&id)
                                .ok()
                                .flatten()
                                .map(|d| d.content.clone());

                            serde_json::json!({
                                "id": id,
                                "score": score,
                                "content": content.map(|c| truncate(&c, 500)),
                            })
                        })
                        .collect();

                    return Ok(ToolResult::ok_json(&serde_json::json!({
                        "collection": args.collection,
                        "query": args.query,
                        "result_count": items.len(),
                        "search_engine": "tantivy_bm25",
                        "results": items,
                    })));
                }
                Err(e) => {
                    return Ok(ToolResult::err(format!("Full-text search failed: {}", e)));
                }
            }
        }

        // Fallback: simple text matching on all documents
        let all_docs = collection
            .docs
            .all()
            .map_err(|e| LlmError::Internal(format!("Failed to read documents: {}", e)))?;

        let query_lower = args.query.to_lowercase();
        let mut matched: Vec<serde_json::Value> = all_docs
            .into_iter()
            .filter_map(|doc| {
                let lower = doc.content.to_lowercase();
                if lower.contains(&query_lower) {
                    let count = lower.matches(&query_lower).count();
                    let score = count as f64 / doc.content.len().max(1) as f64 * 1000.0;
                    Some(serde_json::json!({
                        "id": doc.id,
                        "score": score,
                        "content": truncate(&doc.content, 500),
                    }))
                } else {
                    None
                }
            })
            .collect();

        // Sort by score descending
        matched.sort_by(|a, b| {
            b["score"]
                .as_f64()
                .unwrap_or(0.0)
                .partial_cmp(&a["score"].as_f64().unwrap_or(0.0))
                .unwrap_or(std::cmp::Ordering::Equal)
        });
        matched.truncate(limit);

        Ok(ToolResult::ok_json(&serde_json::json!({
            "collection": args.collection,
            "query": args.query,
            "result_count": matched.len(),
            "search_engine": "fallback_text_match",
            "results": matched,
        })))
    }
}

fn truncate(s: &str, max: usize) -> String {
    if s.len() <= max {
        s.to_string()
    } else {
        let mut end = max;
        while !s.is_char_boundary(end) && end > 0 {
            end -= 1;
        }
        format!("{}...", &s[..end])
    }
}

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

    #[test]
    fn test_fts_search_metadata() {
        let store = Arc::new(
            CollectionStore::open_or_create(std::env::temp_dir().join("test_fts_meta")).unwrap(),
        );
        let tool = FtsSearchTool::new(store);
        let meta = tool.metadata();
        assert_eq!(meta.name, "db_fts_search");
        assert_eq!(meta.category, "search");
        assert!(meta.is_read_only);
    }
}
