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

use crate::models::{ImportRelation, Symbol};

#[derive(Debug, Clone, Default)]
pub struct ImportResolutionContext {
    python_modules: HashSet<String>,
    js_external_packages: HashSet<String>,
    js_self_package_name: Option<String>,
    go_module_path: Option<String>,
    rust_external_crates: HashSet<String>,
    rust_self_crate_name: Option<String>,
    java_local_classes: HashSet<String>,
    csharp_local_roots: HashSet<String>,
    php_local_symbols: HashSet<String>,
    ruby_local_constant_roots: HashSet<String>,
    ruby_require_root_overrides: HashMap<String, String>,
    dart_external_packages: HashSet<String>,
    dart_self_package_name: Option<String>,
    elixir_external_roots: HashMap<String, String>,
    elixir_external_root_overrides: HashMap<String, String>,
    elixir_local_module_roots: HashSet<String>,
}

impl ImportResolutionContext {
    fn ruby_require_root(&self, required: &str) -> Option<&str> {
        self.ruby_require_root_overrides
            .get(required)
            .map(String::as_str)
            .or_else(|| ruby_require_root(required))
    }

    fn elixir_external_root_module(&self, root: &str) -> Option<&str> {
        self.elixir_external_root_overrides
            .get(root)
            .or_else(|| self.elixir_external_roots.get(root))
            .map(String::as_str)
    }
}

#[derive(Debug, Clone)]
pub(crate) struct ExternalImportBinding {
    pub(crate) module: String,
    pub(crate) callee_name: String,
}

#[derive(Debug, Clone, Default)]
pub(crate) struct ImportBindings {
    pub(crate) bare: HashMap<String, ExternalImportBinding>,
    pub(crate) bare_wildcard_modules: Vec<String>,
    pub(crate) member: HashMap<String, String>,
    pub(crate) external_roots: HashMap<String, ExternalRootBinding>,
}

#[derive(Debug, Clone)]
pub(crate) struct ExternalRootBinding {
    pub(crate) module: String,
    pub(crate) module_from_qualifier: bool,
}

#[derive(Debug, Clone, Default)]
pub(crate) struct ExtractedImports {
    pub(crate) imports: Vec<ImportRelation>,
    pub(crate) bindings: ImportBindings,
}

#[derive(Debug, Clone)]
pub(crate) struct ExternalCallTarget {
    pub(crate) module: String,
    pub(crate) callee_name: String,
}

const JS_BUILTIN_MODULES: &[&str] = &[
    "assert",
    "buffer",
    "child_process",
    "cluster",
    "crypto",
    "dgram",
    "dns",
    "domain",
    "events",
    "fs",
    "http",
    "https",
    "net",
    "os",
    "path",
    "perf_hooks",
    "process",
    "punycode",
    "querystring",
    "readline",
    "repl",
    "stream",
    "string_decoder",
    "timers",
    "tls",
    "trace_events",
    "tty",
    "url",
    "util",
    "v8",
    "vm",
    "worker_threads",
    "zlib",
];

pub fn build_import_resolution_context(
    root_path: &Path,
    candidate_files: &[PathBuf],
) -> ImportResolutionContext {
    build_import_resolution_context_with_overrides(
        root_path,
        candidate_files,
        HashMap::new(),
        HashMap::new(),
    )
}

pub fn build_import_resolution_context_with_overrides(
    root_path: &Path,
    candidate_files: &[PathBuf],
    ruby_require_root_overrides: HashMap<String, String>,
    elixir_external_root_overrides: HashMap<String, String>,
) -> ImportResolutionContext {
    ImportResolutionContext {
        python_modules: build_python_module_index(root_path, candidate_files),
        js_external_packages: load_js_external_packages(root_path),
        js_self_package_name: load_js_self_package_name(root_path),
        go_module_path: load_go_module_path(root_path),
        rust_external_crates: load_rust_external_crates(root_path),
        rust_self_crate_name: load_rust_self_crate_name(root_path),
        java_local_classes: build_java_local_class_index(candidate_files),
        csharp_local_roots: build_csharp_local_roots(candidate_files),
        php_local_symbols: build_php_local_symbol_index(candidate_files),
        ruby_local_constant_roots: build_ruby_local_constant_roots(candidate_files),
        ruby_require_root_overrides,
        dart_external_packages: load_dart_external_packages(root_path),
        dart_self_package_name: load_dart_self_package_name(root_path),
        elixir_external_roots: load_elixir_external_roots(root_path),
        elixir_external_root_overrides,
        elixir_local_module_roots: build_elixir_local_module_roots(candidate_files),
    }
}

pub(crate) fn parse_import_statement(
    language: &str,
    text: &str,
    rel_path: &str,
    import_context: &ImportResolutionContext,
    extracted: &mut ExtractedImports,
) {
    match language {
        "python" => parse_python_import_statement(text, rel_path, import_context, extracted),
        "javascript" | "typescript" => {
            parse_js_import_statement(text, rel_path, import_context, extracted)
        }
        "go" => parse_go_import_statement(text, rel_path, import_context, extracted),
        "rust" => parse_rust_import_statement(text, rel_path, import_context, extracted),
        "java" => parse_java_import_statement(text, rel_path, import_context, extracted),
        "csharp" => parse_csharp_import_statement(text, rel_path, import_context, extracted),
        "php" => parse_php_import_statement(text, rel_path, import_context, extracted),
        "swift" => parse_swift_import_statement(text, rel_path, extracted),
        "ruby" => parse_ruby_import_statement(text, rel_path, import_context, extracted),
        "dart" => parse_dart_import_statement(text, rel_path, import_context, extracted),
        "elixir" => parse_elixir_import_statement(text, rel_path, import_context, extracted),
        _ => extracted.imports.push(ImportRelation {
            file_path: rel_path.to_string(),
            module_name: text.to_string(),
        }),
    }
}

pub(crate) fn seed_import_bindings(
    language: &str,
    import_context: &ImportResolutionContext,
    bindings: &mut ImportBindings,
) {
    match language {
        "rust" => {
            for root in rust_external_roots(import_context) {
                bindings.external_roots.insert(
                    root.clone(),
                    ExternalRootBinding {
                        module: root,
                        module_from_qualifier: true,
                    },
                );
            }
        }
        "elixir" => {
            for (root, module) in &import_context.elixir_external_roots {
                if import_context.elixir_local_module_roots.contains(root) {
                    continue;
                }
                let module = import_context
                    .elixir_external_root_module(root)
                    .unwrap_or(module);
                bindings.external_roots.insert(
                    root.clone(),
                    ExternalRootBinding {
                        module: module.to_string(),
                        module_from_qualifier: true,
                    },
                );
            }
            for (root, module) in &import_context.elixir_external_root_overrides {
                if import_context.elixir_external_roots.contains_key(root)
                    || import_context.elixir_local_module_roots.contains(root)
                {
                    continue;
                }
                bindings.external_roots.insert(
                    root.clone(),
                    ExternalRootBinding {
                        module: module.clone(),
                        module_from_qualifier: true,
                    },
                );
            }
        }
        _ => {}
    }
}

