/**
 * Scope-chain lookup primitives shared across language providers.
 *
 * Five functions:
 *   - `findReceiverTypeBinding` — walk scope.typeBindings up the chain
 *     for a receiver name.
 *   - `lookupBindingsAt` — read finalized + augmented binding refs at
 *     one scope, deduped by `def.nodeId`. The dual-source-aware
 *     primitive every other binding lookup composes with.
 *   - `findClassBindingInScope` — walk scope.bindings + the indexes via
 *     `lookupBindingsAt` for a class-kind binding.
 *   - `findOwnedMember` — find a method/field owned by a class def
 *     across all parsed files by (ownerId, simpleName).
 *   - `findExportedDef` — find a file-level exported def (top-of-module
 *     class / function) by simpleName.
 *
 * Next-consumer contract: every OO or module-capable language hits the
 * same pre-finalize / post-finalize binding split and the same
 * "resolve member on owner with MRO" pattern. All four are reusable
 * as-is for TypeScript, Java, Kotlin, Ruby, etc.
 */
const EMPTY_BINDINGS = Object.freeze([]);
/**
 * Look up binding refs at `scopeId` for `name`, consulting both the
 * finalize-owned `bindings` channel and the post-finalize
 * `bindingAugmentations` channel (see invariant I8 in
 * `contract/scope-resolver.ts`). Finalized refs come first; augmented
 * refs append, deduped by `def.nodeId` so a sibling that's also
 * explicitly imported doesn't double-emit.
 *
 * Returns a shared frozen empty array when neither channel has the
 * name — callers can compare against `=== EMPTY_BINDINGS` if they
 * want a fast-path miss check. The bucket arrays are returned by
 * reference when only one channel populates them; the merged path
 * allocates a fresh array.
 *
 * Walker primitives (`findClassBindingInScope`,
 * `findCallableBindingInScope`, `findExportedDefByName`) and
 * post-finalize passes that read finalized bindings (e.g.
 * `propagateImportedReturnTypes`, `namespace-targets`) MUST go
 * through this helper instead of `scopes.bindings.get(...)` directly,
 * so the augmentation channel is always visible.
 */
export function lookupBindingsAt(scopeId, name, scopes) {
    const finalized = scopes.bindings.get(scopeId)?.get(name);
    const augmented = scopes.bindingAugmentations.get(scopeId)?.get(name);
    const workspace = scopes.workspaceFqnBindings?.get(name);
    const fLen = finalized?.length ?? 0;
    const aLen = augmented?.length ?? 0;
    const wLen = workspace?.length ?? 0;
    if (fLen === 0 && aLen === 0 && wLen === 0)
        return EMPTY_BINDINGS;
    if (aLen === 0 && wLen === 0)
        return finalized;
    if (fLen === 0 && wLen === 0)
        return augmented;
    if (fLen === 0 && aLen === 0)
        return workspace;
    const seen = new Set();
    const out = [];
    if (fLen > 0) {
        for (const r of finalized) {
            seen.add(r.def.nodeId);
            out.push(r);
        }
    }
    if (aLen > 0) {
        for (const r of augmented) {
            if (seen.has(r.def.nodeId))
                continue;
            seen.add(r.def.nodeId);
            out.push(r);
        }
    }
    if (wLen > 0) {
        for (const r of workspace) {
            if (seen.has(r.def.nodeId))
                continue;
            out.push(r);
        }
    }
    return out;
}
const EMPTY_NAMES = Object.freeze([]);
/**
 * Return the union of bound names at `scopeId` across both the
 * finalized and augmented channels. Companion to `lookupBindingsAt`
 * for callers that need to iterate every name at a scope (e.g.
 * `propagateImportedReturnTypes`). Order is not guaranteed; callers
 * that need stable iteration should sort externally.
 *
 * Fast paths (zero allocation) when at most one channel is populated:
 * returns the underlying `Map.keys()` iterator directly. Only when both
 * channels carry names do we materialize a `Set` for deduplication.
 */
