//! Elrond: Lifecycle Manager & Code Cartographer
//!
//! Elrond is responsible for scanning the workspace, parsing source files using
//! regex-based heuristics, maintaining the knowledge graph (Kuzu), and keeping
//! the vector store (LanceDb) in sync.  It serves as the foundational indexing
//! layer that all other agents depend on for structural code understanding.
//!
//! Named after the Lord of Rivendell who sees far and keeps the histories --
//! Elrond keeps the living map of the entire codebase.

use std::path::{Path, PathBuf};
use std::sync::Arc;

use anyhow::{Context, Result};
use regex::Regex;
use tracing::{debug, info, warn};
use walkdir::WalkDir;

use crate::db::{KuzuGraph, LanceDb};
use crate::types::{FileEntity, FunctionDef, ImportDef, TypeDef};

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

/// Directories to always skip during workspace scanning.
const SKIP_DIRS: &[&str] = &[
    ".git",
    "node_modules",
    "target",
    "dist",
    "build",
    ".next",
    "__pycache__",
    ".venv",
    "venv",
    ".tox",
    "vendor",
    ".idea",
    ".vscode",
];

// Embedding is handled by crate::search::embedder

// ---------------------------------------------------------------------------
// FileEvent
// ---------------------------------------------------------------------------

/// Represents a filesystem event that Elrond should react to.
#[derive(Debug, Clone, PartialEq)]
pub enum FileEvent {
    /// A new file was created.
    Created(PathBuf),
    /// An existing file was modified.
    Modified(PathBuf),
    /// A file was deleted.
    Deleted(PathBuf),
}

// ---------------------------------------------------------------------------
// ScanStats
// ---------------------------------------------------------------------------

/// Summary statistics returned after a workspace scan.
#[derive(Debug, Clone, Default)]
pub struct ScanStats {
    pub files_scanned: usize,
    pub files_indexed: usize,
    pub files_skipped: usize,
    pub errors: usize,
}

// ---------------------------------------------------------------------------
// Elrond
// ---------------------------------------------------------------------------

/// Result of Elrond's execution health check.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ExecutionHealthCheck {
    /// Whether the execution appears to be on track.
    pub healthy: bool,
    /// Whether execution should be halted immediately.
    pub should_halt: bool,
    /// Human-readable reason for the assessment.
    pub reason: String,
    /// Recommendations for improving the execution.
    pub recommendations: Vec<String>,
}

/// The Elrond agent -- lifecycle manager and code cartographer.
pub struct Elrond {
    kuzu: Arc<KuzuGraph>,
    lancedb: Arc<LanceDb>,
    workspace_root: PathBuf,
    llm: Option<Arc<dyn crate::types::LlmProvider>>,
}

impl Elrond {
    // -----------------------------------------------------------------------
    // Construction
    // -----------------------------------------------------------------------

    /// Create a new Elrond agent.
    pub fn new(
        kuzu: Arc<KuzuGraph>,
        lancedb: Arc<LanceDb>,
        workspace_root: PathBuf,
    ) -> Self {
        Self {
            kuzu,
            lancedb,
            workspace_root,
            llm: None,
        }
    }

    /// Attach an LLM provider for execution health checks.
    pub fn with_llm(mut self, llm: Arc<dyn crate::types::LlmProvider>) -> Self {
        self.llm = Some(llm);
        self
    }

    // -----------------------------------------------------------------------
    // Workspace scanning
    // -----------------------------------------------------------------------

    /// Walk the entire workspace directory tree, parse every recognised source
    /// file, and feed the results into the knowledge graph and vector store.
    pub async fn scan_workspace(&self) -> Result<ScanStats> {
        info!(root = %self.workspace_root.display(), "Starting workspace scan");

        let mut stats = ScanStats::default();
        let paths = self.collect_source_files()?;

        for path in &paths {
            stats.files_scanned += 1;
            let path_str = path.to_string_lossy().to_string();

            match self.index_file(&path_str).await {
                Ok(()) => {
                    stats.files_indexed += 1;
                    debug!(path = %path_str, "Indexed file");
                }
                Err(e) => {
                    stats.errors += 1;
                    warn!(path = %path_str, error = %e, "Failed to index file");
                }
            }
        }

        info!(
            scanned = stats.files_scanned,
            indexed = stats.files_indexed,
            errors = stats.errors,
            "Workspace scan complete"
        );

        Ok(stats)
    }

    /// Collect all source file paths under the workspace root, respecting
    /// skip-directory rules and only including files with recognised extensions.
    fn collect_source_files(&self) -> Result<Vec<PathBuf>> {
        let mut paths = Vec::new();

        for entry in WalkDir::new(&self.workspace_root)
            .follow_links(false)
            .into_iter()
            .filter_entry(|e| !Self::should_skip_entry(e))
        {
            let entry = entry.context("Failed to read directory entry")?;
            if entry.file_type().is_file() {
                let path = entry.path();
                if self.is_source_file(path) {
                    paths.push(path.to_path_buf());
                }
            }
        }

        Ok(paths)
    }

    /// Returns `true` when a `walkdir` entry should be pruned from traversal.
    fn should_skip_entry(entry: &walkdir::DirEntry) -> bool {
        if entry.file_type().is_dir() {
            if let Some(name) = entry.file_name().to_str() {
                return SKIP_DIRS.contains(&name);
            }
        }
        false
    }

    /// Returns `true` when the file has a recognised source extension.
    fn is_source_file(&self, path: &Path) -> bool {
        let lang = Self::detect_language(path);
        lang != "unknown"
    }

    // -----------------------------------------------------------------------
    // Language detection
    // -----------------------------------------------------------------------

    /// Map a file extension to a language identifier string.
    pub fn detect_language(path: &Path) -> String {
        let ext = path
            .extension()
            .and_then(|e| e.to_str())
            .unwrap_or("");

        match ext {
            "rs" => "rust",
            "ts" => "typescript",
            "tsx" => "typescriptreact",
            "js" => "javascript",
            "jsx" => "javascriptreact",
            "py" => "python",
            "go" => "go",
            "java" => "java",
            "rb" => "ruby",
            "cpp" | "cc" | "cxx" => "cpp",
            "c" | "h" => "c",
            "cs" => "csharp",
            "swift" => "swift",
            "kt" | "kts" => "kotlin",
            "scala" => "scala",
            "php" => "php",
            "lua" => "lua",
            "zig" => "zig",
            "ex" | "exs" => "elixir",
            "erl" => "erlang",
            "hs" => "haskell",
            "ml" | "mli" => "ocaml",
            "sh" | "bash" => "shell",
            "sql" => "sql",
            "proto" => "protobuf",
            _ => "unknown",
        }
        .to_string()
    }

    // -----------------------------------------------------------------------
    // File parsing
    // -----------------------------------------------------------------------

    /// Parse a single source file using regex-based heuristics.
    ///
    /// The file content is read from disk, the language is detected from the
    /// extension, and language-appropriate regexes are used to extract
    /// functions, types, imports, and exports.
    pub fn parse_file(&self, path: &Path) -> Result<FileEntity> {
        let content = std::fs::read_to_string(path)
            .with_context(|| format!("Failed to read file: {}", path.display()))?;

        let language = Self::detect_language(path);
        let relative = self.relative_path(path);

        let functions = self.extract_functions(&content, &language);
        let types = self.extract_types(&content, &language);
        let imports = self.extract_imports(&content, &language);
        let exports = self.extract_exports(&content, &language);

        Ok(FileEntity {
            path: relative,
            language,
            functions,
            types,
            imports,
            exports,
        })
    }

    /// Parse file content directly (useful for testing without disk I/O).
    pub fn parse_content(
        &self,
        path_str: &str,
        content: &str,
        language: &str,
    ) -> FileEntity {
        let functions = self.extract_functions(content, language);
        let types = self.extract_types(content, language);
        let imports = self.extract_imports(content, language);
        let exports = self.extract_exports(content, language);

        FileEntity {
            path: path_str.to_string(),
            language: language.to_string(),
            functions,
            types,
            imports,
            exports,
        }
    }

