/**
 * Receiver-bound CALLS / ACCESSES emit pass — generic 7-case
 * dispatcher consuming `ScopeResolver` for the language-specific bits
 * (super recognizer, field-fallback toggle).
 *
 * **Contract Invariant I4 — case order is load-bearing.** The cases
 * are evaluated in this order; the FIRST that emits an edge wins:
 *
 *   1. **super branch** — `provider.isSuperReceiver(receiverName)` →
 *      MRO walk skipping self
 *   2. **Case 0 (compound)** — receiver has `.` or `(` → compound resolver
 *   3. **Case 1 (namespace)** — receiver in `namespaceTargets` → exported def
 *   4. **Case 2 (class-name / static receiver)** — receiver resolves to a
 *      class-like binding (Class/Interface/Struct/Record/Enum/Trait) → MRO
 *      walk on that class. Also handles static-style invocations
 *      (`ILogger.Warn(...)`) with kind-aware reason/confidence for
 *      read/write ACCESSES.
 *   5. **Case 3 (dotted typeBinding for namespace prefix)** —
 *      `typeRef.rawName` like `models.User`
 *   6. **Case 3b (chain-typebinding)** — `typeRef.rawName` has a dot
 *      but not a namespace prefix → compound resolver
 *   7. **Case 4 (simple typeBinding)** — `typeRef.rawName` has no dot →
 *      MRO walk + `findOwnedMember`
 *   8. **Case 5 (value-receiver bridge)** — receiver is a `Const`/`Variable`
 *      whose `nodeId` is referenced as an `ownerId` in `model.methods`
 *      (object-literal services). Last-resort fallback for lowercase
 *      receivers with no class-like or type-binding match. Mirrors
 *      the legacy DAG bridge in `call-processor.ts`.
 *
 * Reordering or merging cases changes resolution semantics.
 *
 * **Contract Invariant I5 — pre-seeding `seen` is forbidden.** The
 * orchestrator runs this pass FIRST (before `emitReferencesViaLookup`)
 * and consumes the populated `handledSites` set. Pre-seeding `seen`
 * from the shared resolver's emissions (an old optimization) actively
 * suppresses correct emissions for sites the shared resolver also
 * resolved to a wrong target.
 */