pub(crate) fn resolve_external_callee(
    import_context: &ImportResolutionContext,
    import_bindings: &ImportBindings,
    symbols: &[Symbol],
    callee_name: &str,
    root_alias: Option<&str>,
    qualifier_path: Option<&str>,
    is_bare_call: bool,
) -> Option<ExternalCallTarget> {
    if is_bare_call {
        if symbols.iter().any(|symbol| symbol.name == callee_name) {
            return None;
        }
        if let Some(binding) = import_bindings.bare.get(callee_name) {
            return Some(ExternalCallTarget {
                module: binding.module.clone(),
                callee_name: binding.callee_name.clone(),
            });
        }
        if import_bindings.bare_wildcard_modules.len() == 1 {
            return Some(ExternalCallTarget {
                module: import_bindings.bare_wildcard_modules[0].clone(),
                callee_name: callee_name.to_string(),
            });
        }
        // Multiple wildcard imports make the source module ambiguous, so fail closed.
        return None;
    }

    let root_alias = root_alias?;
    if symbols.iter().any(|symbol| symbol.name == root_alias) {
        return None;
    }
    if let Some(module) = import_bindings.member.get(root_alias) {
        return Some(ExternalCallTarget {
            module: module.clone(),
            callee_name: callee_name.to_string(),
        });
    }

    let qualifier_path = qualifier_path?;
    if qualifier_path.starts_with('\\') {
        let module = qualifier_path.trim_start_matches('\\');
        if module.is_empty() {
            return None;
        }
        let local_symbol = format!("{module}\\{callee_name}");
        if import_context.php_local_symbols.contains(module)
            || import_context.php_local_symbols.contains(&local_symbol)
        {
            return None;
        }
        return Some(ExternalCallTarget {
            module: module.to_string(),
            callee_name: callee_name.to_string(),
        });
    }
    let root_binding = import_bindings.external_roots.get(root_alias)?;
    let module = if root_binding.module_from_qualifier {
        qualifier_path.to_string()
    } else {
        root_binding.module.clone()
    };
    Some(ExternalCallTarget {
        module,
        callee_name: callee_name.to_string(),
    })
}

fn build_python_module_index(root_path: &Path, candidate_files: &[PathBuf]) -> HashSet<String> {
    let mut modules = HashSet::new();

    for path in candidate_files {
        let Ok(rel) = path.strip_prefix(root_path) else {
            continue;
        };
        let ext = rel
            .extension()
            .and_then(|ext| ext.to_str())
            .unwrap_or_default()
            .to_ascii_lowercase();
        if !matches!(ext.as_str(), "py" | "pyi") {
            continue;
        }

        let mut module = rel
            .with_extension("")
            .to_string_lossy()
            .replace(['/', '\\'], ".");
        if module.ends_with(".__init__") {
            module.truncate(module.len() - ".__init__".len());
        }
        if module.is_empty() {
            continue;
        }
        modules.insert(module.clone());

        if let Some(stripped) = module.strip_prefix("src.") {
            modules.insert(stripped.to_string());
        }
    }

    modules
}

fn load_js_external_packages(root_path: &Path) -> HashSet<String> {
    let package_json = root_path.join("package.json");
    let Ok(contents) = std::fs::read_to_string(package_json) else {
        return HashSet::new();
    };
    let Ok(json) = serde_json::from_str::<serde_json::Value>(&contents) else {
        return HashSet::new();
    };

    let mut packages = HashSet::new();
    for field in [
        "dependencies",
        "devDependencies",
        "peerDependencies",
        "optionalDependencies",
        "bundledDependencies",
    ] {
        if let Some(map) = json.get(field).and_then(|value| value.as_object()) {
            packages.extend(map.keys().cloned());
        }
    }
    packages
}

fn load_js_self_package_name(root_path: &Path) -> Option<String> {
    let package_json = root_path.join("package.json");
    let contents = std::fs::read_to_string(package_json).ok()?;
    let json = serde_json::from_str::<serde_json::Value>(&contents).ok()?;
    json.get("name")
        .and_then(|value| value.as_str())
        .map(ToOwned::to_owned)
}

fn load_go_module_path(root_path: &Path) -> Option<String> {
    let contents = std::fs::read_to_string(root_path.join("go.mod")).ok()?;
    contents.lines().find_map(|line| {
        let line = line.trim();
        line.strip_prefix("module ")
            .map(str::trim)
            .filter(|module| !module.is_empty())
            .map(ToOwned::to_owned)
    })
}

fn load_rust_external_crates(root_path: &Path) -> HashSet<String> {
    let Ok(contents) = std::fs::read_to_string(root_path.join("Cargo.toml")) else {
        return HashSet::new();
    };
    let Ok(cargo_toml) = toml::from_str::<toml::Table>(&contents) else {
        return HashSet::new();
    };
    let mut crates = HashSet::new();

    for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
        collect_rust_dependency_keys(cargo_toml.get(section), &mut crates);
    }

    if let Some(targets) = cargo_toml.get("target").and_then(toml::Value::as_table) {
        for target in targets.values() {
            for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
                collect_rust_dependency_keys(target.get(section), &mut crates);
            }
        }
    }

    crates
}

fn load_rust_self_crate_name(root_path: &Path) -> Option<String> {
    let contents = std::fs::read_to_string(root_path.join("Cargo.toml")).ok()?;
    let cargo_toml = toml::from_str::<toml::Table>(&contents).ok()?;
    cargo_toml
        .get("package")
        .and_then(|package| package.get("name"))
        .and_then(toml::Value::as_str)
        .map(normalize_rust_crate_name)
        .filter(|name| !name.is_empty())
}

fn collect_rust_dependency_keys(value: Option<&toml::Value>, crates: &mut HashSet<String>) {
    let Some(table) = value.and_then(toml::Value::as_table) else {
        return;
    };
    for name in table.keys() {
        let name = normalize_rust_crate_name(name);
        if !name.is_empty() {
            crates.insert(name);
        }
    }
}

fn normalize_rust_crate_name(name: &str) -> String {
    name.trim().replace('-', "_")
}

fn build_java_local_class_index(candidate_files: &[PathBuf]) -> HashSet<String> {
    let mut classes = HashSet::new();
    for path in candidate_files {
        let ext = path
            .extension()
            .and_then(|ext| ext.to_str())
            .unwrap_or_default();
        if ext != "java" {
            continue;
        }
        let Ok(contents) = std::fs::read_to_string(path) else {
            continue;
        };
        let package = contents.lines().find_map(|line| {
            let line = line.trim();
            line.strip_prefix("package ")
                .map(|rest| rest.trim().trim_end_matches(';').trim().to_string())
        });
        for class_name in java_declared_types(&contents) {
            classes.insert(class_name.clone());
            if let Some(package) = package.as_deref()
                && !package.is_empty()
            {
                classes.insert(format!("{package}.{class_name}"));
            }
        }
    }
    classes
}

fn build_csharp_local_roots(candidate_files: &[PathBuf]) -> HashSet<String> {
    let mut roots = HashSet::new();
    for path in candidate_files {
        let ext = path
            .extension()
            .and_then(|ext| ext.to_str())
            .unwrap_or_default();
        if ext != "cs" {
            continue;
        }
        let Ok(contents) = std::fs::read_to_string(path) else {
            continue;
        };
        for line in contents.lines() {
            let line = line.trim();
            if let Some(rest) = line.strip_prefix("namespace ") {
                let namespace = rest
                    .trim()
                    .trim_end_matches([';', '{'])
                    .split_whitespace()
                    .next()
                    .unwrap_or_default();
                if let Some(root) = namespace.split('.').next()
                    && !root.is_empty()
                {
                    roots.insert(root.to_string());
                }
            }
        }
        for type_name in csharp_declared_types(&contents) {
            roots.insert(type_name);
        }
    }
    roots
}

fn build_php_local_symbol_index(candidate_files: &[PathBuf]) -> HashSet<String> {
    let mut symbols = HashSet::new();
    for path in candidate_files {
        let ext = path
            .extension()
            .and_then(|ext| ext.to_str())
            .unwrap_or_default();
        if ext != "php" {
            continue;
        }
        let Ok(contents) = std::fs::read_to_string(path) else {
            continue;
        };
        let namespace = contents.lines().find_map(|line| {
            let line = line.trim();
            line.strip_prefix("namespace ")
                .map(|rest| rest.trim().trim_end_matches([';', '{']).to_string())
        });
        for name in php_declared_symbols(&contents) {
            symbols.insert(name.clone());
            if let Some(namespace) = namespace.as_deref()
                && !namespace.is_empty()
            {
                symbols.insert(format!("{namespace}\\{name}"));
            }
        }
    }
    symbols
}