    /// Compute a workspace-relative path string.
    fn relative_path(&self, path: &Path) -> String {
        path.strip_prefix(&self.workspace_root)
            .unwrap_or(path)
            .to_string_lossy()
            .to_string()
    }

    // -----------------------------------------------------------------------
    // Function extraction
    // -----------------------------------------------------------------------

    fn extract_functions(&self, content: &str, language: &str) -> Vec<FunctionDef> {
        let patterns = Self::function_patterns(language);
        let mut results = Vec::new();

        for (pattern, public_pattern) in &patterns {
            let re = match Regex::new(pattern) {
                Ok(r) => r,
                Err(_) => continue,
            };
            let pub_re = public_pattern.as_ref().and_then(|p| Regex::new(p).ok());

            for mat in re.find_iter(content) {
                let caps = match re.captures(&content[mat.start()..]) {
                    Some(c) => c,
                    None => continue,
                };

                let name = match caps.get(1) {
                    Some(m) => m.as_str().to_string(),
                    None => continue,
                };

                let line_start = Self::byte_offset_to_line(content, mat.start());
                let line_end = Self::estimate_function_end(content, mat.start(), language);

                let is_public = match &pub_re {
                    Some(pre) => pre.is_match(&content[mat.start()..mat.end() + 20.min(content.len() - mat.end())]),
                    None => Self::is_public_by_default(language),
                };

                // Grab the whole matched line as the signature
                let sig_line_start = content[..mat.start()].rfind('\n').map(|p| p + 1).unwrap_or(0);
                let sig_line_end = content[mat.start()..].find('\n').map(|p| mat.start() + p).unwrap_or(content.len());
                let signature = content[sig_line_start..sig_line_end].trim().to_string();

                results.push(FunctionDef {
                    name,
                    signature,
                    line_start,
                    line_end,
                    is_public,
                });
            }
        }

        results
    }

    /// Return (capture_pattern, optional_public_pattern) tuples for a language.
    fn function_patterns(language: &str) -> Vec<(String, Option<String>)> {
        match language {
            "rust" => vec![
                (
                    r"(?:pub\s+)?(?:async\s+)?fn\s+(\w+)".to_string(),
                    Some(r"^pub\s+".to_string()),
                ),
            ],
            "typescript" | "typescriptreact" | "javascript" | "javascriptreact" => vec![
                (
                    r"(?:export\s+)?(?:async\s+)?function\s+(\w+)".to_string(),
                    Some(r"^export\s+".to_string()),
                ),
                (
                    r"(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(".to_string(),
                    None,
                ),
                (
                    r"(?:export\s+)?(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{".to_string(),
                    Some(r"^export\s+".to_string()),
                ),
            ],
            "python" => vec![
                (
                    r"(?:async\s+)?def\s+(\w+)".to_string(),
                    None,  // Python uses _ prefix convention, not a keyword
                ),
            ],
            "go" => vec![
                (
                    r"func\s+(?:\([^)]+\)\s+)?(\w+)".to_string(),
                    None,  // Go uses capitalisation for public
                ),
            ],
            "java" | "csharp" | "kotlin" => vec![
                (
                    r"(?:public|private|protected|internal)?\s*(?:static\s+)?(?:async\s+)?(?:\w+\s+)+(\w+)\s*\(".to_string(),
                    Some(r"public\s+".to_string()),
                ),
            ],
            "ruby" => vec![
                (
                    r"def\s+(\w+[!?]?)".to_string(),
                    None,
                ),
            ],
            "cpp" | "c" => vec![
                (
                    r"(?:\w+[\s*&]+)+(\w+)\s*\([^)]*\)\s*\{".to_string(),
                    None,
                ),
            ],
            "swift" => vec![
                (
                    r"(?:public\s+|private\s+|internal\s+|open\s+)?func\s+(\w+)".to_string(),
                    Some(r"(?:public|open)\s+".to_string()),
                ),
            ],
            "scala" => vec![
                (
                    r"def\s+(\w+)".to_string(),
                    None,
                ),
            ],
            "php" => vec![
                (
                    r"(?:public|private|protected)?\s*function\s+(\w+)".to_string(),
                    Some(r"public\s+".to_string()),
                ),
            ],
            _ => vec![
                // Generic fallback: look for common function-like patterns
                (r"(?:fn|func|function|def)\s+(\w+)".to_string(), None),
            ],
        }
    }

    // -----------------------------------------------------------------------
    // Type extraction
    // -----------------------------------------------------------------------

    fn extract_types(&self, content: &str, language: &str) -> Vec<TypeDef> {
        let patterns = Self::type_patterns(language);
        let mut results = Vec::new();

        for (pattern, kind_label) in &patterns {
            let re = match Regex::new(pattern) {
                Ok(r) => r,
                Err(_) => continue,
            };

            for caps in re.captures_iter(content) {
                let name = match caps.get(1) {
                    Some(m) => m.as_str().to_string(),
                    None => continue,
                };

                let mat = caps.get(0).unwrap();
                let fields = self.extract_type_fields(content, mat.end(), language);

                results.push(TypeDef {
                    name,
                    kind: kind_label.to_string(),
                    fields,
                });
            }
        }

        results
    }

    /// Return (regex, kind_label) pairs for type definitions per language.
    fn type_patterns(language: &str) -> Vec<(String, String)> {
        match language {
            "rust" => vec![
                (r"(?:pub\s+)?struct\s+(\w+)".to_string(), "struct".to_string()),
                (r"(?:pub\s+)?enum\s+(\w+)".to_string(), "enum".to_string()),
                (r"(?:pub\s+)?trait\s+(\w+)".to_string(), "trait".to_string()),
                (r"(?:pub\s+)?type\s+(\w+)".to_string(), "type_alias".to_string()),
            ],
            "typescript" | "typescriptreact" => vec![
                (r"(?:export\s+)?interface\s+(\w+)".to_string(), "interface".to_string()),
                (r"(?:export\s+)?class\s+(\w+)".to_string(), "class".to_string()),
                (r"(?:export\s+)?type\s+(\w+)".to_string(), "type_alias".to_string()),
                (r"(?:export\s+)?enum\s+(\w+)".to_string(), "enum".to_string()),
            ],
            "javascript" | "javascriptreact" => vec![
                (r"(?:export\s+)?class\s+(\w+)".to_string(), "class".to_string()),
            ],
            "python" => vec![
                (r"class\s+(\w+)".to_string(), "class".to_string()),
            ],
            "go" => vec![
                (r"type\s+(\w+)\s+struct".to_string(), "struct".to_string()),
                (r"type\s+(\w+)\s+interface".to_string(), "interface".to_string()),
            ],
            "java" | "csharp" | "kotlin" => vec![
                (r"(?:public\s+)?class\s+(\w+)".to_string(), "class".to_string()),
                (r"(?:public\s+)?interface\s+(\w+)".to_string(), "interface".to_string()),
                (r"(?:public\s+)?enum\s+(\w+)".to_string(), "enum".to_string()),
            ],
            "ruby" => vec![
                (r"class\s+(\w+)".to_string(), "class".to_string()),
                (r"module\s+(\w+)".to_string(), "module".to_string()),
            ],
            "cpp" | "c" => vec![
                (r"(?:class|struct)\s+(\w+)".to_string(), "class".to_string()),
                (r"enum\s+(?:class\s+)?(\w+)".to_string(), "enum".to_string()),
            ],
            "swift" => vec![
                (r"(?:public\s+)?class\s+(\w+)".to_string(), "class".to_string()),
                (r"(?:public\s+)?struct\s+(\w+)".to_string(), "struct".to_string()),
                (r"(?:public\s+)?enum\s+(\w+)".to_string(), "enum".to_string()),
                (r"(?:public\s+)?protocol\s+(\w+)".to_string(), "protocol".to_string()),
            ],
            _ => vec![
                (r"(?:struct|class|interface|enum)\s+(\w+)".to_string(), "type".to_string()),
            ],
        }
    }

