/**
 * Overload narrowing — pick candidates from a list of same-named
 * method / function overloads using the call-site's arity and
 * argument-type signals.
 *
 * Used by both `receiver-bound-calls.ts::pickOverload` (explicit
 * receiver member call) and `free-call-fallback.ts::pickImplicitThisOverload`
 * (implicit `this` free-call inside a class-like body). Shared to keep
 * narrowing semantics in lockstep across the two sites.
 *
 * Semantics (first-wins; callers take `result[0]`):
 *   1. If `argCount` is undefined, arity is a pass-through.
 *   2. Exact-required-match wins over variadic. Variadic is detected
 *      via a `parameterTypes` entry equal to `'params'` or starting
 *      with `'params '` (C# `params` / variadic marker).
 *   3. If the arity filter empties the set AND any candidate had
 *      unknown bounds (both `parameterCount` and `requiredParameterCount`
 *      undefined), fall back to the full overload list — the empty
 *      result may be due to missing metadata rather than a real mismatch.
 *      If EVERY rejected candidate had definite arity bounds, trust the
 *      filter and return empty — the call is genuinely arity-incompatible
 *      (e.g., PHP `f(int $req, ...$rest)` called with zero args).
 *   4. If `argTypes` is present, filter further by per-slot type
 *      equality. An empty string in `argTypes[i]` means "unknown" and
 *      counts as a match. Mismatches disqualify. A non-empty typed
 *      result wins; otherwise return the arity-filtered candidates.
 *   4b. When the exact-type filter from step 4 returns empty AND a
 *       `conversionRankFn` is provided (via `hookCtx`), rank candidates
 *       via pairwise dominance comparison (ISO C++ [over.ics.rank]):
 *       F1 beats F2 only when F1 is not worse for every arg and better
 *       for at least one. Non-dominated candidates are returned;
 *       multiple survivors are genuinely ambiguous.
 *   4c. Final per-candidate constraint filter (SFINAE / `requires`).
 *       When `constraintCompatibility` is provided via `hookCtx`, drop
 *       candidates whose template constraints provably fail at the
 *       call site. Three-valued; `'unknown'` keeps the candidate
 *       (monotonicity).
 *   5. Empty input returns empty output.
 */