fn build_ruby_local_constant_roots(candidate_files: &[PathBuf]) -> HashSet<String> {
    let mut roots = HashSet::new();
    for path in candidate_files {
        let ext = path
            .extension()
            .and_then(|ext| ext.to_str())
            .unwrap_or_default();
        if !matches!(ext, "rb" | "rake" | "gemspec") {
            continue;
        }
        let Ok(contents) = std::fs::read_to_string(path) else {
            continue;
        };
        for line in contents.lines() {
            let line = line.trim_start();
            let Some(rest) = line
                .strip_prefix("class ")
                .or_else(|| line.strip_prefix("module "))
            else {
                continue;
            };
            let name = rest
                .split(|ch: char| ch.is_whitespace() || matches!(ch, '<' | '(' | ';' | '#'))
                .next()
                .unwrap_or_default()
                .trim_start_matches("::");
            if let Some(root) = name.split("::").next()
                && is_ruby_constant_name(root)
            {
                roots.insert(root.to_string());
            }
        }
    }
    roots
}

fn load_dart_external_packages(root_path: &Path) -> HashSet<String> {
    let contents = std::fs::read_to_string(root_path.join("pubspec.yaml")).unwrap_or_default();
    let Ok(yaml) = serde_yaml::from_str::<serde_yaml::Value>(&contents) else {
        return HashSet::new();
    };

    let mut packages = HashSet::new();
    for field in ["dependencies", "dev_dependencies", "dependency_overrides"] {
        if let Some(map) = yaml.get(field).and_then(|value| value.as_mapping()) {
            for key in map.keys().filter_map(|key| key.as_str()) {
                if !key.is_empty() && key != "sdk" {
                    packages.insert(key.to_string());
                }
            }
        }
    }
    packages
}

fn load_dart_self_package_name(root_path: &Path) -> Option<String> {
    let contents = std::fs::read_to_string(root_path.join("pubspec.yaml")).ok()?;
    let yaml = serde_yaml::from_str::<serde_yaml::Value>(&contents).ok()?;
    yaml.get("name")
        .and_then(|value| value.as_str())
        .map(ToOwned::to_owned)
}

fn build_elixir_local_module_roots(candidate_files: &[PathBuf]) -> HashSet<String> {
    let mut roots = HashSet::new();
    for path in candidate_files {
        let ext = path
            .extension()
            .and_then(|ext| ext.to_str())
            .unwrap_or_default();
        if !matches!(ext, "ex" | "exs") {
            continue;
        }
        let Ok(contents) = std::fs::read_to_string(path) else {
            continue;
        };
        for line in contents.lines() {
            let line = line.trim_start();
            let Some(rest) = line.strip_prefix("defmodule ") else {
                continue;
            };
            let module = rest
                .split(|ch: char| ch.is_whitespace() || matches!(ch, ',' | '(' | '['))
                .next()
                .unwrap_or_default();
            if let Some(root) = module.split('.').next()
                && is_elixir_alias(root)
            {
                roots.insert(root.to_string());
            }
        }
    }
    roots
}

fn load_elixir_external_roots(root_path: &Path) -> HashMap<String, String> {
    let deps = load_elixir_dependency_names(root_path);
    let mut roots = HashMap::new();
    for dep in deps {
        if let Some(dep_roots) = elixir_dependency_roots(&dep) {
            for root in dep_roots {
                roots.insert(root.clone(), root.clone());
            }
        }
    }
    roots
}

fn load_elixir_dependency_names(root_path: &Path) -> HashSet<String> {
    let mut deps = HashSet::new();
    if let Ok(contents) = std::fs::read_to_string(root_path.join("mix.exs")) {
        for line in contents.lines() {
            let trimmed = line.trim();
            if let Some(rest) = trimmed.strip_prefix("{:") {
                let dep = rest
                    .split(|ch: char| !(ch.is_ascii_alphanumeric() || ch == '_'))
                    .next()
                    .unwrap_or_default();
                if !dep.is_empty() {
                    deps.insert(dep.to_string());
                }
            }
        }
    }
    if let Ok(contents) = std::fs::read_to_string(root_path.join("mix.lock")) {
        for line in contents
            .lines()
            .map(str::trim)
            .filter(|line| !line.is_empty())
        {
            let Some(start) = line.find('"') else {
                continue;
            };
            let rest = &line[start + 1..];
            let Some(end) = rest.find('"') else {
                continue;
            };
            let dep = &rest[..end];
            if dep
                .chars()
                .all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
            {
                deps.insert(dep.to_string());
            }
        }
    }
    deps
}

fn parse_python_import_statement(
    text: &str,
    rel_path: &str,
    import_context: &ImportResolutionContext,
    extracted: &mut ExtractedImports,
) {
    if let Some(rest) = text.strip_prefix("import ") {
        for entry in split_top_level(rest, ',') {
            let entry = entry.trim();
            if entry.is_empty() {
                continue;
            }

            let (module, alias) = split_alias(entry);
            extracted.imports.push(ImportRelation {
                file_path: rel_path.to_string(),
                module_name: module.to_string(),
            });

            if is_external_python_module(module, import_context) {
                let local_alias = alias
                    .map(ToOwned::to_owned)
                    .unwrap_or_else(|| module.split('.').next().unwrap_or(module).to_string());
                extracted
                    .bindings
                    .member
                    .insert(local_alias, module.to_string());
            }
        }
        return;
    }

    let Some(rest) = text.strip_prefix("from ") else {
        extracted.imports.push(ImportRelation {
            file_path: rel_path.to_string(),
            module_name: text.to_string(),
        });
        return;
    };
    let Some((module, imported)) = rest.split_once(" import ") else {
        extracted.imports.push(ImportRelation {
            file_path: rel_path.to_string(),
            module_name: text.to_string(),
        });
        return;
    };

    let module = module.trim();
    extracted.imports.push(ImportRelation {
        file_path: rel_path.to_string(),
        module_name: module.to_string(),
    });

    if !is_external_python_module(module, import_context) {
        return;
    }

    let imported = imported.trim().trim_matches(|ch| matches!(ch, '(' | ')'));
    for entry in split_top_level(imported, ',') {
        let entry = entry.trim();
        if entry.is_empty() || entry == "*" {
            continue;
        }
        let (imported_name, alias) = split_alias(entry);
        let local_alias = alias.unwrap_or(imported_name).to_string();
        extracted.bindings.bare.insert(
            local_alias.clone(),
            ExternalImportBinding {
                module: module.to_string(),
                callee_name: imported_name.to_string(),
            },
        );
        extracted
            .bindings
            .member
            .insert(local_alias, module.to_string());
    }
}

fn parse_js_import_statement(
    text: &str,
    rel_path: &str,
    import_context: &ImportResolutionContext,
    extracted: &mut ExtractedImports,
) {
    let normalized = collapse_whitespace(text);
    let Some(specifier) = extract_js_module_specifier(&normalized) else {
        extracted.imports.push(ImportRelation {
            file_path: rel_path.to_string(),
            module_name: normalized,
        });
        return;
    };

    extracted.imports.push(ImportRelation {
        file_path: rel_path.to_string(),
        module_name: specifier.clone(),
    });

    if !is_external_js_module(&specifier, import_context) {
        return;
    }

    let Some(clause) = extract_js_import_clause(&normalized) else {
        return;
    };
    let clause = clause.trim();
    if clause.is_empty() || (clause.starts_with("type ") && !clause.contains(',')) {
        return;
    }

    for part in split_top_level(clause, ',') {
        let part = part.trim();
        if part.is_empty() {
            continue;
        }
        if let Some(alias) = part.strip_prefix("* as ") {
            let alias = alias.trim();
            if !alias.is_empty() {
                extracted
                    .bindings
                    .member
                    .insert(alias.to_string(), specifier.clone());
            }
            continue;
        }
        if part.starts_with('{') && part.ends_with('}') {
            let inner = &part[1..part.len() - 1];
            for item in split_top_level(inner, ',') {
                let item = item.trim();
                if item.is_empty() || item.starts_with("type ") {
                    continue;
                }
                let (imported_name, alias) = split_alias(item);
                let local_alias = alias.unwrap_or(imported_name).to_string();
                extracted.bindings.bare.insert(
                    local_alias.clone(),
                    ExternalImportBinding {
                        module: specifier.clone(),
                        callee_name: imported_name.to_string(),
                    },
                );
                extracted
                    .bindings
                    .member
                    .insert(local_alias, specifier.clone());
            }
            continue;
        }

        let alias = part.strip_prefix("type ").unwrap_or(part).trim();
        if alias.is_empty() {
            continue;
        }
        extracted.bindings.bare.insert(
            alias.to_string(),
            ExternalImportBinding {
                module: specifier.clone(),
                callee_name: "default".to_string(),
            },
        );
        extracted
            .bindings
            .member
            .insert(alias.to_string(), specifier.clone());
    }
}

