//! Built-in Tool: db_tree_search — Phase-91 T91.3
//!
//! Provides tree-indexed document search for LLM Function Calling.
//!
//! Features:
//! - Search across tree-indexed documents by text matching
//! - Return hierarchical context (path breadcrumbs, summaries)
//! - List all indexed documents and their structure
//! - Integration with DynamicTreeManager for access tracking
//! - Shadow node filtering (Phase-95)

use std::sync::Arc;

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

use crate::tree_index::store::TreeIndexStore;
use crate::tree_index::dynamic::DynamicTreeManager;
use crate::tree_index::types::{
    TreeNode, tree_entity_types, tree_property_keys,
};
use crate::rag::tree_retriever::TreeRetriever;

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

/// Tool for searching tree-indexed documents.
///
/// Supports two operations via `action`:
/// - `search` (default): Search for nodes matching a query
/// - `list_docs`: List all indexed documents with structure overview
/// - `get_toc`: Get the table-of-contents for a specific document
pub struct TreeSearchTool {
    store: Arc<TreeIndexStore>,
    dynamic_manager: Option<Arc<DynamicTreeManager>>,
}

impl TreeSearchTool {
    /// Create a new tree search tool.
    pub fn new(store: Arc<TreeIndexStore>) -> Self {
        Self {
            store,
            dynamic_manager: None,
        }
    }

    /// Create with a DynamicTreeManager for access tracking.
    pub fn with_dynamic_manager(mut self, manager: Arc<DynamicTreeManager>) -> Self {
        self.dynamic_manager = Some(manager);
        self
    }

    /// Score a node against a query (text matching).
    fn score_node(node: &TreeNode, query_lower: &str) -> f64 {
        let mut score = 0.0;

        // Title match
        let title_lower = node.title.to_lowercase();
        if title_lower.contains(query_lower) {
            score += 3.0;
        } else {
            let query_words: Vec<&str> = query_lower.split_whitespace().collect();
            let matches = query_words
                .iter()
                .filter(|w| w.len() >= 2 && title_lower.contains(**w))
                .count();
            score += matches as f64 * 0.8;
        }

        // Summary match
        if let Some(ref summary) = node.summary {
            let summary_lower = summary.to_lowercase();
            if summary_lower.contains(query_lower) {
                score += 2.0;
            } else {
                let query_words: Vec<&str> = query_lower.split_whitespace().collect();
                let matches = query_words
                    .iter()
                    .filter(|w| w.len() >= 2 && summary_lower.contains(**w))
                    .count();
                score += matches as f64 * 0.5;
            }
        }

        // Content match
        let content_lower = node.content.text.to_lowercase();
        if !content_lower.is_empty() {
            let query_words: Vec<&str> = query_lower.split_whitespace().collect();
            let total_matches: usize = query_words
                .iter()
                .filter(|w| w.len() >= 2)
                .map(|w| content_lower.matches(w).count())
                .sum();
            if total_matches > 0 {
                let density = total_matches as f64 / content_lower.len().max(1) as f64 * 100.0;
                score += density.min(2.0);
            }
        }

        score
    }

    /// Check if a node is a shadow (demoted) node.
    fn is_shadow(node: &TreeNode) -> bool {
        node.properties
            .get("entity_type")
            .and_then(|v| v.as_str())
            .map(|t| t == tree_entity_types::TREE_SECTION_SHADOW)
            .unwrap_or(false)
    }

    /// Collect scored nodes from a tree, filtering shadows.
    fn collect_nodes(
        node: &TreeNode,
        query_lower: &str,
        path: &[String],
        doc_id: &str,
        results: &mut Vec<NodeResult>,
    ) {
        if Self::is_shadow(node) {
            return;
        }

        let score = Self::score_node(node, query_lower);

        let mut current_path = path.to_vec();
        current_path.push(node.title.clone());

        if score > 0.0 {
            let has_shadows = node
                .properties
                .get(tree_property_keys::HAS_SHADOWS)
                .and_then(|v| v.as_bool())
                .unwrap_or(false);

            let shadow_count = node
                .properties
                .get("shadow_child_count")
                .and_then(|v| v.as_u64())
                .unwrap_or(0) as usize;

            results.push(NodeResult {
                doc_id: doc_id.to_string(),
                node_id: node.node_id.clone(),
                title: node.title.clone(),
                level: node.level,
                score,
                path: current_path.clone(),
                summary: node.summary.clone(),
                content_preview: truncate(&node.content.text, 300),
                token_count: node.content.token_count,
                has_merged_sections: has_shadows,
                merged_section_count: shadow_count,
            });
        }

        for child in &node.children {
            Self::collect_nodes(child, query_lower, &current_path, doc_id, results);
        }
    }