export function namesAtScope(scopeId, scopes) {
    const finalized = scopes.bindings.get(scopeId);
    const augmented = scopes.bindingAugmentations.get(scopeId);
    const fSize = finalized?.size ?? 0;
    const aSize = augmented?.size ?? 0;
    if (fSize === 0 && aSize === 0)
        return EMPTY_NAMES;
    if (aSize === 0)
        return finalized.keys();
    if (fSize === 0)
        return augmented.keys();
    const out = new Set(finalized.keys());
    for (const name of augmented.keys())
        out.add(name);
    return out;
}
/**
 * True when a def's `type` names a class-like declaration — every kind
 * that collapses to `@scope.class` in the scope-extractor query contract.
 *
 * Semantics widened historically from `'Class' | 'Interface'` to cover
 * C#-shape languages (struct, record, enum, trait). Languages that emit
 * only `'Class'` are unaffected — the extra kinds never appear in their
 * parsed output.
 */
export function isClassLike(t) {
    return (t === 'Class' ||
        t === 'Interface' ||
        t === 'Struct' ||
        t === 'Record' ||
        t === 'Enum' ||
        t === 'Trait');
}
/**
 * Walk the scope chain from `startScope` looking for a typeBinding
 * named `receiverName`. Returns the TypeRef or undefined if no binding
 * exists in the chain.
 */
export function findReceiverTypeBinding(startScope, receiverName, scopes) {
    let currentId = startScope;
    const visited = new Set();
    while (currentId !== null) {
        if (visited.has(currentId))
            return undefined;
        visited.add(currentId);
        const scope = scopes.scopeTree.getScope(currentId);
        if (scope === undefined)
            return undefined;
        const typeRef = scope.typeBindings.get(receiverName);
        if (typeRef !== undefined)
            return typeRef;
        currentId = scope.parent;
    }
    return undefined;
}
/**
 * Look up a class-like binding by name in the given scope's chain.
 *
 * "Class-like" covers `Class | Interface | Struct | Record | Enum |
 * Trait` via the shared `isClassLike` predicate — every kind that
 * collapses to `@scope.class` in the scope-extractor query contract.
 *
 * Walks the scope chain upward and consults TWO sources at each step:
 *   1. `scope.bindings` — populated during scope-extraction Pass 2 with
 *      local declarations (`origin: 'local'`).
 *   2. The cross-file finalized + augmented bindings, via
 *      `lookupBindingsAt` (per I8: finalized = canonical immutable
 *      output; augmented = post-finalize hooks like
 *      `populateNamespaceSiblings`).
 *
 * Without (2) we'd miss every cross-file class-receiver call.
 */
export function findClassBindingInScope(startScope, receiverName, scopes) {
    const local = walkScopeChain(startScope, receiverName, scopes, (def) => isClassLike(def.type));
    if (local !== undefined)
        return local;
    // Fallback for languages (Go) where namespace-style imports don't
    // create scope bindings: resolve via QualifiedNameIndex. Only fires
    // when the scope-chain walk found nothing; single-match wins.
    const qnames = scopes.qualifiedNames.get(receiverName);
    if (qnames.length === 1) {
        const def = scopes.defs.get(qnames[0]);
        if (def !== undefined && isClassLike(def.type))
            return def;
    }
    // Second fallback: dotted names like "models.User" — try the simple
    // name (tail after last dot) for languages where defs are indexed by
    // simple name (Go). Only when the dotted lookup fails.
    if (receiverName.includes('.')) {
        const simple = receiverName.slice(receiverName.lastIndexOf('.') + 1);
        if (simple.length > 0 && simple !== receiverName) {
            const simpleIds = scopes.qualifiedNames.get(simple);
            if (simpleIds.length === 1) {
                const def = scopes.defs.get(simpleIds[0]);
                if (def !== undefined && isClassLike(def.type))
                    return def;
            }
        }
    }
    return undefined;
}
/**
 * Predicate for value-receiver bridge: the labels for which
 * `reconcileOwnership` registers methods/fields under the def's
 * `nodeId` as the `ownerId`. Explicit allowlist so future NodeLabel
 * additions (Module, Namespace, TypeAlias, EnumMember, etc.) do NOT
 * silently widen the bridge — adding a new ownerable label requires
 * touching both this predicate and `reconcileOwnership`.
 *
 * See: `scope-resolution/pipeline/reconcile-ownership.ts` Property /
 * Variable / Const / Static registration block.
 */
