// Per-language module consolidation.
// Concrete implementations of traits defined in astmap-core.
// Provides dispatch (parser_for, resolver_for) and DefaultLanguageRegistry.

pub mod go;
pub mod java;
pub mod kotlin;
pub mod python;
pub mod rust;
pub mod typescript;

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

use tracing::trace;

// Re-export types and traits from core so language implementations
// can keep using `super::*`.
pub use astmap_core::{
    ExtractedCall, ExtractedImport, ExtractedSymbol, ExtractedTypeRef, LanguageParser,
    LanguageRegistry, LanguageResolver, ParseResult,
};

// ── Shared utilities used by language modules ──

/// Find the symbol that contains the given line number (innermost/smallest range).
pub fn find_enclosing_symbol(symbols: &[ExtractedSymbol], line: usize) -> Option<&str> {
    symbols
        .iter()
        .filter(|sym| line >= sym.start_line && line <= sym.end_line)
        .min_by_key(|sym| sym.end_line - sym.start_line)
        .map(|s| s.name.as_str())
}

// ── Shared utilities used by language modules (depend on tree-sitter) ──

pub fn node_text(node: &tree_sitter::Node, source: &str) -> String {
    source[node.start_byte()..node.end_byte()].to_string()
}

pub fn first_line(node: &tree_sitter::Node, source: &str) -> String {
    let text = &source[node.start_byte()..node.end_byte()];
    text.lines().next().unwrap_or("").to_string()
}

/// Build an ExtractedSymbol from a tree-sitter node.
///
/// Every language extracts name/kind from different node shapes, but the
/// conversion from a tree-sitter Node to position fields is always the same.
pub fn symbol_from_node(
    name: String,
    kind: astmap_core::SymbolKind,
    node: &tree_sitter::Node,
    source: &str,
    parent_index: Option<usize>,
) -> ExtractedSymbol {
    ExtractedSymbol {
        name,
        kind,
        signature: Some(first_line(node, source)),
        start_line: node.start_position().row + 1,
        end_line: node.end_position().row + 1,
        start_byte: node.start_byte(),
        end_byte: node.end_byte(),
        parent_index,
    }
}

/// Parse source code with a tree-sitter language and language-specific extractors.
///
/// The parse-tree construction and `ParseResult` assembly is identical for every
/// language; only the extraction callbacks differ.
pub fn parse_with_tree_sitter(
    source: &str,
    language: &tree_sitter::Language,
    extract_symbols: impl FnOnce(
        tree_sitter::Node,
        &str,
        &mut Vec<ExtractedSymbol>,
        &mut Vec<ExtractedImport>,
    ),
    extract_calls: impl FnOnce(tree_sitter::Node, &str, &[ExtractedSymbol]) -> Vec<ExtractedCall>,
    extract_type_refs: impl FnOnce(tree_sitter::Node, &str, &[ExtractedSymbol]) -> Vec<ExtractedTypeRef>,
) -> ParseResult {
    let mut parser = tree_sitter::Parser::new();
    parser
        .set_language(language)
        .expect("failed to set language");

    let tree = match parser.parse(source, None) {
        Some(t) => t,
        None => {
            return ParseResult {
                symbols: vec![],
                imports: vec![],
                calls: vec![],
                type_refs: vec![],
                has_errors: true,
            };
        }
    };

    let has_errors = tree.root_node().has_error();
    let mut symbols = Vec::new();
    let mut imports = Vec::new();

    extract_symbols(tree.root_node(), source, &mut symbols, &mut imports);

    let calls = extract_calls(tree.root_node(), source, &symbols);
    let type_refs = extract_type_refs(tree.root_node(), source, &symbols);

    ParseResult {
        symbols,
        calls,
        type_refs,
        imports,
        has_errors,
    }
}