    /// Record access on retrieved nodes via DynamicTreeManager.
    fn record_accesses(&self, entity_ids: &[String]) {
        if let Some(ref manager) = self.dynamic_manager {
            for eid in entity_ids {
                let _ = manager.record_access(eid);
            }
        }
    }
}

/// Arguments for the tree search tool.
#[derive(Debug, Deserialize)]
struct TreeSearchArgs {
    /// Action to perform: "search" | "list_docs" | "get_toc"
    #[serde(default = "default_action")]
    action: String,

    /// Text query for search action.
    #[serde(default)]
    query: Option<String>,

    /// Document ID for get_toc action.
    #[serde(default)]
    doc_id: Option<String>,

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

    /// Whether to include content preview (default: true).
    #[serde(default = "default_true")]
    include_content: bool,
}

fn default_action() -> String {
    "search".to_string()
}

fn default_top_k() -> usize {
    5
}

fn default_true() -> bool {
    true
}

/// A single search result node.
#[derive(Debug, Serialize)]
struct NodeResult {
    doc_id: String,
    node_id: String,
    title: String,
    level: u8,
    score: f64,
    path: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    summary: Option<String>,
    #[serde(skip_serializing_if = "String::is_empty")]
    content_preview: String,
    token_count: usize,
    #[serde(skip_serializing_if = "is_false")]
    has_merged_sections: bool,
    #[serde(skip_serializing_if = "is_zero")]
    merged_section_count: usize,
}

fn is_false(v: &bool) -> bool {
    !*v
}
fn is_zero(v: &usize) -> bool {
    *v == 0
}

/// Document summary for list_docs action.
#[derive(Debug, Serialize)]
struct DocSummary {
    doc_id: String,
    doc_name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    description: Option<String>,
    node_count: usize,
    max_depth: usize,
    source_format: String,
}

#[async_trait]
impl ToolExecutor for TreeSearchTool {
    fn metadata(&self) -> ToolMetadata {
        ToolMetadata::new(
            "db_tree_search",
            "Search tree-indexed documents by structure and content. \
             Tree indexes preserve document hierarchy (headings, sections, sub-sections), \
             enabling structure-aware retrieval. \
             Actions: 'search' (find relevant sections), 'list_docs' (list all indexed documents), \
             'get_toc' (get table of contents for a document).",
            serde_json::json!({
                "type": "object",
                "properties": {
                    "action": {
                        "type": "string",
                        "enum": ["search", "list_docs", "get_toc"],
                        "description": "Action to perform. 'search' finds relevant sections by query. 'list_docs' lists all indexed documents. 'get_toc' returns the table of contents for a specific document.",
                        "default": "search"
                    },
                    "query": {
                        "type": "string",
                        "description": "Text query to search for (required for 'search' action)"
                    },
                    "doc_id": {
                        "type": "string",
                        "description": "Document ID (required for 'get_toc' action)"
                    },
                    "top_k": {
                        "type": "integer",
                        "description": "Maximum number of results to return (default: 5)",
                        "default": 5,
                        "minimum": 1,
                        "maximum": 50
                    },
                    "include_content": {
                        "type": "boolean",
                        "description": "Whether to include content preview in results (default: true)",
                        "default": true
                    }
                },
                "required": []
            }),
        )
        .with_category("search")
    }

    async fn execute(&self, arguments: &str) -> LlmResult<ToolResult> {
        let start = std::time::Instant::now();

        let args: TreeSearchArgs = serde_json::from_str(arguments)
            .map_err(|e| LlmError::ResponseParseError(format!("Invalid arguments: {}", e)))?;

        let result = match args.action.as_str() {
            "list_docs" => self.execute_list_docs(),
            "get_toc" => self.execute_get_toc(&args),
            "search" | _ => self.execute_search(&args),
        };

        let duration_ms = start.elapsed().as_millis() as u64;
        result.map(|r| r.with_duration(duration_ms))
    }
}

