use std::collections::HashSet;
use std::rc::Rc;

use aether_path::FilePath;
use biome_rowan::TextSize;
use oak_semantic::semantic_index::DefinitionKind;
use oak_semantic::DefinitionId;
use oak_semantic::ScopeId;

use crate::Db;
use crate::Definition;
use crate::ExportEntry;
use crate::File;
use crate::ImportLayer;
use crate::Name;
use crate::NamespaceVisibility;

#[salsa::tracked]
impl<'db> File {
    /// Resolve `name` against this file's lazy / end-of-file state.
    ///
    /// "Lazy" because this is the binding view a *function body* would see
    /// when it actually runs: by then the file has executed end-to-end, all
    /// `library()` calls have attached, and `source()` chains have been
    /// followed. We can't do full call-site analysis, so we over-approximate
    /// to the EOF state of the file.
    ///
    /// Lookup order:
    /// 1. **`exports()` chain**: file-top-level locals plus
    ///    `source()`-forwarded entries. `ExportEntry::Import` is chased
    ///    through `exports(target)` until it lands on a `Local`. Cycles in
    ///    `source()` resolve to empty exports via `exports`'s `cycle_fn`.
    /// 2. **`imports_by_sourcing_file()` walk**: the file's own context plus
    ///    one per file that sources it, each checked in priority order and the
    ///    results unioned across contexts. `File` siblings are checked via their
    ///    exports chain only (not their full `resolve`), to avoid the cycle
    ///    that recursing would create. `Package` and `From` layers call
    ///    [`Package::resolve`] with `Exported` visibility.
    ///
    /// Returns every definition the name reaches in the first binding layer of
    /// each context, so a name with two top-level bindings yields both, as does
    /// a name bound in two different files that each source this one (see
    /// [`resolve_per_sourcing_file`]). The own-file `exports()` chain shadows
    /// imports, matching R: if the file binds the name at top level we stop
    /// there and never fall through to a package.
    ///
    /// Each returned `Definition` is keyed by `(file, scope, name)`, so
    /// downstream queries that only depend on identity stay cached across
    /// edits that shift the binding's source position. Consumers that need
    /// a position or the bound expression read `def.kind(db)` and project
    /// per-variant.
    ///
    /// Uses end-of-file visibility: every collation sibling and only
    /// unconditional top-level attaches are visible, including `library()`
    /// calls after the query's source position. [`File::resolve_at`] applies
    /// position-specific visibility.
    #[salsa::tracked(returns(clone))]
    pub fn resolve(self, db: &'db dyn Db, name: Name<'db>) -> Vec<Definition<'db>> {
        let exported = self.resolve_export(db, name);
        if !exported.is_empty() {
            return exported;
        }

        resolve_per_sourcing_file(db, self.imports_by_sourcing_file(db), name)
    }

    /// Resolve the name at `offset` to its definition(s).
    ///
    /// Returns every binding that could reach the use, so a name defined on
    /// both arms of an `if`/`else` yields two. A cursor on a binding's own name
    /// resolves to that binding. Empty means nothing reachable binds the name
    /// here.
    ///
    /// Not `#[salsa::tracked]` because keying on `(self, offset)` would
    /// balloon the cache. The `Definition` entities it returns are minted by
    /// the tracked [`File::definitions()`] and looked up via
    /// [`File::definition()`], so they stay cached and identity-stable even
    /// though this lookup isn't.
    pub fn resolve_at(self, db: &'db dyn Db, offset: TextSize) -> Vec<Definition<'db>> {
        let index = self.semantic_index(db);
        let Some((use_scope, use_id, use_site)) = index.use_at(offset) else {
            // Cursor on a binding's own name (a def site, not a use): jump to
            // it, like rust-analyzer / ty.
            return match index.definition_at(offset) {
                Some((scope, def_id, _)) => {
                    self.definition(db, scope, def_id).into_iter().collect()
                },
                None => Vec::new(),
            };
        };
        let name = index
            .symbols(use_scope)
            .symbol(use_site.symbol())
            .name()
            .to_string();
        let name = Name::new(db, name.as_str());

        // Get local definitions for that use
        let reaching: Vec<(ScopeId, DefinitionId)> =
            index.reaching_definitions(use_scope, use_id).collect();

        let mut definitions: Vec<Definition<'db>> = reaching
            .into_iter()
            .flat_map(|(scope, def_id)| self.resolve_definition(db, scope, def_id))
            .collect();

        if index.use_is_bound(use_scope, use_id) {
            return definitions;
        }

        // At top level, `ImportView::at()` sees preceding collation files. In
        // a function body, it sees the complete collation but keeps attaches
        // offset- and scope-aware.
        //
        // An unbound path can resolve through imports, so retain in-file
        // definitions and add distinct imported ones.
        let imported: Vec<Definition<'db>> =
            resolve_per_sourcing_file(db, &self.imports_by_sourcing_file_at(db, offset), name)
                .into_iter()
                .filter(|def| !definitions.contains(def))
                .collect();
        definitions.extend(imported);

        definitions
    }

    fn resolve_definition(
        self,
        db: &'db dyn Db,
        scope_id: ScopeId,
        def_id: DefinitionId,
    ) -> Vec<Definition<'db>> {
        let index = self.semantic_index(db);
        if let DefinitionKind::Import {
            file: target_url,
            name: forwarded,
            ..
        } = index.definitions(scope_id)[def_id].kind()
        {
            let Some(target) = db.file_by_path(&FilePath::from_url(target_url)) else {
                return Vec::new();
            };
            return target.resolve_export(db, Name::new(db, forwarded.as_str()));
        }
        self.definition(db, scope_id, def_id).into_iter().collect()
    }

    /// Walk this file's exports for `name`, chasing `source()`-forwarded
    /// `Import` entries through target exports until they land on `Local`
    /// definitions. Returns every definition the name reaches, so a name with
    /// two top-level bindings, or forwards from two different sourced files,
    /// yields all of them.
    ///
    /// Order follows `exports()`: entries are visited in definition order and
    /// imports are chased in place, so the last element is the binding R picks
    /// at runtime (statements run in order, last write wins). Several top-level
    /// locals collapse to one `Local` marker, so they come out grouped at that
    /// marker's position rather than each at its own offset, but the runtime
    /// winner is still last.
    ///
    /// Cycles resolve to empty via `exports`'s `cycle_fn`.
    #[salsa::tracked(returns(clone))]
    pub(crate) fn resolve_export(self, db: &'db dyn Db, name: Name<'db>) -> Vec<Definition<'db>> {
        let mut results = Vec::new();

        // `visited` keys on `(file, name)` so each binding is minted once even
        // when several forwards converge on it, and so an `Import` cycle (which
        // `exports()`'s `cycle_result` already breaks) can't loop here. The
        // `Rc<str>` is cheap to clone (refcount bump).
        let mut visited: HashSet<(File, Rc<str>)> = HashSet::new();
        self.collect_exports(
            db,
            Rc::from(name.text(db).as_str()),
            &mut visited,
            &mut results,
        );

        results
    }

    /// Append every definition `name` reaches in `self`'s exports to `results`,
    /// in `exports()` order, recursing into `Import` forwards in place. See
    /// [`File::resolve_export`] for the ordering contract.
    fn collect_exports(
        self,
        db: &'db dyn Db,
        name: Rc<str>,
        visited: &mut HashSet<(File, Rc<str>)>,
        results: &mut Vec<Definition<'db>>,
    ) {
        if !visited.insert((self, name.clone())) {
            return;
        }

        let Some(entries) = self.exports(db).get(name.as_ref()) else {
            return;
        };

        for entry in entries {
            match entry {
                ExportEntry::Local => {
                    // The `Local` marker doesn't carry a `def_id`, so recover
                    // from the semantic index the file-scope `def_id`s still in
                    // effect once the file has run top to bottom, and mint each
                    // through the single site. Usually that's one def; a name
                    // still bound on several paths (both arms of a top-level
                    // `if`/`else`) fans out to several here.
                    //
                    // `exports()` also lists the `Import`-kind defs that
                    // `source()` emits at file scope. Skip them: they're the
                    // forwards already chased through the `Import` entries, and
                    // minting one here would add a bogus target at the empty
                    // `source()` call span.
                    let index = self.semantic_index(db);
                    for (def_id, def) in index.export(name.as_ref()) {
                        if matches!(def.kind(), DefinitionKind::Import { .. }) {
                            continue;
                        }
                        results.extend(self.definition(db, ScopeId::from(0), def_id));
                    }
                },

                ExportEntry::Import {
                    file,
                    name: forwarded,
                } => {
                    file.collect_exports(db, Rc::from(forwarded.as_str()), visited, results);
                },
            }
        }
    }
}