import { collectNamespaceTargets } from '../scope/namespace-targets.js';
import { findClassBindingInScope, findEnclosingClassDef, findExportedDef, findOwnedMember, findReceiverTypeBinding, findValueBindingInScope, isClassLike, } from '../scope/walkers.js';
import { tryEmitEdge, tryEmitEdgeWithExplicitTargetId } from '../graph-bridge/edges.js';
import { resolveCompoundReceiverClass } from '../passes/compound-receiver.js';
import { resolveDefGraphId } from '../graph-bridge/ids.js';
import { narrowOverloadCandidates, isOverloadAmbiguousAfterNormalization, } from './overload-narrowing.js';
import { extractTemplateArguments, stripTemplateArguments, } from '../../utils/template-arguments.js';
function normalizeTemplateArgToken(value) {
    return value.replace(/\s+/g, '');
}
function resolveClassBindingForName(scopeId, rawClassName, scopes) {
    const direct = findClassBindingInScope(scopeId, rawClassName, scopes);
    if (direct !== undefined)
        return direct;
    if (!rawClassName.includes('<'))
        return undefined;
    const baseName = stripTemplateArguments(rawClassName).replace(/\s+/g, '');
    if (baseName.length === 0)
        return undefined;
    const wantedArgs = extractTemplateArguments(rawClassName)?.map(normalizeTemplateArgToken);
    if (wantedArgs !== undefined && wantedArgs.length > 0) {
        // qualifiedNames is a Map and may not contain the stripped base name at all
        // (e.g., unresolved type binding or only template-qualified entries), so
        // default to [] before checking `.length`.
        const qnameIds = scopes.qualifiedNames.get(baseName) ?? [];
        if (qnameIds.length === 0) {
            return findClassBindingInScope(scopeId, baseName, scopes);
        }
        const matches = [];
        for (const id of qnameIds) {
            const def = scopes.defs.get(id);
            if (def === undefined || !isClassLike(def.type))
                continue;
            const defArgs = def.templateArguments?.map(normalizeTemplateArgToken);
            if (defArgs !== undefined &&
                defArgs.length === wantedArgs.length &&
                defArgs.every((value, i) => value === wantedArgs[i])) {
                matches.push(def);
            }
        }
        if (matches.length === 1)
            return matches[0];
        // Scope extractor only records class definitions with bodies in C++, so
        // forward declarations are not expected here. Keep fallback behavior for
        // safety in non-ODR or mixed-language edge cases.
    }
    return findClassBindingInScope(scopeId, baseName, scopes);
}
export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, handledSites, provider, index, model, options = {}) {
    let emitted = 0;
    // Per-pass dedup so the multiple cases don't double-emit if two of
    // them resolve the same site to the same target. NEVER pre-seed
    // from the reference index — see Contract Invariant I5.
    const seen = new Set();
    const fieldFallback = provider.fieldFallbackOnMethodLookup ?? true;
    const collapse = provider.collapseMemberCallsByCallerTarget === true;
    const hoistTypeBindingsToModule = provider.hoistTypeBindingsToModule === true;
    const compoundOpts = {
        fieldFallback,
        unwrapCollectionAccessor: provider.unwrapCollectionAccessor,
        hoistTypeBindingsToModule,
    };
    // Build an interface → implementors map from IMPLEMENTS edges.
    // Maps Interface graph-id → list of implementor class scope-def-ids.
    // We translate graph-ids back to scope-resolution DefIds via
    // `parsedFiles.localDefs` lookup so downstream `findOwnedMember`
    // (which keys by DefId) can find the implementor's members.
    const graphIdToClassDef = new Map();
    for (const parsed of parsedFiles) {
        for (const def of parsed.localDefs) {
            if (def.type !== 'Class' && def.type !== 'Interface')
                continue;
            const graphId = resolveDefGraphId(parsed.filePath, def, nodeLookup);
            if (graphId !== undefined)
                graphIdToClassDef.set(graphId, def);
        }
    }
    const implementorsByInterfaceDefId = new Map();
    for (const rel of graph.iterRelationshipsByType('IMPLEMENTS')) {
        const ifaceDef = graphIdToClassDef.get(rel.targetId);
        const implDef = graphIdToClassDef.get(rel.sourceId);
        if (ifaceDef === undefined || implDef === undefined)
            continue;
        let list = implementorsByInterfaceDefId.get(ifaceDef.nodeId);
        if (list === undefined) {
            list = [];
            implementorsByInterfaceDefId.set(ifaceDef.nodeId, list);
        }
        list.push(implDef);
    }
    /** Emit secondary CALLS edges with reason='interface-dispatch'
     *  when the primary receiver-typed edge targeted an Interface's
     *  method. Each implementing class's same-named method gets a
     *  secondary edge (excluding the primary target itself). */
    const emitInterfaceDispatchFor = (ownerDef, memberName, primaryMemberDef, site, confidence) => {
        if (ownerDef.type !== 'Interface')
            return 0;
        const impls = implementorsByInterfaceDefId.get(ownerDef.nodeId);
        if (impls === undefined)
            return 0;
        let n = 0;
        for (const implDef of impls) {
            const implMember = findOwnedMember(implDef.nodeId, memberName, model);
            if (implMember === undefined)
                continue;
            if (implMember.nodeId === primaryMemberDef.nodeId)
                continue;
            const ok = tryEmitEdge(graph, scopes, nodeLookup, site, implMember, 'interface-dispatch', seen, confidence, collapse);
            if (ok)
                n++;
        }
        return n;
    };
    for (const parsed of parsedFiles) {
        const namespaceTargets = collectNamespaceTargets(parsed, scopes);
        for (const site of parsed.referenceSites) {
            if (site.kind !== 'call' && site.kind !== 'read' && site.kind !== 'write')
                continue;
            if (site.explicitReceiver === undefined)
                continue;
            const receiverName = site.explicitReceiver.name;
            const memberName = site.name;
            const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
            // ── super branch ─────────────────────────────────────────────
            // Languages with caller-context-dependent super classification
            // (C++) define `isSuperReceiverInContext`; we prefer it. Simple
            // text-only languages (Python, Java, PHP) use the plain hook.
            const isSuper = provider.isSuperReceiverInContext !== undefined
                ? provider.isSuperReceiverInContext(receiverName, site.inScope, scopes)
                : provider.isSuperReceiver(receiverName);
            if (isSuper) {
                const enclosingClass = findEnclosingClassDef(site.inScope, scopes);
                if (enclosingClass !== undefined) {
                    // For super-receiver dispatch (`parent::`, `base.`, `super()`),
                    // walk the inheritance-only ancestor chain when the language
                    // exposes it. PHP's `parent::` semantically bypasses composed
                    // traits; other languages without mixin augmentation have no
                    // `extendsOnlyMroFor` and fall back to `mroFor`.
                    const extendsOnly = scopes.methodDispatch.extendsOnlyMroFor;
                    const ancestors = extendsOnly !== undefined
                        ? extendsOnly(enclosingClass.nodeId)
                        : scopes.methodDispatch.mroFor(enclosingClass.nodeId);
                    let memberDef;
                    for (const ownerId of ancestors) {
                        memberDef = findOwnedMember(ownerId, memberName, model);
                        if (memberDef !== undefined)
                            break;
                    }
                    if (memberDef !== undefined) {
                        // Super/base calls resolve through the MRO chain, not
                        // through imports — the ancestor method is found by
                        // walking `methodDispatch.mroFor(enclosingClass)`, which
                        // is independent of whether a `using` / `import` directive
                        // brought the ancestor into scope. We emit the canonical
                        // `'global'` tier (ARCHITECTURE.md § Scope-Resolution
                        // Pipeline — edge vocabulary).
                        //
                        // Known legacy-path asymmetry: the C# legacy DAG also
                        // classifies `base.Save()` as `'global'` (same-graph); the
                        // Python legacy DAG classifies `super().save()` as
                        // `'import-resolved'` because Python's ancestor lookup
                        // flows through `typeEnv.lookup(...)` which resolves the
                        // superclass via its `import`/`from … import …` binding.
                        // Closing that gap requires realigning the legacy tier
                        // classifier and is tracked separately.
                        const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, 'global', seen, 0.85, collapse);
                        if (ok)
                            emitted++;
                        // Always mark handled when the site was resolved, even
                        // if the edge was deduplicated (collapse mode), so
                        // `emitReferencesViaLookup` doesn't re-emit from the
                        // reference index.
                        handledSites.add(siteKey);
                        continue;
                    }
                }
            }
            // ── Case 0: compound receiver ────────────────────────────────
            if (receiverName.includes('.') || receiverName.includes('(')) {
                const currentClass = resolveCompoundReceiverClass(receiverName, site.inScope, scopes, index, compoundOpts);
                if (currentClass !== undefined) {
                    const chain = [currentClass.nodeId, ...scopes.methodDispatch.mroFor(currentClass.nodeId)];
                    let memberDef;
                    // Static-only filter (#1756 / U3): same shape as Case 4's
                    // chain walk (skip-and-walk-on) but without overload
                    // narrowing — Case 0 uses `findOwnedMember` directly. When
                    // an owner's resolved candidate is static-only (Kotlin
                    // companion-promoted), continue to the next ancestor in
                    // the MRO chain so a legitimate instance member can bind.
                    // If the entire chain is static-only, no edge is emitted —
                    // unlike Case 4, Case 0 does NOT mark the site handled in
                    // that situation because compound receivers (`a.b.c()`)
                    // are not pre-emitted by `emitReferencesViaLookup` (the
                    // reference index has no compound-receiver entry for
                    // shapes like `Logger.create("a")`), so there's no wrong
                    // target to suppress.
                    for (const ownerId of chain) {
                        const candidate = findOwnedMember(ownerId, memberName, model);
                        if (candidate === undefined)
                            continue;
                        if (provider.isStaticOnly?.(candidate) === true) {
                            // Skip static-only candidate; walk to next ancestor.
                            continue;
                        }
                        memberDef = candidate;
                        break;
                    }
                    if (memberDef !== undefined) {
                        const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global', seen, 0.85, collapse);
                        if (ok)
                            emitted++;
                        // Always mark handled when the site was resolved, even
                        // if the edge was deduplicated (collapse mode), so
                        // `emitReferencesViaLookup` doesn't re-emit from the
                        // reference index.
                        handledSites.add(siteKey);
                        continue;
                    }
                }
            }
            // ── Case 0.5: implicit `this` receiver ───────────────────────
            // C++ `this->member()` (and same-shape receivers in other OO
            // languages) should resolve against the enclosing class + MRO
            // even when there is no explicit `this` typeBinding in scope.
            //
            // **Static-only filter dependency (#1756 / U3):** this case does
            // NOT currently consult `provider.isStaticOnly`. Today it fires
            // only for C++ (the sole `resolveThisViaEnclosingClass === true`
            // language), which has no static-only semantics. Kotlin — the
            // current `isStaticOnly` consumer — leaves `resolveThisVia
            // EnclosingClass` unset, so Case 0.5 is dead code for Kotlin
            // crossover suppression and U3 leaves it untouched. If any
            // future language enables BOTH `resolveThisViaEnclosingClass`
            // AND `isStaticOnly`, the chain-walk below MUST adopt the
            // skip-and-walk-on filter pattern used by Cases 0, 3b, and 4.
            if (provider.resolveThisViaEnclosingClass === true && receiverName === 'this') {
                const enclosingClass = findEnclosingClassDef(site.inScope, scopes);
                if (enclosingClass !== undefined) {
                    const chain = [
                        enclosingClass.nodeId,
                        ...scopes.methodDispatch.mroFor(enclosingClass.nodeId),
                    ];
                    let memberDef;
                    let ambiguous = false;
                    let hiddenByName = false;
                    for (const ownerId of chain) {
                        const methodOverloads = model.methods.lookupAllByOwner(ownerId, memberName);
                        if (methodOverloads.length > 0) {
                            const narrowed = narrowOverloadCandidates(methodOverloads, site.arity, site.argumentTypes, {
                                argumentTypeClasses: site.argumentTypeClasses,
                                conversionRankFn: provider.conversionRankFn,
                                constraintCompatibility: provider.constraintCompatibility,
                            });
                            if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity)) {
                                ambiguous = true;
                                break;
                            }
                            if (narrowed.length === 0) {
                                // C++ name hiding: if the derived class declares `f`, base-class
                                // overloads named `f` are hidden for member lookup
                                // ([basic.lookup.classref]). A non-viable derived overload set
                                // therefore terminates lookup instead of falling through to base.
                                hiddenByName = true;
                                break;
                            }
                            // Multiple tied survivors with distinct param types (e.g.
                            // h(int,double) vs h(double,int) both scoring 2) → ambiguous.
                            if (narrowed.length > 1) {
                                ambiguous = true;
                                break;
                            }
                            memberDef = narrowed[0] ?? methodOverloads[0];
                            break;
                        }
                        // Field/property lookup intentionally runs only after the method
                        // lookup above: in C++ member-name lookup, functions with this
                        // name hide same-named base members; we therefore prefer method
                        // candidates first and only target a field when no methods with
                        // this name exist on the current owner.
                        memberDef = model.fields.lookupFieldByOwner(ownerId, memberName);
                        if (memberDef !== undefined) {
                            break;
                        }
                    }
                    if (ambiguous) {
                        handledSites.add(siteKey);
                        continue;
                    }
                    if (hiddenByName) {
                        handledSites.add(siteKey);
                        continue;
                    }
                    if (memberDef !== undefined) {
                        const reason = site.kind === 'write' || site.kind === 'read'
                            ? site.kind
                            : memberDef.filePath !== parsed.filePath
                                ? 'import-resolved'
                                : 'global';
                        const confidence = site.kind === 'write' || site.kind === 'read' ? 1.0 : 0.85;
                        const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, reason, seen, confidence, collapse);
                        if (ok)
                            emitted++;
                        handledSites.add(siteKey);
                        continue;
                    }
                }
            }
            // ── Case 1: namespace receiver ───────────────────────────────
            const targetFiles = namespaceTargets.get(receiverName);
            if (targetFiles !== undefined) {
                let found = false;
                for (const targetFile of targetFiles) {
                    const memberDef = findExportedDef(targetFile, memberName, index);
                    if (memberDef !== undefined) {
                        const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global', seen, 0.85, collapse);
                        if (ok)
                            emitted++;
                        handledSites.add(siteKey);
                        found = true;
                        break;
                    }
                }
                if (found)
                    continue;
            }
            // ── Case 1.5: qualified namespace-receiver (language-specific) ───
            // Languages whose qualified-name semantics need workspace-wide
            // namespace-scope walking (C++ `outer::foo()`, including inline-
            // namespace transitive traversal) implement `resolveQualifiedReceiverMember`.
            // Runs before Case 2 so namespace receivers don't accidentally match a
            // class with the same simple name.
            if (provider.resolveQualifiedReceiverMember !== undefined) {
                const memberDef = provider.resolveQualifiedReceiverMember(receiverName, memberName, site.inScope, scopes, parsedFiles, site);
                if (memberDef === 'ambiguous') {
                    // Same-name ambiguity across inline-namespace children (#1564):
                    // suppress edge emission, mark site handled.
                    options.recordResolutionOutcome?.({
                        kind: 'suppressed',
                        phase: 'receiver-bound-calls',
                        filePath: parsed.filePath,
                        name: site.name,
                        range: site.atRange,
                        reason: 'inline-ns-ambiguous',
                        candidateIds: [],
                    });
                    handledSites.add(siteKey);
                    continue;
                }
                if (memberDef !== undefined) {
                    const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global', seen, 0.85, collapse);
                    if (ok)
                        emitted++;
                    handledSites.add(siteKey);
                    continue;
                }
            }
            // ── Case 2: class-name receiver ──────────────────────────────
            const classDef = findClassBindingInScope(site.inScope, receiverName, scopes);
            if (classDef !== undefined) {
                const chain = [classDef.nodeId, ...scopes.methodDispatch.mroFor(classDef.nodeId)];
                let memberDef;
                for (const ownerId of chain) {
                    memberDef = findOwnedMember(ownerId, memberName, model);
                    if (memberDef !== undefined) {
                        // The MRO chain is most-derived-first ([classDef, ...ancestors]).
                        // If the most-derived definition is arity-incompatible with the
                        // call site, PHP throws ArgumentCountError at runtime — it does
                        // NOT silently dispatch to an ancestor. Terminate the chain walk
                        // so no edge is emitted, rather than falling through to an
                        // arity-compatible ancestor (which would be a false positive).
                        if (narrowOverloadCandidates([memberDef], site.arity, site.argumentTypes).length === 0) {
                            memberDef = undefined;
                            break;
                        }
                        break;
                    }
                }
                if (memberDef !== undefined) {
                    const reason = site.kind === 'write' || site.kind === 'read'
                        ? site.kind
                        : memberDef.filePath !== parsed.filePath
                            ? 'import-resolved'
                            : 'global';
                    const confidence = site.kind === 'write' || site.kind === 'read' ? 1.0 : 0.85;
                    const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, reason, seen, confidence, collapse);
                    if (ok)
                        emitted++;
                    handledSites.add(siteKey);
                    continue;
                }
            }
            // ── Case 3: dotted typeBinding (`u: models.User`) ────────────
            const typeRef = findReceiverTypeBinding(site.inScope, receiverName, scopes);
            if (typeRef !== undefined && typeRef.rawName.includes('.')) {
                const [nsName, ...classNameParts] = typeRef.rawName.split('.');
                const className = classNameParts.join('.');
                const targetFiles3 = namespaceTargets.get(nsName);
                if (targetFiles3 !== undefined && className.length > 0) {
                    let found3 = false;
                    for (const targetFile3 of targetFiles3) {
                        const classDef3 = findExportedDef(targetFile3, className, index);
                        if (classDef3 !== undefined) {
                            const memberDef = findOwnedMember(classDef3.nodeId, memberName, model);
                            if (memberDef !== undefined) {
                                const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global', seen);
                                if (ok) {
                                    emitted++;
                                    handledSites.add(siteKey);
                                }
                                found3 = true;
                                break;
                            }
                        }
                    }
                    if (found3)
                        continue;
                }
            }
            // ── Case 3b: chain-typebinding (`city → user.get_city`) ──────
            // Also handles compound member-call rawNames (`city → addr.get_city()`)
            // where the rawName includes both `.` and `()` — Ruby's
            // member-call-return captures produce this shape.
            const chainHead = typeRef !== undefined && typeRef.rawName.includes('.')
                ? (typeRef.rawName.split('.', 1)[0] ?? '')
                : undefined;
            if (typeRef !== undefined && chainHead !== undefined && !namespaceTargets.has(chainHead)) {
                // Try the plain dotted-field walk first — covers property /
                // collection-accessor shapes (`.Values`, Kotlin `.size`) and
                // field chains. Fall back to call-form (`x()`) which treats
                // the last segment as a method invocation. For rawNames that
                // already contain `()` (Ruby member-call-return captures),
                // pass through directly — the compound resolver handles the
                // full expression including the call syntax.
                let ownerDef = resolveCompoundReceiverClass(typeRef.rawName, typeRef.declaredAtScope, scopes, index, compoundOpts);
                if (ownerDef === undefined && !typeRef.rawName.includes('(')) {
                    ownerDef = resolveCompoundReceiverClass(typeRef.rawName + '()', typeRef.declaredAtScope, scopes, index, compoundOpts);
                }
                if (ownerDef !== undefined) {
                    const chain = [ownerDef.nodeId, ...scopes.methodDispatch.mroFor(ownerDef.nodeId)];
                    let memberDef;
                    // Static-only filter (#1756 / U3): mirrors Case 0's chain
                    // walk — `findOwnedMember` without overload narrowing. When
                    // a static-only candidate is found at an ancestor, walk on
                    // so a legitimate instance member can bind. If the entire
                    // chain is static-only, no edge is emitted (Case 3b is fed
                    // by chain-typebinding receivers, not pre-emitted by
                    // `emitReferencesViaLookup` for compound shapes, so no
                    // handled-site marker is needed for chain-only-static).
                    for (const ownerId of chain) {
                        const candidate = findOwnedMember(ownerId, memberName, model);
                        if (candidate === undefined)
                            continue;
                        if (provider.isStaticOnly?.(candidate) === true) {
                            continue;
                        }
                        memberDef = candidate;
                        break;
                    }
                    if (memberDef !== undefined) {
                        const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global', seen, 0.85, collapse);
                        if (ok)
                            emitted++;
                        // Always mark handled when the site was resolved, even
                        // if the edge was deduplicated (collapse mode), so
                        // `emitReferencesViaLookup` doesn't re-emit from the
                        // reference index.
                        handledSites.add(siteKey);
                        continue;
                    }
                }
            }
            // ── Case 4: simple typeBinding (`u: U`) ──────────────────────
            if (typeRef !== undefined && !typeRef.rawName.includes('.')) {
                let ownerDef = resolveClassBindingForName(site.inScope, typeRef.rawName, scopes);
                // `findClassBindingInScope(..., typeRef.rawName)` only works when
                // rawName is itself a class symbol reachable through scope bindings.
                // For languages with namespace-style imports (Go), imported types
                // don't create bindings. Fall back to QualifiedNameIndex — single-
                // match wins; ambiguous/missing falls through.
                if (ownerDef === undefined) {
                    const qnameIds = scopes.qualifiedNames.get(typeRef.rawName);
                    if (qnameIds.length === 1) {
                        const qdef = scopes.defs.get(qnameIds[0]);
                        if (qdef !== undefined && isClassLike(qdef.type))
                            ownerDef = qdef;
                    }
                }
                // Map for-of tuple bindings (`__MAP_TUPLE_i__:mapId`), callable
                // aliases (`getUser` → User), and other compound-friendly shapes
                // need the compound resolver keyed by the receiver identifier.
                if (ownerDef === undefined) {
                    ownerDef = resolveCompoundReceiverClass(receiverName, site.inScope, scopes, index, compoundOpts);
                }
                if (ownerDef !== undefined) {
                    const chain = [ownerDef.nodeId, ...scopes.methodDispatch.mroFor(ownerDef.nodeId)];
                    let memberDef;
                    let ambiguous = false;
                    let ambiguousOwnerId;
                    // Track whether the chain walk filtered out any static-only
                    // candidates. When it did and the chain ended with no
                    // legitimate instance member, we mark the site as handled so
                    // `emitReferencesViaLookup` doesn't re-emit a wrong target
                    // from the pre-resolved reference index (which has no
                    // static-only awareness).
                    let allFilteredStaticOnly = false;
                    // Static-only filter (#1756 / U2): the filter must run INSIDE
                    // the chain walk and BEFORE arity narrowing.
                    //
                    // INSIDE: when a derived owner's only candidates are static-
                    // only (Kotlin companion-promoted), `pickFirstNonStaticOnly`
                    // returns `undefined` and the loop `continue`s to the next
                    // ancestor in the MRO chain — giving a legitimate ancestor
                    // instance method a chance to bind. The earlier after-chain
                    // filter aborted the entire site instead, producing a false
                    // negative whenever the most-derived owner shadowed an
                    // ancestor's instance method with a static-only companion
                    // member.
                    //
                    // BEFORE narrowing: filtering survivors of `lookupAllByOwner`
                    // (rather than survivors of `narrowOverloadCandidates`) means
                    // a same-arity static + instance pair on one owner doesn't
                    // collapse to `OVERLOAD_AMBIGUOUS`. Kotlin compile-resolves
                    // such a pair unambiguously to the instance method because
                    // companion members are not legal instance-dispatch
                    // candidates.
                    for (const ownerId of chain) {
                        const picked = pickFirstNonStaticOnly(ownerId, memberName, site, model, provider);
                        if (picked === OVERLOAD_AMBIGUOUS) {
                            ambiguous = true;
                            ambiguousOwnerId = ownerId;
                            break;
                        }
                        if (picked === STATIC_ONLY_FILTERED) {
                            // At least one static-only candidate was filtered out at
                            // this owner; remember so we can mark handled if the
                            // chain ends with no legitimate match.
                            allFilteredStaticOnly = true;
                            continue;
                        }
                        if (picked !== undefined) {
                            memberDef = picked;
                            break;
                        }
                        // `picked === undefined` means this owner had no member of
                        // this name at all. Walk on to the next ancestor in the
                        // MRO chain.
                    }
                    if (ambiguous) {
                        // Suppress and mark handled so `emitReferencesViaLookup`
                        // doesn't re-emit the pre-resolved reference. See
                        // OVERLOAD_AMBIGUOUS docstring for the upstream cause.
                        recordReceiverOverloadSuppression(options.recordResolutionOutcome, parsed.filePath, site, ambiguousOwnerId ?? ownerDef.nodeId, memberName, model, provider);
                        handledSites.add(siteKey);
                        continue;
                    }
                    if (memberDef === undefined && allFilteredStaticOnly) {
                        // The chain ended with no candidates because every viable
                        // owner had only static-only members. Mark handled so
                        // `emitReferencesViaLookup` doesn't re-emit a wrong target
                        // from the pre-resolved reference index. Parallels the old
                        // after-chain `isStaticOnly` suppression block.
                        handledSites.add(siteKey);
                        continue;
                    }
                    if (memberDef !== undefined) {
                        // For read/write ACCESSES, mirror the legacy DAG's reason
                        // convention so consumers asserting `reason === 'write'`
                        // keep working.
                        const reason = site.kind === 'write' || site.kind === 'read'
                            ? site.kind
                            : memberDef.filePath !== parsed.filePath
                                ? 'import-resolved'
                                : 'global';
                        const confidence = site.kind === 'write' || site.kind === 'read' ? 1.0 : 0.85;
                        const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, reason, seen, confidence, collapse);
                        if (ok)
                            emitted++;
                        // Interface dispatch: when the primary owner is an
                        // Interface, emit secondary CALLS edges to every
                        // implementing class's same-named method.
                        emitted += emitInterfaceDispatchFor(ownerDef, memberName, memberDef, site, confidence);
                        // Always mark handled when the site was resolved, even
                        // if the edge was deduplicated (collapse mode), so
                        // `emitReferencesViaLookup` doesn't re-emit from the
                        // reference index.
                        handledSites.add(siteKey);
                        continue;
                    }
                }
            }
            // ── Case 5: value-receiver bridge (object-literal services) ──
            // When prior cases couldn't resolve the receiver as a class or
            // type binding, fall back to value-binding resolution. Covers:
            //
            //   export const fooService = { getUser(id) {...} };
            //   import { fooService } from './service';
            //   fooService.getUser(id);   // ← resolve here
            //
            // `fooService` is a `Const`/`Variable` (not class-like, no typeBinding
            // for unannotated literals), so Cases 2-4 skip it. Scope-resolution
            // defs for non-class values carry a synthetic id, so we translate to
            // the canonical graph node ID via `resolveDefGraphId` before owner-
            // indexed lookup — the parser writes the graph node ID as `ownerId`
            // on the method symbol-table entry to match.
            //
            // Object-literal methods do not carry a `qualifiedName` (no class
            // owner to seed it), so the picked def cannot round-trip through
            // `tryEmitEdge` → `resolveDefGraphId`. We use
            // `tryEmitEdgeWithExplicitTargetId` instead, passing `picked.nodeId`
            // directly — same dedup-key shape, collapse-flag honoring, and
            // caller resolution as `tryEmitEdge`.
            const valueDef = findValueBindingInScope(site.inScope, receiverName, scopes);
            if (valueDef !== undefined) {
                const ownerGraphId = resolveDefGraphId(valueDef.filePath, valueDef, nodeLookup) ?? valueDef.nodeId;
                const picked = pickOverload(ownerGraphId, memberName, site, model, provider);
                if (picked === OVERLOAD_AMBIGUOUS) {
                    recordReceiverOverloadSuppression(options.recordResolutionOutcome, parsed.filePath, site, ownerGraphId, memberName, model, provider);
                    handledSites.add(siteKey);
                    continue;
                }
                if (picked !== undefined) {
                    // Static-only filter (#1756 / U3): unlike Case 4 there's no
                    // MRO chain to walk here — Case 5 dispatches on a single
                    // owner via `pickOverload`. When the picked candidate is
                    // static-only (Kotlin companion-promoted), suppress the
                    // edge entirely and mark the site handled so
                    // `emitReferencesViaLookup` doesn't re-emit a wrong target
                    // from the pre-resolved reference index. Matches the after-
                    // chain handled-marker semantic used by Case 4's
                    // all-filtered fall-through.
                    if (provider.isStaticOnly?.(picked) === true) {
                        handledSites.add(siteKey);
                        continue;
                    }
                    const reason = site.kind === 'write' || site.kind === 'read'
                        ? site.kind
                        : picked.filePath !== parsed.filePath
                            ? 'import-resolved'
                            : 'global';
                    const confidence = site.kind === 'write' || site.kind === 'read' ? 1.0 : 0.85;
                    const ok = tryEmitEdgeWithExplicitTargetId(graph, scopes, nodeLookup, site, picked.nodeId, reason, seen, confidence, collapse);
                    if (ok)
                        emitted++;
                    handledSites.add(siteKey);
                    continue;
                }
            }
        }
    }
    return emitted;
}
/** Resolve a member by name on a class def, narrowing by argument
 *  types when multiple overloads share the name. Falls back to the
 *  first-seen def (legacy `findOwnedMember` semantics) when there's
 *  no narrowing signal or when `argumentTypes` is unavailable. */
