use std::sync::Arc;

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

use crate::db::LanceDb;
use crate::types::{LlmProvider, SearchRequest, SearchResult};

/// Tier 3 deep agentic search: uses an LLM to plan and execute a
/// multi-step search strategy. This is the most expensive tier but
/// handles complex, open-ended queries that require reasoning about
/// where relevant code might live.
pub struct Tier3Search {
    llm: Arc<dyn LlmProvider>,
    vector_db: Option<Arc<LanceDb>>,
    workspace_root: std::path::PathBuf,
}

impl Tier3Search {
    pub fn new(
        llm: Arc<dyn LlmProvider>,
        vector_db: Option<Arc<LanceDb>>,
        workspace_root: std::path::PathBuf,
    ) -> Self {
        Self {
            llm,
            vector_db,
            workspace_root,
        }
    }

    /// Execute a deep agentic search:
    /// 1. Ask the LLM to generate a search plan (file patterns + grep terms).
    /// 2. Execute each step of the plan.
    /// 3. Feed intermediate results back to the LLM for refinement.
    /// 4. Return the final ranked results.
    pub async fn search(&self, request: &SearchRequest) -> Result<Vec<SearchResult>> {
        info!(query = %request.query, "Tier3: starting deep agentic search");

        // Step 1: Generate search plan from LLM.
        let plan = self.generate_search_plan(&request.query).await?;
        debug!(steps = plan.steps.len(), "Tier3: search plan generated");

        // Step 2: Execute each step and collect results.
        let mut all_results: Vec<SearchResult> = Vec::new();

        for (i, step) in plan.steps.iter().enumerate() {
            debug!(step = i + 1, action = %step.description, "Tier3: executing plan step");

            let step_results = match step.action {
                SearchAction::GrepPattern => {
                    self.execute_grep(&step.pattern, request).await?
                }
                SearchAction::FileGlob => {
                    self.execute_glob(&step.pattern, request).await?
                }
                SearchAction::VectorQuery => {
                    self.execute_vector(&step.pattern, request).await?
                }
            };

            debug!(
                step = i + 1,
                count = step_results.len(),
                "Tier3: step results collected"
            );

            all_results.extend(step_results);
        }

        // Step 3: Deduplicate and ask LLM to rank/filter.
        let deduped = Self::deduplicate(all_results);
        let ranked = self.llm_rank_results(&request.query, deduped).await?;

        // Step 4: Truncate to max_results.
        let mut final_results = ranked;
        final_results.truncate(request.max_results);

        info!(
            query = %request.query,
            count = final_results.len(),
            "Tier3: deep agentic search complete"
        );

        Ok(final_results)
    }

    /// Ask the LLM to produce a structured search plan.
    async fn generate_search_plan(&self, query: &str) -> Result<SearchPlan> {
        let system = r#"You are a code search planner. Given a query about a codebase, produce a search plan as a series of steps. Each step should be one of:
- GREP:<pattern> -- search for a regex pattern in file contents
- GLOB:<pattern> -- find files matching a glob pattern
- VECTOR:<query> -- semantic search for a concept

Output each step on its own line, with nothing else. Output 2-5 steps.
Example:
GREP:fn handle_request
GLOB:**/handler*.rs
VECTOR:HTTP request handling and routing"#;

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

        let steps: Vec<SearchStep> = response
            .lines()
            .filter(|line| !line.trim().is_empty())
            .filter_map(|line| {
                let line = line.trim();
                if let Some(pattern) = line.strip_prefix("GREP:") {
                    Some(SearchStep {
                        action: SearchAction::GrepPattern,
                        pattern: pattern.trim().to_string(),
                        description: format!("Grep for '{}'", pattern.trim()),
                    })
                } else if let Some(pattern) = line.strip_prefix("GLOB:") {
                    Some(SearchStep {
                        action: SearchAction::FileGlob,
                        pattern: pattern.trim().to_string(),
                        description: format!("Glob for '{}'", pattern.trim()),
                    })
                } else {
                    line.strip_prefix("VECTOR:").map(|pattern| SearchStep {
                        action: SearchAction::VectorQuery,
                        pattern: pattern.trim().to_string(),
                        description: format!("Vector search for '{}'", pattern.trim()),
                    })
                }
            })
            .take(5)
            .collect();

        // If LLM returned nothing useful, create a default plan.
        let steps = if steps.is_empty() {
            vec![
                SearchStep {
                    action: SearchAction::VectorQuery,
                    pattern: query.to_string(),
                    description: "Fallback vector search".to_string(),
                },
                SearchStep {
                    action: SearchAction::GrepPattern,
                    pattern: query.to_string(),
                    description: "Fallback grep search".to_string(),
                },
            ]
        } else {
            steps
        };

        Ok(SearchPlan { steps })
    }

