/**
 * C++ argument-dependent lookup (ADL / Koenig lookup).
 *
 * When ordinary unqualified lookup fails for a free-call site, ADL also
 * considers candidates declared in the **associated namespaces** of the
 * call's argument types (ISO C++ `[basic.lookup.argdep]`). The canonical
 * pattern V1 unlocks:
 *
 *   namespace audit { struct Event; void record(Event); }
 *   namespace app   { void run() { audit::Event e; record(e); } }
 *
 * Without ADL: `record(e)` is unresolved because `app::run` doesn't
 * `using` anything. With V1 ADL: `audit::record` is discovered via
 * `audit::Event`'s associated namespace.
 *
 * ## Current boundary
 *
 * The current implementation covers class-typed arguments (value, pointer,
 * and reference) and template specializations with explicit type arguments:
 *   - `audit::Event e`, `audit::Event* p`, `audit::Event** pp`
 *   - `audit::Event& r`, `audit::Event&& rr`
 *   - `std::vector<audit::Event>` (template namespace + template-arg namespaces)
 *
 * V2 additionally walks class ancestors (via MRO), so base-class enclosing
 * namespaces also contribute associated namespaces.
 *
 * **GitNexus approximation (not strict ISO C++ ADL):** passing a qualified
 * function reference like `utils::worker` contributes `utils` to the associated
 * set, enabling resolution of unqualified calls like `with_callback(utils::worker)`
 * to `utils::with_callback`. Under ISO C++ `[basic.lookup.argdep]`, associated
 * entities for function-type arguments come from the **parameter types and return
 * type** of each function in the overload set — NOT the function's enclosing
 * namespace. For `void worker()`, the standard-compliant associated set is empty.
 * GitNexus instead contributes the enclosing namespace of any Function/Method
 * def whose simple name matches, because it enables the dominant real-world ADL
 * pattern at reasonable precision cost.
 *
 * For qualified refs (e.g. `utils::worker`) the namespace is confirmed via a
 * workspace lookup (only contributed when a Function/Method named `worker` exists
 * in `utils`). For unqualified refs the workspace is searched for any Function
 * def with that simple name. Locally-declared function-pointer variables
 * (e.g. `void (*g)()`) and function parameters are excluded from this path.
 *
 * ADL candidates are merged with ordinary unqualified-lookup candidates
 * in the free-call fallback before overload narrowing.
 *
 * ## Parenthesized-name suppression
 *
 * `(f)(s)` MUST NOT trigger ADL — the parenthesized name forces ordinary
 * lookup only. `captures.ts` records sites whose `function` child is a
 * `parenthesized_expression` into `noAdlSites`; `pickCppAdlCandidates`
 * short-circuits when the site key is present.
 *
 * ## State lifecycle
 *
 * Three module-level maps populated per pipeline invocation, cleared via
 * `clearCppAdlState()` (called from `clearFileLocalNames`):
 *
 *   - `argInfoBySite` — per-call-site argument shape (capture-time)
 *   - `noAdlSites` — call sites with parenthesized function (capture-time)
 *   - `classToNamespaceQualifiedName` — class def → its enclosing namespace
 *     qualified name (`populateCppAssociatedNamespaces` time)
 *
 * The class→namespace map uses qualified names (not scope IDs) because
 * C++ namespaces are open: `namespace N { ... }` in file A and
 * `namespace N { ... }` in file B produce two distinct Namespace scopes
 * but logically share the same namespace. ADL must consider candidates
 * declared in either file.
 */
import { isCppInlineNamespaceScope } from './inline-namespaces.js';
const argInfoBySite = new Map();
const noAdlSites = new Set();
const classToNamespaceQualifiedName = new Map();
function siteKey(filePath, line, col) {
    return `${filePath}:${line}:${col}`;
}
/** Record per-call-site argument info. Called once per call site from
 *  `emitCppScopeCaptures`. */