function pickOverload(ownerId, memberName, site, model, provider) {
    const overloads = model.methods.lookupAllByOwner(ownerId, memberName);
    if (overloads.length === 0) {
        // Non-callable member (field / property / variable) — ACCESSES
        // write/read sites target these too. Fall back to the field
        // registry so owner-scoped attribute access resolves.
        return model.fields.lookupFieldByOwner(ownerId, memberName);
    }
    if (overloads.length === 1)
        return overloads[0];
    const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, {
        argumentTypeClasses: site.argumentTypeClasses,
        conversionRankFn: provider.conversionRankFn,
        constraintCompatibility: provider.constraintCompatibility,
    });
    // When narrowing leaves >1 candidate that share identical normalized
    // parameter-types (e.g., C++ `f(int)` vs `f(long)` both collapsed to
    // `['int']` by `normalizeCppParamType`), suppress the edge entirely.
    // The graph schema has no ambiguous-target edge model, so emitting one
    // would arbitrarily pick a candidate and lie about the call's target.
    // PR #1520 review follow-up plan U2 / Claude review Finding 5.
    if (isOverloadAmbiguousAfterNormalization(candidates, site.arity))
        return OVERLOAD_AMBIGUOUS;
    // When conversion-rank scoring leaves >1 tied candidate with distinct
    // parameter types (e.g. h(int,double) vs h(double,int) both scoring 2),
    // suppress rather than picking arbitrarily — C++ would call this
    // ambiguous. Mirrors ADL merged-candidate suppression behavior.
    if (candidates.length > 1)
        return OVERLOAD_AMBIGUOUS;
    return candidates[0] ?? overloads[0];
}
/**
 * Sentinel returned by `pickOverload` when narrowing leaves >1 candidate
 * sharing identical normalized parameter-types. Callers should suppress
 * the CALLS edge AND mark the site as handled so `emitReferencesViaLookup`
 * does not re-emit from the pre-resolved reference index. See
 * `pickOverload` JSDoc for the upstream cause (per-language normalizer
 * collapses distinct types in arity-metadata).
 */
