use anyhow::{anyhow, Context, Result};
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use tree_sitter::{Parser, Range, Tree};

use crate::backend::{BackendMetadata, RealFsSearchBackend, SearchBackend};
use crate::content::{
    load_search_content_with_backend, ContentState, LoadedContent, SearchDiagnostic,
};
use crate::parser::{AstNode, LogicalOperator, PredicateKey};
use crate::predicates::PredicateEvaluator;
use crate::SemanticSkipReason;

/// The result of an evaluation for a single file.
#[derive(Debug, Clone)]
pub enum MatchResult {
    // For simple, non-hunkable predicates like `ext:rs` or `size:>10kb`
    Boolean(bool),
    // For code-aware predicates that can identify specific code blocks.
    Hunks(Vec<Range>),
}

/// Holds the context for a single file being evaluated.
/// It lazily loads content and caches the tree-sitter AST.
pub struct FileContext {
    pub path: PathBuf,
    pub root: PathBuf,
    pub display_path: PathBuf,
    backend: Arc<dyn SearchBackend>,
    metadata: Option<BackendMetadata>,
    content: Option<LoadedContent>,
    // Cache for the parsed tree-sitter AST
    tree: Option<Tree>,
    /// Language key for the cached tree (used to reparse when dialect changes).
    tree_language_key: Option<String>,
    /// Cached SQL dialect/profile key chosen for this file.
    sql_profile_key: Option<String>,
    diagnostics: Vec<SearchDiagnostic>,
    semantic_skip_reasons: Vec<SemanticSkipReason>,
}

impl FileContext {
    pub fn new(path: PathBuf, root: PathBuf) -> Self {
        Self::with_backend(path, root, Arc::new(RealFsSearchBackend))
    }

    pub fn with_backend(path: PathBuf, root: PathBuf, backend: Arc<dyn SearchBackend>) -> Self {
        let canonical_root = backend.normalize_root(&root).unwrap_or(root.clone());
        let identity = backend
            .normalize_path(&canonical_root, &root, &path)
            .unwrap_or(crate::backend::BackendPathIdentity {
                display_path: path.clone(),
                resolved_path: path.clone(),
                root_relative_path: None,
                resolution: crate::PathResolution::Fallback,
            });
        FileContext {
            display_path: identity.display_path,
            path: identity.resolved_path,
            root: canonical_root,
            backend,
            metadata: None,
            content: None,
            tree: None,
            tree_language_key: None,
            sql_profile_key: None,
            diagnostics: Vec::new(),
            semantic_skip_reasons: Vec::new(),
        }
    }

    pub fn backend(&self) -> &Arc<dyn SearchBackend> {
        &self.backend
    }

    pub fn resolved_path(&self) -> &Path {
        &self.path
    }

    pub fn root_path(&self) -> &Path {
        &self.root
    }

    pub fn root_relative_path(&self) -> Option<&Path> {
        self.path.strip_prefix(&self.root).ok()
    }

    pub fn metadata(&mut self) -> Result<&BackendMetadata> {
        if self.metadata.is_none() {
            self.metadata = Some(self.backend.stat(&self.path)?);
        }
        Ok(self.metadata.as_ref().unwrap())
    }

    pub fn normalized_path(&self, path: &Path) -> Result<crate::backend::BackendPathIdentity> {
        self.backend.normalize_path(&self.root, &self.root, path)
    }

    pub fn get_content(&mut self) -> Result<&str> {
        if self.content.is_none() {
            self.content = Some(load_search_content_with_backend(
                self.backend.as_ref(),
                &self.path,
                &self.display_path,
            )?);
        }
        Ok(self.content.as_ref().unwrap().content.as_ref())
    }

    pub fn get_content_arc(&mut self) -> Result<Arc<str>> {
        if self.content.is_none() {
            self.content = Some(load_search_content_with_backend(
                self.backend.as_ref(),
                &self.path,
                &self.display_path,
            )?);
        }
        Ok(self.content.as_ref().unwrap().content.clone())
    }

    pub fn content_state(&mut self) -> Result<ContentState> {
        if self.content.is_none() {
            self.content = Some(load_search_content_with_backend(
                self.backend.as_ref(),
                &self.path,
                &self.display_path,
            )?);
        }
        Ok(self.content.as_ref().unwrap().state.clone())
    }