fn parse_go_import_statement(
    text: &str,
    rel_path: &str,
    import_context: &ImportResolutionContext,
    extracted: &mut ExtractedImports,
) {
    let Some(rest) = text.trim().strip_prefix("import") else {
        extracted.imports.push(ImportRelation {
            file_path: rel_path.to_string(),
            module_name: text.to_string(),
        });
        return;
    };

    let rest = rest.trim();
    if rest.starts_with('(') {
        let block = rest.trim_start_matches('(').trim_end_matches(')');
        for line in block.lines() {
            parse_go_import_spec(line.trim(), rel_path, import_context, extracted);
        }
    } else {
        parse_go_import_spec(rest, rel_path, import_context, extracted);
    }
}

fn parse_go_import_spec(
    text: &str,
    rel_path: &str,
    import_context: &ImportResolutionContext,
    extracted: &mut ExtractedImports,
) {
    let text = text.split("//").next().unwrap_or(text).trim();
    if text.is_empty() {
        return;
    }
    let Some(module) = extract_quoted_string(text) else {
        return;
    };

    extracted.imports.push(ImportRelation {
        file_path: rel_path.to_string(),
        module_name: module.clone(),
    });

    if !is_external_go_module(&module, import_context) {
        return;
    }

    let alias = text[..text.find(['"', '`']).unwrap_or(0)].trim();
    if matches!(alias, "_" | ".") {
        return;
    }
    let local_alias = if alias.is_empty() {
        go_default_package_alias(&module)
    } else {
        alias.to_string()
    };
    if !local_alias.is_empty() {
        extracted.bindings.member.insert(local_alias, module);
    }
}

fn parse_rust_import_statement(
    text: &str,
    rel_path: &str,
    import_context: &ImportResolutionContext,
    extracted: &mut ExtractedImports,
) {
    let Some(rest) = text.trim().strip_prefix("use ") else {
        extracted.imports.push(ImportRelation {
            file_path: rel_path.to_string(),
            module_name: text.to_string(),
        });
        return;
    };
    let rest = rest.trim().trim_end_matches(';').trim();
    extracted.imports.push(ImportRelation {
        file_path: rel_path.to_string(),
        module_name: rest.to_string(),
    });

    if let Some((prefix, group)) = split_rust_use_group(rest) {
        register_rust_group_imports(prefix, group, import_context, extracted);
        return;
    }

    if rest.contains('*') {
        // Glob imports are intentionally not expanded because exported names are unknown here.
        return;
    }

    register_rust_path_import(rest, import_context, extracted);
}

fn register_rust_group_imports(
    prefix: &str,
    group: &str,
    import_context: &ImportResolutionContext,
    extracted: &mut ExtractedImports,
) {
    for item in split_top_level(group, ',') {
        if item.is_empty() {
            continue;
        }
        if let Some((nested_prefix, nested_group)) = split_rust_use_group(item) {
            let Some(full_prefix) = rust_join_use_path(prefix, nested_prefix) else {
                continue;
            };
            register_rust_group_imports(&full_prefix, nested_group, import_context, extracted);
            continue;
        }
        if item.contains('*') {
            // Glob imports are intentionally not expanded because exported names are unknown here.
            continue;
        }
        let Some(path) = rust_join_use_path(prefix, item) else {
            continue;
        };
        register_rust_path_import(&path, import_context, extracted);
    }
}

fn register_rust_path_import(
    path_and_alias: &str,
    import_context: &ImportResolutionContext,
    extracted: &mut ExtractedImports,
) {
    let normalized = path_and_alias.trim();
    if normalized.is_empty() {
        return;
    }
    let (path, alias) = split_alias(normalized);
    let segments: Vec<&str> = path.split("::").filter(|part| !part.is_empty()).collect();
    let Some(root) = segments.first().copied() else {
        return;
    };
    if !is_external_rust_root(root, import_context) {
        return;
    }

    extracted.bindings.external_roots.insert(
        root.to_string(),
        ExternalRootBinding {
            module: root.to_string(),
            module_from_qualifier: true,
        },
    );

    let Some(imported_name) = segments.last().copied() else {
        return;
    };
    let local_alias = alias.unwrap_or(imported_name);
    if local_alias.is_empty() {
        return;
    }

    let module = if segments.len() > 1 {
        segments[..segments.len() - 1].join("::")
    } else {
        root.to_string()
    };
    extracted.bindings.bare.insert(
        local_alias.to_string(),
        ExternalImportBinding {
            module: module.clone(),
            callee_name: imported_name.to_string(),
        },
    );
    extracted
        .bindings
        .member
        .insert(local_alias.to_string(), path.to_string());
}

fn parse_java_import_statement(
    text: &str,
    rel_path: &str,
    import_context: &ImportResolutionContext,
    extracted: &mut ExtractedImports,
) {
    let normalized = text.trim().trim_end_matches(';').trim();
    let Some(rest) = normalized.strip_prefix("import ") else {
        extracted.imports.push(ImportRelation {
            file_path: rel_path.to_string(),
            module_name: normalized.to_string(),
        });
        return;
    };

    let (is_static, target) = rest
        .strip_prefix("static ")
        .map(|target| (true, target.trim()))
        .unwrap_or((false, rest.trim()));
    extracted.imports.push(ImportRelation {
        file_path: rel_path.to_string(),
        module_name: target.to_string(),
    });

    if target.ends_with(".*") {
        return;
    }

    if is_static {
        let Some((class_path, member_name)) = target.rsplit_once('.') else {
            return;
        };
        if !is_external_java_class(class_path, import_context) {
            return;
        }
        extracted.bindings.bare.insert(
            member_name.to_string(),
            ExternalImportBinding {
                module: class_path.to_string(),
                callee_name: member_name.to_string(),
            },
        );
        return;
    }

    if !is_external_java_class(target, import_context) {
        return;
    }
    let class_alias = target.rsplit('.').next().unwrap_or(target);
    extracted
        .bindings
        .member
        .insert(class_alias.to_string(), target.to_string());
}

fn parse_csharp_import_statement(
    text: &str,
    rel_path: &str,
    import_context: &ImportResolutionContext,
    extracted: &mut ExtractedImports,
) {
    let normalized = text.trim().trim_end_matches(';').trim();
    let Some(rest) = normalized.strip_prefix("using ") else {
        extracted.imports.push(ImportRelation {
            file_path: rel_path.to_string(),
            module_name: normalized.to_string(),
        });
        return;
    };

    if let Some(target) = rest.strip_prefix("static ") {
        let target = target.trim();
        extracted.imports.push(ImportRelation {
            file_path: rel_path.to_string(),
            module_name: target.to_string(),
        });
        if is_external_csharp_path(target, import_context) {
            extracted
                .bindings
                .bare_wildcard_modules
                .push(target.to_string());
        }
        return;
    }

    if let Some((alias, target)) = rest.split_once('=') {
        let alias = alias.trim();
        let target = target.trim();
        extracted.imports.push(ImportRelation {
            file_path: rel_path.to_string(),
            module_name: target.to_string(),
        });
        if !alias.is_empty() && is_external_csharp_path(target, import_context) {
            extracted
                .bindings
                .member
                .insert(alias.to_string(), target.to_string());
        }
        return;
    }

    let namespace = rest.trim();
    extracted.imports.push(ImportRelation {
        file_path: rel_path.to_string(),
        module_name: namespace.to_string(),
    });
    if !is_external_csharp_path(namespace, import_context) {
        return;
    }
    if let Some(root) = namespace.split('.').next()
        && !root.is_empty()
    {
        extracted.bindings.external_roots.insert(
            root.to_string(),
            ExternalRootBinding {
                module: root.to_string(),
                module_from_qualifier: true,
            },
        );
    }
}