export function markCppAdlSiteArgs(filePath, line, col, args) {
    argInfoBySite.set(siteKey(filePath, line, col), args);
}
/** Mark a call site as ADL-suppressed (function child wrapped in
 *  `parenthesized_expression`, e.g. `(f)(s)`). */
export function markCppAdlSiteNoAdl(filePath, line, col) {
    noAdlSites.add(siteKey(filePath, line, col));
}
/** Clear ADL state. Called from `clearFileLocalNames` so all C++ resolver
 *  per-pipeline state is reset together. */
export function clearCppAdlState() {
    argInfoBySite.clear();
    noAdlSites.clear();
    classToNamespaceQualifiedName.clear();
}
/**
 * Walk `parsed.scopes` to record each Class def's enclosing namespace
 * qualified name. Run from the cpp resolver's `populateOwners` hook so
 * the index is available before any resolution pass consults it.
 *
 * Computes the namespace's qualified name by walking parent scope chain
 * and looking up Namespace defs in each parent's `ownedDefs`. The
 * resulting name is dot-joined (matching `populateClassOwnedMembers`'s
 * dotted convention; conversion to `::` is consumer-internal).
 */
export function populateCppAssociatedNamespaces(parsed) {
    const scopesById = new Map();
    for (const scope of parsed.scopes)
        scopesById.set(scope.id, scope);
    for (const scope of parsed.scopes) {
        if (scope.kind !== 'Class')
            continue;
        const nsQName = computeEnclosingNamespaceQName(scope, scopesById);
        if (nsQName === '')
            continue;
        for (const def of scope.ownedDefs) {
            if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface')
                continue;
            classToNamespaceQualifiedName.set(def.nodeId, nsQName);
        }
    }
    // Enum defs live in Namespace scopes directly (not inside Class scopes).
    // Map each Enum def to its enclosing namespace so ADL on enum-typed
    // arguments contributes the correct associated namespace.
    for (const scope of parsed.scopes) {
        if (scope.kind !== 'Namespace')
            continue;
        const nsQName = computeNamespaceQName(scope, scopesById);
        if (nsQName === '')
            continue;
        for (const def of scope.ownedDefs) {
            if (def.type !== 'Enum')
                continue;
            classToNamespaceQualifiedName.set(def.nodeId, nsQName);
        }
    }
}
/**
 * ADL candidate collector. Returns:
 *   - `readonly SymbolDefinition[]` — ADL candidates to merge with
 *     ordinary unqualified lookup candidates.
 *   - `undefined` — no ADL candidates.
 *
 * Fires only when:
 *   - the call site is not in `noAdlSites` (parenthesized form), AND
 *   - at least one argument resolves to a named class type (value,
 *     pointer, or reference; but not function pointer, literal, or primitive).
 */
