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

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

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

use super::deduplicator::Deduplicator;
use super::grep_executor::GrepExecutor;
use super::plan_parser::{PlanParser, SearchPlan};
use super::result_ranker::ResultRanker;
use super::search::AgenticSearchConfig;

/// Deep search performs a full LLM-planned search from scratch. The LLM is
/// given the query and asked to produce a [`SearchPlan`] describing which
/// grep patterns to run and which files to read. The plan is then executed
/// and results are ranked.
pub struct DeepSearch {
    llm: Arc<dyn LlmProvider>,
    workspace_root: PathBuf,
    grep_executor: GrepExecutor,
    plan_parser: PlanParser,
    result_ranker: ResultRanker,
    deduplicator: Deduplicator,
    config: AgenticSearchConfig,
}

impl DeepSearch {
    pub fn new(
        llm: Arc<dyn LlmProvider>,
        workspace_root: PathBuf,
        grep_executor: GrepExecutor,
        plan_parser: PlanParser,
        result_ranker: ResultRanker,
        deduplicator: Deduplicator,
        config: AgenticSearchConfig,
    ) -> Self {
        Self {
            llm,
            workspace_root,
            grep_executor,
            plan_parser,
            result_ranker,
            deduplicator,
            config,
        }
    }

    /// Execute a deep search for the given query.
    ///
    /// 1. Ask the LLM to produce a search plan.
    /// 2. Execute all grep commands from the plan in parallel.
    /// 3. Read all files listed in the plan.
    /// 4. Merge results, deduplicate, and rank.
    #[instrument(skip(self))]
    pub async fn search(&self, query: &str) -> Result<Vec<SearchResult>> {
        if !self.llm.is_available() {
            debug!("DeepSearch: LLM unavailable, returning empty results");
            return Ok(Vec::new());
        }

        info!(query = query, "starting deep search");

        // ---- Step 1: Generate search plan ----
        let plan = self.generate_plan(query).await?;

        if plan.is_empty() {
            warn!("LLM returned an empty search plan");
            return Ok(Vec::new());
        }

        debug!(
            grep_count = plan.grep_commands.len(),
            file_count = plan.files_to_read.len(),
            "search plan generated"
        );

        // ---- Step 2: Execute greps in parallel ----
        let grep_patterns: Vec<String> = plan
            .grep_commands
            .into_iter()
            .take(self.config.max_grep_commands)
            .collect();

        let grep_results = if !grep_patterns.is_empty() {
            self.grep_executor.execute_many(&grep_patterns).await?
        } else {
            Vec::new()
        };

        debug!(
            grep_result_count = grep_results.len(),
            "grep phase complete"
        );

        // ---- Step 3: Read files ----
        let file_results = self.read_files(&plan.files_to_read).await;

        debug!(
            file_result_count = file_results.len(),
            "file reading phase complete"
        );

        // ---- Step 4: Merge, deduplicate, rank ----
        let mut all_results = grep_results;
        all_results.extend(file_results);

        let deduped = self.deduplicator.deduplicate(all_results);
        let ranked = self.result_ranker.rank(query, deduped).await?;

        info!(
            result_count = ranked.len(),
            "deep search complete"
        );

        Ok(ranked)
    }

    // ---- internal ----

    /// Ask the LLM to produce a search plan for the query.
    async fn generate_plan(&self, query: &str) -> Result<SearchPlan> {
        let system = "You are a code search planner. Given a user query about a codebase, \
                       produce a search plan as a JSON object with two fields:\n\
                       - \"grep_commands\": an array of grep pattern strings to search for \
                         (e.g. [\"fn process\", \"struct Config\", \"impl.*Handler\"])\n\
                       - \"files_to_read\": an array of file paths that are likely relevant \
                         (e.g. [\"src/main.rs\", \"src/config.rs\"])\n\n\
                       Think about what code patterns, function names, type names, imports, \
                       or comments would be relevant to the query. Generate patterns that will \
                       find definitions, usages, and related code. Respond ONLY with the JSON \
                       object.";

        let prompt = format!(
            "Query: \"{}\"\n\n\
             Generate a search plan to find all relevant code for this query. \
             Include grep patterns for function definitions, type definitions, \
             trait implementations, imports, and usage sites.",
            query,
        );

        let response = tokio::time::timeout(
            std::time::Duration::from_secs(30),
            self.llm.complete_with_system(system, &prompt),
        )
        .await
        .map_err(|_| anyhow::anyhow!("LLM deep search plan generation timed out after 30s"))??;
        let plan = self.plan_parser.parse(&response)?;

        Ok(plan)
    }

    /// Read files listed in the search plan, converting their contents into
    /// search results.
    async fn read_files(&self, file_paths: &[String]) -> Vec<SearchResult> {
        let mut results = Vec::new();

        let files_to_read: Vec<_> = file_paths
            .iter()
            .take(self.config.max_files_to_read)
            .collect();

        for relative_path in files_to_read {
            let full_path = if PathBuf::from(relative_path).is_absolute() {
                PathBuf::from(relative_path)
            } else {
                self.workspace_root.join(relative_path)
            };

            // Safety check: ensure the path is within the workspace.
            if !self.grep_executor.is_safe_path(relative_path) {
                warn!(path = %relative_path, "skipping file outside workspace");
                continue;
            }

            match self.read_file_contents(&full_path).await {
                Ok(content) => {
                    // Skip files that are too large.
                    if content.len() > self.config.max_file_size {
                        debug!(
                            path = %relative_path,
                            size = content.len(),
                            max = self.config.max_file_size,
                            "skipping oversized file"
                        );
                        continue;
                    }

                    results.push(SearchResult {
                        file_path: relative_path.clone(),
                        content,
                        relevance_score: 0.5, // will be refined by ranker
                        line_number: None,     // whole-file read
                    });
                }
                Err(e) => {
                    debug!(path = %relative_path, error = %e, "failed to read file");
                }
            }
        }

        results
    }

    /// Read the contents of a single file.
    async fn read_file_contents(&self, path: &PathBuf) -> Result<String> {
        let content = tokio::fs::read_to_string(path).await?;
        Ok(content)
    }
}