export function isOwnableValueLabel(t) {
    return t === 'Const' || t === 'Variable' || t === 'Property' || t === 'Static';
}
/**
 * Look up a value-binding (Const/Variable/Property/Static) by name in
 * the given scope's chain. Used by the value-receiver-owner bridge
 * for object-literal services such as:
 *
 *   export const fooService = { getUser(id) {...} };
 *
 * where `fooService` is a `Const`/`Variable` whose `nodeId` is the
 * `ownerId` of the member method. Neither `findClassBindingInScope`
 * (rejects non-class-like) nor `findReceiverTypeBinding` (no typeBinding
 * for an unannotated literal) finds it.
 *
 * Mirrors `findClassBindingInScope` exactly; only the accepted def-type
 * predicate differs.
 */
export function findValueBindingInScope(startScope, receiverName, scopes) {
    return walkScopeChain(startScope, receiverName, scopes, (def) => isOwnableValueLabel(def.type));
}
/**
 * Generic scope-chain walker. Walks from `startScope` toward the root,
 * consulting both the local `scope.bindings` channel and the dual-source
 * `lookupBindingsAt` view (finalized + augmented). At each scope, local
 * bindings are exhausted BEFORE imported/augmented bindings — preserves
 * JavaScript-style lexical scoping where a local `const x` shadows an
 * imported `x` of the same name.
 *
 * Returns the first binding `def` matching `predicate`. Cycles in the
 * scope graph terminate the walk (defensive — should not occur in
 * well-formed inputs).
 */
function walkScopeChain(startScope, name, scopes, predicate) {
    let currentId = startScope;
    const visited = new Set();
    while (currentId !== null) {
        if (visited.has(currentId))
            return undefined;
        visited.add(currentId);
        const scope = scopes.scopeTree.getScope(currentId);
        if (scope === undefined)
            return undefined;
        // Local first: a `const x` in this scope shadows any imported `x`.
        const localBindings = scope.bindings.get(name);
        if (localBindings !== undefined) {
            for (const b of localBindings) {
                if (predicate(b.def))
                    return b.def;
            }
        }
        // Then imported/augmented bindings — only consulted when no local match.
        const importedBindings = lookupBindingsAt(currentId, name, scopes);
        for (const b of importedBindings) {
            if (predicate(b.def))
                return b.def;
        }
        currentId = scope.parent;
    }
    return undefined;
}
/**
 * Look up a callable (Function/Method/Constructor) by name in the
 * given scope's chain. Uses the dual-source pattern (scope.bindings +
 * `lookupBindingsAt` for finalized + augmented) so cross-file
 * imports are visible — without it free calls to imported functions
 * never resolve via the post-pass.
 *
 * Mirrors `findClassBindingInScope` exactly; only the accepted
 * def-type predicate differs.
 */
export function findCallableBindingInScope(startScope, callableName, scopes) {
    return findAllCallableBindingsInScope(startScope, callableName, scopes)[0];
}
/**
 * Look up all callable bindings (Function/Method/Constructor) by name
 * from the nearest scope in the chain that binds `callableName`.
 *
 * Preserves the original scope-walk boundary used by
 * `findCallableBindingInScope`: once any callable binding is found in a
 * scope, outer scopes are not consulted.
 */