    pub fn content_diagnostics(&mut self) -> Result<Vec<SearchDiagnostic>> {
        if self.content.is_none() {
            self.content = Some(load_search_content_with_backend(
                self.backend.as_ref(),
                &self.path,
                &self.display_path,
            )?);
        }
        Ok(self.content.as_ref().unwrap().diagnostics.clone())
    }

    // Lazily parses the file with tree-sitter and caches the result.
    pub fn get_tree(
        &mut self,
        language_key: &str,
        language: tree_sitter::Language,
    ) -> Result<&Tree> {
        if self.tree.is_none() || self.tree_language_key.as_deref() != Some(language_key) {
            let path_display = self.path.display().to_string();
            let content = self.get_content()?;
            let mut parser = Parser::new();
            parser.set_language(&language).with_context(|| {
                format!("Failed to set language for tree-sitter parser on {path_display}")
            })?;
            let tree = parser
                .parse(content, None)
                .ok_or_else(|| anyhow!("Tree-sitter failed to parse {path_display}"))?;
            self.tree = Some(tree);
            self.tree_language_key = Some(language_key.to_string());
        }
        Ok(self.tree.as_ref().unwrap())
    }

    pub fn has_tree_for(&self, language_key: &str) -> bool {
        self.tree.is_some() && self.tree_language_key.as_deref() == Some(language_key)
    }

    /// Returns the cached SQL profile key, if any.
    pub fn sql_profile_key(&self) -> Option<&str> {
        self.sql_profile_key.as_deref()
    }

    /// Stores the SQL profile key chosen for this file.
    pub fn set_sql_profile_key(&mut self, key: &str) {
        self.sql_profile_key = Some(key.to_string());
    }

    pub fn push_diagnostic(&mut self, diagnostic: SearchDiagnostic) {
        self.diagnostics.push(diagnostic);
    }

    pub fn push_semantic_skip(&mut self, reason: SemanticSkipReason, message: impl Into<String>) {
        self.semantic_skip_reasons.push(reason);
        self.diagnostics.push(SearchDiagnostic::semantic_skip(
            self.display_path.clone(),
            message,
        ));
    }

    pub fn take_diagnostics(&mut self) -> Vec<SearchDiagnostic> {
        std::mem::take(&mut self.diagnostics)
    }

    pub fn take_semantic_skip_reasons(&mut self) -> Vec<SemanticSkipReason> {
        std::mem::take(&mut self.semantic_skip_reasons)
    }
}

/// The main evaluator struct. It holds the AST and the predicate registry.
pub struct Evaluator {
    ast: AstNode,
    registry: HashMap<PredicateKey, Box<dyn PredicateEvaluator + Send + Sync>>,
}

impl Evaluator {
    pub fn new(
        ast: AstNode,
        registry: HashMap<PredicateKey, Box<dyn PredicateEvaluator + Send + Sync>>,
    ) -> Self {
        Evaluator { ast, registry }
    }

    /// Evaluates the query for a given file path.
    pub fn evaluate(&self, context: &mut FileContext) -> Result<MatchResult> {
        self.evaluate_node(&self.ast, context)
    }

    /// Recursively evaluates an AST node.
    fn evaluate_node(&self, node: &AstNode, context: &mut FileContext) -> Result<MatchResult> {
        match node {
            AstNode::Predicate(key, value) => self.evaluate_predicate(key, value, context),
            AstNode::LogicalOp(op, left, right) => {
                let left_res = self.evaluate_node(left, context)?;

                // Short-circuit AND if left is false
                if *op == LogicalOperator::And && !left_res.is_match() {
                    return Ok(MatchResult::Boolean(false));
                }

                // Short-circuit OR if left is a full-file match
                if *op == LogicalOperator::Or {
                    if let MatchResult::Boolean(true) = left_res {
                        return Ok(left_res);
                    }
                }

                let right_res = self.evaluate_node(right, context)?;
                Ok(left_res.combine_with(right_res, op))
            }
            AstNode::Not(inner_node) => {
                // If the inner predicate of a NOT is not in the registry (e.g., a content
                // predicate during the metadata-only pass), we cannot definitively say the file
                // *doesn't* match. We must assume it *could* match and let the full evaluator decide.
                if let AstNode::Predicate(key, _) = &**inner_node {
                    if !self.registry.contains_key(key) {
                        return Ok(MatchResult::Boolean(true));
                    }
                }
                let result = self.evaluate_node(inner_node, context)?;
                Ok(MatchResult::Boolean(!result.is_match()))
            }
        }
    }

