/**
 * `lookupCore` — the shared 7-step canonical resolution algorithm
 * (RFC §4.2; Ring 2 SHARED #917).
 *
 * Pure function. Given a name, a starting scope, and per-kind parameters,
 * walks lexical scopes + optional type-binding MRO + optional owner
 * contributor + global qualified-name fallback, and returns a ranked
 * `Resolution[]` with per-candidate evidence.
 *
 * All three public registries (`ClassRegistry` / `MethodRegistry` /
 * `FieldRegistry`) dispatch into this function, differing only in the
 * parameters they pass. The CHOICE of which steps fire is expressed
 * through `LookupParams`, not through different algorithms per kind.
 *
 * ## Algorithm (RFC §4.2, verbatim names)
 *
 * **Step 1 — Lexical scope-chain walk.** From `startScope`, walk
 *   parent-ward. At each scope, consult `scope.bindings.get(name)`:
 *     - Filter candidates whose `def.type ∈ acceptedKinds`.
 *     - For each surviving candidate, record a raw signal with the
 *       binding's origin + the current scope-chain depth.
 *     - **Hard shadow.** If `bindings.get(name)` is non-empty (including
 *       non-kind-matching candidates), stop walking. The name is
 *       lexically bound here; outer scopes are not consulted.
 *
 * **Step 2 — Type-binding resolution.** When `useReceiverTypeBinding`
 *   is true, resolve the receiver's type at `startScope` (from
 *   `scope.typeBindings`), then walk the MRO via
 *   `MethodDispatchIndex.mroFor(ownerDefId)`. Membership per owner comes
 *   through `RegistryContext.methodDispatch` + owner lookups into
 *   `scope.ownedDefs`; each hit records a raw signal with the owner's
 *   MRO depth.
 *
 * **Step 3 — Owner-scoped contributor.** When
 *   `params.ownerScopedContributor` is present, merge its `byName(name)`
 *   hits with `origin: 'local'` (they are declared directly on the
 *   receiver). Distinct from Step 2 — Step 2 walks the MRO; Step 3 only
 *   looks at the directly-declared owner members.
 *
 * **Step 4 — Kind filter (emit `kind-match` evidence).** Already
 *   applied during Steps 1-3; this step just adds a `kind-match` signal
 *   at weight 0 to every candidate for debuggability (so the evidence
 *   array is self-describing).
 *
 * **Step 5 — Arity filter.** Call `providers.arityCompatibility(callsite,
 *   def)` per surviving candidate. Verdicts: `compatible` / `unknown` /
 *   `incompatible`. If at least one candidate is `compatible`, drop
 *   `incompatible` ones. Otherwise keep all (the penalty weight alone
 *   will rank them lower but they remain in the result).
 *
 * **Step 6 — Global fallback.** When Steps 1-3 produced **no**
 *   candidates and the name contains a `.`, consult the
 *   `QualifiedNameIndex` via `lookupQualified` — see §4.5. The `scope`
 *   argument is NOT passed here because global lookup is scope-agnostic.
 *
 * **Step 7 — Rank + tie-break.** Compose evidence, compute confidence
 *   (sum capped at 1.0), sort by the RFC Appendix B cascade.
 *
 * ## What this module does NOT do
 *
 *   - No AST reads (pure data in, pure data out).
 *   - No `@codragraph/cli/` imports.
 *   - No language switches. Language-specific behavior flows exclusively
 *     through `providers.*` and the `params` object.
 *   - No caching. Callers that want memoization can wrap this function.
 */
import { composeEvidence, confidenceFromEvidence } from './evidence.js';
import { compareByConfidenceWithTiebreaks } from './tie-breaks.js';
import { lookupQualified } from './lookup-qualified.js';
/**
 * Run the 7-step lookup. Returns a non-empty `Resolution[]` when any
 * candidate was found; an empty array otherwise. Callers consume `[0]`
 * for the best answer and optionally inspect the rest for alternates.
 */
export function lookupCore(name, startScope, params, ctx) {
    const acceptedKinds = new Set(params.acceptedKinds);
    const perCandidate = new Map();
    // ── Step 1: lexical scope-chain walk ──────────────────────────────────
    const lexicalShadowed = walkLexicalChain(name, startScope, acceptedKinds, ctx, perCandidate);
    // ── Step 2: type-binding / MRO walk (methods/fields) ──────────────────
    if (params.useReceiverTypeBinding && ctx.methodDispatch !== undefined) {
        walkReceiverTypeBinding(name, startScope, acceptedKinds, params, ctx, perCandidate);
    }
    // ── Step 3: owner-scoped contributor ──────────────────────────────────
    if (params.ownerScopedContributor !== null) {
        seedFromOwnerScopedContributor(name, params.ownerScopedContributor, acceptedKinds, perCandidate);
    }
    // ── Step 4: kind-match evidence (emitted by composeEvidence directly) ──
    // Handled inside `composeEvidence`.
    // ── Step 5: arity filter ──────────────────────────────────────────────
    if (params.callsite !== undefined) {
        applyArityFilter(params.callsite, perCandidate, ctx);
    }
    // ── Step 6: global fallback (only when Steps 1-3 produced nothing) ──
    if (perCandidate.size === 0 && !lexicalShadowed && name.includes('.')) {
        const globals = lookupQualified(name, { acceptedKinds: params.acceptedKinds }, ctx);
        if (globals.length > 0)
            return globals;
    }
    if (perCandidate.size === 0)
        return EMPTY;
    // ── Step 7: compose evidence + rank ──────────────────────────────────
    return rankCandidates(perCandidate);
}
function ensureCandidate(perCandidate, def) {
    const existing = perCandidate.get(def.nodeId);
    if (existing !== undefined)
        return existing;
    const fresh = {
        def,
        signals: { kindMatch: true },
        tieBreakKey: { scopeDepth: 0, mroDepth: 0, origin: 'local' },
    };
    perCandidate.set(def.nodeId, fresh);
    return fresh;
}
// ─── Step 1 implementation ─────────────────────────────────────────────────
/**
 * Walk the lexical scope chain from `startScope` upward. Returns `true`
 * iff a scope with any `bindings.get(name)` entries was found — the
 * caller uses this to decide whether to run the global fallback.
 */