export function findAllCallableBindingsInScope(startScope, callableName, scopes) {
    let currentId = startScope;
    const visited = new Set();
    while (currentId !== null) {
        if (visited.has(currentId))
            return [];
        visited.add(currentId);
        const scope = scopes.scopeTree.getScope(currentId);
        if (scope === undefined)
            return [];
        const out = [];
        const seen = new Set();
        const pushCallable = (def) => {
            if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor')
                return;
            if (seen.has(def.nodeId))
                return;
            seen.add(def.nodeId);
            out.push(def);
        };
        const localBindings = scope.bindings.get(callableName);
        if (localBindings !== undefined) {
            for (const b of localBindings) {
                pushCallable(b.def);
            }
        }
        const importedBindings = lookupBindingsAt(currentId, callableName, scopes);
        for (const b of importedBindings) {
            pushCallable(b.def);
        }
        if (out.length > 0)
            return out;
        currentId = scope.parent;
    }
    return [];
}
/**
 * ISO C++ `[basic.lookup.unqual]` §7: ADL is suppressed when ordinary
 * unqualified lookup finds:
 *   - a name that is NOT a function or function template, OR
 *   - a block-scope function declaration that is NOT a using-declaration.
 *
 * Combined walker that stops at the **nearest scope** where `name` has any
 * binding (callable or non-callable) and returns:
 *   - `callables`: Function/Method/Constructor defs found at that scope
 *   - `nonCallableFound`: a non-function binding was present (variable, class, etc.)
 *   - `blockScopeDeclFound`: a callable was found at a Function or Block scope
 *     (block-scope function declaration that blocks ADL)
 *
 * One pass, one stop — no divergence between callable collection and blocker
 * detection.
 */
export function findCallableBindingsAndAdlBlocker(startScope, name, scopes) {
    let currentId = startScope;
    const visited = new Set();
    while (currentId !== null) {
        if (visited.has(currentId))
            return { callables: [], nonCallableFound: false, blockScopeDeclFound: false };
        visited.add(currentId);
        const scope = scopes.scopeTree.getScope(currentId);
        if (scope === undefined)
            return { callables: [], nonCallableFound: false, blockScopeDeclFound: false };
        const callables = [];
        const seen = new Set();
        let nonCallableFound = false;
        let anyBinding = false;
        const process = (def) => {
            anyBinding = true;
            if (def.type === 'Function' || def.type === 'Method' || def.type === 'Constructor') {
                if (!seen.has(def.nodeId)) {
                    seen.add(def.nodeId);
                    callables.push(def);
                }
            }
            else {
                nonCallableFound = true;
            }
        };
        const localBindings = scope.bindings.get(name);
        if (localBindings !== undefined) {
            for (const b of localBindings) {
                process(b.def);
            }
        }
        const importedBindings = lookupBindingsAt(currentId, name, scopes);
        for (const b of importedBindings) {
            process(b.def);
        }
        if (anyBinding) {
            // ISO C++: a block-scope function declaration (Function or Block scope)
            // that is NOT a using-declaration blocks ADL. If we found callables at
            // a function/block scope, ADL must be suppressed.
            const blockScopeDeclFound = callables.length > 0 && (scope.kind === 'Function' || scope.kind === 'Block');
            return { callables, nonCallableFound, blockScopeDeclFound };
        }
        currentId = scope.parent;
    }
    return { callables: [], nonCallableFound: false, blockScopeDeclFound: false };
}
/**
 * Populate `ownerId` on every def structurally owned by a Class
 * scope — methods (defs in Function scopes whose parent is Class)
 * and class-body fields (defs directly in Class scopes).
 *
 * Generic OO ownership rule. Languages that want richer ownership
 * (e.g. inner-class qualification) can compose with this as a base
 * step.
 *
 * Mutates `parsed.localDefs` in place via type cast — `SymbolDefinition`
 * is `readonly` for consumers but the extractor returns plain objects.
 * Defs are shared by reference between `localDefs` and `Scope.ownedDefs`,
 * so this single mutation is visible from both sides.
 */