    /// Evaluates a single predicate.
    fn evaluate_predicate(
        &self,
        key: &PredicateKey,
        value: &str,
        context: &mut FileContext,
    ) -> Result<MatchResult> {
        if let Some(evaluator) = self.registry.get(key) {
            evaluator.evaluate(context, key, value)
        } else {
            // If a predicate is not in the current registry (e.g., a content predicate
            // during the metadata-only pass), it's considered a "pass" for this stage.
            Ok(MatchResult::Boolean(true))
        }
    }
}

impl MatchResult {
    /// Returns true if the result is considered a match.
    pub fn is_match(&self) -> bool {
        match self {
            MatchResult::Boolean(b) => *b,
            MatchResult::Hunks(h) => !h.is_empty(),
        }
    }

    /// Combines two match results based on a logical operator.
    pub fn combine_with(self, other: MatchResult, op: &LogicalOperator) -> Self {
        match op {
            LogicalOperator::And => self.combine_and(other),
            LogicalOperator::Or => self.combine_or(other),
        }
    }

    // Helper for AND logic
    fn combine_and(self, other: MatchResult) -> Self {
        if !self.is_match() || !other.is_match() {
            return MatchResult::Boolean(false);
        }
        match (self, other) {
            // Both are hunks: combine them, sort, and deduplicate.
            (MatchResult::Hunks(mut a), MatchResult::Hunks(b)) => {
                a.extend(b);
                a.sort_by_key(|r| r.start_byte);
                a.dedup();
                MatchResult::Hunks(a)
            }
            // One is a hunk, the other is a full-file match (true). Keep the hunks.
            (h @ MatchResult::Hunks(_), MatchResult::Boolean(true)) => h,
            (MatchResult::Boolean(true), h @ MatchResult::Hunks(_)) => h,
            // Both are full-file matches.
            (MatchResult::Boolean(true), MatchResult::Boolean(true)) => MatchResult::Boolean(true),
            // Should be unreachable due to the initial `is_match` check.
            _ => MatchResult::Boolean(false),
        }
    }

