/**
 * Build a `(filePath, name) → graphNodeId` lookup over the graph's
 * Function/Method/Class/Constructor nodes. Two keys per node:
 *
 *   - simple name (`User` / `save`) — legacy fallback
 *   - qualified name when derivable from the node id (`User.save`)
 *
 * The qualified key is the authoritative one when two classes in the
 * same file define a method with the same simple name
 * (`class User: def save` + `class Document: def save`). Without it,
 * the simple-name key collides and every `document.save()` CALLS edge
 * would silently target `User.save`. Method node ids encode the
 * qualifier (`Method:file.py:User.save#1`), so we parse it back out.
 *
 * Language-agnostic seam. Any language provider migrating to the
 * registry-primary path can consume this to translate scope-resolution
 * `SymbolDefinition.nodeId` values into the legacy graph-node ID
 * format that downstream consumers (queries, edges, MCP) expect.
 */

import type { NodeLabel, ParameterTypeClass } from 'gitnexus-shared';
import type { KnowledgeGraph } from '../../../graph/types.js';
import { isOverloadableCallable } from '../../utils/callable-labels.js';
import { templateConstraintsIdTag } from '../../utils/template-arguments.js';
import { parameterShapeIdTag } from '../../utils/method-props.js';

export type GraphNodeLookup = ReadonlyMap<string, string>;

/**
 * Parse a qualified name out of a Function/Method node id.
 *
 * Node id format: `${label}:${filePath}:${qualifiedName}${arityTag}`,
 * where `arityTag` is `#<n>` (or empty). Strips the known-length
 * label + filePath prefix so colons inside `filePath` (Windows
 * `C:\...`) don't break the parse. Returns `undefined` when the id
 * doesn't match the expected shape.
 */
function parseQualifiedFromId(id: string, label: NodeLabel, filePath: string): string | undefined {
  const prefix = `${label}:${filePath}:`;
  if (!id.startsWith(prefix)) return undefined;
  const suffix = id.slice(prefix.length);
  if (suffix.length === 0) return undefined;
  const hash = suffix.indexOf('#');
  return hash === -1 ? suffix : suffix.slice(0, hash);
}

function stripCallableDisambiguatorTags(qualifiedName: string): string {
  return qualifiedName.replace(/~shape:.*$/, '').replace(/~c:[a-z0-9]+$/, '');
}

/**
 * Build a qualified-key string in a separate keyspace from simple-key
 * strings. Prefix `<q>` can't appear in a valid filePath on any OS, so
 * no collision between the two keyspaces is possible.
 *
 * Includes the node label so a top-level `def save` (Function,
 * qualifier = `save`) doesn't alias a class method `User.save` (Method,
 * simple name = `save`) whose Function-typed qualifier would collapse
 * to the same simple-key slot in a single map.
 */
export function qualifiedKey(filePath: string, label: NodeLabel, qualifiedName: string): string {
  return `<q>:${filePath}::${label}::${qualifiedName}`;
}

/** Simple-name key (legacy fallback keyspace — no `<q>` prefix). */
export function simpleKey(filePath: string, name: string): string {
  return `${filePath}::${name}`;
}