impl TreeSearchTool {
    /// Execute `search` action.
    fn execute_search(&self, args: &TreeSearchArgs) -> LlmResult<ToolResult> {
        let query = args.query.as_deref().unwrap_or("");
        if query.is_empty() {
            return Ok(ToolResult::err(
                "The 'query' parameter is required for 'search' action",
            ));
        }

        let query_lower = query.to_lowercase();
        let top_k = args.top_k.min(50).max(1);

        // Get all documents
        let documents = self.store.list_documents();
        if documents.is_empty() {
            return Ok(ToolResult::ok_json(&serde_json::json!({
                "result_count": 0,
                "message": "No tree-indexed documents found. Use the tree index import API to index documents first.",
                "results": []
            })));
        }

        let mut all_results: Vec<NodeResult> = Vec::new();

        for (doc_id, _metadata) in &documents {
            let doc = match self.store.get_tree(doc_id) {
                Ok(d) => d,
                Err(_) => continue,
            };

            for root in &doc.structure {
                Self::collect_nodes(root, &query_lower, &[], doc_id, &mut all_results);
            }
        }

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

        // Record access (Phase-95)
        let entity_ids: Vec<String> = all_results
            .iter()
            .map(|r| format!("{}:{}", r.doc_id, r.node_id))
            .collect();
        self.record_accesses(&entity_ids);

        // Strip content preview if not requested
        if !args.include_content {
            for r in &mut all_results {
                r.content_preview = String::new();
            }
        }

        Ok(ToolResult::ok_json(&serde_json::json!({
            "query": query,
            "result_count": all_results.len(),
            "results": all_results
        })))
    }

    /// Execute `list_docs` action.
    fn execute_list_docs(&self) -> LlmResult<ToolResult> {
        let documents = self.store.list_documents();

        let mut doc_summaries: Vec<DocSummary> = Vec::new();

        for (doc_id, metadata) in &documents {
            let doc = match self.store.get_tree(doc_id) {
                Ok(d) => d,
                Err(_) => continue,
            };

            doc_summaries.push(DocSummary {
                doc_id: doc_id.clone(),
                doc_name: metadata.doc_name.clone(),
                description: metadata.description.clone(),
                node_count: doc.total_nodes(),
                max_depth: doc.max_depth(),
                source_format: metadata.source_format.clone(),
            });
        }

        Ok(ToolResult::ok_json(&serde_json::json!({
            "document_count": doc_summaries.len(),
            "documents": doc_summaries
        })))
    }

    /// Execute `get_toc` action.
    fn execute_get_toc(&self, args: &TreeSearchArgs) -> LlmResult<ToolResult> {
        let doc_id = match args.doc_id.as_deref() {
            Some(id) if !id.is_empty() => id,
            _ => {
                return Ok(ToolResult::err(
                    "The 'doc_id' parameter is required for 'get_toc' action",
                ));
            }
        };

        let doc = self
            .store
            .get_tree(doc_id)
            .map_err(|e| LlmError::NotFound(format!("Document '{}' not found: {}", doc_id, e)))?;

        let toc = TreeRetriever::format_toc(&doc);

        Ok(ToolResult::ok_json(&serde_json::json!({
            "doc_id": doc_id,
            "doc_name": doc.metadata.doc_name,
            "node_count": doc.total_nodes(),
            "max_depth": doc.max_depth(),
            "toc": toc
        })))
    }
}