/// Collect type references by walking the tree for nodes of a given kind
/// and filtering out language-specific builtins.
///
/// Rust and TypeScript both look for `"type_identifier"` nodes; Python uses
/// a different strategy so it does not use this helper.
pub fn collect_type_refs_by_kind(
    root: tree_sitter::Node,
    source: &str,
    symbols: &[ExtractedSymbol],
    node_kind: &str,
    is_builtin: &dyn Fn(&str) -> bool,
) -> Vec<ExtractedTypeRef> {
    let mut refs = Vec::new();
    walk_type_refs(root, source, symbols, node_kind, is_builtin, &mut refs);
    refs
}

fn walk_type_refs(
    node: tree_sitter::Node,
    source: &str,
    symbols: &[ExtractedSymbol],
    node_kind: &str,
    is_builtin: &dyn Fn(&str) -> bool,
    refs: &mut Vec<ExtractedTypeRef>,
) {
    if node.kind() == node_kind {
        let type_name = node_text(&node, source);
        let line = node.start_position().row + 1;

        if !is_builtin(&type_name) {
            if let Some(from_sym) = find_enclosing_symbol(symbols, line) {
                refs.push(ExtractedTypeRef {
                    from_symbol: from_sym.to_string(),
                    to_type: type_name,
                    line,
                });
            }
        }
    }

    for i in 0..node.child_count() {
        if let Some(child) = node.child(i as u32) {
            walk_type_refs(child, source, symbols, node_kind, is_builtin, refs);
        }
    }
}

// ── Shared JVM helper ──

/// Detect JVM source roots by checking for conventional directories.
/// Used by both Java and Kotlin resolvers to find where source files live.
pub fn detect_jvm_source_roots(root: &Path) -> Vec<PathBuf> {
    let candidates = [
        "src/main/java",
        "src/main/kotlin",
        "src/test/java",
        "src/test/kotlin",
        "src/commonMain/kotlin",
        "src/jvmMain/kotlin",
        "src/androidMain/kotlin",
        "src",
    ];

    let mut roots = Vec::new();
    for candidate in &candidates {
        let path = root.join(candidate);
        if path.is_dir() {
            roots.push(path);
        }
    }

    if roots.is_empty() {
        roots.push(root.to_path_buf());
    }
    roots
}

// ── Language registration and dispatch ──

pub(crate) struct LanguageSupport {
    pub name: &'static str,
    pub extensions: &'static [&'static str],
    pub parser: &'static dyn LanguageParser,
    pub resolver_factory: fn(&Path) -> Box<dyn LanguageResolver>,
    /// Config files that trigger a full re-scan when changed (e.g., "go.mod").
    pub config_files: &'static [&'static str],
    /// Given a file's relative path, return sibling expansion rules (or None).
    pub sibling_fn: fn(&str) -> Option<astmap_core::SiblingExpansion>,
}

/// All registered languages. Add new languages here.
fn all() -> &'static [LanguageSupport] {
    static LANGS: OnceLock<Vec<LanguageSupport>> = OnceLock::new();
    LANGS.get_or_init(|| {
        vec![
            rust::lang(),
            typescript::lang(),
            python::lang(),
            go::lang(),
            java::lang(),
            kotlin::lang(),
        ]
    })
}

pub fn parser_for(ext: &str) -> Option<&'static dyn LanguageParser> {
    all().iter().find(|l| l.extensions.contains(&ext)).map(|l| {
        trace!("selected parser: {} for extension: {}", l.name, ext);
        l.parser
    })
}

pub fn resolver_for(ext: &str, root: &Path) -> Option<Box<dyn LanguageResolver>> {
    all()
        .iter()
        .find(|l| l.extensions.contains(&ext))
        .map(|l| (l.resolver_factory)(root))
}

pub fn supported_extensions() -> &'static [&'static str] {
    static EXTENSIONS: OnceLock<Vec<&str>> = OnceLock::new();
    EXTENSIONS.get_or_init(|| {
        all()
            .iter()
            .flat_map(|l| l.extensions.iter().copied())
            .collect()
    })
}