fn parse_php_import_statement(
    text: &str,
    rel_path: &str,
    import_context: &ImportResolutionContext,
    extracted: &mut ExtractedImports,
) {
    let normalized = text.trim().trim_end_matches(';').trim();
    let Some(rest) = normalized.strip_prefix("use ") else {
        extracted.imports.push(ImportRelation {
            file_path: rel_path.to_string(),
            module_name: normalized.to_string(),
        });
        return;
    };
    let (kind, rest) = if let Some(target) = rest.strip_prefix("function ") {
        (PhpImportKind::Function, target.trim())
    } else if let Some(target) = rest.strip_prefix("const ") {
        (PhpImportKind::Const, target.trim())
    } else {
        (PhpImportKind::ClassLike, rest.trim())
    };

    if rest.contains('*') {
        extracted.imports.push(ImportRelation {
            file_path: rel_path.to_string(),
            module_name: rest.to_string(),
        });
        return;
    }

    if let Some((base, group)) = split_php_use_group(rest) {
        for item in split_top_level(group, ',') {
            if let Some(target) = php_join_use_path(base, item) {
                register_php_import_item(&target, kind, rel_path, import_context, extracted);
            }
        }
        return;
    }

    if rest.contains('{') || rest.contains('}') {
        extracted.imports.push(ImportRelation {
            file_path: rel_path.to_string(),
            module_name: rest.to_string(),
        });
        return;
    }

    for item in split_top_level(rest, ',') {
        register_php_import_item(item, kind, rel_path, import_context, extracted);
    }
}

#[derive(Clone, Copy)]
enum PhpImportKind {
    ClassLike,
    Function,
    Const,
}

fn register_php_import_item(
    item: &str,
    kind: PhpImportKind,
    rel_path: &str,
    import_context: &ImportResolutionContext,
    extracted: &mut ExtractedImports,
) {
    let item = item.trim();
    if item.is_empty() {
        return;
    }
    let (target, alias) = split_alias(item);
    let target = target.trim_start_matches('\\');
    extracted.imports.push(ImportRelation {
        file_path: rel_path.to_string(),
        module_name: target.to_string(),
    });
    if !is_external_php_symbol(target, import_context) {
        return;
    }

    let imported_name = target.rsplit('\\').next().unwrap_or(target);
    let local_alias = alias.unwrap_or(imported_name);
    if matches!(kind, PhpImportKind::Function) {
        let module = target
            .rsplit_once('\\')
            .map(|(module, _)| module)
            .unwrap_or(target);
        extracted.bindings.bare.insert(
            local_alias.to_string(),
            ExternalImportBinding {
                module: module.to_string(),
                callee_name: imported_name.to_string(),
            },
        );
    } else {
        extracted
            .bindings
            .member
            .insert(local_alias.to_string(), target.to_string());
    }
}

fn split_php_use_group(text: &str) -> Option<(&str, &str)> {
    let (base, group) = split_rust_use_group(text)?;
    if base.is_empty() || group.is_empty() {
        return None;
    }
    Some((base, group))
}

fn php_join_use_path(prefix: &str, item: &str) -> Option<String> {
    let prefix = prefix.trim().trim_start_matches('\\');
    let (item_path, alias) = split_alias(item);
    let item_path = item_path.trim().trim_start_matches('\\');
    if item_path.is_empty() {
        return None;
    }

    let path = if prefix.is_empty() {
        item_path.to_string()
    } else if prefix.ends_with('\\') {
        format!("{prefix}{item_path}")
    } else {
        format!("{prefix}\\{item_path}")
    };

    Some(match alias {
        Some(alias) if !alias.is_empty() => format!("{path} as {alias}"),
        _ => path,
    })
}

fn parse_swift_import_statement(text: &str, rel_path: &str, extracted: &mut ExtractedImports) {
    let normalized = text.trim();
    let Some(rest) = normalized.strip_prefix("import ") else {
        extracted.imports.push(ImportRelation {
            file_path: rel_path.to_string(),
            module_name: normalized.to_string(),
        });
        return;
    };

    let mut tokens = rest.split_whitespace();
    let mut module_token = tokens.next().unwrap_or_default();
    if matches!(
        module_token,
        "class" | "struct" | "enum" | "protocol" | "func" | "typealias" | "var" | "let"
    ) {
        module_token = tokens.next().unwrap_or_default();
    }
    let module = module_token.split('.').next().unwrap_or_default();
    extracted.imports.push(ImportRelation {
        file_path: rel_path.to_string(),
        module_name: rest.to_string(),
    });
    if module.is_empty()
        || matches!(
            module,
            "class" | "struct" | "enum" | "protocol" | "func" | "typealias" | "var" | "let"
        )
    {
        return;
    }

    extracted.bindings.external_roots.insert(
        module.to_string(),
        ExternalRootBinding {
            module: module.to_string(),
            module_from_qualifier: false,
        },
    );
}

fn parse_ruby_import_statement(
    text: &str,
    rel_path: &str,
    import_context: &ImportResolutionContext,
    extracted: &mut ExtractedImports,
) {
    let normalized = text.trim();
    let Some(method) = normalized.split_whitespace().next() else {
        return;
    };

    let literal = extract_quoted_string(normalized);
    extracted.imports.push(ImportRelation {
        file_path: rel_path.to_string(),
        module_name: literal.clone().unwrap_or_else(|| normalized.to_string()),
    });

    if method != "require" {
        return;
    }
    let Some(required) = literal else {
        return;
    };
    let Some(root) = import_context.ruby_require_root(&required) else {
        return;
    };
    if import_context.ruby_local_constant_roots.contains(root) {
        return;
    }
    extracted.bindings.external_roots.insert(
        root.to_string(),
        ExternalRootBinding {
            module: required,
            module_from_qualifier: false,
        },
    );
}

fn parse_dart_import_statement(
    text: &str,
    rel_path: &str,
    import_context: &ImportResolutionContext,
    extracted: &mut ExtractedImports,
) {
    let normalized = collapse_whitespace(text);
    let Some(uri) = extract_quoted_string(&normalized) else {
        extracted.imports.push(ImportRelation {
            file_path: rel_path.to_string(),
            module_name: normalized,
        });
        return;
    };

    extracted.imports.push(ImportRelation {
        file_path: rel_path.to_string(),
        module_name: uri.clone(),
    });

    if !normalized.starts_with("import ") || !is_external_dart_uri(&uri, import_context) {
        return;
    }
    let Some(alias) = dart_import_alias(&normalized) else {
        return;
    };
    extracted.bindings.member.insert(alias, uri);
}

fn parse_elixir_import_statement(
    text: &str,
    rel_path: &str,
    import_context: &ImportResolutionContext,
    extracted: &mut ExtractedImports,
) {
    let normalized = collapse_whitespace(text);
    let Some((keyword, rest)) = normalized.split_once(' ') else {
        extracted.imports.push(ImportRelation {
            file_path: rel_path.to_string(),
            module_name: normalized,
        });
        return;
    };
    let target = rest.split([',', ' ']).next().unwrap_or_default().trim();
    extracted.imports.push(ImportRelation {
        file_path: rel_path.to_string(),
        module_name: if target.is_empty() {
            normalized.clone()
        } else {
            target.to_string()
        },
    });

    if !matches!(keyword, "alias" | "require") || !is_elixir_alias_path(target) {
        return;
    }
    let Some(root) = target.split('.').next() else {
        return;
    };
    if import_context.elixir_local_module_roots.contains(root) {
        return;
    }
    let Some(module) = import_context.elixir_external_root_module(root) else {
        return;
    };

    if keyword == "alias" {
        let alias = elixir_alias_as(&normalized)
            .unwrap_or_else(|| target.rsplit('.').next().unwrap_or(target).to_string());
        extracted.bindings.member.insert(alias, target.to_string());
    }
    extracted.bindings.external_roots.insert(
        root.to_string(),
        ExternalRootBinding {
            module: module.to_string(),
            module_from_qualifier: true,
        },
    );
}