/// Truncate a string to max chars, preserving char boundaries.
fn truncate(s: &str, max_chars: usize) -> String {
    if s.len() <= max_chars {
        s.to_string()
    } else {
        let mut end = max_chars;
        while !s.is_char_boundary(end) && end > 0 {
            end -= 1;
        }
        format!("{}...", &s[..end])
    }
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::social::{SocialStoreV2, SocialStoreV2Config};
    use crate::tree_index::types::{TreeDocument, TreeMetadata, TreeNode};

    fn create_test_tool() -> (TreeSearchTool, Arc<TreeIndexStore>, tempfile::TempDir) {
        let tmp = tempfile::tempdir().unwrap();
        let config = SocialStoreV2Config::new(tmp.path());
        let graph = Arc::new(SocialStoreV2::new(config));
        let store = Arc::new(TreeIndexStore::new(graph));
        let tool = TreeSearchTool::new(store.clone());
        (tool, store, tmp)
    }

    fn sample_doc() -> TreeDocument {
        let meta = TreeMetadata::new("test-paper")
            .with_description("A sample research paper");

        let mut root = TreeNode::new("0001", "Introduction", 1)
            .with_content("This paper discusses deep learning.");

        let child = TreeNode::new("0002", "Related Work", 2)
            .with_content("Previous work on neural networks and transformers.")
            .with_summary("Survey of related approaches");

        root.add_child(child);

        TreeDocument::new(meta, vec![root])
    }

    #[test]
    fn test_tool_metadata() {
        let (tool, _, _tmp) = create_test_tool();
        let meta = tool.metadata();
        assert_eq!(meta.name, "db_tree_search");
        assert_eq!(meta.category, "search");
        assert!(meta.is_read_only);
        assert!(meta.description.contains("tree-indexed"));

        let def = meta.to_tool_definition();
        assert_eq!(def.function.name, "db_tree_search");
    }

    // Note: SocialStoreV2 uses internal block_on() which conflicts with tokio
    // runtime. We use a dedicated thread + oneshot runtime for async tests.
    fn run_async<F: std::future::Future<Output = T> + Send + 'static, T: Send + 'static>(f: F) -> T {
        std::thread::spawn(move || {
            tokio::runtime::Runtime::new().unwrap().block_on(f)
        }).join().unwrap()
    }

    #[test]
    fn test_search_action() {
        let (tool, store, _tmp) = create_test_tool();
        let doc = sample_doc();
        store.import_tree(&doc).unwrap();

        let tool = Arc::new(tool);
        let t = tool.clone();
        let result = run_async(async move {
            t.execute(r#"{"action":"search","query":"neural networks"}"#).await.unwrap()
        });

        assert!(result.success);
        let output: serde_json::Value = serde_json::from_str(&result.output).unwrap();
        let count = output["result_count"].as_u64().unwrap();
        assert!(count > 0, "Should find results for 'neural networks'");
    }

    #[test]
    fn test_search_no_query() {
        let (tool, _, _tmp) = create_test_tool();

        let tool = Arc::new(tool);
        let t = tool.clone();
        let result = run_async(async move {
            t.execute(r#"{"action":"search"}"#).await.unwrap()
        });

        assert!(!result.success);
        assert!(result.error.unwrap().contains("query"));
    }

    #[test]
    fn test_list_docs_action() {
        let (tool, store, _tmp) = create_test_tool();
        let doc = sample_doc();
        store.import_tree(&doc).unwrap();

        let tool = Arc::new(tool);
        let t = tool.clone();
        let result = run_async(async move {
            t.execute(r#"{"action":"list_docs"}"#).await.unwrap()
        });

        assert!(result.success);
        let output: serde_json::Value = serde_json::from_str(&result.output).unwrap();
        assert_eq!(output["document_count"].as_u64().unwrap(), 1);

        let docs = output["documents"].as_array().unwrap();
        assert_eq!(docs[0]["doc_name"].as_str().unwrap(), "test-paper");
    }

    #[test]
    fn test_get_toc_action() {
        let (tool, store, _tmp) = create_test_tool();
        let doc = sample_doc();
        let doc_id = doc.id.clone();
        store.import_tree(&doc).unwrap();

        let tool = Arc::new(tool);
        let t = tool.clone();
        let args = format!(r#"{{"action":"get_toc","doc_id":"{}"}}"#, doc_id);
        let result = run_async(async move {
            t.execute(&args).await.unwrap()
        });

        assert!(result.success);
        let output: serde_json::Value = serde_json::from_str(&result.output).unwrap();
        assert!(output["toc"].as_str().unwrap().contains("Introduction"));
    }

    #[test]
    fn test_get_toc_missing_doc_id() {
        let (tool, _, _tmp) = create_test_tool();

        let tool = Arc::new(tool);
        let t = tool.clone();
        let result = run_async(async move {
            t.execute(r#"{"action":"get_toc"}"#).await.unwrap()
        });

        assert!(!result.success);
        assert!(result.error.unwrap().contains("doc_id"));
    }

    #[test]
    fn test_empty_store() {
        let (tool, _, _tmp) = create_test_tool();

        let tool = Arc::new(tool);
        let t = tool.clone();
        let result = run_async(async move {
            t.execute(r#"{"action":"search","query":"anything"}"#).await.unwrap()
        });

        assert!(result.success);
        let output: serde_json::Value = serde_json::from_str(&result.output).unwrap();
        assert_eq!(output["result_count"].as_u64().unwrap(), 0);
    }
}