export function populateClassOwnedMembers(parsed) {
    const scopesById = new Map();
    for (const scope of parsed.scopes)
        scopesById.set(scope.id, scope);
    // Promote a def's qualifiedName from `methodName` to `ClassName.methodName`
    // when the def sits inside a class. Without this, two classes in the
    // same file that share a method name collide at the graph-bridge lookup
    // (`node-lookup.ts` keys by (filePath, qualifiedName) and falls back to
    // simple name only). Python's scope query doesn't emit
    // `@declaration.qualified_name` for nested methods, so the finalized
    // defs arrive here with simple names — we stamp the qualifier while
    // we're already walking class scopes for ownerId.
    const qualify = (def, classDef) => {
        const q = def.qualifiedName;
        if (q === undefined || q.length === 0)
            return;
        if (q.includes('.'))
            return; // already qualified (dotted)
        const classQ = classDef.qualifiedName;
        if (classQ === undefined || classQ.length === 0)
            return;
        def.qualifiedName = `${classQ}.${q}`;
    };
    // Depth invariant (verified empirically against Python scope-extractor
    // 2026-04-21): a nested `def helper` declared inside a method body
    // lives in its OWN Function scope whose parent is the method's Function
    // scope (not the Class scope). That means the `parentScope.kind ===
    // 'Class'` branch below only matches DIRECT class-scope children —
    // method defs themselves — and never stamps arbitrary nested defs with
    // `ownerId = classDef.nodeId`. If an adversarial reviewer raises this
    // as a potential false-attribution bug, verify first with a scope dump
    // on `class U: def save(self): def helper(): ...` — helper.ownerId will
    // remain undefined. The theoretical concern is real only if the
    // extractor ever stops creating scopes for inner defs.
    for (const scope of parsed.scopes) {
        // Methods: function scope whose parent is a Class scope. Owner is
        // the parent's class-like def.
        if (scope.parent !== null) {
            const parentScope = scopesById.get(scope.parent);
            if (parentScope !== undefined && parentScope.kind === 'Class') {
                const classDef = parentScope.ownedDefs.find((d) => isClassLike(d.type));
                if (classDef !== undefined) {
                    for (const def of scope.ownedDefs) {
                        def.ownerId = classDef.nodeId;
                        qualify(def, classDef);
                    }
                }
            }
        }
        // Class-body fields: defs directly owned by a Class scope (the
        // class-like def itself excluded).
        if (scope.kind === 'Class') {
            const classDef = scope.ownedDefs.find((d) => isClassLike(d.type));
            if (classDef !== undefined) {
                for (const def of scope.ownedDefs) {
                    if (def === classDef)
                        continue;
                    def.ownerId = classDef.nodeId;
                    qualify(def, classDef);
                }
            }
        }
    }
}
/**
 * Walk a scope chain upward looking for the innermost enclosing
 * Class scope and return that class's def. Used by per-language
 * `super` receiver branches to discover the dispatch base.
 */
export function findEnclosingClassDef(startScope, scopes) {
    let currentId = startScope;
    const visited = new Set();
    while (currentId !== null) {
        if (visited.has(currentId))
            return undefined;
        visited.add(currentId);
        const scope = scopes.scopeTree.getScope(currentId);
        if (scope === undefined)
            return undefined;
        if (scope.kind === 'Class') {
            const cd = scope.ownedDefs.find((d) => isClassLike(d.type));
            if (cd !== undefined)
                return cd;
        }
        currentId = scope.parent;
    }
    return undefined;
}
/**
 * Find a free-function def by simple name across all parsed files,
 * preferring scope-chain-visible bindings (import + finalized scope
 * bindings) before falling back to a workspace-wide simple-name scan.
 *
 * The fallback scan is intentionally loose so per-language compound
 * resolvers can find a callable target even when the binding chain
 * doesn't surface it (e.g. cross-package re-exports the finalize
 * pass missed). Strictly-typed languages may want to disable the
 * fallback by simply not calling this helper from their compound
 * resolver.
 */