pub fn config_files() -> &'static [&'static str] {
    static CONFIGS: OnceLock<Vec<&str>> = OnceLock::new();
    CONFIGS.get_or_init(|| {
        let mut files: Vec<&str> = all()
            .iter()
            .flat_map(|l| l.config_files.iter().copied())
            .collect();
        files.dedup();
        files
    })
}

pub fn sibling_expansion(rel_path: &str) -> Option<astmap_core::SiblingExpansion> {
    let ext = Path::new(rel_path).extension()?.to_str()?;
    let lang = all().iter().find(|l| l.extensions.contains(&ext))?;
    (lang.sibling_fn)(rel_path)
}

// ── DefaultLanguageRegistry ──

/// Concrete implementation of LanguageRegistry backed by the static dispatch tables.
pub struct DefaultLanguageRegistry;

impl LanguageRegistry for DefaultLanguageRegistry {
    fn parser_for(&self, ext: &str) -> Option<&dyn LanguageParser> {
        parser_for(ext)
    }

    fn resolver_for(&self, ext: &str, root: &Path) -> Option<Box<dyn LanguageResolver>> {
        resolver_for(ext, root)
    }

    fn supported_extensions(&self) -> &[&str] {
        supported_extensions()
    }

    fn config_files(&self) -> &[&str] {
        config_files()
    }