function walkLexicalChain(name, startScope, acceptedKinds, ctx, perCandidate) {
    let currentId = startScope;
    let depth = 0;
    const visited = new Set();
    while (currentId !== null) {
        if (visited.has(currentId))
            return false;
        visited.add(currentId);
        const scope = ctx.scopes.getScope(currentId);
        if (scope === undefined)
            return false;
        const bindings = scope.bindings.get(name);
        if (bindings !== undefined && bindings.length > 0) {
            for (const binding of bindings) {
                if (!acceptedKinds.has(binding.def.type))
                    continue;
                recordLexicalHit(perCandidate, binding, depth);
            }
            return true; // hard shadow regardless of kind-filter survivorship
        }
        currentId = scope.parent;
        depth++;
    }
    return false;
}
function recordLexicalHit(perCandidate, binding, scopeChainDepth) {
    const state = ensureCandidate(perCandidate, binding.def);
    state.signals.origin = binding.origin;
    state.signals.scopeChainDepth = scopeChainDepth;
    if (binding.via?.linkStatus === 'unresolved') {
        state.signals.viaUnlinkedImport = true;
    }
    if (binding.via?.kind === 'dynamic-unresolved') {
        state.signals.dynamicUnresolved = true;
    }
    state.tieBreakKey.scopeDepth = scopeChainDepth;
    state.tieBreakKey.origin = binding.origin;
}
// ─── Step 2 implementation ─────────────────────────────────────────────────
function walkReceiverTypeBinding(name, startScope, acceptedKinds, params, ctx, perCandidate) {
    const ownerDefId = resolveReceiverOwner(startScope, params, ctx);
    if (ownerDefId === undefined)
        return;
    if (ctx.methodDispatch === undefined)
        return;
    const ownerDef = ctx.defs.get(ownerDefId);
    if (ownerDef === undefined)
        return;
    // Walk the owner itself at depth 0, then its MRO chain.
    const walk = [ownerDefId, ...ctx.methodDispatch.mroFor(ownerDefId)];
    for (let mroDepth = 0; mroDepth < walk.length; mroDepth++) {
        const currentOwnerId = walk[mroDepth];
        const members = collectOwnedMembers(currentOwnerId, name, ctx);
        for (const def of members) {
            if (!acceptedKinds.has(def.type))
                continue;
            recordTypeBindingHit(perCandidate, def, mroDepth, ownerDefId);
        }
    }
}
function resolveReceiverOwner(startScope, params, ctx) {
    // Explicit receiver: consult the callsite scope's typeBindings for the
    // named receiver; the attached TypeRef identifies the owner. Without a
    // ready resolveTypeRef call (that module is separate), we do a direct
    // lookup and trust the caller to have populated the binding.
    if (params.explicitReceiver !== undefined) {
        return lookupReceiverType(startScope, params.explicitReceiver.name, ctx);
    }
    // Implicit `self` / `this` — the scope's typeBindings should carry it.
    for (const implicitName of IMPLICIT_RECEIVERS) {
        const owner = lookupReceiverType(startScope, implicitName, ctx);
        if (owner !== undefined)
            return owner;
    }
    return undefined;
}
const IMPLICIT_RECEIVERS = Object.freeze(['self', 'this']);
function lookupReceiverType(startScope, receiverName, ctx) {
    let currentId = startScope;
    const visited = new Set();
    while (currentId !== null) {
        if (visited.has(currentId))
            return undefined;
        visited.add(currentId);
        const scope = ctx.scopes.getScope(currentId);
        if (scope === undefined)
            return undefined;
        const typeRef = scope.typeBindings.get(receiverName);
        if (typeRef !== undefined) {
            // rawName must resolve to a def via qualifiedNames; if it doesn't, we
            // can't claim the receiver type. No fallback — that's what
            // `resolveTypeRef` would do, but we keep this path lean and let
            // callers pre-resolve if they want the richer semantics.
            const candidateIds = ctx.qualifiedNames.get(typeRef.rawName);
            if (candidateIds.length === 1)
                return candidateIds[0];
            // Ambiguous (≥ 2) or missing (0) — caller must pre-resolve via
            // `resolveTypeRef` (#916) if they want the richer semantics. We
            // intentionally do NOT re-implement a simple-name fallback here.
            return undefined;
        }
        currentId = scope.parent;
    }
    return undefined;
}
function collectOwnedMembers(ownerDefId, memberName, ctx) {
    // An owner's members are defs whose `ownerId === ownerDefId` and whose
    // simple name matches `memberName`. We iterate `defs.byId` — O(D) per
    // call today. A future by-owner index would make this O(K); tracked as
    // a follow-up optimization before Ring 3 flips go production.
    const out = [];
    for (const def of ctx.defs.byId.values()) {
        if (def.ownerId !== ownerDefId)
            continue;
        if (simpleNameOf(def) !== memberName)
            continue;
        out.push(def);
    }
    return out;
}
function simpleNameOf(def) {
    if (def.qualifiedName === undefined || def.qualifiedName.length === 0)
        return undefined;
    const dot = def.qualifiedName.lastIndexOf('.');
    return dot === -1 ? def.qualifiedName : def.qualifiedName.slice(dot + 1);
}
function recordTypeBindingHit(perCandidate, def, mroDepth, receiverOwner) {
    const state = ensureCandidate(perCandidate, def);
    const existingMroDepth = state.signals.typeBindingMroDepth;
    const firstHit = existingMroDepth === undefined;
    // Only replace if this hit is shallower (smaller MRO depth). The local
    // const lets TS narrow to `number` in the `else` branch so no `!`
    // assertion is needed.
    if (firstHit || mroDepth < existingMroDepth) {
        state.signals.typeBindingMroDepth = mroDepth;
        state.tieBreakKey.mroDepth = mroDepth;
    }
    if (def.ownerId === receiverOwner) {
        state.signals.ownerMatch = true;
    }
    // Pure type-binding candidates (no lexical hit) would otherwise keep the
    // `ensureCandidate` default `tieBreakKey.origin === 'local'`, making the
    // Appendix B cascade lump them with local-origin candidates. Demote them
    // to `'import'` — the strongest non-local origin — only when no earlier
    // phase set an origin for this candidate. Lexical hits from Step 1 set
    // `signals.origin` before Step 2 runs, so the guard skips them; Step 3
    // (`seedFromOwnerScopedContributor`) runs AFTER Step 2 and unconditionally
    // overrides `tieBreakKey.origin` back to `'local'` for direct-owner
    // members, so any same-def overlap still ends up ranked correctly.
    if (firstHit && state.signals.origin === undefined) {
        state.tieBreakKey.origin = 'import';
    }
}
// ─── Step 3 implementation ─────────────────────────────────────────────────
function seedFromOwnerScopedContributor(name, contributor, acceptedKinds, perCandidate) {
    for (const def of contributor.byName(name)) {
        if (!acceptedKinds.has(def.type))
            continue;
        const state = ensureCandidate(perCandidate, def);
        // Treat the contributor's direct membership as `origin: 'local'` —
        // strongest visibility, no scope-chain penalty.
        state.signals.origin = 'local';
        state.signals.scopeChainDepth = 0;
        state.signals.ownerMatch = def.ownerId === contributor.ownerDefId;
        state.tieBreakKey.origin = 'local';
    }
}
// ─── Step 5 implementation ─────────────────────────────────────────────────
function applyArityFilter(callsite, perCandidate, ctx) {
    const arityFn = ctx.providers.arityCompatibility;
    if (arityFn === undefined) {
        // No provider → record 'unknown' for every candidate; keeps signal
        // shape uniform for composeEvidence.
        for (const state of perCandidate.values()) {
            state.signals.arityVerdict = 'unknown';
        }
        return;
    }
    let anyCompatible = false;
    for (const state of perCandidate.values()) {
        const verdict = arityFn(callsite, state.def);
        state.signals.arityVerdict = verdict;
        if (verdict === 'compatible')
            anyCompatible = true;
    }
    if (!anyCompatible)
        return;
    // Filter: when at least one compatible candidate exists, drop incompatibles.
    for (const [defId, state] of perCandidate) {
        if (state.signals.arityVerdict === 'incompatible') {
            perCandidate.delete(defId);
        }
    }
}
// ─── Step 7 implementation ─────────────────────────────────────────────────
function rankCandidates(perCandidate) {
    const resolutions = [];
    const tieKeys = new Map();
    for (const state of perCandidate.values()) {
        const evidence = composeEvidence(state.signals);
        const confidence = confidenceFromEvidence(evidence);
        resolutions.push({ def: state.def, confidence, evidence });
        tieKeys.set(state.def.nodeId, { ...state.tieBreakKey });
    }
    resolutions.sort((a, b) => compareByConfidenceWithTiebreaks(a, b, tieKeys));
    return Object.freeze(resolutions);
}
// ─── Constants ──────────────────────────────────────────────────────────────
const EMPTY = Object.freeze([]);
//# sourceMappingURL=lookup-core.js.map