export function narrowOverloadCandidates(overloads, argCount, argTypes, hookCtx) {
    if (overloads.length === 0)
        return [];
    const arityMatches = argCount === undefined
        ? overloads
        : overloads.filter((d) => {
            const max = d.parameterCount;
            const min = d.requiredParameterCount;
            if (max !== undefined && argCount > max) {
                // Variadic marker check is C#-specific (the 'params' keyword).
                // Other languages use their own marker — PHP uses '...' (see
                // `languages/php/arity-metadata.ts:46`), Python uses '*args'-
                // shaped metadata that lives outside `parameterTypes` entirely.
                // This branch is dead code for those languages because they
                // set `parameterCount = undefined` for variadic functions,
                // which keeps `max` undefined and skips this check entirely.
                // Adding new variadic markers here changes behavior for those
                // other languages too — don't extend without auditing each
                // adapter's `arity-metadata.ts`. Finding 9 of PR #1497.
                const variadic = d.parameterTypes !== undefined &&
                    d.parameterTypes.some((t) => t === 'params' || t.startsWith('params '));
                if (!variadic)
                    return false;
            }
            if (min !== undefined && argCount < min)
                return false;
            return true;
        });
    // When the arity filter empties the set, only fall back to the full
    // overload list if some candidate had unknown bounds — otherwise the
    // empty result is authoritative (every candidate definitively failed
    // arity, e.g., PHP variadic with required-prefix called with too few
    // args).
    const anyUnknownBounds = overloads.some((d) => d.parameterCount === undefined && d.requiredParameterCount === undefined);
    const candidates = arityMatches.length > 0 ? arityMatches : anyUnknownBounds ? overloads : [];
    let result = candidates;
    if (argTypes !== undefined && argTypes.length > 0) {
        const typed = candidates.filter((d) => {
            const params = d.parameterTypes;
            if (params === undefined)
                return false;
            for (let i = 0; i < argTypes.length && i < params.length; i++) {
                if (argTypes[i] === '')
                    continue;
                if (!exactTypeSlotMatches(argTypes[i], params[i], hookCtx?.argumentTypeClasses?.[i], d.parameterTypeClasses?.[i])) {
                    return false;
                }
            }
            return true;
        });
        if (typed.length > 0) {
            result = typed;
        }
        else if (hookCtx?.conversionRankFn !== undefined) {
            // ── Conversion-rank scoring (step 4b) ──────────────────────────
            // The exact-type filter rejected every candidate. Rank via
            // pairwise dominance: F1 beats F2 only when F1 is not worse for
            // every arg and better for at least one. Non-dominated candidates
            // are returned; multiple survivors are genuinely ambiguous. When
            // ranking also yields empty, fall through to the arity-filtered
            // `candidates` set — matches pre-#1606 behavior.
            const ranked = rankByConversion(candidates, argTypes, hookCtx.conversionRankFn, hookCtx.argumentTypeClasses);
            if (ranked.length > 0)
                result = ranked;
        }
    }
    // Constraint filter (step 4c; Tier-A — SFINAE / `requires` clauses).
    // Runs after arity, exact-type, and conversion-rank filters so the
    // hook only sees candidates already viable on the other axes.
    // Three-valued: `'compatible'` and `'unknown'` keep the candidate
    // (monotonicity — adding a predicate must never cause a wrong edge);
    // only `'incompatible'` drops it. Candidates without
    // `templateConstraints` are always kept.
    //
    // No fallback to the unconstrained set when this filter empties the
    // candidate list: a fully-`'incompatible'` verdict is authoritative.
    // The downstream `OVERLOAD_AMBIGUOUS` sentinel still guards the empty
    // case, so a buggy hook that wrongly returns `'incompatible'` for
    // every candidate degrades to today's "suppress edge" behavior rather
    // than emitting a wrong edge.
    if (hookCtx?.constraintCompatibility !== undefined && argCount !== undefined) {
        const callsite = { arity: argCount };
        const ctx = argTypes !== undefined
            ? {
                argumentTypes: argTypes,
                ...(hookCtx.argumentTypeClasses !== undefined
                    ? { argumentTypeClasses: hookCtx.argumentTypeClasses }
                    : {}),
            }
            : {};
        result = result.filter((def) => {
            if (def.templateConstraints === undefined)
                return true;
            return hookCtx.constraintCompatibility(callsite, def, ctx) !== 'incompatible';
        });
    }
    return result;
}
function exactTypeSlotMatches(argType, paramType, argTypeClass, paramTypeClass) {
    if (argType !== paramType)
        return false;
    // C++ normalizes away pointer markers (`int*` -> `int`). When both sides
    // provide shape sidecars, do not let that collapse make `int` exactly match
    // `int*`. Unknown sidecar evidence preserves the previous string-only path.
    if (argTypeClass === undefined || paramTypeClass === undefined)
        return true;
    if (argTypeClass.indirection === 'unknown' || paramTypeClass.indirection === 'unknown') {
        return true;
    }
    return isPointerShape(argTypeClass) === isPointerShape(paramTypeClass);
}
function isPointerShape(typeClass) {
    return typeClass.indirection === 'pointer' && typeClass.pointerDepth > 0;
}
/**
 * Pairwise dominance comparison (ISO C++ [over.ics.rank]).
 *
 * F1 is a better match than F2 when F1's conversion rank is **not
 * worse** for every argument AND **strictly better** for at least one.
 * Candidates dominated by any other viable candidate are removed.
 * If more than one non-dominated candidate remains, they are genuinely
 * ambiguous — callers suppress the edge rather than picking arbitrarily.
 *
 * Candidates with at least one `Infinity`-ranked slot (incompatible
 * type) are excluded before pairwise comparison begins.
 */