export function findExportedDefByName(name, inScope, scopes, index) {
    let currentId = inScope;
    const visited = new Set();
    while (currentId !== null) {
        if (visited.has(currentId))
            break;
        visited.add(currentId);
        const scope = scopes.scopeTree.getScope(currentId);
        if (scope === undefined)
            break;
        const local = scope.bindings.get(name);
        if (local !== undefined) {
            for (const b of local) {
                if (b.def.type === 'Function' || b.def.type === 'Method')
                    return b.def;
            }
        }
        const finalized = lookupBindingsAt(currentId, name, scopes);
        for (const b of finalized) {
            if (b.def.type === 'Function' || b.def.type === 'Method')
                return b.def;
        }
        currentId = scope.parent;
    }
    // Workspace-wide fallback: iterate every file's Module scope (via
    // the scope-tied `moduleScopeByFile` lookup) and return the first
    // locally-declared callable binding matching `name`. First-seen-
    // by-file wins; bindings filtered to `origin === 'local'` and the
    // callable types Function/Method/Constructor. We walk scopes here
    // rather than consult `SemanticModel.symbols.lookupCallableByName`
    // because the `origin === 'local'` module-export-visibility filter
    // is a scope concept the raw symbol index doesn't express.
    for (const [, moduleScope] of index.moduleScopeByFile) {
        const refs = moduleScope.bindings.get(name);
        if (refs === undefined)
            continue;
        for (const ref of refs) {
            if (ref.origin !== 'local')
                continue;
            const t = ref.def.type;
            if (t === 'Function' || t === 'Method' || t === 'Constructor')
                return ref.def;
        }
    }
    return undefined;
}
/**
 * Find a member of a class by simple name — delegates to
 * `SemanticModel.methods` (methods / functions / constructors) with a
 * fallback to `SemanticModel.fields` (properties / fields /
 * variables). After `runScopeResolution`'s reconciliation pass
 * populates both registries from `parsed.localDefs[i].ownerId`
 * (post-`populateOwners`), this is the single authoritative view of
 * class membership — no parallel scope-resolution index needed.
 *
 * Returns the first-seen overload for methods without arity or
 * return-type narrowing. Callers that need arity-aware dispatch use
 * `lookupMethodByOwner(owner, name, argCount)` directly.
 */
export function findOwnedMember(ownerDefId, memberName, model) {
    const method = model.methods.lookupAllByOwner(ownerDefId, memberName)[0];
    if (method !== undefined)
        return method;
    return model.fields.lookupFieldByOwner(ownerDefId, memberName);
}
/**
 * Find a file-level def (top-of-module class / function / variable)
 * by simple name — consults the target file's Module scope's
 * finalized bindings. Only defs bound at module-scope with
 * `origin === 'local'` qualify, matching the historical
 * "module-export-visible" semantics. Class methods and class-body
 * fields bind at their containing class scope and are naturally
 * excluded.
 *
 * Reads from `WorkspaceResolutionIndex.moduleScopeByFile` (scope-tied
 * lookup that doesn't live on `SemanticModel`). This intentionally
 * does NOT call `lookupBindingsAt`: `findExportedDef` answers "what
 * did the target file declare locally at module scope?", while
 * `bindingAugmentations` models importer-side visibility created by
 * post-finalize hooks. Callers that need importer-visible exports use
 * `findExportedDefByName`, which is dual-channel aware.
 */
export function findExportedDef(targetFile, memberName, index) {
    const moduleScope = index.moduleScopeByFile.get(targetFile);
    if (moduleScope === undefined)
        return undefined;
    const refs = moduleScope.bindings.get(memberName);
    if (refs === undefined)
        return undefined;
    for (const ref of refs) {
        if (ref.origin === 'local')
            return ref.def;
    }
    return undefined;
}