export function pickCppAdlCandidates(site, callerParsed, scopes, parsedFiles) {
    const key = siteKey(callerParsed.filePath, site.atRange.startLine, site.atRange.startCol);
    if (noAdlSites.has(key))
        return undefined;
    const args = argInfoBySite.get(key);
    if (args === undefined || args.length === 0)
        return undefined;
    // Collect associated namespace QNames from every participating class-typed arg
    // and from function-reference args.
    const associatedNamespaces = new Set();
    for (const arg of args) {
        collectAssociatedNamespacesForAdlArg(arg, scopes, associatedNamespaces);
        if (arg.functionRefText !== undefined) {
            collectFunctionRefNamespaces(arg.functionRefText, parsedFiles, associatedNamespaces);
        }
    }
    if (associatedNamespaces.size === 0)
        return undefined;
    // Walk every namespace scope in every parsed file; collect callable
    // ownedDefs whose enclosing namespace matches one of the associated
    // QNames AND whose simple name matches the call's name.
    // ISO C++: inline namespaces are transparent — candidates in inline
    // children of an associated namespace are also ADL-reachable.
    const candidates = [];
    const seenKey = new Set();
    for (const parsed of parsedFiles) {
        const scopesById = new Map();
        for (const sc of parsed.scopes)
            scopesById.set(sc.id, sc);
        for (const scope of parsed.scopes) {
            if (scope.kind !== 'Namespace')
                continue;
            const qName = computeNamespaceQName(scope, scopesById);
            if (!associatedNamespaces.has(qName)) {
                // Check if this is an inline-namespace child of an associated NS.
                // ISO C++ inline namespaces are transparent for ADL: if the outer
                // namespace is in the associated set, candidates in the inline child
                // are also reachable.
                if (!isCppInlineNamespaceScope(scope.id))
                    continue;
                const parentScope = scope.parent !== null ? scopesById.get(scope.parent) : undefined;
                if (parentScope === undefined || parentScope.kind !== 'Namespace')
                    continue;
                const parentQName = computeNamespaceQName(parentScope, scopesById);
                if (!associatedNamespaces.has(parentQName))
                    continue;
            }
            for (const def of scope.ownedDefs) {
                if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor') {
                    continue;
                }
                const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
                if (simple !== site.name)
                    continue;
                // Dedup by nodeId — using normalized parameter-types as the key
                // would collapse `process(int)`/`process(long)`-style overloads
                // (both normalize to `['int']`) before
                // `isOverloadAmbiguousAfterNormalization` can detect them.
                if (seenKey.has(def.nodeId))
                    continue;
                seenKey.add(def.nodeId);
                candidates.push(def);
            }
        }
        // ISO C++ `[basic.lookup.argdep]` §2: hidden friend functions declared
        // inside a class body are visible via ADL when the class is an associated
        // class. Scan Class scopes whose enclosing namespace is in the associated
        // set for callable ownedDefs matching the call name. This enables the
        // canonical "hidden friend" idiom:
        //   struct Foo { friend void swap(Foo&, Foo&) {} };
        for (const scope of parsed.scopes) {
            if (scope.kind !== 'Class')
                continue;
            // Check if ANY class def in this scope has an associated namespace.
            let isAssociatedClass = false;
            for (const def of scope.ownedDefs) {
                if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface')
                    continue;
                const nsQName = classToNamespaceQualifiedName.get(def.nodeId);
                if (nsQName !== undefined && associatedNamespaces.has(nsQName)) {
                    isAssociatedClass = true;
                    break;
                }
            }
            if (!isAssociatedClass)
                continue;
            // Also scan Function scopes that are direct children of this class
            // scope — friend function definitions create their own Function scope
            // underneath the Class scope.
            for (const childScope of parsed.scopes) {
                if (childScope.parent !== scope.id)
                    continue;
                if (childScope.kind !== 'Function')
                    continue;
                for (const def of childScope.ownedDefs) {
                    if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor') {
                        continue;
                    }
                    const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
                    if (simple !== site.name)
                        continue;
                    if (seenKey.has(def.nodeId))
                        continue;
                    seenKey.add(def.nodeId);
                    candidates.push(def);
                }
            }
            for (const def of scope.ownedDefs) {
                if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor') {
                    continue;
                }
                const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
                if (simple !== site.name)
                    continue;
                if (seenKey.has(def.nodeId))
                    continue;
                seenKey.add(def.nodeId);
                candidates.push(def);
            }
        }
    }
    if (candidates.length === 0)
        return undefined;
    return candidates;
}
function collectAssociatedNamespacesForAdlArg(arg, scopes, associatedNamespaces) {
    // For template args this may be the template name itself (e.g. `vector`);
    // simple-name lookup can match project classes with the same name (known
    // V1/V2 simplification).
    addAssociatedNamespaceForClassName(arg.simpleClassName, scopes, associatedNamespaces);
    // Includes template-owner namespaces (e.g. `std` in std::vector<T>). If
    // that surfaces extra candidates, merged-candidate overload narrowing in
    // free-call-fallback suppresses arbitrary edge emission.
    if (arg.templateNamespace.length > 0)
        associatedNamespaces.add(arg.templateNamespace);
    for (const ns of arg.templateArgNamespaces) {
        if (ns.length > 0)
            associatedNamespaces.add(ns);
    }
    for (const className of arg.templateArgClassNames) {
        addAssociatedNamespaceForClassName(className, scopes, associatedNamespaces);
    }
}
function addAssociatedNamespaceForClassName(simpleClassName, scopes, associatedNamespaces) {
    if (simpleClassName.length === 0)
        return;
    const classLookup = findCppClassDefBySimpleName(simpleClassName, scopes);
    if (classLookup === undefined)
        return;
    const { classDef, ambiguous } = classLookup;
    const nsQName = classToNamespaceQualifiedName.get(classDef.nodeId);
    if (nsQName !== undefined)
        associatedNamespaces.add(nsQName);
    // Preserve V1 collision behavior for the direct class namespace, but avoid
    // amplifying a same-simple-name collision by walking an arbitrary class's
    // full MRO chain.
    if (ambiguous)
        return;
    for (const ancestorDefId of scopes.methodDispatch.mroFor(classDef.nodeId)) {
        const ancestorNsQName = classToNamespaceQualifiedName.get(ancestorDefId);
        if (ancestorNsQName !== undefined)
            associatedNamespaces.add(ancestorNsQName);
    }
}
/** Walk upward from a Class scope, finding the innermost enclosing
 *  Namespace scope, and return that namespace's qualified name (dot-
 *  joined, outermost-first). Returns '' when the class has no enclosing
 *  namespace (e.g., declared at translation-unit scope). */