    /// Extract field names from the block following a type definition.
    /// This is a best-effort heuristic that reads the braced body.
    fn extract_type_fields(&self, content: &str, after_name_pos: usize, language: &str) -> Vec<String> {
        let remaining = &content[after_name_pos..];

        // Find the opening brace
        let brace_pos = match remaining.find('{') {
            Some(p) => p,
            None => return Vec::new(),
        };

        // Find the matching closing brace (simple nesting counter)
        let body_start = brace_pos + 1;
        let mut depth: i32 = 1;
        let mut body_end = remaining.len();
        for (i, ch) in remaining[body_start..].char_indices() {
            match ch {
                '{' => depth += 1,
                '}' => {
                    depth -= 1;
                    if depth == 0 {
                        body_end = body_start + i;
                        break;
                    }
                }
                _ => {}
            }
        }

        let body = &remaining[body_start..body_end];
        self.parse_fields_from_body(body, language)
    }

    /// Parse field names out of a type body block.
    fn parse_fields_from_body(&self, body: &str, language: &str) -> Vec<String> {
        let mut fields = Vec::new();

        let field_re = match language {
            "rust" => Regex::new(r"(?:pub\s+)?(\w+)\s*:").ok(),
            "typescript" | "typescriptreact" => Regex::new(r"(\w+)\s*[?]?\s*:").ok(),
            "go" => Regex::new(r"(\w+)\s+\w+").ok(),
            "python" => Regex::new(r"(\w+)\s*[:]").ok(),
            "java" | "csharp" | "kotlin" => Regex::new(r"(?:private|public|protected)?\s*\w+\s+(\w+)\s*[;=]").ok(),
            _ => Regex::new(r"(\w+)\s*:").ok(),
        };

        if let Some(re) = field_re {
            for caps in re.captures_iter(body) {
                if let Some(m) = caps.get(1) {
                    let field_name = m.as_str().to_string();
                    // Filter out common noise keywords
                    if !Self::is_keyword(&field_name, language) {
                        fields.push(field_name);
                    }
                }
            }
        }

        fields
    }

    /// Returns true when the identifier is a language keyword (to avoid
    /// false positives in field extraction).
    fn is_keyword(name: &str, language: &str) -> bool {
        let common = &["fn", "let", "mut", "pub", "self", "return", "if", "else",
                        "for", "while", "loop", "match", "use", "impl", "where",
                        "const", "static", "async", "await", "move", "ref", "in",
                        "true", "false", "type", "struct", "enum", "trait",
                        "function", "var", "class", "interface", "import", "export",
                        "def", "from", "pass", "None", "new", "this", "super",
                        "void", "int", "string", "bool", "float", "double"];

        if common.contains(&name) {
            return true;
        }

        match language {
            "rust" => matches!(name, "crate" | "mod" | "extern" | "unsafe" | "dyn" | "Box" | "Vec" | "Option" | "Result" | "String"),
            "python" => matches!(name, "self" | "cls" | "lambda" | "yield" | "global" | "nonlocal" | "with" | "as" | "try" | "except"),
            _ => false,
        }
    }

    // -----------------------------------------------------------------------
    // Import extraction
    // -----------------------------------------------------------------------

    fn extract_imports(&self, content: &str, language: &str) -> Vec<ImportDef> {
        let patterns = Self::import_patterns(language);
        let mut results = Vec::new();

        for (pattern, source_group, items_group) in &patterns {
            let re = match Regex::new(pattern) {
                Ok(r) => r,
                Err(_) => continue,
            };

            for caps in re.captures_iter(content) {
                let source = caps
                    .get(*source_group)
                    .map(|m| m.as_str().to_string())
                    .unwrap_or_default();

                if source.is_empty() {
                    continue;
                }

                let items: Vec<String> = if *items_group > 0 {
                    caps.get(*items_group)
                        .map(|m| {
                            m.as_str()
                                .split(',')
                                .map(|s| s.trim().trim_matches(|c| c == '{' || c == '}').trim().to_string())
                                .filter(|s| !s.is_empty())
                                .collect()
                        })
                        .unwrap_or_default()
                } else {
                    Vec::new()
                };

                results.push(ImportDef { source, items });
            }
        }

        results
    }