    // Helper for OR logic
    fn combine_or(self, other: MatchResult) -> Self {
        match (self, other) {
            // If either is a full-file match, the result is a full-file match.
            (MatchResult::Boolean(true), _) | (_, MatchResult::Boolean(true)) => {
                MatchResult::Boolean(true)
            }
            // Both are hunks: combine them, sort, and deduplicate.
            (MatchResult::Hunks(mut a), MatchResult::Hunks(b)) => {
                a.extend(b);
                a.sort_by_key(|r| r.start_byte);
                a.dedup();
                MatchResult::Hunks(a)
            }
            // One is a hunk, the other is a non-match. Keep the hunks.
            (h @ MatchResult::Hunks(_), MatchResult::Boolean(false)) => h,
            (MatchResult::Boolean(false), h @ MatchResult::Hunks(_)) => h,
            // Both are non-matches.
            (MatchResult::Boolean(false), MatchResult::Boolean(false)) => {
                MatchResult::Boolean(false)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::parser::LogicalOperator;
    use std::fs;
    use tempfile::tempdir;
    use tree_sitter::Point;
    use tree_sitter_rust::LANGUAGE;

    #[test]
    fn test_match_result_is_match_boolean() {
        assert!(MatchResult::Boolean(true).is_match());
        assert!(!MatchResult::Boolean(false).is_match());
    }

    #[test]
    fn test_match_result_is_match_hunks() {
        let empty_hunks = MatchResult::Hunks(vec![]);
        assert!(!empty_hunks.is_match());

        let with_hunks = MatchResult::Hunks(vec![Range {
            start_byte: 0,
            end_byte: 10,
            start_point: Point { row: 0, column: 0 },
            end_point: Point { row: 0, column: 10 },
        }]);
        assert!(with_hunks.is_match());
    }

    #[test]
    fn test_combine_and_both_false() {
        let result1 = MatchResult::Boolean(false);
        let result2 = MatchResult::Boolean(false);
        let combined = result1.combine_with(result2, &LogicalOperator::And);
        assert!(!combined.is_match());
    }

    #[test]
    fn test_combine_and_left_false() {
        let result1 = MatchResult::Boolean(false);
        let result2 = MatchResult::Boolean(true);
        let combined = result1.combine_with(result2, &LogicalOperator::And);
        assert!(!combined.is_match());
    }

    #[test]
    fn test_combine_and_right_false() {
        let result1 = MatchResult::Boolean(true);
        let result2 = MatchResult::Boolean(false);
        let combined = result1.combine_with(result2, &LogicalOperator::And);
        assert!(!combined.is_match());
    }

    #[test]
    fn test_combine_and_hunks_with_empty() {
        let hunks = vec![Range {
            start_byte: 0,
            end_byte: 10,
            start_point: Point { row: 0, column: 0 },
            end_point: Point { row: 0, column: 10 },
        }];
        let result1 = MatchResult::Hunks(hunks);
        let result2 = MatchResult::Hunks(vec![]);
        let combined = result1.combine_with(result2, &LogicalOperator::And);
        assert!(!combined.is_match());
    }

    #[test]
    fn test_combine_or_left_true() {
        let result1 = MatchResult::Boolean(true);
        let result2 = MatchResult::Boolean(false);
        let combined = result1.combine_with(result2, &LogicalOperator::Or);
        assert!(combined.is_match());
        if let MatchResult::Boolean(true) = combined {
            // expected
        } else {
            panic!("Expected Boolean(true)");
        }
    }

    #[test]
    fn test_combine_or_right_true() {
        let result1 = MatchResult::Boolean(false);
        let result2 = MatchResult::Boolean(true);
        let combined = result1.combine_with(result2, &LogicalOperator::Or);
        assert!(combined.is_match());
    }

    #[test]
    fn test_combine_or_hunks_with_false() {
        let hunks = vec![Range {
            start_byte: 0,
            end_byte: 10,
            start_point: Point { row: 0, column: 0 },
            end_point: Point { row: 0, column: 10 },
        }];
        let result1 = MatchResult::Hunks(hunks.clone());
        let result2 = MatchResult::Boolean(false);
        let combined = result1.combine_with(result2, &LogicalOperator::Or);
        if let MatchResult::Hunks(h) = combined {
            assert_eq!(h.len(), 1);
        } else {
            panic!("Expected Hunks");
        }
    }

    #[test]
    fn test_combine_or_false_with_hunks() {
        let hunks = vec![Range {
            start_byte: 0,
            end_byte: 10,
            start_point: Point { row: 0, column: 0 },
            end_point: Point { row: 0, column: 10 },
        }];
        let result1 = MatchResult::Boolean(false);
        let result2 = MatchResult::Hunks(hunks.clone());
        let combined = result1.combine_with(result2, &LogicalOperator::Or);
        if let MatchResult::Hunks(h) = combined {
            assert_eq!(h.len(), 1);
        } else {
            panic!("Expected Hunks");
        }
    }

    #[test]
    fn test_file_context_sql_profile() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("test.sql");
        fs::write(&file_path, "SELECT 1;").unwrap();

        let mut context = FileContext::new(file_path, dir.path().to_path_buf());
        assert!(context.sql_profile_key().is_none());

        context.set_sql_profile_key("sqlpg");
        assert_eq!(context.sql_profile_key(), Some("sqlpg"));
    }

    #[test]
    fn test_combine_and_hunk_with_true() {
        let hunks = vec![Range {
            start_byte: 0,
            end_byte: 10,
            start_point: Point { row: 0, column: 0 },
            end_point: Point { row: 0, column: 10 },
        }];
        let result1 = MatchResult::Hunks(hunks.clone());
        let result2 = MatchResult::Boolean(true);
        let combined = result1.combine_with(result2, &LogicalOperator::And);
        if let MatchResult::Hunks(h) = combined {
            assert_eq!(h.len(), 1);
        } else {
            panic!("Expected Hunks");
        }
    }

    #[test]
    fn test_combine_and_true_with_hunk() {
        let hunks = vec![Range {
            start_byte: 0,
            end_byte: 10,
            start_point: Point { row: 0, column: 0 },
            end_point: Point { row: 0, column: 10 },
        }];
        let result1 = MatchResult::Boolean(true);
        let result2 = MatchResult::Hunks(hunks.clone());
        let combined = result1.combine_with(result2, &LogicalOperator::And);
        if let MatchResult::Hunks(h) = combined {
            assert_eq!(h.len(), 1);
        } else {
            panic!("Expected Hunks");
        }
    }

    #[test]
    fn test_combine_and_both_true() {
        let result1 = MatchResult::Boolean(true);
        let result2 = MatchResult::Boolean(true);
        let combined = result1.combine_with(result2, &LogicalOperator::And);
        if let MatchResult::Boolean(true) = combined {
            // expected
        } else {
            panic!("Expected Boolean(true)");
        }
    }

    #[test]
    fn test_combine_with_hunks_and() {
        let hunks1 = vec![Range {
            start_byte: 10,
            end_byte: 20,
            start_point: Point { row: 0, column: 0 },
            end_point: Point { row: 0, column: 0 },
        }];
        let hunks2 = vec![Range {
            start_byte: 30,
            end_byte: 40,
            start_point: Point { row: 0, column: 0 },
            end_point: Point { row: 0, column: 0 },
        }];
        let result1 = MatchResult::Hunks(hunks1);
        let result2 = MatchResult::Hunks(hunks2);

        let combined = result1.combine_with(result2, &LogicalOperator::And);

        if let MatchResult::Hunks(h) = combined {
            assert_eq!(h.len(), 2);
            assert_eq!(h[0].start_byte, 10);
            assert_eq!(h[1].start_byte, 30);
        } else {
            panic!("Expected Hunks result");
        }
    }

    #[test]
    fn test_combine_with_hunks_or() {
        let hunks1 = vec![Range {
            start_byte: 10,
            end_byte: 20,
            start_point: Point { row: 0, column: 0 },
            end_point: Point { row: 0, column: 0 },
        }];
        let hunks2 = vec![Range {
            start_byte: 30,
            end_byte: 40,
            start_point: Point { row: 0, column: 0 },
            end_point: Point { row: 0, column: 0 },
        }];
        let result1 = MatchResult::Hunks(hunks1);
        let result2 = MatchResult::Hunks(hunks2);

        let combined = result1.combine_with(result2, &LogicalOperator::Or);

        if let MatchResult::Hunks(h) = combined {
            assert_eq!(h.len(), 2);
        } else {
            panic!("Expected Hunks result");
        }
    }

    #[test]
    fn test_file_context_content_caching() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("test.txt");
        fs::write(&file_path, "hello").unwrap();

        let mut context = FileContext::new(file_path.clone(), dir.path().to_path_buf());

        // First access should read from file
        assert_eq!(context.get_content().unwrap(), "hello");
        assert!(context.content.is_some());

        // Modify the file content
        fs::write(&file_path, "world").unwrap();

        // Second access should return cached content
        assert_eq!(context.get_content().unwrap(), "hello");
    }

    #[test]
    fn test_file_context_tree_caching() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("test.rs");
        fs::write(&file_path, "fn main() {}").unwrap();

        let mut context = FileContext::new(file_path, dir.path().to_path_buf());
        let language: tree_sitter::Language = LANGUAGE.into();

        // First access should parse and cache the tree
        let tree1_sexp = context
            .get_tree("rust", language.clone())
            .unwrap()
            .root_node()
            .to_sexp();
        assert!(context.tree.is_some());

        // Second access should return the cached tree
        let tree2_sexp = context
            .get_tree("rust", language)
            .unwrap()
            .root_node()
            .to_sexp();

        assert_eq!(tree1_sexp, tree2_sexp);
    }

