use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;

use anyhow::Result;
use tracing::{debug, trace, warn};

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

use super::tier1_vector::Tier1Search;

/// Tier 2 hybrid search: combines Tier 1 vector results with a
/// regex/grep pass over the workspace to improve precision for
/// queries that contain identifiable tokens (function names, types, etc.).
pub struct Tier2Search {
    tier1: Tier1Search,
    workspace_root: std::path::PathBuf,
}

impl Tier2Search {
    pub fn new(vector_db: Arc<LanceDb>, workspace_root: std::path::PathBuf) -> Self {
        Self {
            tier1: Tier1Search::new(vector_db),
            workspace_root,
        }
    }

    /// Execute hybrid search:
    /// 1. Run Tier 1 vector search.
    /// 2. Run a parallel grep search over the workspace.
    /// 3. Merge and re-rank results, boosting files found by both methods.
    pub async fn search(&self, request: &SearchRequest) -> Result<Vec<SearchResult>> {
        // Run vector search.
        let vector_results = self.tier1.search(request).await.unwrap_or_else(|e| {
            warn!(error = %e, "Tier2: vector search failed, continuing with grep only");
            Vec::new()
        });

        // Run grep search.
        let grep_results = self.grep_search(request).await.unwrap_or_else(|e| {
            warn!(error = %e, "Tier2: grep search failed, continuing with vector only");
            Vec::new()
        });

        debug!(
            vector_count = vector_results.len(),
            grep_count = grep_results.len(),
            "Tier2: merging results"
        );

        let merged = self.merge_results(vector_results, grep_results, request.max_results);

        debug!(
            query = %request.query,
            count = merged.len(),
            "Tier2: hybrid search complete"
        );

        Ok(merged)
    }

    /// Search the workspace using regex/glob pattern matching.
    async fn grep_search(&self, request: &SearchRequest) -> Result<Vec<SearchResult>> {
        let query = &request.query;
        let mut results = Vec::new();

        // Extract key tokens from the query to use as grep patterns.
        let patterns = Self::extract_grep_patterns(query);
        if patterns.is_empty() {
            trace!("No grep patterns extracted from query");
            return Ok(results);
        }

        // Compile a regex that matches any of the patterns.
        let regex_str = patterns
            .iter()
            .map(|p| regex::escape(p))
            .collect::<Vec<_>>()
            .join("|");
        let regex = match regex::RegexBuilder::new(&regex_str)
            .case_insensitive(true)
            .build()
        {
            Ok(r) => r,
            Err(e) => {
                warn!(error = %e, "Failed to compile grep regex");
                return Ok(results);
            }
        };

        // Walk the workspace tree.
        let walker = walkdir::WalkDir::new(&self.workspace_root)
            .follow_links(false)
            .max_depth(10)
            .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();

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

            // Respect file type filter.
            if !request.file_types.is_empty()
                && !request
                    .file_types
                    .iter()
                    .any(|ft| path_str.ends_with(ft))
            {
                continue;
            }

            // Only search text files.
            if !Self::is_searchable(path) {
                continue;
            }

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

            let mut match_count = 0;
            let mut first_line: Option<usize> = None;
            let mut context_snippet = String::new();

            for (line_num, line) in content.lines().enumerate() {
                if regex.is_match(line) {
                    match_count += 1;
                    if first_line.is_none() {
                        first_line = Some(line_num + 1);
                        // Capture a context window around the first match.
                        let start = line_num.saturating_sub(2);
                        let end = (line_num + 3).min(content.lines().count());
                        context_snippet = content
                            .lines()
                            .skip(start)
                            .take(end - start)
                            .collect::<Vec<_>>()
                            .join("\n");
                    }
                }
            }

            if match_count > 0 {
                // Score based on match density (matches per 1000 lines).
                let total_lines = content.lines().count().max(1);
                let density = match_count as f64 / total_lines as f64;
                let score = (density * 10.0).min(1.0);

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

        // Sort by relevance descending.
        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)
    }

    /// Merge vector and grep results by deduplicating on file_path and
    /// boosting the score for files that appear in both result sets.
    fn merge_results(
        &self,
        vector_results: Vec<SearchResult>,
        grep_results: Vec<SearchResult>,
        max_results: usize,
    ) -> Vec<SearchResult> {
        let mut by_path: HashMap<String, SearchResult> = HashMap::new();

        // Insert vector results first.
        for r in vector_results {
            by_path.insert(r.file_path.clone(), r);
        }

        // Merge in grep results: boost score if already present, or insert if new.
        for r in grep_results {
            if let Some(existing) = by_path.get_mut(&r.file_path) {
                // Both sources found this file -- boost relevance.
                existing.relevance_score =
                    (existing.relevance_score + r.relevance_score * 0.5).min(1.0);
                // Prefer the grep snippet if it has a line number (more precise).
                if r.line_number.is_some() && existing.line_number.is_none() {
                    existing.line_number = r.line_number;
                    existing.content = r.content;
                }
            } else {
                // Grep-only result: slight penalty since vector didn't find it.
                let mut adjusted = r;
                adjusted.relevance_score *= 0.8;
                by_path.insert(adjusted.file_path.clone(), adjusted);
            }
        }

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

    /// Extract meaningful tokens from a query to use as grep patterns.
    /// Filters out common English stop words and very short tokens.
    fn extract_grep_patterns(query: &str) -> Vec<String> {
        let stop_words = [
            "the", "a", "an", "is", "are", "was", "were", "be", "been", "being", "have", "has",
            "had", "do", "does", "did", "will", "would", "could", "should", "may", "might",
            "shall", "can", "need", "dare", "ought", "used", "to", "of", "in", "for", "on",
            "with", "at", "by", "from", "as", "into", "through", "during", "before", "after",
            "above", "below", "between", "out", "off", "over", "under", "again", "further",
            "then", "once", "here", "there", "when", "where", "why", "how", "all", "each",
            "every", "both", "few", "more", "most", "other", "some", "such", "no", "nor", "not",
            "only", "own", "same", "so", "than", "too", "very", "just", "because", "but", "and",
            "or", "if", "while", "about", "up", "it", "its", "this", "that", "these", "those",
            "what", "which", "who", "whom", "find", "show", "get", "look", "search", "me",
            "i", "my", "we", "our", "you", "your", "he", "she", "they", "them",
        ];

        query
            .split(|c: char| !c.is_alphanumeric() && c != '_' && c != '-')
            .filter(|w| w.len() >= 3)
            .filter(|w| !stop_words.contains(&w.to_lowercase().as_str()))
            .map(|w| w.to_string())
            .collect()
    }

    /// Quick check whether a file extension suggests a searchable text file.
    fn is_searchable(path: &Path) -> bool {
        let searchable_extensions = [
            "rs", "py", "js", "ts", "tsx", "jsx", "go", "java", "c", "cpp", "h", "hpp",
            "rb", "php", "swift", "kt", "scala", "sh", "bash", "zsh", "fish", "toml",
            "yaml", "yml", "json", "xml", "html", "css", "scss", "less", "md", "txt",
            "sql", "graphql", "proto", "tf", "hcl", "lua", "vim", "el", "clj",
            "ex", "exs", "erl", "hs", "ml", "mli", "r", "R", "jl", "dart", "zig",
            "nim", "v", "svelte", "vue", "astro",
        ];

        path.extension()
            .and_then(|ext| ext.to_str())
            .map(|ext| searchable_extensions.contains(&ext))
            .unwrap_or(false)
    }
}