    /// Execute a grep step by walking files and matching the pattern.
    async fn execute_grep(
        &self,
        pattern: &str,
        request: &SearchRequest,
    ) -> Result<Vec<SearchResult>> {
        let regex = match regex::RegexBuilder::new(pattern)
            .case_insensitive(true)
            .build()
        {
            Ok(r) => r,
            Err(_) => {
                // If the pattern is not valid regex, escape it and retry.
                regex::Regex::new(&regex::escape(pattern))?
            }
        };

        let mut results = Vec::new();
        let walker = walkdir::WalkDir::new(&self.workspace_root)
            .follow_links(false)
            .max_depth(12)
            .into_iter()
            .filter_map(|e| e.ok())
            .filter(|e| e.file_type().is_file());

        for entry in walker {
            let path = entry.path();
            let path_str = path.to_string_lossy().to_string();

            if request.exclude_paths.iter().any(|ex| path_str.contains(ex)) {
                continue;
            }

            // Read file content.
            let content = match tokio::fs::read_to_string(path).await {
                Ok(c) => c,
                Err(_) => continue,
            };

            let mut matches = Vec::new();
            for (line_num, line) in content.lines().enumerate() {
                if regex.is_match(line) {
                    matches.push((line_num + 1, line.to_string()));
                }
            }

            if !matches.is_empty() {
                let first_line = matches[0].0;
                let snippet: String = matches
                    .iter()
                    .take(5)
                    .map(|(ln, text)| format!("{ln}: {text}"))
                    .collect::<Vec<_>>()
                    .join("\n");

                let score = (matches.len() as f64 / content.lines().count().max(1) as f64 * 10.0)
                    .min(1.0);

                results.push(SearchResult {
                    file_path: path_str,
                    content: snippet,
                    relevance_score: score,
                    line_number: Some(first_line),
                });
            }
        }

        results.sort_by(|a, b| {
            b.relevance_score
                .partial_cmp(&a.relevance_score)
                .unwrap_or(std::cmp::Ordering::Equal)
        });
        results.truncate(request.max_results);
        Ok(results)
    }

    /// Execute a glob step by matching file paths.
    async fn execute_glob(
        &self,
        pattern: &str,
        request: &SearchRequest,
    ) -> Result<Vec<SearchResult>> {
        let full_pattern = if pattern.starts_with('/') || pattern.starts_with("**/") {
            let root = self.workspace_root.to_string_lossy();
            if pattern.starts_with("**/") {
                format!("{}/{}", root, pattern)
            } else {
                pattern.to_string()
            }
        } else {
            let root = self.workspace_root.to_string_lossy();
            format!("{}/{}", root, pattern)
        };

        let mut results = Vec::new();

        let paths = match glob::glob(&full_pattern) {
            Ok(p) => p,
            Err(_) => return Ok(results),
        };

        for entry in paths.filter_map(|e| e.ok()) {
            let path_str = entry.to_string_lossy().to_string();

            if request.exclude_paths.iter().any(|ex| path_str.contains(ex)) {
                continue;
            }

            let content = match tokio::fs::read_to_string(&entry).await {
                Ok(c) => c,
                Err(_) => continue,
            };

            // Take the first 20 lines as a preview.
            let preview: String = content.lines().take(20).collect::<Vec<_>>().join("\n");

            results.push(SearchResult {
                file_path: path_str,
                content: preview,
                relevance_score: 0.6, // Glob matches get a moderate base score.
                line_number: Some(1),
            });
        }

        results.truncate(request.max_results);
        Ok(results)
    }