export const OVERLOAD_AMBIGUOUS = Symbol('overload-ambiguous');
/**
 * Sentinel returned by `pickFirstNonStaticOnly` when the only candidates
 * at the queried owner were filtered out by `provider.isStaticOnly`. Lets
 * the Case 4 chain walk distinguish "owner had no member of this name"
 * (return `undefined`, continue silently) from "owner had only static-
 * only members" (return this sentinel, continue and remember so the
 * post-chain handled-marker logic can suppress wrong-target re-emission
 * from `emitReferencesViaLookup`). See #1756 / remediation plan U2.
 */
const STATIC_ONLY_FILTERED = Symbol('static-only-filtered');
/**
 * Receiver-bound member lookup that filters static-only candidates BEFORE
 * arity narrowing. Wraps the raw `lookupAllByOwner` → `narrowOverloadCandidates`
 * pipeline so:
 *
 *   1. Candidates flagged by `provider.isStaticOnly` (Kotlin companion-
 *      promoted methods today) never enter the narrowing stage. A same-
 *      name same-arity static + instance pair on one owner therefore does
 *      NOT collapse to `OVERLOAD_AMBIGUOUS` — the instance member wins
 *      unambiguously, matching Kotlin's compile-time resolution.
 *   2. The chain walk in `emitReceiverBoundCalls` Case 4 can fall through
 *      to ancestors when only static-only candidates exist at the
 *      most-derived owner (returns `STATIC_ONLY_FILTERED`), rather than
 *      aborting the site as the previous after-chain filter did.
 *
 * Returns:
 *   - `undefined` — no member with this name on this owner; chain walk
 *     continues silently.
 *   - `STATIC_ONLY_FILTERED` — at least one candidate existed but every
 *     one was static-only; chain walk continues and remembers so the
 *     post-chain handled-marker can fire if no ancestor binds.
 *   - `OVERLOAD_AMBIGUOUS` — narrowing on the surviving non-static
 *     candidates left >1 ambiguous match; chain walk aborts and the
 *     site is marked handled (existing sentinel handling preserved).
 *   - `SymbolDefinition` — single survivor (the chosen target).
 *
 * See remediation plan `docs/plans/2026-05-22-002-fix-lang-kotlin-1782-
 * remediation-plan.md` § U2 for the full rationale.
 */