function computeEnclosingNamespaceQName(classScope, scopesById) {
    let parentId = classScope.parent;
    while (parentId !== null) {
        const parent = scopesById.get(parentId);
        if (parent === undefined)
            return '';
        if (parent.kind === 'Namespace') {
            return computeNamespaceQName(parent, scopesById);
        }
        parentId = parent.parent;
    }
    return '';
}
/** Walk upward from a Namespace scope collecting each enclosing
 *  Namespace's simple name (innermost last). Returns the dot-joined
 *  qualified name (e.g., `outer.inner`). The namespace's own def lives
 *  in its OWN scope's `ownedDefs` (the C++ extractor stamps the
 *  namespace-decl def into the namespace scope itself, not the parent
 *  module scope). */
function computeNamespaceQName(nsScope, scopesById) {
    const segments = [];
    let currentId = nsScope.parent;
    let current = nsScope;
    // Outer guard against pathological cycles in malformed scope trees.
    let safety = 64;
    while (current !== undefined && safety-- > 0) {
        const nsDef = findNamespaceDefInScope(current);
        if (nsDef === undefined) {
            // No name found — bail out. Returning a partial QName would risk
            // false ADL associations.
            return '';
        }
        const simple = nsDef.qualifiedName?.split('.').pop() ?? nsDef.qualifiedName ?? '';
        segments.unshift(simple);
        // Walk up to next enclosing namespace (skipping non-namespace parents).
        let nextId = currentId;
        let nextNs;
        while (nextId !== null) {
            const nx = scopesById.get(nextId);
            if (nx === undefined)
                break;
            if (nx.kind === 'Namespace') {
                nextNs = nx;
                currentId = nx.parent;
                break;
            }
            nextId = nx.parent;
        }
        current = nextNs;
    }
    return segments.join('.');
}
/** Find the Namespace def attached to this scope (the namespace's own
 *  decl, stamped into its own `ownedDefs` by the C++ extractor). Returns
 *  the first Namespace-type def encountered — for normal C++ the scope
 *  carries exactly one Namespace-typed self def. */