    /// Execute a vector search step using LanceDb.
    async fn execute_vector(
        &self,
        query: &str,
        request: &SearchRequest,
    ) -> Result<Vec<SearchResult>> {
        if let Some(ref vdb) = self.vector_db {
            let sub_request = SearchRequest {
                query: query.to_string(),
                max_results: request.max_results,
                file_types: request.file_types.clone(),
                exclude_paths: request.exclude_paths.clone(),
            };
            let tier1 = super::tier1_vector::Tier1Search::new(Arc::clone(vdb));
            tier1.search(&sub_request).await
        } else {
            Ok(Vec::new())
        }
    }

    /// Ask the LLM to rank and filter the collected results by relevance.
    async fn llm_rank_results(
        &self,
        query: &str,
        results: Vec<SearchResult>,
    ) -> Result<Vec<SearchResult>> {
        if results.is_empty() {
            return Ok(results);
        }

        // Build a summary of results for the LLM.
        let summaries: Vec<String> = results
            .iter()
            .enumerate()
            .map(|(i, r)| {
                let snippet = r.content.lines().take(3).collect::<Vec<_>>().join(" | ");
                format!("[{}] {} (score={:.2}): {}", i, r.file_path, r.relevance_score, snippet)
            })
            .collect();

        let system = "You are a search result ranker. Given a query and a list of search results, output the indices of the most relevant results in order of relevance, one per line. Output only the numbers.";
        let prompt = format!(
            "Query: {}\n\nResults:\n{}",
            query,
            summaries.join("\n")
        );

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

        // Parse indices from LLM response.
        let indices: Vec<usize> = response
            .lines()
            .filter_map(|line| {
                line.trim()
                    .trim_start_matches('[')
                    .trim_end_matches(']')
                    .parse::<usize>()
                    .ok()
            })
            .filter(|&i| i < results.len())
            .collect();

        if indices.is_empty() {
            // LLM didn't produce useful ranking -- return results as-is.
            return Ok(results);
        }

        // Rebuild results in the LLM-specified order, adjusting scores.
        let mut ranked = Vec::new();
        for (rank, &idx) in indices.iter().enumerate() {
            let mut result = results[idx].clone();
            // Assign a new score based on ranking position.
            result.relevance_score = 1.0 - (rank as f64 * 0.05);
            ranked.push(result);
        }

        // Append any results not mentioned by the LLM at the end with lower scores.
        let mentioned: std::collections::HashSet<usize> = indices.iter().copied().collect();
        for (i, r) in results.iter().enumerate() {
            if !mentioned.contains(&i) {
                let mut r = r.clone();
                r.relevance_score *= 0.5;
                ranked.push(r);
            }
        }

        Ok(ranked)
    }

    /// Remove duplicate results by file_path, keeping the one with the highest score.
    fn deduplicate(results: Vec<SearchResult>) -> Vec<SearchResult> {
        let mut best_by_path: std::collections::HashMap<String, SearchResult> =
            std::collections::HashMap::new();

        for r in results {
            let existing = best_by_path.get(&r.file_path);
            if existing.is_none() || existing.unwrap().relevance_score < r.relevance_score {
                best_by_path.insert(r.file_path.clone(), r);
            }
        }

        let mut deduped: Vec<SearchResult> = best_by_path.into_values().collect();
        deduped.sort_by(|a, b| {
            b.relevance_score
                .partial_cmp(&a.relevance_score)
                .unwrap_or(std::cmp::Ordering::Equal)
        });
        deduped
    }
}

// ---- Internal plan types ----

struct SearchPlan {
    steps: Vec<SearchStep>,
}

struct SearchStep {
    action: SearchAction,
    pattern: String,
    description: String,
}

enum SearchAction {
    GrepPattern,
    FileGlob,
    VectorQuery,
}
