use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;

use anyhow::Result;
use tracing::{info, instrument};

use crate::types::{LlmProvider, SearchResult};

use super::deduplicator::Deduplicator;
use super::deep::DeepSearch;
use super::grep_executor::GrepExecutor;
use super::hybrid::HybridSearch;
use super::plan_parser::PlanParser;
use super::result_ranker::ResultRanker;

/// Configuration for the agentic search system.
#[derive(Debug, Clone)]
pub struct AgenticSearchConfig {
    /// Maximum number of files the deep search will read in one pass.
    pub max_files_to_read: usize,
    /// Maximum file size (in bytes) that will be read. Larger files are
    /// skipped to avoid blowing up the context window.
    pub max_file_size: usize,
    /// Timeout for a single grep subprocess.
    pub grep_timeout: Duration,
    /// Maximum number of result lines accepted from a single grep invocation.
    pub max_grep_results: usize,
    /// Maximum number of grep commands the LLM is allowed to generate per
    /// search operation.
    pub max_grep_commands: usize,
}

impl Default for AgenticSearchConfig {
    fn default() -> Self {
        Self {
            max_files_to_read: 20,
            max_file_size: 1_048_576, // 1 MB
            grep_timeout: Duration::from_secs(30),
            max_grep_results: 100,
            max_grep_commands: 10,
        }
    }
}

/// The top-level agentic search engine. It exposes two search modes:
///
/// - **Hybrid search** (`hybrid_search`): augments an existing set of
///   candidate results by asking an LLM whether they are sufficient and, if
///   not, generating additional grep searches.
/// - **Deep search** (`deep_search`): performs a full LLM-planned search
///   from scratch, including planning, grep execution, file reading,
///   deduplication, and ranking.
pub struct AgenticSearch {
    llm: Arc<dyn LlmProvider>,
    workspace_root: PathBuf,
    config: AgenticSearchConfig,
}

impl AgenticSearch {
    /// Create a new `AgenticSearch` with the given LLM provider and workspace
    /// root, using default configuration.
    pub fn new(llm: Arc<dyn LlmProvider>, workspace_root: PathBuf) -> Self {
        Self::with_config(llm, workspace_root, AgenticSearchConfig::default())
    }

    /// Create a new `AgenticSearch` with explicit configuration.
    pub fn with_config(
        llm: Arc<dyn LlmProvider>,
        workspace_root: PathBuf,
        config: AgenticSearchConfig,
    ) -> Self {
        Self {
            llm,
            workspace_root,
            config,
        }
    }

    /// Hybrid search: evaluate existing `candidates` against `query` using the
    /// LLM, optionally generating and running additional grep searches to fill
    /// gaps.
    ///
    /// This is the recommended entry point when you already have tier-1/tier-2
    /// results that may or may not be sufficient.
    #[instrument(skip(self, candidates), fields(candidate_count = candidates.len()))]
    pub async fn hybrid_search(
        &self,
        query: &str,
        candidates: Vec<SearchResult>,
    ) -> Result<Vec<SearchResult>> {
        info!(query = query, "AgenticSearch::hybrid_search");

        let hybrid = HybridSearch::new(
            Arc::clone(&self.llm),
            self.make_grep_executor(),
            PlanParser::new(),
            ResultRanker::new(Arc::clone(&self.llm)),
            Deduplicator::new(),
            self.config.clone(),
        );

        hybrid.search(query, candidates).await
    }

    /// Deep search: plan and execute a complete search from scratch.
    ///
    /// The LLM generates a full search plan, grep commands are executed in
    /// parallel, files are read, and results are merged, deduplicated, and
    /// ranked.
    #[instrument(skip(self))]
    pub async fn deep_search(&self, query: &str) -> Result<Vec<SearchResult>> {
        info!(query = query, "AgenticSearch::deep_search");

        let deep = DeepSearch::new(
            Arc::clone(&self.llm),
            self.workspace_root.clone(),
            self.make_grep_executor(),
            PlanParser::new(),
            ResultRanker::new(Arc::clone(&self.llm)),
            Deduplicator::new(),
            self.config.clone(),
        );

        deep.search(query).await
    }

    /// Create a new `GrepExecutor` configured from the current settings.
    /// Useful for callers that need to run grep searches directly.
    pub fn make_grep_executor(&self) -> GrepExecutor {
        GrepExecutor::new(
            self.workspace_root.clone(),
            self.config.grep_timeout,
            self.config.max_grep_results,
        )
    }

    /// Access the configuration.
    pub fn config(&self) -> &AgenticSearchConfig {
        &self.config
    }

    /// Access the workspace root path.
    pub fn workspace_root(&self) -> &PathBuf {
        &self.workspace_root
    }
}

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

    struct MockLlm;

    #[async_trait::async_trait]
    impl LlmProvider for MockLlm {
        async fn complete(&self, _prompt: &str) -> anyhow::Result<String> {
            Ok(r#"{"grep_commands": ["fn main"], "files_to_read": []}"#.to_string())
        }
        async fn complete_with_system(&self, _system: &str, _prompt: &str) -> anyhow::Result<String> {
            Ok(r#"{"sufficient": true}"#.to_string())
        }
    }

    #[test]
    fn test_default_config() {
        let config = AgenticSearchConfig::default();
        assert_eq!(config.max_files_to_read, 20);
        assert_eq!(config.max_file_size, 1_048_576);
        assert_eq!(config.grep_timeout, Duration::from_secs(30));
        assert_eq!(config.max_grep_results, 100);
        assert_eq!(config.max_grep_commands, 10);
    }

    #[test]
    fn test_agentic_search_creation() {
        let llm: Arc<dyn LlmProvider> = Arc::new(MockLlm);
        let search = AgenticSearch::new(llm, PathBuf::from("/tmp/workspace"));
        assert_eq!(search.config().max_files_to_read, 20);
    }

    #[test]
    fn test_agentic_search_custom_config() {
        let llm: Arc<dyn LlmProvider> = Arc::new(MockLlm);
        let config = AgenticSearchConfig {
            max_files_to_read: 50,
            max_file_size: 2_000_000,
            grep_timeout: Duration::from_secs(60),
            max_grep_results: 200,
            max_grep_commands: 20,
        };
        let search = AgenticSearch::with_config(llm, PathBuf::from("/tmp/workspace"), config);
        assert_eq!(search.config().max_files_to_read, 50);
        assert_eq!(search.config().max_grep_commands, 20);
    }
}