function findNamespaceDefInScope(scope) {
    for (const def of scope.ownedDefs) {
        if (def.type === 'Namespace')
            return def;
    }
    return undefined;
}
/** Find a class-like or enum def by simple name across the workspace.
 *  V1 still arbitrary-picks the first match on collisions (multiple defs
 *  share the simple name), but reports the collision so callers can avoid
 *  amplifying that uncertainty (for example by skipping MRO expansion).
 *  C++ ADL strictness would require full type-driven lookup.
 *
 *  ISO C++ `[basic.lookup.argdep]` §2: enumerations contribute their
 *  enclosing namespace to the associated set, just like class types. */
function findCppClassDefBySimpleName(simpleName, scopes) {
    let firstMatch;
    for (const def of scopes.defs.byId.values()) {
        if (def.type !== 'Class' &&
            def.type !== 'Struct' &&
            def.type !== 'Interface' &&
            def.type !== 'Enum')
            continue;
        const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
        if (simple !== simpleName)
            continue;
        if (firstMatch === undefined) {
            firstMatch = def;
            continue;
        }
        return { classDef: firstMatch, ambiguous: true };
    }
    if (firstMatch === undefined)
        return undefined;
    return { classDef: firstMatch, ambiguous: false };
}
/**
 * Contribute associated namespaces for a function-reference argument.
 *
 * - **Qualified refs** (`utils::worker`, `outer::inner::fn`): the namespace
 *   is extracted from the qualifier text (converting `::` to `.` for dot-joined
 *   QName matching). A workspace lookup then **verifies** that a Function or
 *   Method def named `worker` (the simple name after the last `::`) actually
 *   exists in the extracted namespace. This prevents false positives from
 *   namespace-qualified variables, enum values, and static data members, which
 *   also produce `qualified_identifier` AST nodes in tree-sitter-cpp (the
 *   AST node type alone does not distinguish functions from non-function names).
 * - **Unqualified refs** (`worker`): the workspace is searched for any
 *   Function/Method def whose simple name matches. Every distinct enclosing
 *   namespace found is added — overloads across the same namespace produce
 *   a single entry; GitNexus does not select a specific overload at this stage.
 */
function collectFunctionRefNamespaces(refText, parsedFiles, out) {
    const colonIdx = refText.lastIndexOf('::');
    if (colonIdx !== -1) {
        // Qualified ref: extract namespace prefix and normalise :: → dot notation.
        const nsText = refText.slice(0, colonIdx).replace(/::/g, '.');
        if (nsText === '')
            return;
        const simpleName = refText.slice(colonIdx + 2);
        // Verify that a Function/Method named `simpleName` exists in `nsText`.
        // Without this guard every `a::b` qualified_identifier arg (variable,
        // enum value, static member, type alias) would blindly contribute `a`
        // to the associated set and risk a false-positive CALLS edge.
        for (const parsed of parsedFiles) {
            const scopesById = new Map();
            for (const sc of parsed.scopes)
                scopesById.set(sc.id, sc);
            for (const scope of parsed.scopes) {
                if (scope.kind !== 'Namespace')
                    continue;
                if (computeNamespaceQName(scope, scopesById) !== nsText)
                    continue;
                for (const def of scope.ownedDefs) {
                    if (def.type !== 'Function' && def.type !== 'Method')
                        continue;
                    const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
                    if (simple === simpleName) {
                        out.add(nsText);
                        return; // Namespace confirmed; no need to scan further files.
                    }
                }
            }
        }
        return;
    }
    // Unqualified: search all namespace scopes for a Function def with this
    // simple name and contribute its enclosing namespace.
    for (const parsed of parsedFiles) {
        const scopesById = new Map();
        for (const sc of parsed.scopes)
            scopesById.set(sc.id, sc);
        for (const scope of parsed.scopes) {
            if (scope.kind !== 'Namespace')
                continue;
            for (const def of scope.ownedDefs) {
                if (def.type !== 'Function' && def.type !== 'Method')
                    continue;
                const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
                if (simple !== refText)
                    continue;
                const nsQName = computeNamespaceQName(scope, scopesById);
                if (nsQName !== '')
                    out.add(nsQName);
            }
        }
    }
}