function rankByConversion(candidates, argTypes, rankFn, argTypeClasses) {
    // Step 1: compute per-slot ranks and exclude non-viable candidates.
    const viable = [];
    for (const d of candidates) {
        const params = d.parameterTypes;
        if (params === undefined)
            continue;
        const ranks = [];
        let ok = true;
        for (let i = 0; i < argTypes.length; i++) {
            const paramType = parameterTypeAt(params, i);
            if (paramType === undefined) {
                ok = false;
                break;
            }
            if (argTypes[i] === '') {
                ranks.push(0); // unknown arg → any-match (rank 0)
                continue;
            }
            const r = rankFn(argTypes[i], paramType, argTypeClasses?.[i], parameterTypeClassAt(d.parameterTypeClasses, i));
            if (!isFinite(r)) {
                ok = false;
                break;
            }
            ranks.push(r);
        }
        if (!ok)
            continue;
        viable.push({ def: d, ranks });
    }
    if (viable.length <= 1)
        return viable.map((v) => v.def);
    // Step 2: pairwise dominance — remove candidates dominated by any other.
    const dominated = new Set();
    for (let i = 0; i < viable.length; i++) {
        if (dominated.has(i))
            continue;
        for (let j = i + 1; j < viable.length; j++) {
            if (dominated.has(j))
                continue;
            const cmp = pairwiseCompare(viable[i].ranks, viable[j].ranks);
            if (cmp < 0)
                dominated.add(j); // i dominates j
            else if (cmp > 0)
                dominated.add(i); // j dominates i
        }
    }
    return viable.filter((_, idx) => !dominated.has(idx)).map((v) => v.def);
}
function parameterTypeAt(params, argIndex) {
    if (argIndex < params.length)
        return params[argIndex];
    return params[params.length - 1] === '...' ? '...' : undefined;
}
function parameterTypeClassAt(params, argIndex) {
    if (params === undefined)
        return undefined;
    if (argIndex < params.length)
        return params[argIndex];
    return params[params.length - 1]?.base === '...' ? params[params.length - 1] : undefined;
}
/**
 * Compare two per-slot rank vectors.
 * Returns  -1 if `a` dominates `b` (not worse everywhere, better somewhere),
 *          +1 if `b` dominates `a`,
 *           0 if neither dominates (incomparable or equal).
 */
function pairwiseCompare(a, b) {
    let aBetter = false;
    let bBetter = false;
    const len = Math.min(a.length, b.length);
    for (let i = 0; i < len; i++) {
        if (a[i] < b[i])
            aBetter = true;
        else if (b[i] < a[i])
            bBetter = true;
        if (aBetter && bBetter)
            return 0; // incomparable — early exit
    }
    if (aBetter && !bBetter)
        return -1;
    if (bBetter && !aBetter)
        return 1;
    return 0;
}
/**
 * Detect when >1 candidate share identical `parameterTypes` after the
 * per-language normalizer has collapsed distinct underlying types. This
 * signals "the resolver cannot pick the right overload — the
 * normalization that helps single-candidate flows now hides a real
 * ambiguity" and lets callers suppress the edge rather than pick
 * arbitrarily.
 *
 * Concrete trigger (PR #1520 review follow-up plan U2, Claude review
 * Finding 5): the C++ `arity-metadata.ts` normalizer collapses `int`,
 * `long`, `short`, `unsigned`, and `size_t` to `'int'`. Without this
 * check, `process(int)` and `process(long)` both end up with
 * `parameterTypes === ['int']`, and `pickOverload` arbitrarily picks
 * the first — emitting a false CALLS edge to the wrong overload.
 *
 * Returns false when:
 *   - 0 or 1 candidates (no ambiguity to detect)
 *   - any candidate has undefined `parameterTypes` (can't compare)
 *   - candidates differ in arity or in any parameter-type slot
 *
 * Other languages: this check is a precondition gate, not a behavior
 * change for normal narrowing. Languages whose normalizers do not
 * collapse distinct types (verified by grep over `*-arity-metadata.ts`
 * — no `int → int` collapse outside C++) will never produce >1
 * candidate with identical `parameterTypes` from genuinely distinct
 * declarations, so this returns false for them. The branch is
 * effectively C++-only in practice.
 */
export function isOverloadAmbiguousAfterNormalization(candidates, argCount) {
    if (candidates.length < 2)
        return false;
    const first = candidates[0].parameterTypes;
    if (first === undefined)
        return false;
    // When argCount is provided, compare only the first `argCount` slots —
    // this catches default-argument ambiguity: `void f(int); void f(int, int = 0);`
    // called with `f(1)` (argCount=1) leaves both candidates viable because
    // default args make them arity-compatible, and their first slot is
    // identical even though full parameterTypes lengths differ.
    // Without argCount, fall back to full-sequence comparison (the original
    // int/long normalization-collapse case).
    const compareUpTo = argCount !== undefined ? argCount : first.length;
    if (compareUpTo === 0)
        return false;
    if (first.length < compareUpTo)
        return false;
    for (let i = 1; i < candidates.length; i++) {
        const p = candidates[i].parameterTypes;
        if (p === undefined)
            return false;
        if (p.length < compareUpTo)
            return false;
        for (let j = 0; j < compareUpTo; j++) {
            if (p[j] !== first[j])
                return false;
        }
        // When argCount is NOT provided, also require length equality so
        // distinct-arity candidates that happen to share a prefix don't
        // collapse to ambiguous (preserves the original int/long contract).
        if (argCount === undefined && p.length !== first.length)
            return false;
    }
    return true;
}