    #[test]
    fn test_file_context_binary_file() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("binary.bin");
        // Write binary content with null bytes
        fs::write(&file_path, b"hello\x00world").unwrap();

        let mut context = FileContext::new(file_path, dir.path().to_path_buf());
        let content = context.get_content().unwrap();
        // Binary files return empty content
        assert_eq!(content, "");
        assert!(!context.content_state().unwrap().is_loaded());
    }

    #[test]
    fn test_file_context_secret_file() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("secrets.txt");
        fs::write(&file_path, "aws_secret_access_key=AKIAIOSFODNN7EXAMPLE").unwrap();

        let mut context = FileContext::new(file_path, dir.path().to_path_buf());
        let content = context.get_content().unwrap();
        // Secret files return empty content
        assert_eq!(content, "");
        assert!(!context.content_state().unwrap().is_loaded());
    }

    #[test]
    fn test_file_context_tree_different_language() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("test.rs");
        fs::write(&file_path, "fn main() {}").unwrap();

        let mut context = FileContext::new(file_path, dir.path().to_path_buf());
        let language: tree_sitter::Language = LANGUAGE.into();

        // First parse with rust
        let _ = context.get_tree("rust", language.clone()).unwrap();
        assert_eq!(context.tree_language_key, Some("rust".to_string()));

        // Parse again with different key - should reparse
        let _ = context.get_tree("other", language).unwrap();
        assert_eq!(context.tree_language_key, Some("other".to_string()));
    }
}