fn collapse_whitespace(text: &str) -> String {
    text.split_whitespace().collect::<Vec<_>>().join(" ")
}

fn extract_js_module_specifier(text: &str) -> Option<String> {
    if let Some((_, after_from)) = text.rsplit_once(" from ") {
        return extract_quoted_string(after_from);
    }
    let rest = text.strip_prefix("import ")?;
    extract_quoted_string(rest)
}

fn extract_js_import_clause(text: &str) -> Option<&str> {
    let rest = text.strip_prefix("import ")?;
    let (clause, _) = rest.rsplit_once(" from ")?;
    Some(clause)
}

fn extract_quoted_string(text: &str) -> Option<String> {
    let quote = text.find(['"', '\'', '`'])?;
    let quote_char = text[quote..].chars().next()?;
    let after_quote = &text[quote + quote_char.len_utf8()..];
    let end = after_quote.find(quote_char)?;
    Some(after_quote[..end].to_string())
}

fn go_default_package_alias(module: &str) -> String {
    let module = module.trim_end_matches('/');
    let last_segment = module.rsplit('/').next().unwrap_or(module);
    last_segment
        .split_once(".v")
        .map(|(name, _)| name)
        .unwrap_or(last_segment)
        .replace('-', "_")
}

fn split_alias(text: &str) -> (&str, Option<&str>) {
    if let Some((name, alias)) = text.split_once(" as ") {
        (name.trim(), Some(alias.trim()))
    } else {
        (text.trim(), None)
    }
}

fn split_rust_use_group(text: &str) -> Option<(&str, &str)> {
    let mut depth = 0usize;
    let mut start = None;

    for (idx, ch) in text.char_indices() {
        match ch {
            '{' => {
                if depth == 0 {
                    start = Some(idx);
                }
                depth += 1;
            }
            '}' if depth > 0 => {
                depth -= 1;
                if depth == 0 {
                    let start = start?;
                    if text[idx + ch.len_utf8()..].trim().is_empty() {
                        return Some((text[..start].trim(), text[start + 1..idx].trim()));
                    }
                    return None;
                }
            }
            _ => {}
        }
    }

    None
}

fn rust_join_use_path(prefix: &str, item: &str) -> Option<String> {
    let prefix = prefix.trim().trim_end_matches("::").trim();
    let item = item.trim();
    if item.is_empty() {
        return None;
    }

    let (item_path, alias) = split_alias(item);
    let item_path = item_path.trim();
    if item_path.is_empty() {
        return None;
    }

    let path = if item_path == "self" {
        if prefix.is_empty() {
            return None;
        }
        prefix.to_string()
    } else if prefix.is_empty() {
        item_path.to_string()
    } else {
        format!("{prefix}::{item_path}")
    };

    Some(match alias {
        Some(alias) if !alias.is_empty() => format!("{path} as {alias}"),
        _ => path,
    })
}

fn split_top_level(text: &str, delimiter: char) -> Vec<&str> {
    let mut parts = Vec::new();
    let mut start = 0;
    let mut paren_depth = 0usize;
    let mut brace_depth = 0usize;
    let mut bracket_depth = 0usize;
    let mut in_single = false;
    let mut in_double = false;

    for (idx, ch) in text.char_indices() {
        match ch {
            '\'' if !in_double => in_single = !in_single,
            '"' if !in_single => in_double = !in_double,
            '(' if !in_single && !in_double => paren_depth += 1,
            ')' if !in_single && !in_double && paren_depth > 0 => paren_depth -= 1,
            '{' if !in_single && !in_double => brace_depth += 1,
            '}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
            '[' if !in_single && !in_double => bracket_depth += 1,
            ']' if !in_single && !in_double && bracket_depth > 0 => bracket_depth -= 1,
            ch if ch == delimiter
                && !in_single
                && !in_double
                && paren_depth == 0
                && brace_depth == 0
                && bracket_depth == 0 =>
            {
                parts.push(text[start..idx].trim());
                start = idx + ch.len_utf8();
            }
            _ => {}
        }
    }

    if start <= text.len() {
        parts.push(text[start..].trim());
    }

    parts
}

fn is_external_python_module(module: &str, import_context: &ImportResolutionContext) -> bool {
    if module.starts_with('.') {
        return false;
    }

    !import_context.python_modules.iter().any(|local_module| {
        local_module == module
            || local_module.starts_with(&format!("{module}."))
            || module.starts_with(&format!("{local_module}."))
    })
}

fn is_external_js_module(module: &str, import_context: &ImportResolutionContext) -> bool {
    if module.starts_with("node:") {
        return true;
    }
    if module.starts_with("./")
        || module.starts_with("../")
        || module.starts_with('/')
        || module.starts_with('#')
        || module.starts_with("~/")
        || module.starts_with("@/")
    {
        return false;
    }

    let Some(package_name) = js_package_name(module) else {
        return false;
    };
    if import_context.js_self_package_name.as_deref() == Some(package_name) {
        return false;
    }

    import_context.js_external_packages.contains(package_name)
        || JS_BUILTIN_MODULES.contains(&package_name)
}

fn is_external_go_module(module: &str, import_context: &ImportResolutionContext) -> bool {
    if module.starts_with('.') {
        return false;
    }
    if let Some(self_module) = import_context.go_module_path.as_deref()
        && (module == self_module || module.starts_with(&format!("{self_module}/")))
    {
        return false;
    }
    true
}

fn rust_external_roots(import_context: &ImportResolutionContext) -> HashSet<String> {
    let mut roots = import_context.rust_external_crates.clone();
    roots.extend(
        ["std", "core", "alloc", "proc_macro", "test"]
            .into_iter()
            .map(ToOwned::to_owned),
    );
    if let Some(self_crate) = import_context.rust_self_crate_name.as_deref() {
        roots.remove(self_crate);
    }
    roots
}

fn java_declared_types(contents: &str) -> Vec<String> {
    declared_types(contents, &["class", "interface", "enum", "record"])
}

fn csharp_declared_types(contents: &str) -> Vec<String> {
    declared_types(
        contents,
        &["class", "interface", "enum", "record", "struct"],
    )
}

fn declared_types(contents: &str, keywords: &[&str]) -> Vec<String> {
    let mut names = Vec::new();
    let tokens: Vec<&str> = contents
        .split(|ch: char| !(ch.is_ascii_alphanumeric() || ch == '_'))
        .filter(|token| !token.is_empty())
        .collect();
    for window in tokens.windows(2) {
        if keywords.contains(&window[0]) {
            names.push(window[1].to_string());
        }
    }
    names
}

fn php_declared_symbols(contents: &str) -> Vec<String> {
    let mut names = Vec::new();
    let tokens: Vec<&str> = contents
        .split(|ch: char| !(ch.is_ascii_alphanumeric() || ch == '_'))
        .filter(|token| !token.is_empty())
        .collect();
    for window in tokens.windows(2) {
        if matches!(
            window[0],
            "class" | "interface" | "trait" | "enum" | "function"
        ) {
            names.push(window[1].to_string());
        }
    }
    names
}

fn is_external_java_class(class_path: &str, import_context: &ImportResolutionContext) -> bool {
    !import_context.java_local_classes.contains(class_path)
        && class_path
            .rsplit('.')
            .next()
            .is_none_or(|class_name| !import_context.java_local_classes.contains(class_name))
}

fn is_external_csharp_path(path: &str, import_context: &ImportResolutionContext) -> bool {
    path.split('.')
        .next()
        .is_some_and(|root| !import_context.csharp_local_roots.contains(root))
}