export function buildGraphNodeLookup(graph: KnowledgeGraph): GraphNodeLookup {
  const lookup = new Map<string, string>();
  for (const node of graph.iterNodes()) {
    const props = node.properties as {
      filePath?: string;
      name?: string;
      qualifiedName?: string;
      templateArguments?: readonly string[];
    };
    if (props.filePath === undefined || props.name === undefined) continue;
    if (!isLinkableLabel(node.label)) continue;

    // Primary key: fully-qualified name + label, in a separate
    // keyspace from simple names. Class nodes carry `qualifiedName`
    // in their properties (set by the parsing processor).
    // Method/Function nodes do not, so derive the qualifier from the
    // node id — that's where the parse-phase encoded it. Including
    // the label avoids a collision when a free Function's qualifier
    // happens to equal a Method's simple name (e.g. top-level
    // `def save` vs `class User: def save`).
    const qualified =
      props.qualifiedName ?? parseQualifiedFromId(node.id, node.label, props.filePath);
    if (qualified !== undefined && qualified.length > 0) {
      const keyQualified = stripCallableDisambiguatorTags(qualified);
      const qKey = qualifiedKey(props.filePath, node.label, keyQualified);
      if (!lookup.has(qKey)) lookup.set(qKey, node.id);
      // Overload-disambiguating key: include parameter types so two
      // same-arity overloads (e.g. `Lookup(int)` vs `Lookup(string)`)
      // map to distinct graph nodes. Legacy parse-phase encodes the
      // type tag into the node id; we register both that node id and
      // a parameter-types-suffixed key so resolveDefGraphId can find
      // the right overload by matching its def's parameterTypes.
      const pTypes = (props as { parameterTypes?: readonly string[] }).parameterTypes;
      if (pTypes !== undefined && pTypes.length > 0 && isOverloadableCallable(node.label)) {
        const pKey = qualifiedKey(
          props.filePath,
          node.label,
          `${keyQualified}~${pTypes.join(',')}`,
        );
        // Each overload is unique — set unconditionally.
        if (!lookup.has(pKey)) lookup.set(pKey, node.id);
      }
      // Arity-disambiguating key: include the parameter count so two same-name
      // overloads of DIFFERENT arity route to distinct graph nodes even when the
      // shorter overload carries no parameter types (e.g. a Kotlin zero-arg
      // secondary constructor vs a 2-arg one — both share the qualified key, whose
      // first-write-wins assignment is source-order-dependent). The structure-phase
      // node id encodes `#<arity>`; this mirrors it in the lookup keyspace so
      // resolveDefGraphId can match by the def's own parameterCount. Same-arity
      // overloads collapse onto one arity key (first-write-wins) — identical to the
      // pre-existing qualified-key behavior, so no regression there.
      const pCount = (props as { parameterCount?: number }).parameterCount;
      if (pCount !== undefined && isOverloadableCallable(node.label)) {
        const aKey = qualifiedKey(props.filePath, node.label, `${keyQualified}#${pCount}`);
        if (!lookup.has(aKey)) lookup.set(aKey, node.id);
      }
      const pClasses = (props as { parameterTypeClasses?: readonly ParameterTypeClass[] })
        .parameterTypeClasses;
      const shapeTag = parameterShapeIdTag(pTypes, pClasses);
      if (shapeTag !== '' && isOverloadableCallable(node.label)) {
        const shapeKey = qualifiedKey(props.filePath, node.label, `${keyQualified}${shapeTag}`);
        if (!lookup.has(shapeKey)) lookup.set(shapeKey, node.id);
      }
      // SFINAE / `requires`-clause disambiguation (issue #1579) — register
      // a constraint-fingerprinted key so resolveDefGraphId can locate the
      // correct overload by hashing the def's `templateConstraints`. Mirrors
      // the parameter-types key but keys on the opaque constraint payload
      // instead, separating two `process<T>` overloads whose
      // `parameterTypes=['T']` would otherwise collide.
      const tConstraints = (props as { templateConstraints?: unknown }).templateConstraints;
      if (tConstraints !== undefined && (node.label === 'Function' || node.label === 'Method')) {
        const cKey = qualifiedKey(
          props.filePath,
          node.label,
          `${keyQualified}${templateConstraintsIdTag(tConstraints)}`,
        );
        lookup.set(cKey, node.id);
      }
      if (
        (node.label === 'Class' ||
          node.label === 'Struct' ||
          node.label === 'Interface' ||
          node.label === 'Enum' ||
          node.label === 'Record') &&
        props.templateArguments !== undefined &&
        props.templateArguments.length > 0
      ) {
        const tKey = qualifiedKey(
          props.filePath,
          node.label,
          `${keyQualified}~${props.templateArguments.join(',')}`,
        );
        if (!lookup.has(tKey)) lookup.set(tKey, node.id);
      }
    }

    // Fallback key: simple name. First-wins within a file — used when
    // the caller doesn't know the qualifier (unqualified free-call
    // fallback, cross-file resolution where MethodRegistry already
    // disambiguated the owner).
    const sKey = simpleKey(props.filePath, props.name);
    if (!lookup.has(sKey)) lookup.set(sKey, node.id);
  }
  return lookup;
}

export function isLinkableLabel(label: NodeLabel): boolean {
  return (
    label === 'Function' ||
    label === 'Method' ||
    label === 'Constructor' ||
    label === 'Class' ||
    label === 'Interface' ||
    label === 'Struct' ||
    label === 'Enum' ||
    // Trait nodes are linkable so MRO builders can bridge PHP/Rust trait
    // defs between scope-resolution DefIds and the graph's node ids.
    // IMPLEMENTS edges from classes to traits are otherwise invisible to
    // the scope-resolution MRO pass.
    label === 'Trait' ||
    // Variable / Property are linkable too — receiver-bound write/read
    // ACCESSES edges target field nodes (e.g. `user.name = "x"` →
    // ACCESSES edge to User's `name` Variable/Property node).
    label === 'Variable' ||
    label === 'Property' ||
    // Const is linkable so the value-receiver-owner bridge in
    // `receiver-bound-calls.ts` Case 5 can translate the scope-resolution
    // `Variable` def for `export const fooService = {...}` to the canonical
    // `Const:filePath:name` graph node id, against which object-literal
    // method symbols register their `ownerId` (PR #1718 / issue #1358).
    label === 'Const' ||
    // Macro nodes are linkable so a macro invocation (`log!(…)`) resolved
    // via `MacroRegistry` can bridge its scope-resolution `Macro` def to
    // the legacy `@definition.macro` graph node and emit the `USES` edge
    // (Rust #1934 F72; also covers C/C++ `#define` macro defs).
    label === 'Macro'
  );
}