/// Resolve `name` against a single import layer, returning every definition it
/// reaches there (empty means the layer doesn't bind it, so the caller falls
/// through to the next layer). Shared by [`File::resolve`] and
/// [`File::resolve_at`].
///
/// `File` siblings are chased through their exports only, not their full
/// `resolve`: recursing would walk the sibling's own imports, which include
/// *this* file, and salsa would cycle on any unbound name. Exports-only is also
/// what R's namespace semantics asks for, a package's namespace is the merged
/// exports of its collation files.
pub(crate) fn resolve_import_layer<'db>(
    db: &'db dyn Db,
    layer: &ImportLayer,
    name: Name<'db>,
) -> Vec<Definition<'db>> {
    // Reduce the layer to the package it binds `name` to, then resolve against
    // that package's exports. A `File` sibling is the exception, resolved
    // through its own exports chain.
    let package = match layer {
        ImportLayer::File(file) => return file.resolve_export(db, name),
        ImportLayer::SourcingFile {
            file,
            exports_so_far,
        } => {
            // Checking the snapshot here rather than inside `resolve_export()`
            // prevents a `semantic_index` read. Since the index is `no_eq` this
            // would compromise the `File::resolve` firewall.
            if !exports_so_far.contains(name.text(db).as_str()) {
                return Vec::new();
            }
            return file.resolve_export(db, name);
        },
        ImportLayer::Package(package) => *package,
        ImportLayer::From(importer) => {
            match importer.imported_from(db).get(name.text(db).as_str()) {
                Some(source) => match db.package_by_name(source) {
                    Some(package) => package,
                    None => return Vec::new(),
                },
                None => return Vec::new(),
            }
        },
    };
    package.resolve(db, name, NamespaceVisibility::Exported)
}

/// Resolve `name` in every context of [`File::imports_by_sourcing_file`] and
/// union the results.
///
/// Within one sourcing context, first hit wins as usual. On the other hand,
/// two files sourcing the same target do not mask each other, they provide
/// alternative contexts. Contexts converging on one binding (a shared sourced
/// file, a search-path package) dedupe.
///
/// `Vec::contains()` preserves deterministic insertion order. The linear scan
/// is acceptable because each context yields few definitions.
fn resolve_per_sourcing_file<'db>(
    db: &'db dyn Db,
    contexts: &[Vec<ImportLayer>],
    name: Name<'db>,
) -> Vec<Definition<'db>> {
    let mut results: Vec<Definition<'db>> = Vec::new();
    for context in contexts {
        for layer in context {
            let defs = resolve_import_layer(db, layer, name);
            if defs.is_empty() {
                continue;
            }
            for def in defs {
                if !results.contains(&def) {
                    results.push(def);
                }
            }
            break;
        }
    }
    results
}