fn is_external_php_symbol(path: &str, import_context: &ImportResolutionContext) -> bool {
    !import_context.php_local_symbols.contains(path)
        && path
            .rsplit('\\')
            .next()
            .is_none_or(|name| !import_context.php_local_symbols.contains(name))
}

fn is_external_rust_root(root: &str, import_context: &ImportResolutionContext) -> bool {
    if matches!(root, "crate" | "self" | "super") {
        return false;
    }
    if import_context.rust_self_crate_name.as_deref() == Some(root) {
        return false;
    }
    import_context.rust_external_crates.contains(root)
        || matches!(root, "std" | "core" | "alloc" | "proc_macro" | "test")
}

/// Returns a curated Ruby `require` to constant-root mapping.
///
/// Keep the bundled map small and documented in
/// `assets/import_roots/ruby_require_roots.json`; use runtime overrides when
/// constructing `ImportResolutionContext` for project-specific roots.
fn ruby_require_root(required: &str) -> Option<&'static str> {
    bundled_ruby_require_roots()
        .get(required)
        .map(String::as_str)
}

fn is_ruby_constant_name(name: &str) -> bool {
    name.chars()
        .next()
        .is_some_and(|ch| ch.is_ascii_uppercase())
        && name
            .chars()
            .all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
}

fn dart_import_alias(text: &str) -> Option<String> {
    let after_as = text.split_once(" as ")?.1;
    let alias = after_as
        .split_whitespace()
        .next()
        .unwrap_or_default()
        .trim_end_matches(';');
    if alias.is_empty() {
        None
    } else {
        Some(alias.to_string())
    }
}

fn is_external_dart_uri(uri: &str, import_context: &ImportResolutionContext) -> bool {
    if uri.starts_with("dart:") {
        return true;
    }
    let Some(package) = uri
        .strip_prefix("package:")
        .and_then(|rest| rest.split('/').next())
    else {
        return false;
    };
    import_context.dart_self_package_name.as_deref() != Some(package)
        && import_context.dart_external_packages.contains(package)
}

/// Returns curated Elixir dependency module roots.
///
/// Hex package names do not mechanically map to Elixir module roots. Maintain
/// the bundled map in `assets/import_roots/elixir_dependency_roots.json`; use
/// runtime overrides when constructing `ImportResolutionContext` if discovery
/// can provide more precise roots.
fn elixir_dependency_roots(dep: &str) -> Option<&'static [String]> {
    bundled_elixir_dependency_roots()
        .get(dep)
        .map(Vec::as_slice)
}

fn bundled_ruby_require_roots() -> &'static HashMap<String, String> {
    static ROOTS: OnceLock<HashMap<String, String>> = OnceLock::new();
    ROOTS.get_or_init(|| {
        serde_json::from_str(include_str!(
            "../../assets/import_roots/ruby_require_roots.json"
        ))
        .expect("bundled Ruby require roots JSON parses")
    })
}

fn bundled_elixir_dependency_roots() -> &'static HashMap<String, Vec<String>> {
    static ROOTS: OnceLock<HashMap<String, Vec<String>>> = OnceLock::new();
    ROOTS.get_or_init(|| {
        serde_json::from_str(include_str!(
            "../../assets/import_roots/elixir_dependency_roots.json"
        ))
        .expect("bundled Elixir dependency roots JSON parses")
    })
}

fn is_elixir_alias(name: &str) -> bool {
    name.chars()
        .next()
        .is_some_and(|ch| ch.is_ascii_uppercase())
        && name
            .chars()
            .all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
}

fn is_elixir_alias_path(path: &str) -> bool {
    path.split('.').all(is_elixir_alias)
}

fn elixir_alias_as(text: &str) -> Option<String> {
    let after = text.split_once(" as: ")?.1;
    let alias = after
        .split([',', ' ', ')', ']'])
        .next()
        .unwrap_or_default()
        .trim();
    if is_elixir_alias(alias) {
        Some(alias.to_string())
    } else {
        None
    }
}