function pickFirstNonStaticOnly(ownerId, memberName, site, model, provider) {
    const rawOverloads = model.methods.lookupAllByOwner(ownerId, memberName);
    if (rawOverloads.length === 0) {
        // Non-callable member (field / property / variable) — ACCESSES
        // write/read sites target these too. Static-only filtering doesn't
        // apply to fields, so delegate straight to `lookupFieldByOwner`.
        return model.fields.lookupFieldByOwner(ownerId, memberName);
    }
    const isStaticOnly = provider.isStaticOnly;
    let overloads = rawOverloads;
    let filteredAny = false;
    if (isStaticOnly !== undefined) {
        const survivors = [];
        for (const candidate of rawOverloads) {
            if (isStaticOnly(candidate) === true) {
                filteredAny = true;
                continue;
            }
            survivors.push(candidate);
        }
        overloads = survivors;
    }
    if (overloads.length === 0) {
        // Every candidate was static-only; the caller (Case 4 chain walk)
        // should walk on to the next owner AND remember that filtering
        // happened so it can mark the site handled if the whole chain
        // ends with no legitimate match.
        return filteredAny ? STATIC_ONLY_FILTERED : undefined;
    }
    if (overloads.length === 1)
        return overloads[0];
    const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, {
        argumentTypeClasses: site.argumentTypeClasses,
        conversionRankFn: provider.conversionRankFn,
        constraintCompatibility: provider.constraintCompatibility,
    });
    // Same ambiguity handling as `pickOverload`: when normalization
    // collapses the surviving overloads into a single bucket (e.g., C++
    // `f(int)`/`f(long)` normalized to `['int']`), suppress rather than
    // arbitrarily picking. When narrowing leaves >1 distinct candidate
    // with no tie-breaker, suppress for the same reason.
    if (isOverloadAmbiguousAfterNormalization(candidates, site.arity))
        return OVERLOAD_AMBIGUOUS;
    if (candidates.length > 1)
        return OVERLOAD_AMBIGUOUS;
    return candidates[0] ?? overloads[0];
}
function recordReceiverOverloadSuppression(record, filePath, site, ownerId, memberName, model, provider) {
    if (record === undefined)
        return;
    const overloads = model.methods.lookupAllByOwner(ownerId, memberName);
    const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, {
        argumentTypeClasses: site.argumentTypeClasses,
        conversionRankFn: provider.conversionRankFn,
        constraintCompatibility: provider.constraintCompatibility,
    });
    const reason = isOverloadAmbiguousAfterNormalization(candidates, site.arity)
        ? 'overload-ambiguous-normalization'
        : hasConversionRankingSignal(site, provider)
            ? 'conversion-rank-tied'
            : 'overload-ambiguous';
    record({
        kind: 'suppressed',
        phase: 'receiver-bound-calls',
        filePath,
        name: site.name,
        range: site.atRange,
        reason,
        candidateIds: candidates.map((d) => d.nodeId),
    });
}
function hasConversionRankingSignal(site, provider) {
    return (provider.conversionRankFn !== undefined &&
        site.argumentTypes !== undefined &&
        site.argumentTypes.length > 0);
}