    /// Return (regex, source_capture_group, items_capture_group) tuples.
    /// An items_capture_group of 0 means "no items group".
    fn import_patterns(language: &str) -> Vec<(String, usize, usize)> {
        match language {
            "rust" => vec![
                // use crate::foo::bar;
                (r"use\s+([\w:]+(?:::\{[^}]+\})?)".to_string(), 1, 0),
            ],
            "typescript" | "typescriptreact" | "javascript" | "javascriptreact" => vec![
                // import { Foo, Bar } from 'source'
                (r#"import\s+\{([^}]+)\}\s+from\s+['"](.*?)['"]"#.to_string(), 2, 1),
                // import Foo from 'source'
                (r#"import\s+(\w+)\s+from\s+['"](.*?)['"]"#.to_string(), 2, 1),
                // import * as Foo from 'source'
                (r#"import\s+\*\s+as\s+(\w+)\s+from\s+['"](.*?)['"]"#.to_string(), 2, 1),
                // import 'source' (side-effect)
                (r#"import\s+['"](.*?)['"]"#.to_string(), 1, 0),
                // const foo = require('source')
                (r#"(?:const|let|var)\s+(\w+)\s*=\s*require\s*\(\s*['"](.*?)['"]\s*\)"#.to_string(), 2, 1),
            ],
            "python" => vec![
                // from source import items
                (r"from\s+(\S+)\s+import\s+(.+)".to_string(), 1, 2),
                // import source
                (r"(?m)^import\s+(\S+)".to_string(), 1, 0),
            ],
            "go" => vec![
                // import "source"
                (r#"import\s+"([^"]+)""#.to_string(), 1, 0),
                // Imports inside import() block
                (r#"^\s+"([^"]+)"$"#.to_string(), 1, 0),
            ],
            "java" | "kotlin" => vec![
                (r"import\s+([\w.]+)".to_string(), 1, 0),
            ],
            "ruby" => vec![
                (r#"require\s+['"](.*?)['"]"#.to_string(), 1, 0),
                (r#"require_relative\s+['"](.*?)['"]"#.to_string(), 1, 0),
            ],
            "cpp" | "c" => vec![
                (r#"#include\s+[<"](.*?)[>"]"#.to_string(), 1, 0),
            ],
            "csharp" => vec![
                (r"using\s+([\w.]+)\s*;".to_string(), 1, 0),
            ],
            "swift" => vec![
                (r"import\s+(\w+)".to_string(), 1, 0),
            ],
            _ => vec![
                (r#"(?:import|require|use|include)\s+['"]?([^\s'"]+)"#.to_string(), 1, 0),
            ],
        }
    }

    // -----------------------------------------------------------------------
    // Export extraction
    // -----------------------------------------------------------------------

    fn extract_exports(&self, content: &str, language: &str) -> Vec<String> {
        let patterns = Self::export_patterns(language);
        let mut results = Vec::new();

        for pattern in &patterns {
            let re = match Regex::new(pattern) {
                Ok(r) => r,
                Err(_) => continue,
            };

            for caps in re.captures_iter(content) {
                if let Some(m) = caps.get(1) {
                    let export_name = m.as_str().to_string();
                    if !results.contains(&export_name) {
                        results.push(export_name);
                    }
                }
            }
        }

        results
    }

    fn export_patterns(language: &str) -> Vec<String> {
        match language {
            "rust" => vec![
                r"pub\s+fn\s+(\w+)".to_string(),
                r"pub\s+struct\s+(\w+)".to_string(),
                r"pub\s+enum\s+(\w+)".to_string(),
                r"pub\s+trait\s+(\w+)".to_string(),
                r"pub\s+type\s+(\w+)".to_string(),
                r"pub\s+const\s+(\w+)".to_string(),
                r"pub\s+static\s+(\w+)".to_string(),
                r"pub\s+mod\s+(\w+)".to_string(),
            ],
            "typescript" | "typescriptreact" | "javascript" | "javascriptreact" => vec![
                r"export\s+(?:default\s+)?function\s+(\w+)".to_string(),
                r"export\s+(?:default\s+)?class\s+(\w+)".to_string(),
                r"export\s+(?:default\s+)?(?:const|let|var)\s+(\w+)".to_string(),
                r"export\s+interface\s+(\w+)".to_string(),
                r"export\s+type\s+(\w+)".to_string(),
                r"export\s+enum\s+(\w+)".to_string(),
                r"module\.exports\s*=\s*(\w+)".to_string(),
                r"exports\.(\w+)".to_string(),
            ],
            "python" => vec![
                // Python: everything at module level is implicitly exported,
                // but we look for __all__ items and top-level defs.
                r"def\s+(\w+)".to_string(),
                r"class\s+(\w+)".to_string(),
            ],
            "go" => vec![
                // Go: exported names start with an uppercase letter.
                // We capture function names and let the caller check capitalisation.
                r"func\s+(?:\([^)]+\)\s+)?([A-Z]\w*)".to_string(),
                r"type\s+([A-Z]\w*)\s+".to_string(),
            ],
            _ => vec![
                r"(?:pub|public|export)\s+(?:fn|function|class|struct|enum|const|type|interface)\s+(\w+)".to_string(),
            ],
        }
    }

    // -----------------------------------------------------------------------
    // Indexing
    // -----------------------------------------------------------------------

    /// Parse a file, push the resulting `FileEntity` into the Kuzu graph,
    /// and upsert a placeholder embedding into LanceDb.
    pub async fn index_file(&self, path: &str) -> Result<()> {
        let full_path = self.resolve_path(path);
        let entity = self.parse_file(&full_path)?;

        // Update the knowledge graph
        self.kuzu
            .add_file_to_graph(&entity)
            .await
            .with_context(|| format!("Failed to add {} to knowledge graph", path))?;

        // Generate a text embedding from the file's content summary and upsert
        // into the vector store for Tier 1 semantic search.
        let content_summary = Self::build_content_summary(&entity);
        let embedding = crate::search::embedder::embed_text(&content_summary);
        self.lancedb
            .upsert(&entity.path, &content_summary, embedding)
            .await
            .with_context(|| format!("Failed to upsert {} into vector store", path))?;

        debug!(path = %entity.path, "Indexed file into graph and vector store");
        Ok(())
    }

    /// Build a textual summary of a file entity for embedding.
    fn build_content_summary(entity: &FileEntity) -> String {
        let mut parts = Vec::new();
        parts.push(format!("file: {} ({})", entity.path, entity.language));

        if !entity.functions.is_empty() {
            let fn_names: Vec<&str> = entity.functions.iter().map(|f| f.name.as_str()).collect();
            parts.push(format!("functions: {}", fn_names.join(", ")));
        }
        if !entity.types.is_empty() {
            let type_names: Vec<&str> = entity.types.iter().map(|t| t.name.as_str()).collect();
            parts.push(format!("types: {}", type_names.join(", ")));
        }
        if !entity.imports.is_empty() {
            let import_sources: Vec<&str> = entity.imports.iter().map(|i| i.source.as_str()).collect();
            parts.push(format!("imports: {}", import_sources.join(", ")));
        }
        if !entity.exports.is_empty() {
            parts.push(format!("exports: {}", entity.exports.join(", ")));
        }

        parts.join("\n")
    }

    // -----------------------------------------------------------------------
    // File event handling
    // -----------------------------------------------------------------------

    /// Handle a filesystem event (create, modify, or delete).
    pub async fn handle_file_event(&self, event: FileEvent) -> Result<()> {
        match event {
            FileEvent::Created(ref path) | FileEvent::Modified(ref path) => {
                let path_str = self.relative_path(path);
                info!(path = %path_str, event = ?event, "Handling file event");

                if self.is_source_file(path) {
                    self.index_file(&path_str).await?;
                } else {
                    debug!(path = %path_str, "Skipping non-source file");
                }
            }
            FileEvent::Deleted(ref path) => {
                let path_str = self.relative_path(path);
                info!(path = %path_str, "Handling file deletion");

                // Remove from the knowledge graph
                self.kuzu
                    .remove_file(&path_str)
                    .await
                    .with_context(|| format!("Failed to remove {} from knowledge graph", path_str))?;

                // Remove from the vector store
                self.lancedb
                    .remove(&path_str)
                    .await
                    .with_context(|| format!("Failed to remove {} from vector store", path_str))?;
            }
        }

        Ok(())
    }

    // -----------------------------------------------------------------------
    // Selective re-indexing
    // -----------------------------------------------------------------------

    /// Re-index a set of specific files after they have been modified.
    ///
    /// This is called after AutoForge writes files to ensure the knowledge
    /// graph and vector store reflect the latest state. Files that don't
    /// exist (e.g., deleted) are silently skipped.
    pub async fn index_files(&self, paths: &[std::path::PathBuf]) -> Result<ScanStats> {
        let mut stats = ScanStats::default();

        for path in paths {
            if !path.exists() {
                debug!(path = %path.display(), "Skipping non-existent file during re-index");
                stats.files_skipped += 1;
                continue;
            }

            if !self.is_source_file(path) {
                debug!(path = %path.display(), "Skipping non-source file during re-index");
                stats.files_skipped += 1;
                continue;
            }

            stats.files_scanned += 1;
            let path_str = self.relative_path(path);

            match self.index_file(&path_str).await {
                Ok(()) => {
                    stats.files_indexed += 1;
                    debug!(path = %path_str, "Re-indexed file");
                }
                Err(e) => {
                    stats.errors += 1;
                    warn!(path = %path_str, error = %e, "Failed to re-index file");
                }
            }
        }

        info!(
            scanned = stats.files_scanned,
            indexed = stats.files_indexed,
            skipped = stats.files_skipped,
            errors = stats.errors,
            "Selective re-indexing complete"
        );

        Ok(stats)
    }

    // -----------------------------------------------------------------------
    // Execution health check
    // -----------------------------------------------------------------------

    /// Verify execution health using LLM analysis.
    ///
    /// Called by Aragorn between execution levels (not per-task) to detect
    /// whether the scroll execution is going off the rails. If no LLM is
    /// available, returns a default "healthy" result.
    pub async fn verify_execution_health(
        &self,
        scroll: &crate::types::Scroll,
        completed_tasks: &[crate::types::Task],
        remaining_tasks: &[crate::types::Task],
        failed_tasks: &[(String, String)], // (task_id, error)
    ) -> Result<ExecutionHealthCheck> {
        let llm = match self.llm.as_ref().filter(|l| l.is_available()) {
            Some(llm) => llm,
            None => {
                return Ok(ExecutionHealthCheck {
                    healthy: true,
                    should_halt: false,
                    reason: "No LLM available for health check".to_string(),
                    recommendations: vec![],
                });
            }
        };

        let completed_summary: Vec<String> = completed_tasks
            .iter()
            .map(|t| format!("- [done] {} -> {}", t.description, t.target_file))
            .collect();

        let remaining_summary: Vec<String> = remaining_tasks
            .iter()
            .map(|t| format!("- [pending] {} -> {}", t.description, t.target_file))
            .collect();

        let failed_summary: Vec<String> = failed_tasks
            .iter()
            .map(|(id, err)| format!("- [FAILED] {}: {}", id, err))
            .collect();

        let prompt = format!(
            "## Scroll Intent\n{}\n\n## Completed Tasks\n{}\n\n## Remaining Tasks\n{}\n\n## Failed Tasks\n{}\n\n\
             Assess the execution health. Respond with ONLY valid JSON (no markdown fences):\n\
             {{\"healthy\": bool, \"should_halt\": bool, \"reason\": \"...\", \"recommendations\": [\"...\"]}}",
            scroll.intent,
            if completed_summary.is_empty() { "None".to_string() } else { completed_summary.join("\n") },
            if remaining_summary.is_empty() { "None".to_string() } else { remaining_summary.join("\n") },
            if failed_summary.is_empty() { "None".to_string() } else { failed_summary.join("\n") },
        );

        let system = "You are Elrond, the vigilant lifecycle guardian of the FURNACE system. \
                       Given a scroll's intent, completed tasks, remaining tasks, and failures, \
                       assess whether execution is on track. Check: (1) Is the direction correct? \
                       (2) Are failures recoverable or should execution halt? \
                       (3) Are remaining tasks still relevant? Respond as JSON only.";

        match tokio::time::timeout(
            std::time::Duration::from_secs(60),
            llm.complete_with_system(system, &prompt),
        ).await {
            Err(_) => {
                warn!("Elrond health check LLM call timed out after 60s");
                Ok(ExecutionHealthCheck {
                    healthy: true,
                    should_halt: false,
                    reason: "Health check timed out after 60s".to_string(),
                    recommendations: vec![],
                })
            }
            Ok(Ok(response)) => {
                let cleaned = response.trim().trim_start_matches("```json").trim_end_matches("```").trim();
                match serde_json::from_str::<ExecutionHealthCheck>(cleaned) {
                    Ok(check) => {
                        info!(
                            scroll_id = %scroll.id,
                            healthy = check.healthy,
                            should_halt = check.should_halt,
                            "Elrond health check complete"
                        );
                        Ok(check)
                    }
                    Err(e) => {
                        warn!(error = %e, "Failed to parse Elrond health check response");
                        Ok(ExecutionHealthCheck {
                            healthy: true,
                            should_halt: false,
                            reason: format!("Health check response unparseable: {}", e),
                            recommendations: vec![],
                        })
                    }
                }
            }
            Ok(Err(e)) => {
                warn!(error = %e, "Elrond health check LLM call failed");
                Ok(ExecutionHealthCheck {
                    healthy: true,
                    should_halt: false,
                    reason: format!("Health check unavailable: {}", e),
                    recommendations: vec![],
                })
            }
        }
    }

    // -----------------------------------------------------------------------
    // Query interface
    // -----------------------------------------------------------------------

    /// Find all files of a given language by querying the knowledge graph.
    pub async fn find_files_by_language(&self, language: &str) -> Result<Vec<String>> {
        // Walk the knowledge graph's file store
        let mut matching = Vec::new();

        // Since KuzuGraph stores files by path, we iterate over all files
        // and check their language. For a real graph DB this would be a query.
        let all_paths = self.collect_source_files()?;
        for path in &all_paths {
            let detected = Self::detect_language(path);
            if detected == language {
                matching.push(self.relative_path(path));
            }
        }

        Ok(matching)
    }

    /// Search for functions by name across all indexed files.
    ///
    /// Returns a vector of (file_path, FunctionDef) pairs.
    pub async fn find_functions_by_name(
        &self,
        name: &str,
    ) -> Result<Vec<(String, FunctionDef)>> {
        let mut results = Vec::new();
        let paths = self.collect_source_files()?;

        for path in &paths {
            match self.parse_file(path) {
                Ok(entity) => {
                    for func in &entity.functions {
                        if func.name == name {
                            results.push((entity.path.clone(), func.clone()));
                        }
                    }
                }
                Err(e) => {
                    debug!(path = %path.display(), error = %e, "Skipping unparseable file in search");
                }
            }
        }

        Ok(results)
    }

    /// Query the knowledge graph for files that the given file depends on.
    pub async fn get_file_dependencies(&self, path: &str) -> Result<Vec<String>> {
        self.kuzu
            .get_dependencies(path)
            .await
            .context("Failed to query dependencies")
    }

    /// Query the knowledge graph for files that depend on the given file.
    pub async fn get_file_dependents(&self, path: &str) -> Result<Vec<String>> {
        self.kuzu
            .get_dependents(path)
            .await
            .context("Failed to query dependents")
    }

    /// Retrieve the parsed `FileEntity` for a specific path from the graph.
    pub async fn get_file_entity(&self, path: &str) -> Result<Option<FileEntity>> {
        self.kuzu
            .get_file(path)
            .await
            .context("Failed to get file entity from graph")
    }

    /// Return a count of indexed documents in the vector store.
    pub async fn vector_store_count(&self) -> usize {
        self.lancedb.count().await
    }

    // -----------------------------------------------------------------------
    // Helpers
    // -----------------------------------------------------------------------

    /// Resolve a potentially relative path against the workspace root.
    fn resolve_path(&self, path: &str) -> PathBuf {
        let p = PathBuf::from(path);
        if p.is_absolute() {
            p
        } else {
            self.workspace_root.join(p)
        }
    }

    /// Convert a byte offset into a 1-based line number.
    fn byte_offset_to_line(content: &str, offset: usize) -> usize {
        content[..offset].matches('\n').count() + 1
    }

    /// Estimate the end line of a function starting at `offset`.
    ///
    /// Uses a simple brace-counting heuristic.  For languages that use
    /// indentation (Python), falls back to scanning for the next blank line or
    /// the next definition at the same indentation.
    fn estimate_function_end(content: &str, offset: usize, language: &str) -> usize {
        let start_line = Self::byte_offset_to_line(content, offset);

        if language == "python" {
            return Self::estimate_python_function_end(content, offset, start_line);
        }

        // Brace-based languages: count { } from the function signature onwards.
        let remaining = &content[offset..];
        let brace_start = match remaining.find('{') {
            Some(pos) => pos,
            None => return start_line, // No body found
        };

        let mut depth: i32 = 0;
        for (i, ch) in remaining[brace_start..].char_indices() {
            match ch {
                '{' => depth += 1,
                '}' => {
                    depth -= 1;
                    if depth == 0 {
                        let end_offset = offset + brace_start + i;
                        return Self::byte_offset_to_line(content, end_offset);
                    }
                }
                _ => {}
            }
        }

        // If we never balanced braces, just estimate a reasonable range
        (start_line + 20).min(content.matches('\n').count() + 1)
    }

    /// Estimate function end for Python using indentation heuristics.
    fn estimate_python_function_end(content: &str, _offset: usize, start_line: usize) -> usize {
        let lines: Vec<&str> = content.lines().collect();
        if start_line == 0 || start_line > lines.len() {
            return start_line;
        }

        // Find the indentation of the def line
        let def_line = lines[start_line - 1];
        let def_indent = def_line.len() - def_line.trim_start().len();

        // Walk forward until we find a line at the same or lesser indentation
        // (that isn't blank)
        let mut last_body_line = start_line;
        for (i, line) in lines[start_line..].iter().enumerate() {
            let trimmed = line.trim();
            if trimmed.is_empty() {
                continue;
            }
            let indent = line.len() - trimmed.len();
            if indent <= def_indent {
                break;
            }
            last_body_line = start_line + i + 1;
        }

        last_body_line
    }

    /// In some languages all top-level definitions are public by default.
    fn is_public_by_default(language: &str) -> bool {
        matches!(language, "python" | "javascript" | "javascriptreact" | "ruby" | "go")
    }
}

// ===========================================================================
// Tests
// ===========================================================================

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::TempDir;

    /// Helper: build an Elrond instance backed by a temporary workspace.
    async fn make_test_elrond(tmp: &TempDir) -> Elrond {
        let workspace = tmp.path().to_path_buf();
        let data_dir = tmp.path().join(".furnace_data");
        fs::create_dir_all(&data_dir).unwrap();

        let kuzu = Arc::new(
            KuzuGraph::new(&data_dir.join("graph")).expect("kuzu"),
        );
        let lancedb = Arc::new(
            LanceDb::new(&data_dir.join("vectors")).await.expect("lancedb"),
        );

        Elrond::new(kuzu, lancedb, workspace)
    }

    /// Helper: write a file into the temp workspace.
    fn write_file(tmp: &TempDir, relative: &str, content: &str) -> PathBuf {
        let path = tmp.path().join(relative);
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).unwrap();
        }
        fs::write(&path, content).unwrap();
        path
    }

    // -----------------------------------------------------------------------
    // Test: language detection
    // -----------------------------------------------------------------------

    #[test]
    fn test_detect_language_rust() {
        assert_eq!(Elrond::detect_language(Path::new("main.rs")), "rust");
    }

    #[test]
    fn test_detect_language_typescript() {
        assert_eq!(Elrond::detect_language(Path::new("app.ts")), "typescript");
        assert_eq!(Elrond::detect_language(Path::new("App.tsx")), "typescriptreact");
    }

    #[test]
    fn test_detect_language_javascript() {
        assert_eq!(Elrond::detect_language(Path::new("index.js")), "javascript");
        assert_eq!(Elrond::detect_language(Path::new("App.jsx")), "javascriptreact");
    }

    #[test]
    fn test_detect_language_python() {
        assert_eq!(Elrond::detect_language(Path::new("script.py")), "python");
    }

    #[test]
    fn test_detect_language_go() {
        assert_eq!(Elrond::detect_language(Path::new("main.go")), "go");
    }

    #[test]
    fn test_detect_language_java() {
        assert_eq!(Elrond::detect_language(Path::new("Main.java")), "java");
    }

    #[test]
    fn test_detect_language_ruby() {
        assert_eq!(Elrond::detect_language(Path::new("app.rb")), "ruby");
    }

    #[test]
    fn test_detect_language_cpp() {
        assert_eq!(Elrond::detect_language(Path::new("main.cpp")), "cpp");
        assert_eq!(Elrond::detect_language(Path::new("util.cc")), "cpp");
        assert_eq!(Elrond::detect_language(Path::new("lib.cxx")), "cpp");
    }

    #[test]
    fn test_detect_language_c() {
        assert_eq!(Elrond::detect_language(Path::new("main.c")), "c");
        assert_eq!(Elrond::detect_language(Path::new("header.h")), "c");
    }

    #[test]
    fn test_detect_language_csharp() {
        assert_eq!(Elrond::detect_language(Path::new("Program.cs")), "csharp");
    }

    #[test]
    fn test_detect_language_swift() {
        assert_eq!(Elrond::detect_language(Path::new("App.swift")), "swift");
    }

    #[test]
    fn test_detect_language_kotlin() {
        assert_eq!(Elrond::detect_language(Path::new("Main.kt")), "kotlin");
        assert_eq!(Elrond::detect_language(Path::new("build.kts")), "kotlin");
    }

    #[test]
    fn test_detect_language_unknown() {
        assert_eq!(Elrond::detect_language(Path::new("README.md")), "unknown");
        assert_eq!(Elrond::detect_language(Path::new("Makefile")), "unknown");
    }

    // -----------------------------------------------------------------------
    // Test: Rust parsing
    // -----------------------------------------------------------------------

    #[tokio::test]
    async fn test_parse_rust_file() {
        let tmp = TempDir::new().unwrap();
        let elrond = make_test_elrond(&tmp).await;

        let rust_src = r#"
use std::sync::Arc;
use crate::db::SqliteDb;

pub struct MyStruct {
    pub name: String,
    pub value: i32,
}

pub enum Status {
    Active,
    Inactive,
}

pub fn hello_world() {
    println!("Hello, world!");
}

fn private_helper(x: i32) -> i32 {
    x + 1
}

pub async fn do_work(db: Arc<SqliteDb>) -> Result<()> {
    Ok(())
}
"#;

        let entity = elrond.parse_content("src/lib.rs", rust_src, "rust");

        assert_eq!(entity.language, "rust");
        assert_eq!(entity.path, "src/lib.rs");

        // Functions
        assert!(entity.functions.len() >= 3, "Expected at least 3 functions, got {}", entity.functions.len());
        let fn_names: Vec<&str> = entity.functions.iter().map(|f| f.name.as_str()).collect();
        assert!(fn_names.contains(&"hello_world"), "Missing hello_world: {:?}", fn_names);
        assert!(fn_names.contains(&"private_helper"), "Missing private_helper: {:?}", fn_names);
        assert!(fn_names.contains(&"do_work"), "Missing do_work: {:?}", fn_names);

        // Public detection
        let hello = entity.functions.iter().find(|f| f.name == "hello_world").unwrap();
        assert!(hello.is_public);

        // Types
        assert!(entity.types.len() >= 2, "Expected at least 2 types, got {}", entity.types.len());
        let type_names: Vec<&str> = entity.types.iter().map(|t| t.name.as_str()).collect();
        assert!(type_names.contains(&"MyStruct"), "Missing MyStruct: {:?}", type_names);
        assert!(type_names.contains(&"Status"), "Missing Status: {:?}", type_names);

        // Struct fields
        let my_struct = entity.types.iter().find(|t| t.name == "MyStruct").unwrap();
        assert!(my_struct.fields.contains(&"name".to_string()), "Missing field 'name': {:?}", my_struct.fields);
        assert!(my_struct.fields.contains(&"value".to_string()), "Missing field 'value': {:?}", my_struct.fields);

        // Imports
        assert!(!entity.imports.is_empty(), "Expected some imports");
        let import_sources: Vec<&str> = entity.imports.iter().map(|i| i.source.as_str()).collect();
        assert!(import_sources.iter().any(|s| s.contains("std::sync::Arc")), "Missing std::sync::Arc import: {:?}", import_sources);

        // Exports
        assert!(!entity.exports.is_empty(), "Expected some exports");
        assert!(entity.exports.contains(&"hello_world".to_string()), "Missing export hello_world: {:?}", entity.exports);
    }

    // -----------------------------------------------------------------------
    // Test: TypeScript parsing
    // -----------------------------------------------------------------------

    #[tokio::test]
    async fn test_parse_typescript_file() {
        let tmp = TempDir::new().unwrap();
        let elrond = make_test_elrond(&tmp).await;

        let ts_src = r#"
import { useState } from 'react';
import axios from 'axios';

export interface User {
    name: string;
    email: string;
    age?: number;
}

export class UserService {
    private baseUrl: string;

    constructor(url: string) {
        this.baseUrl = url;
    }
}

export function greet(name: string): string {
    return `Hello, ${name}!`;
}

export async function fetchUser(id: number): Promise<User> {
    const response = await axios.get(`/users/${id}`);
    return response.data;
}

const helper = (x: number) => x + 1;
"#;

        let entity = elrond.parse_content("src/user.ts", ts_src, "typescript");

        assert_eq!(entity.language, "typescript");

        // Functions
        let fn_names: Vec<&str> = entity.functions.iter().map(|f| f.name.as_str()).collect();
        assert!(fn_names.contains(&"greet"), "Missing greet: {:?}", fn_names);
        assert!(fn_names.contains(&"fetchUser"), "Missing fetchUser: {:?}", fn_names);

        // Types
        let type_names: Vec<&str> = entity.types.iter().map(|t| t.name.as_str()).collect();
        assert!(type_names.contains(&"User"), "Missing User interface: {:?}", type_names);
        assert!(type_names.contains(&"UserService"), "Missing UserService class: {:?}", type_names);

        // Interface kind
        let user = entity.types.iter().find(|t| t.name == "User").unwrap();
        assert_eq!(user.kind, "interface");

        // Imports
        assert!(entity.imports.len() >= 2, "Expected at least 2 imports, got {}", entity.imports.len());
        let import_sources: Vec<&str> = entity.imports.iter().map(|i| i.source.as_str()).collect();
        assert!(import_sources.contains(&"react"), "Missing react import: {:?}", import_sources);
        assert!(import_sources.contains(&"axios"), "Missing axios import: {:?}", import_sources);

        // Check that destructured import items are extracted
        let react_import = entity.imports.iter().find(|i| i.source == "react").unwrap();
        assert!(react_import.items.iter().any(|i| i.contains("useState")), "Missing useState item: {:?}", react_import.items);

        // Exports
        assert!(!entity.exports.is_empty());
        assert!(entity.exports.contains(&"greet".to_string()), "Missing export greet: {:?}", entity.exports);
    }

    // -----------------------------------------------------------------------
    // Test: Python parsing
    // -----------------------------------------------------------------------

    #[tokio::test]
    async fn test_parse_python_file() {
        let tmp = TempDir::new().unwrap();
        let elrond = make_test_elrond(&tmp).await;

        let py_src = r#"
from typing import List, Optional
import os
from pathlib import Path

class Animal:
    name: str
    sound: str

    def __init__(self, name: str, sound: str):
        self.name = name
        self.sound = sound

    def speak(self) -> str:
        return f"{self.name} says {self.sound}"

def greet(name: str) -> str:
    return f"Hello, {name}!"

async def fetch_data(url: str) -> dict:
    pass
"#;

        let entity = elrond.parse_content("animals.py", py_src, "python");

        assert_eq!(entity.language, "python");

        // Functions
        let fn_names: Vec<&str> = entity.functions.iter().map(|f| f.name.as_str()).collect();
        assert!(fn_names.contains(&"greet"), "Missing greet: {:?}", fn_names);
        assert!(fn_names.contains(&"fetch_data"), "Missing fetch_data: {:?}", fn_names);
        assert!(fn_names.contains(&"__init__"), "Missing __init__: {:?}", fn_names);
        assert!(fn_names.contains(&"speak"), "Missing speak: {:?}", fn_names);

        // Types
        let type_names: Vec<&str> = entity.types.iter().map(|t| t.name.as_str()).collect();
        assert!(type_names.contains(&"Animal"), "Missing Animal class: {:?}", type_names);

        // Imports
        let import_sources: Vec<&str> = entity.imports.iter().map(|i| i.source.as_str()).collect();
        assert!(import_sources.contains(&"typing"), "Missing typing import: {:?}", import_sources);
        assert!(import_sources.contains(&"os") || import_sources.iter().any(|s| s.contains("os")),
            "Missing os import: {:?}", import_sources);
        assert!(import_sources.contains(&"pathlib"), "Missing pathlib import: {:?}", import_sources);
    }

    // -----------------------------------------------------------------------
    // Test: Go parsing
    // -----------------------------------------------------------------------

    #[tokio::test]
    async fn test_parse_go_file() {
        let tmp = TempDir::new().unwrap();
        let elrond = make_test_elrond(&tmp).await;

        let go_src = r#"
package main

import "fmt"

type Server struct {
    host string
    port int
}

type Handler interface {
    Handle(req Request) Response
}

func NewServer(host string, port int) *Server {
    return &Server{host: host, port: port}
}

func (s *Server) Start() error {
    fmt.Printf("Starting on %s:%d\n", s.host, s.port)
    return nil
}
"#;

        let entity = elrond.parse_content("main.go", go_src, "go");

        assert_eq!(entity.language, "go");

        // Functions
        let fn_names: Vec<&str> = entity.functions.iter().map(|f| f.name.as_str()).collect();
        assert!(fn_names.contains(&"NewServer"), "Missing NewServer: {:?}", fn_names);
        assert!(fn_names.contains(&"Start"), "Missing Start: {:?}", fn_names);

        // Types
        let type_names: Vec<&str> = entity.types.iter().map(|t| t.name.as_str()).collect();
        assert!(type_names.contains(&"Server"), "Missing Server: {:?}", type_names);
        assert!(type_names.contains(&"Handler"), "Missing Handler: {:?}", type_names);

        // Type kinds
        let server = entity.types.iter().find(|t| t.name == "Server").unwrap();
        assert_eq!(server.kind, "struct");
        let handler = entity.types.iter().find(|t| t.name == "Handler").unwrap();
        assert_eq!(handler.kind, "interface");
    }

    // -----------------------------------------------------------------------
    // Test: workspace scanning
    // -----------------------------------------------------------------------

    #[tokio::test]
    async fn test_scan_workspace() {
        let tmp = TempDir::new().unwrap();

        write_file(&tmp, "src/main.rs", "pub fn main() {}");
        write_file(&tmp, "src/lib.rs", "pub fn helper() {}");
        write_file(&tmp, "src/utils.py", "def util(): pass");
        write_file(&tmp, "README.md", "# Hello");
        // This should be skipped:
        write_file(&tmp, "node_modules/dep/index.js", "function dep() {}");
        write_file(&tmp, ".git/config", "");

        let elrond = make_test_elrond(&tmp).await;
        let stats = elrond.scan_workspace().await.unwrap();

        assert!(stats.files_scanned >= 3, "Expected at least 3 source files scanned, got {}", stats.files_scanned);
        assert_eq!(stats.files_indexed, stats.files_scanned, "All scanned files should be indexed");
        assert_eq!(stats.errors, 0, "Expected no errors");
        // README.md should not be scanned (unknown extension)
        // node_modules should be skipped entirely
    }

    // -----------------------------------------------------------------------
    // Test: indexing + knowledge graph
    // -----------------------------------------------------------------------

    #[tokio::test]
    async fn test_index_and_query_graph() {
        let tmp = TempDir::new().unwrap();

        write_file(
            &tmp,
            "src/main.rs",
            r#"
use crate::utils::helper;

pub fn main() {
    helper();
}
"#,
        );
        write_file(
            &tmp,
            "src/utils.rs",
            r#"
pub fn helper() -> String {
    "hello".to_string()
}
"#,
        );

        let elrond = make_test_elrond(&tmp).await;

        // Index both files
        elrond.index_file("src/main.rs").await.unwrap();
        elrond.index_file("src/utils.rs").await.unwrap();

        // Verify files are in the graph
        let main_entity = elrond.get_file_entity("src/main.rs").await.unwrap();
        assert!(main_entity.is_some(), "main.rs should be in the graph");

        let utils_entity = elrond.get_file_entity("src/utils.rs").await.unwrap();
        assert!(utils_entity.is_some(), "utils.rs should be in the graph");

        // Verify vector store has entries
        let count = elrond.vector_store_count().await;
        assert_eq!(count, 2, "Expected 2 vector store entries, got {}", count);

        // Check dependencies (main.rs imports from crate::utils::helper)
        let deps = elrond.get_file_dependencies("src/main.rs").await.unwrap();
        assert!(!deps.is_empty(), "main.rs should have dependencies: {:?}", deps);
    }

    // -----------------------------------------------------------------------
    // Test: file events
    // -----------------------------------------------------------------------

    #[tokio::test]
    async fn test_handle_file_events() {
        let tmp = TempDir::new().unwrap();
        let elrond = make_test_elrond(&tmp).await;

        // Create event
        let file_path = write_file(&tmp, "src/new_file.rs", "pub fn created() {}");
        elrond
            .handle_file_event(FileEvent::Created(file_path.clone()))
            .await
            .unwrap();

        let entity = elrond.get_file_entity("src/new_file.rs").await.unwrap();
        assert!(entity.is_some(), "Created file should be in graph");

        // Modify event
        fs::write(&file_path, "pub fn modified() {}\npub fn extra() {}").unwrap();
        elrond
            .handle_file_event(FileEvent::Modified(file_path.clone()))
            .await
            .unwrap();

        let entity = elrond.get_file_entity("src/new_file.rs").await.unwrap().unwrap();
        let fn_names: Vec<&str> = entity.functions.iter().map(|f| f.name.as_str()).collect();
        assert!(fn_names.contains(&"modified"), "Modified file should have 'modified' function: {:?}", fn_names);

        // Delete event
        fs::remove_file(&file_path).unwrap();
        elrond
            .handle_file_event(FileEvent::Deleted(file_path))
            .await
            .unwrap();

        let entity = elrond.get_file_entity("src/new_file.rs").await.unwrap();
        assert!(entity.is_none(), "Deleted file should be removed from graph");
    }

    // -----------------------------------------------------------------------
    // Test: line number estimation
    // -----------------------------------------------------------------------

    #[test]
    fn test_byte_offset_to_line() {
        let content = "line1\nline2\nline3\nline4\n";
        assert_eq!(Elrond::byte_offset_to_line(content, 0), 1);  // start of line1
        assert_eq!(Elrond::byte_offset_to_line(content, 6), 2);  // start of line2
        assert_eq!(Elrond::byte_offset_to_line(content, 12), 3); // start of line3
    }

    // -----------------------------------------------------------------------
    // Test: skip directory filtering
    // -----------------------------------------------------------------------

    #[tokio::test]
    async fn test_skip_directories() {
        let tmp = TempDir::new().unwrap();

        write_file(&tmp, "src/good.rs", "pub fn good() {}");
        write_file(&tmp, "target/debug/bad.rs", "pub fn bad() {}");
        write_file(&tmp, "node_modules/pkg/bad.js", "function bad() {}");
        write_file(&tmp, ".git/objects/bad", "binary");
        write_file(&tmp, "dist/bundle.js", "function bundle() {}");
        write_file(&tmp, "build/output.js", "function output() {}");

        let elrond = make_test_elrond(&tmp).await;
        let paths = elrond.collect_source_files().unwrap();

        let relative_paths: Vec<String> = paths.iter()
            .map(|p| elrond.relative_path(p))
            .collect();

        assert!(relative_paths.iter().any(|p| p.contains("good.rs")),
            "src/good.rs should be included: {:?}", relative_paths);
        assert!(!relative_paths.iter().any(|p| p.contains("target")),
            "target/ files should be skipped: {:?}", relative_paths);
        assert!(!relative_paths.iter().any(|p| p.contains("node_modules")),
            "node_modules/ files should be skipped: {:?}", relative_paths);
        assert!(!relative_paths.iter().any(|p| p.contains(".git")),
            ".git/ files should be skipped: {:?}", relative_paths);
    }

    // -----------------------------------------------------------------------
    // Test: content summary generation
    // -----------------------------------------------------------------------

    #[test]
    fn test_build_content_summary() {
        let entity = FileEntity {
            path: "src/main.rs".to_string(),
            language: "rust".to_string(),
            functions: vec![
                FunctionDef {
                    name: "main".to_string(),
                    signature: "pub fn main()".to_string(),
                    line_start: 1,
                    line_end: 5,
                    is_public: true,
                },
            ],
            types: vec![
                TypeDef {
                    name: "Config".to_string(),
                    kind: "struct".to_string(),
                    fields: vec!["name".to_string()],
                },
            ],
            imports: vec![
                ImportDef {
                    source: "std::io".to_string(),
                    items: vec![],
                },
            ],
            exports: vec!["main".to_string()],
        };

        let summary = Elrond::build_content_summary(&entity);
        assert!(summary.contains("src/main.rs"), "Summary should contain file path");
        assert!(summary.contains("rust"), "Summary should contain language");
        assert!(summary.contains("main"), "Summary should contain function name");
        assert!(summary.contains("Config"), "Summary should contain type name");
        assert!(summary.contains("std::io"), "Summary should contain import source");
    }

    // -----------------------------------------------------------------------
    // Test: function search across files
    // -----------------------------------------------------------------------

    #[tokio::test]
    async fn test_find_functions_by_name() {
        let tmp = TempDir::new().unwrap();

        write_file(&tmp, "src/a.rs", "pub fn process() {}\nfn helper() {}");
        write_file(&tmp, "src/b.rs", "pub fn process() {}\nfn other() {}");
        write_file(&tmp, "src/c.py", "def process():\n    pass\ndef unrelated():\n    pass");

        let elrond = make_test_elrond(&tmp).await;
        let results = elrond.find_functions_by_name("process").await.unwrap();

        assert!(results.len() >= 2, "Expected at least 2 results for 'process', got {}: {:?}",
            results.len(), results.iter().map(|(p, _)| p.as_str()).collect::<Vec<_>>());
    }

    // -----------------------------------------------------------------------
    // Test: Java parsing
    // -----------------------------------------------------------------------

    #[tokio::test]
    async fn test_parse_java_file() {
        let tmp = TempDir::new().unwrap();
        let elrond = make_test_elrond(&tmp).await;

        let java_src = r#"
import java.util.List;
import com.example.Service;

public class UserController {
    private String name;

    public void handleRequest(Request req) {
        // handle it
    }

    private int computeValue(int x) {
        return x * 2;
    }
}

public interface Controllable {
    void start();
    void stop();
}
"#;

        let entity = elrond.parse_content("UserController.java", java_src, "java");

        // Types
        let type_names: Vec<&str> = entity.types.iter().map(|t| t.name.as_str()).collect();
        assert!(type_names.contains(&"UserController"), "Missing UserController: {:?}", type_names);
        assert!(type_names.contains(&"Controllable"), "Missing Controllable: {:?}", type_names);

        // Imports
        let import_sources: Vec<&str> = entity.imports.iter().map(|i| i.source.as_str()).collect();
        assert!(import_sources.iter().any(|s| s.contains("java.util.List")),
            "Missing java.util.List import: {:?}", import_sources);
    }

    // -----------------------------------------------------------------------
    // Test: Ruby parsing
    // -----------------------------------------------------------------------

    #[tokio::test]
    async fn test_parse_ruby_file() {
        let tmp = TempDir::new().unwrap();
        let elrond = make_test_elrond(&tmp).await;

        let rb_src = r#"
require 'json'
require_relative 'helpers'

class Animal
  def initialize(name)
    @name = name
  end

  def speak
    "..."
  end
end

module Greetable
  def greet
    "Hello!"
  end
end
"#;

        let entity = elrond.parse_content("animal.rb", rb_src, "ruby");

        // Functions
        let fn_names: Vec<&str> = entity.functions.iter().map(|f| f.name.as_str()).collect();
        assert!(fn_names.contains(&"initialize"), "Missing initialize: {:?}", fn_names);
        assert!(fn_names.contains(&"speak"), "Missing speak: {:?}", fn_names);
        assert!(fn_names.contains(&"greet"), "Missing greet: {:?}", fn_names);

        // Types
        let type_names: Vec<&str> = entity.types.iter().map(|t| t.name.as_str()).collect();
        assert!(type_names.contains(&"Animal"), "Missing Animal: {:?}", type_names);
        assert!(type_names.contains(&"Greetable"), "Missing Greetable: {:?}", type_names);

        // Imports
        let import_sources: Vec<&str> = entity.imports.iter().map(|i| i.source.as_str()).collect();
        assert!(import_sources.contains(&"json"), "Missing json require: {:?}", import_sources);
        assert!(import_sources.contains(&"helpers"), "Missing helpers require: {:?}", import_sources);
    }

    // -----------------------------------------------------------------------
    // Test: FileEvent enum
    // -----------------------------------------------------------------------

    #[test]
    fn test_file_event_variants() {
        let path = PathBuf::from("/tmp/test.rs");
        let created = FileEvent::Created(path.clone());
        let modified = FileEvent::Modified(path.clone());
        let deleted = FileEvent::Deleted(path.clone());

        assert_eq!(created, FileEvent::Created(PathBuf::from("/tmp/test.rs")));
        assert_eq!(modified, FileEvent::Modified(PathBuf::from("/tmp/test.rs")));
        assert_eq!(deleted, FileEvent::Deleted(PathBuf::from("/tmp/test.rs")));
        assert_ne!(created, modified);
    }

    // -----------------------------------------------------------------------
    // Test: non-source files are skipped
    // -----------------------------------------------------------------------

    #[tokio::test]
    async fn test_non_source_files_ignored_on_event() {
        let tmp = TempDir::new().unwrap();
        let elrond = make_test_elrond(&tmp).await;

        let md_path = write_file(&tmp, "README.md", "# Hello");

        // Should not error -- just silently skip
        elrond
            .handle_file_event(FileEvent::Created(md_path))
            .await
            .unwrap();

        // Vector store should still be empty since .md is not a source file
        assert_eq!(elrond.vector_store_count().await, 0);
    }
}