fn js_package_name(module: &str) -> Option<&str> {
    if let Some(stripped) = module.strip_prefix('@') {
        let mut segments = stripped.split('/');
        let scope = segments.next()?;
        let package = segments.next()?;
        let consumed = scope.len() + package.len() + 2;
        module.get(..consumed)
    } else {
        module.split('/').next()
    }
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::TempDir;

    use super::*;

    #[test]
    fn loads_rust_inline_table_dependency_names() {
        let tempdir = TempDir::new().expect("tempdir");
        fs::write(
            tempdir.path().join("Cargo.toml"),
            r#"
[package]
name = "app"

[dependencies]
serde = { version = "1.0" }
"tokio-util" = { version = "0.7", features = ["codec"] }
"#,
        )
        .expect("cargo toml");

        let crates = load_rust_external_crates(tempdir.path());

        assert!(crates.contains("serde"));
        assert!(crates.contains("tokio_util"));
    }

    #[test]
    fn loads_rust_dependency_names_from_real_toml_tables() {
        let tempdir = TempDir::new().expect("tempdir");
        fs::write(
            tempdir.path().join("Cargo.toml"),
            r#"
[package]
name = "app"

[dependencies]
serde = "1" # keep comment parsing delegated to TOML

[dev-dependencies]
pretty-assertions = "1"

[build-dependencies]
bindgen = "0.69"

[target.'cfg(unix)'.dependencies]
nix = "0.27"

[target.x86_64-pc-windows-msvc.dev-dependencies]
windows-sys = "0.52"

[target.'cfg(target_os = "linux")'.build-dependencies]
cc = "1"
"#,
        )
        .expect("cargo toml");

        let crates = load_rust_external_crates(tempdir.path());

        for name in [
            "serde",
            "pretty_assertions",
            "bindgen",
            "nix",
            "windows_sys",
            "cc",
        ] {
            assert!(crates.contains(name), "missing {name}");
        }
    }

    #[test]
    fn ignores_rust_non_dependency_toml_tables() {
        let tempdir = TempDir::new().expect("tempdir");
        fs::write(
            tempdir.path().join("Cargo.toml"),
            r#"
[package]
name = "app"

[workspace.dependencies]
workspace-only = "1"

[package.metadata.dependencies]
metadata-only = "1"

[features]
serde = []
"#,
        )
        .expect("cargo toml");

        let crates = load_rust_external_crates(tempdir.path());

        assert!(!crates.contains("workspace_only"));
        assert!(!crates.contains("metadata_only"));
        assert!(!crates.contains("serde"));
    }

    #[test]
    fn normalizes_rust_package_name_from_cargo_toml() {
        let tempdir = TempDir::new().expect("tempdir");
        fs::write(
            tempdir.path().join("Cargo.toml"),
            r#"
[package]
name = "my-crate"
"#,
        )
        .expect("cargo toml");

        assert_eq!(
            load_rust_self_crate_name(tempdir.path()).as_deref(),
            Some("my_crate")
        );
    }

    #[test]
    fn rust_grouped_imports_register_named_bare_bindings() {
        let mut extracted = ExtractedImports::default();

        parse_import_statement(
            "rust",
            "use std::collections::{HashMap, HashSet as Set};",
            "src/lib.rs",
            &ImportResolutionContext::default(),
            &mut extracted,
        );

        let hash_map = extracted
            .bindings
            .bare
            .get("HashMap")
            .expect("HashMap binding");
        assert_eq!(hash_map.module, "std::collections");
        assert_eq!(hash_map.callee_name, "HashMap");
        let set = extracted.bindings.bare.get("Set").expect("Set binding");
        assert_eq!(set.module, "std::collections");
        assert_eq!(set.callee_name, "HashSet");
        assert_eq!(
            extracted.bindings.member.get("Set").map(String::as_str),
            Some("std::collections::HashSet")
        );
        assert!(extracted.bindings.external_roots.contains_key("std"));
    }

    #[test]
    fn rust_glob_imports_do_not_register_individual_bare_bindings() {
        let mut extracted = ExtractedImports::default();

        parse_import_statement(
            "rust",
            "use std::collections::*;",
            "src/lib.rs",
            &ImportResolutionContext::default(),
            &mut extracted,
        );

        assert!(extracted.bindings.bare.is_empty());
        assert!(extracted.bindings.member.is_empty());
    }

    #[test]
    fn php_grouped_imports_register_concrete_member_bindings() {
        let mut extracted = ExtractedImports::default();

        parse_import_statement(
            "php",
            r"use Vendor\Pkg\{Client, Helper as H};",
            "src/sample.php",
            &ImportResolutionContext::default(),
            &mut extracted,
        );

        assert!(
            extracted
                .imports
                .iter()
                .any(|import| import.module_name == r"Vendor\Pkg\Client")
        );
        assert!(
            extracted
                .imports
                .iter()
                .any(|import| import.module_name == r"Vendor\Pkg\Helper")
        );
        assert_eq!(
            extracted.bindings.member.get("Client").map(String::as_str),
            Some(r"Vendor\Pkg\Client")
        );
        assert_eq!(
            extracted.bindings.member.get("H").map(String::as_str),
            Some(r"Vendor\Pkg\Helper")
        );
    }

    #[test]
    fn php_grouped_function_imports_register_concrete_bare_bindings() {
        let mut extracted = ExtractedImports::default();

        parse_import_statement(
            "php",
            r"use function Vendor\Pkg\{work, helper as do_help};",
            "src/sample.php",
            &ImportResolutionContext::default(),
            &mut extracted,
        );

        assert!(
            extracted
                .imports
                .iter()
                .any(|import| import.module_name == r"Vendor\Pkg\work")
        );
        let work = extracted
            .bindings
            .bare
            .get("work")
            .expect("function binding");
        assert_eq!(work.module, r"Vendor\Pkg");
        assert_eq!(work.callee_name, "work");
        let helper = extracted
            .bindings
            .bare
            .get("do_help")
            .expect("aliased function binding");
        assert_eq!(helper.module, r"Vendor\Pkg");
        assert_eq!(helper.callee_name, "helper");
    }

    #[test]
    fn php_grouped_const_imports_preserve_aliases() {
        let mut extracted = ExtractedImports::default();

        parse_import_statement(
            "php",
            r"use const Vendor\Pkg\{VALUE as V};",
            "src/sample.php",
            &ImportResolutionContext::default(),
            &mut extracted,
        );

        assert_eq!(
            extracted.bindings.member.get("V").map(String::as_str),
            Some(r"Vendor\Pkg\VALUE")
        );
    }

    #[test]
    fn loads_elixir_mix_lock_first_quoted_dependency_per_line() {
        let tempdir = TempDir::new().expect("tempdir");
        fs::write(
            tempdir.path().join("mix.lock"),
            r#"%{
  "jason": {:hex, :jason, "1.4.4", "checksum", [:mix], [], "hexpm", "repo"},
  "httpoison": {:hex, :httpoison, "2.2.1", "checksum", [:mix], [], "hexpm", "repo"}
}"#,
        )
        .expect("mix.lock");

        let deps = load_elixir_dependency_names(tempdir.path());

        assert!(deps.contains("jason"));
        assert!(deps.contains("httpoison"));
        assert!(!deps.contains("1"));
        assert!(!deps.contains("hexpm"));
    }

    #[test]
    fn bundled_import_root_data_loads_known_mappings() {
        assert_eq!(ruby_require_root("json"), Some("JSON"));
        assert_eq!(ruby_require_root("unknown_gem"), None);

        let roots = elixir_dependency_roots("jason").expect("jason roots");
        assert_eq!(roots.first().map(String::as_str), Some("Jason"));
        assert_eq!(roots.len(), 1);
        assert!(elixir_dependency_roots("unknown_dep").is_none());
    }

    #[test]
    fn runtime_import_root_overrides_take_precedence() {
        let tempdir = TempDir::new().expect("tempdir");
        fs::write(
            tempdir.path().join("mix.exs"),
            r#"
defp deps do
  [
    {:jason, "~> 1.4"}
  ]
end
"#,
        )
        .expect("mix.exs");

        let context = build_import_resolution_context_with_overrides(
            tempdir.path(),
            &[],
            HashMap::from([("json".to_string(), "RuntimeJSON".to_string())]),
            HashMap::from([
                ("Jason".to_string(), "RuntimeJason".to_string()),
                ("RuntimeOnly".to_string(), "RuntimeOnly".to_string()),
            ]),
        );

        let mut extracted = ExtractedImports::default();
        parse_import_statement(
            "ruby",
            r#"require "json""#,
            "app.rb",
            &context,
            &mut extracted,
        );
        assert!(
            extracted
                .bindings
                .external_roots
                .contains_key("RuntimeJSON")
        );
        assert!(!extracted.bindings.external_roots.contains_key("JSON"));

        let mut bindings = ImportBindings::default();
        seed_import_bindings("elixir", &context, &mut bindings);
        assert_eq!(
            bindings
                .external_roots
                .get("Jason")
                .map(|binding| binding.module.as_str()),
            Some("RuntimeJason")
        );
        assert_eq!(
            bindings
                .external_roots
                .get("RuntimeOnly")
                .map(|binding| binding.module.as_str()),
            Some("RuntimeOnly")
        );
    }

    #[test]
    fn go_default_package_alias_uses_last_segment_before_version_suffix() {
        assert_eq!(go_default_package_alias("gopkg.in/yaml.v3"), "yaml");
        assert_eq!(
            go_default_package_alias("github.com/acme/api-client/"),
            "api_client"
        );
    }

    #[test]
    fn go_backtick_imports_register_external_bindings() {
        let import_context = ImportResolutionContext {
            go_module_path: Some("example.com/local".to_string()),
            ..Default::default()
        };
        let mut extracted = ExtractedImports::default();

        parse_import_statement(
            "go",
            "import api `github.com/acme/api-client`",
            "main.go",
            &import_context,
            &mut extracted,
        );

        assert_eq!(
            extracted
                .imports
                .first()
                .map(|import| import.module_name.as_str()),
            Some("github.com/acme/api-client")
        );
        assert_eq!(
            extracted.bindings.member.get("api").map(String::as_str),
            Some("github.com/acme/api-client")
        );
    }

    #[test]
    fn csharp_declared_types_includes_structs() {
        let names = csharp_declared_types(
            "public struct Point {} class Sample {} interface IThing {} enum Mode {} record Data;",
        );

        assert!(names.iter().any(|name| name == "Point"));
        assert!(names.iter().any(|name| name == "Sample"));
        assert!(names.iter().any(|name| name == "IThing"));
        assert!(names.iter().any(|name| name == "Mode"));
        assert!(names.iter().any(|name| name == "Data"));
    }

    #[test]
    fn empty_php_fully_qualified_namespace_stays_unresolved() {
        let target = resolve_external_callee(
            &ImportResolutionContext::default(),
            &ImportBindings::default(),
            &[],
            "helper",
            Some(""),
            Some("\\"),
            false,
        );

        assert!(target.is_none());
    }

    #[test]
    fn php_local_fully_qualified_class_stays_unresolved() {
        let mut import_context = ImportResolutionContext::default();
        import_context
            .php_local_symbols
            .insert(r"App\Services\Mailer".to_string());

        let target = resolve_external_callee(
            &import_context,
            &ImportBindings::default(),
            &[],
            "send",
            Some("App"),
            Some(r"\App\Services\Mailer"),
            false,
        );

        assert!(target.is_none());
    }

    #[test]
    fn php_local_fully_qualified_function_stays_unresolved() {
        let mut import_context = ImportResolutionContext::default();
        import_context
            .php_local_symbols
            .insert(r"App\Helpers\render".to_string());

        let target = resolve_external_callee(
            &import_context,
            &ImportBindings::default(),
            &[],
            "render",
            Some("App"),
            Some(r"\App\Helpers"),
            false,
        );

        assert!(target.is_none());
    }
}