    fn sibling_expansion(&self, rel_path: &str) -> Option<astmap_core::SiblingExpansion> {
        sibling_expansion(rel_path)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use astmap_core::SymbolKind;

    #[test]
    fn test_parser_for_known_extensions() {
        assert!(parser_for("rs").is_some());
        assert!(parser_for("ts").is_some());
        assert!(parser_for("tsx").is_some());
        assert!(parser_for("go").is_some());
        assert_eq!(parser_for("rs").unwrap().language_name(), "rust");
        assert_eq!(parser_for("ts").unwrap().language_name(), "typescript");
        assert_eq!(parser_for("tsx").unwrap().language_name(), "typescript");
        assert_eq!(parser_for("go").unwrap().language_name(), "go");
    }

    #[test]
    fn test_parser_for_python() {
        assert!(parser_for("py").is_some());
        assert_eq!(parser_for("py").unwrap().language_name(), "python");
    }

    #[test]
    fn test_parser_for_unknown_extension() {
        assert!(parser_for("rb").is_none());
        assert!(parser_for("js").is_none());
        assert!(parser_for("").is_none());
    }

    #[test]
    fn test_supported_extensions() {
        let extensions = supported_extensions();
        assert!(extensions.contains(&"rs"));
        assert!(extensions.contains(&"ts"));
        assert!(extensions.contains(&"tsx"));
        assert!(extensions.contains(&"py"));
        assert!(extensions.contains(&"go"));
        assert!(!extensions.contains(&"js"));
    }

    #[test]
    fn test_resolver_for() {
        let tmp = tempfile::tempdir().unwrap();
        assert!(resolver_for("rs", tmp.path()).is_some());
        assert!(resolver_for("ts", tmp.path()).is_some());
        assert!(resolver_for("tsx", tmp.path()).is_some());
        assert!(resolver_for("py", tmp.path()).is_some());
        assert!(resolver_for("go", tmp.path()).is_some());
    }

    #[test]
    fn test_parser_produces_results() {
        let parser = parser_for("ts").unwrap();
        let result = parser.parse("function foo() {}", &std::path::PathBuf::from("test.ts"));
        assert!(!result.has_errors);
        assert_eq!(result.symbols.len(), 1);
    }

    #[test]
    fn test_node_text_basic() {
        let mut parser = tree_sitter::Parser::new();
        parser
            .set_language(&tree_sitter_rust::LANGUAGE.into())
            .unwrap();
        let source = "fn hello() {}";
        let tree = parser.parse(source, None).unwrap();
        let root = tree.root_node();
        let text = node_text(&root, source);
        assert_eq!(text, source);
    }

    #[test]
    fn test_node_text_child_node() {
        let mut parser = tree_sitter::Parser::new();
        parser
            .set_language(&tree_sitter_rust::LANGUAGE.into())
            .unwrap();
        let source = "fn hello() {}";
        let tree = parser.parse(source, None).unwrap();
        // Get the function_item node, then the name (identifier) child
        let func = tree.root_node().child(0).unwrap();
        let name_node = func.child_by_field_name("name").unwrap();
        let text = node_text(&name_node, source);
        assert_eq!(text, "hello");
    }

    #[test]
    fn test_first_line_single_line() {
        let mut parser = tree_sitter::Parser::new();
        parser
            .set_language(&tree_sitter_rust::LANGUAGE.into())
            .unwrap();
        let source = "fn hello() {}";
        let tree = parser.parse(source, None).unwrap();
        let root = tree.root_node();
        let line = first_line(&root, source);
        assert_eq!(line, "fn hello() {}");
    }

    #[test]
    fn test_first_line_multi_line() {
        let mut parser = tree_sitter::Parser::new();
        parser
            .set_language(&tree_sitter_rust::LANGUAGE.into())
            .unwrap();
        let source = "fn hello() {\n    42\n}";
        let tree = parser.parse(source, None).unwrap();
        let func = tree.root_node().child(0).unwrap();
        let line = first_line(&func, source);
        assert_eq!(line, "fn hello() {");
    }

    #[test]
    fn test_all_returns_known_languages() {
        let langs = all();
        assert!(langs.len() >= 4);
        let names: Vec<&str> = langs.iter().map(|l| l.name).collect();
        assert!(names.contains(&"rust"));
        assert!(names.contains(&"typescript"));
        assert!(names.contains(&"python"));
        assert!(names.contains(&"go"));
    }

    #[test]
    fn test_default_language_registry_parser_for() {
        let registry = DefaultLanguageRegistry;
        assert!(registry.parser_for("rs").is_some());
        assert!(registry.parser_for("ts").is_some());
        assert!(registry.parser_for("py").is_some());
        assert!(registry.parser_for("js").is_none());
    }

    #[test]
    fn test_default_language_registry_resolver_for() {
        let registry = DefaultLanguageRegistry;
        let tmp = tempfile::tempdir().unwrap();
        assert!(registry.resolver_for("rs", tmp.path()).is_some());
        assert!(registry.resolver_for("tsx", tmp.path()).is_some());
        assert!(registry.resolver_for("py", tmp.path()).is_some());
        assert!(registry.resolver_for("rb", tmp.path()).is_none());
    }

    #[test]
    fn test_default_language_registry_supported_extensions() {
        let registry = DefaultLanguageRegistry;
        let exts = registry.supported_extensions();
        assert!(exts.contains(&"rs"));
        assert!(exts.contains(&"ts"));
        assert!(exts.contains(&"tsx"));
        assert!(exts.contains(&"py"));
        assert!(!exts.contains(&"js"));
    }

    #[test]
    fn test_resolver_for_unknown_extension() {
        let tmp = tempfile::tempdir().unwrap();
        assert!(resolver_for("rb", tmp.path()).is_none());
        assert!(resolver_for("", tmp.path()).is_none());
    }

    #[test]
    fn test_find_enclosing_symbol_innermost() {
        let symbols = vec![
            ExtractedSymbol {
                name: "Outer".to_string(),
                kind: SymbolKind::Struct,
                signature: None,
                start_line: 1,
                end_line: 20,
                start_byte: 0,
                end_byte: 200,
                parent_index: None,
            },
            ExtractedSymbol {
                name: "inner_fn".to_string(),
                kind: SymbolKind::Method,
                signature: None,
                start_line: 5,
                end_line: 10,
                start_byte: 50,
                end_byte: 100,
                parent_index: Some(0),
            },
        ];
        assert_eq!(find_enclosing_symbol(&symbols, 7), Some("inner_fn"));
        assert_eq!(find_enclosing_symbol(&symbols, 15), Some("Outer"));
        assert_eq!(find_enclosing_symbol(&symbols, 25), None);
    }
}
