/**
 * `emitScopeCaptures` for Java.
 *
 * Drives the Java scope query against tree-sitter-java and groups raw
 * matches into `CaptureMatch[]` for the central extractor. Layers:
 *
 *   1. **Decomposed import declarations** — each `import_declaration`
 *      is re-emitted with `@import.kind/source/name` markers.
 *   2. **Receiver binding synthesis** — `this`/`super` type-bindings
 *      on instance methods.
 *   3. **Arity metadata** on method/constructor declarations.
 *   4. **Reference arity** on call sites.
 *
 * The returned captures are deterministic. Class-annotation facts are also
 * recorded for the worker side-channel consumed after scope resolution.
 */
import { type Capture, type CaptureMatch, type ScopeId } from 'gitnexus-shared';
import {
  materializeClassAnnotationFacts,
  recordClassAnnotationCapture,
} from '../../frameworks/spring/bean-candidates.js';
import {
  javaLocalTypeDeclarationContainer,
  nodeIfType,
  nodeToCapture,
  synthesizeJavaTypeIdentity,
  syntheticCapture,
} from '../../utils/ast-helpers.js';
import { splitImportDeclaration } from './import-decomposer.js';
import { computeJavaArityMetadata } from './arity-metadata.js';
import { synthesizeJavaReceiverBinding } from './receiver-binding.js';
import { getJavaParser, getJavaScopeQuery } from './query.js';
import { recordCacheHit, recordCacheMiss } from './cache-stats.js';
import { getTreeSitterBufferSize } from '../../constants.js';
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
import {
  setJavaClassAnnotationFacts,
  setJavaSpringAopFacts,
  setJavaSpringConfigConsumerFacts,
  setJavaSpringConditionalFacts,
  setJavaSpringDiFacts,
  setJavaSpringNonHttpHandlerFacts,
} from './capture-side-channel.js';
import { captureJavaPackageFact } from './package-facts.js';
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
import { captureJavaSpringConfigConsumerFacts } from './spring-config-bindings.js';
import { captureJavaSpringDiClassFact, type JavaSpringDiClassFact } from './spring-di.js';
import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
import { captureJavaSpringAopFacts, type JavaSpringAopFact } from './spring-aop.js';
import {
  captureJavaSpringConditionalFacts,
  type JavaSpringConditionalFact,
} from './spring-conditionals.js';
import {
  captureJavaSpringNonHttpHandlerFacts,
  type JavaSpringNonHttpHandlerFact,
} from './spring-non-http-handlers.js';
import { synthesizeJavaRecordComponentAccessorCaptures } from './record-components.js';

/** Declaration anchors that carry function-like arity metadata. */
const FUNCTION_DECL_TAGS = ['@declaration.method', '@declaration.constructor'] as const;

/** tree-sitter-java node types that the method extractor accepts. */
const FUNCTION_NODE_TYPES = [
  'method_declaration',
  'constructor_declaration',
  'compact_constructor_declaration',
] as const;

const JAVA_CALLABLE_CAPTURE_OPTIONS = {
  functionNodeTypes: new Set([...FUNCTION_NODE_TYPES, 'lambda_expression']),
  callNodeTypes: new Set(['method_invocation']),
  parameterListNodeTypes: new Set(['formal_parameters', 'argument_list']),
  parameterNodeTypes: new Set(['formal_parameter', 'spread_parameter', 'receiver_parameter']),
  bindingNodeTypes: new Set(['variable_declarator']),
  assignmentNodeTypes: new Set(['assignment_expression']),
  identifierNodeTypes: new Set(['identifier', 'type_identifier']),
  callableReferenceNodeTypes: new Set(['method_reference']),
  // java.util.function SAM names, MINUS 'get' and 'test': those two collide
  // with ubiquitous non-functional APIs (Map/List/Optional/Future.get,
  // Predicate-unrelated test methods), emitting a spurious callable-object
  // invoke fact for every container access (#2522 review). Supplier.get /
  // Predicate.test dispatch is deliberately traded away until the check can
  // gate on the receiver's declared type.
  callableProtocolMethods: new Set(['run', 'apply', 'accept', 'call']),
  normalizeQualifiedName: (raw: string) => raw.replaceAll('::', '.'),
} as const;

/** Visibility of a local type begins at its declaration and ends with its
 * immediately enclosing block (JLS 6.3). A Java-only synthetic Block scope
 * models that range without changing shared resolver selection semantics. */
function javaLocalTypeVisibilityScope(node: SyntaxNode): CaptureMatch | undefined {
  const container = javaLocalTypeDeclarationContainer(node);
  if (container === null) return undefined;
  return {
    '@scope.block': {
      name: '@scope.block',
      range: {
        startLine: node.startPosition.row + 1,
        startCol: node.startPosition.column,
        endLine: container.endPosition.row + 1,
        endCol: container.endPosition.column,
      },
      text: node.text,
    },
  };
}

/** Suppress read.member emissions when the field_access is already
 *  covered by a method_invocation (object of a call) or an
 *  assignment_expression (write target). */
function shouldEmitReadMember(memberNode: SyntaxNode): boolean {
  const parent = memberNode.parent;
  if (parent === null) return true;

  switch (parent.type) {
    case 'assignment_expression':
      return parent.childForFieldName('left')?.id !== memberNode.id;
    default:
      return true;
  }
}

export function emitJavaScopeCaptures(
  sourceText: string,
  filePath: string,
  cachedTree?: unknown,
): readonly CaptureMatch[] {
  let tree = cachedTree as ReturnType<ReturnType<typeof getJavaParser>['parse']> | undefined;
  if (tree === undefined) {
    tree = parseSourceSafe(getJavaParser(), sourceText, undefined, {
      bufferSize: getTreeSitterBufferSize(sourceText),
    });
    recordCacheMiss();
  } else {
    recordCacheHit();
  }
  captureJavaPackageFact(filePath, tree.rootNode);

  const rawMatches = getJavaScopeQuery().matches(tree.rootNode);
  const out: CaptureMatch[] = [];
  const classAnnotations = new Map<ScopeId, Set<string>>();
  const springAopFacts: JavaSpringAopFact[] = [];
  const springAopTypeNodeIds = new Set<number>();
  const springConditionalFacts: JavaSpringConditionalFact[] = [];
  const springDiFacts: JavaSpringDiClassFact[] = [];
  const springNonHttpHandlerFacts: JavaSpringNonHttpHandlerFact[] = [];
  const springDiClassNodeIds = new Set<number>();

  for (const m of rawMatches) {
    const grouped: Record<string, Capture> = {};
    // Parallel tag -> captured SyntaxNode map. The tree-sitter query already
    // hands us each matched node as `c.node`, so anchors resolve via a
    // type-guarded lookup (`nodeIfType`) instead of re-deriving them with
    // `findNodeAtRange(tree.rootNode, ...)` per match — the
    // O(matches × rootChildren) root-walk fixed for go #1848 / python #1918 /
    // rust/csharp #1915, mirrored here for java (#1951). Every Java scope-query
    // anchor below captures directly ON the node the old root-walk re-derived
    // (verified against JAVA_SCOPE_QUERY in query.ts), so the type check is exact.
    const nodeMap: Record<string, SyntaxNode> = {};
    for (const c of m.captures) {
      const tag = '@' + c.name;
      grouped[tag] = nodeToCapture(tag, c.node);
      nodeMap[tag] = c.node;
    }
    if (Object.keys(grouped).length === 0) continue;

    const springAopTypeNode = [
      nodeIfType(nodeMap['@scope.class'], 'class_declaration'),
      nodeIfType(nodeMap['@scope.class'], 'interface_declaration'),
    ].find((node): node is SyntaxNode => node !== null);
    if (springAopTypeNode !== undefined && !springAopTypeNodeIds.has(springAopTypeNode.id)) {
      springAopTypeNodeIds.add(springAopTypeNode.id);
      springAopFacts.push(...captureJavaSpringAopFacts(springAopTypeNode, filePath));
    }

    const springDiClassNode = nodeIfType(nodeMap['@scope.class'], 'class_declaration');
    if (springDiClassNode !== null && !springDiClassNodeIds.has(springDiClassNode.id)) {
      springDiClassNodeIds.add(springDiClassNode.id);
      springConditionalFacts.push(
        ...captureJavaSpringConditionalFacts(springDiClassNode, filePath),
      );
      springNonHttpHandlerFacts.push(
        ...captureJavaSpringNonHttpHandlerFacts(springDiClassNode, filePath),
      );
      const fact = captureJavaSpringDiClassFact(springDiClassNode, filePath);
      if (fact !== null) springDiFacts.push(fact);
    }

    const annotatedClass = grouped['@class-annotation.class'];
    const annotationName = grouped['@class-annotation.name'];
    if (annotatedClass !== undefined && annotationName !== undefined) {
      recordClassAnnotationCapture(classAnnotations, filePath, annotatedClass, annotationName.text);
      continue;
    }

    const typeDeclaration = [
      nodeMap['@declaration.class'],
      nodeMap['@declaration.enum'],
      nodeMap['@declaration.record'],
      nodeMap['@declaration.interface'],
    ].find((node): node is SyntaxNode => node !== undefined);
    const localTypeIdentity =
      typeDeclaration === undefined ? undefined : synthesizeJavaTypeIdentity(typeDeclaration);
    if (
      localTypeIdentity?.bindingName !== undefined &&
      grouped['@declaration.name'] !== undefined &&
      typeDeclaration !== undefined
    ) {
      grouped['@declaration.binding-name'] = grouped['@declaration.name'];
      grouped['@declaration.name'] = syntheticCapture(
        '@declaration.name',
        typeDeclaration,
        localTypeIdentity.name,
      );
      const visibilityScope = javaLocalTypeVisibilityScope(typeDeclaration);
      if (visibilityScope !== undefined) out.push(visibilityScope);
    }

    // Decompose each `import_declaration`. `@import.statement` is captured
    // directly on the `import_declaration` node.
    if (grouped['@import.statement'] !== undefined) {
      const stmtNode = nodeIfType(nodeMap['@import.statement'], 'import_declaration');
      if (stmtNode !== null) {
        const decomposed = splitImportDeclaration(stmtNode);
        if (decomposed !== null) {
          out.push(decomposed);
          continue;
        }
      }
      // Structural receiver chain for a call whose receiver is itself an
      // expression, so resolution can type it by folding over structure
      // instead of re-parsing the receiver's source text. Self-gating: a
      // non-call match, an absent receiver, or a chain with no nameable base
      // all leave `grouped` untouched.
      synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
      out.push(grouped);
      continue;
    }

    // Skip free-call matches that are actually member calls. The query
    // matches ALL method_invocations as @reference.call.free (without
    // negation) because tree-sitter-java's query engine drops !object
    // patterns when a positive object: pattern exists for the same node
    // type. Filter here: if the match has @reference.call.free but also
    // has @reference.receiver, it's a member call — skip the free match
    // (the separate @reference.call.member match covers it).
    if (
      grouped['@reference.call.free'] !== undefined &&
      grouped['@reference.receiver'] !== undefined
    ) {
      continue;
    }

    // Normalize a `new`-expression receiver to its constructed type's simple
    // name: `new Local().inner()` binds the WHOLE `object_creation_expression`
    // as `@reference.receiver`, so its raw text is `"new Local()"` — a string
    // that can never match a scope binding, so the compound-receiver resolver
    // silently falls through to name-only fallback resolution and picks the
    // wrong same-named method on a collision (#2564). Rewriting the text to
    // just `Local` lets Case 2 (class-name / static receiver) in
    // receiver-bound-calls.ts resolve it via its normal MRO walk. Mirrors the
    // established `normalizePhpReceiver` precedent (php/captures.ts) — a
    // language-local capture rewrite, no shared-pipeline change.
    if (grouped['@reference.receiver'] !== undefined) {
      const receiverNode = nodeIfType(nodeMap['@reference.receiver'], 'object_creation_expression');
      const typeNode = receiverNode?.childForFieldName('type');
      const simpleName = typeNode ? javaBaseSimpleNameOf(typeNode) : undefined;
      if (simpleName !== undefined) {
        grouped['@reference.receiver'] = syntheticCapture(
          '@reference.receiver',
          receiverNode!,
          simpleName,
        );
      }
    }

    // Filter read.member when it's a child of method_invocation or assignment.
    // `@reference.read.member` is captured directly on the `field_access` node.
    if (grouped['@reference.read.member'] !== undefined) {
      const memberNode = nodeIfType(nodeMap['@reference.read.member'], 'field_access');
      if (memberNode === null || !shouldEmitReadMember(memberNode)) {
        continue;
      }
    }

    // Synthesize `this` / `super` receiver type-bindings on every
    // instance method-like.
    if (grouped['@scope.function'] !== undefined) {
      // Structural receiver chain for a call whose receiver is itself an
      // expression, so resolution can type it by folding over structure
      // instead of re-parsing the receiver's source text. Self-gating: a
      // non-call match, an absent receiver, or a chain with no nameable base
      // all leave `grouped` untouched.
      synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
      out.push(grouped);
      // `@scope.function` is captured directly on the method/constructor node.
      const fnNode = findFunctionNode(nodeMap['@scope.function']);
      if (fnNode !== null) {
        for (const synth of synthesizeJavaReceiverBinding(fnNode)) {
          out.push(synth);
        }
      }
      continue;
    }

    // Synthesize arity metadata on function-like declarations.
    const declTag = FUNCTION_DECL_TAGS.find((t) => grouped[t] !== undefined);
    if (declTag !== undefined) {
      // FUNCTION_DECL_TAGS (@declaration.method/.constructor) are captured
      // directly on the method/constructor node.
      const fnNode = findFunctionNode(nodeMap[declTag]);
      if (fnNode !== null) {
        const arity = computeJavaArityMetadata(fnNode);
        if (arity.parameterCount !== undefined) {
          grouped['@declaration.parameter-count'] = syntheticCapture(
            '@declaration.parameter-count',
            fnNode,
            String(arity.parameterCount),
          );
        }
        if (arity.requiredParameterCount !== undefined) {
          grouped['@declaration.required-parameter-count'] = syntheticCapture(
            '@declaration.required-parameter-count',
            fnNode,
            String(arity.requiredParameterCount),
          );
        }
        if (arity.parameterTypes !== undefined) {
          grouped['@declaration.parameter-types'] = syntheticCapture(
            '@declaration.parameter-types',
            fnNode,
            JSON.stringify(arity.parameterTypes),
          );
        }
      }
    }

    // Synthesize `@reference.arity` on every callsite.
    const callTag = (
      ['@reference.call.free', '@reference.call.member', '@reference.call.constructor'] as const
    ).find((t) => grouped[t] !== undefined);
    if (callTag !== undefined && grouped['@reference.arity'] === undefined) {
      // @reference.call.free/.member are captured on the `method_invocation`;
      // @reference.call.constructor on the `object_creation_expression`. The
      // captured node IS the call node the old findNodeAtRange re-derived.
      const callNode = nodeIfType(
        nodeMap[callTag],
        'method_invocation',
        'object_creation_expression',
      );
      if (callNode !== null) {
        const argList = callNode.childForFieldName('arguments');
        // Exclude interleaved comments — tree-sitter-java emits `block_comment` /
        // `line_comment` as named children of argument_list, which would inflate
        // arity (and arity feeds call-processor symbol-ID generation). #1920
        const args =
          argList === null
            ? []
            : argList.namedChildren.filter(
                (c) => c !== null && c.type !== 'block_comment' && c.type !== 'line_comment',
              );
        grouped['@reference.arity'] = syntheticCapture(
          '@reference.arity',
          callNode,
          String(args.length),
        );

        const argTypes = args.map((arg) => inferArgType(arg!));
        grouped['@reference.parameter-types'] = syntheticCapture(
          '@reference.parameter-types',
          callNode,
          JSON.stringify(argTypes),
        );

        const argNames = args.map((a) => (a!.type === 'identifier' ? a!.text : ''));
        if (argNames.some((n) => n !== '')) {
          grouped['@reference.arg-names'] = syntheticCapture(
            '@reference.arg-names',
            callNode,
            JSON.stringify(argNames),
          );
        }
      }
    }

    // Structural receiver chain for a call whose receiver is itself an
    // expression, so resolution can type it by folding over structure
    // instead of re-parsing the receiver's source text. Self-gating: a
    // non-call match, an absent receiver, or a chain with no nameable base
    // all leave `grouped` untouched.
    synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
    out.push(grouped);
  }

  setJavaClassAnnotationFacts(filePath, materializeClassAnnotationFacts(classAnnotations));
  setJavaSpringConfigConsumerFacts(
    filePath,
    captureJavaSpringConfigConsumerFacts(tree.rootNode, filePath),
  );
  setJavaSpringAopFacts(filePath, springAopFacts);
  setJavaSpringConditionalFacts(filePath, springConditionalFacts);
  setJavaSpringDiFacts(filePath, springDiFacts);
  setJavaSpringNonHttpHandlerFacts(filePath, springNonHttpHandlerFacts);

  return [
    ...resolveVarTypeBindings(out),
    ...synthesizeJavaInheritanceReferences(tree.rootNode),
    ...synthesizeJavaExplicitConstructorReferences(tree.rootNode),
    ...synthesizeJavaAnonymousClassDeclarations(tree.rootNode),
    ...synthesizeJavaRecordComponentAccessorCaptures(tree.rootNode),
    ...synthesizeCallableFlowCaptures(tree.rootNode, JAVA_CALLABLE_CAPTURE_OPTIONS),
  ];
}

/**
 * Synthesize `@declaration.class` matches for anonymous class bodies
 * (`new Runnable() { ... }`), named by the same javac-compatible authority
 * (`synthesizeJavaTypeIdentity` → `Worker$N`) the structure phase
 * uses — the two layers agree by construction (#2550).
 *
 * The anchor is the `class_body` node: it shares its range with the
 * `(object_creation_expression (class_body) @scope.class)` scope rule in
 * query.ts, so the def is owned by that Class scope's `ownedDefs`
 * (making `populateClassOwnedMembers` stamp `ownerId` on the anonymous
 * class's methods) and the name auto-hoists to the enclosing scope —
 * exactly the binding shape a named class declaration produces.
 */
function synthesizeJavaAnonymousClassDeclarations(rootNode: SyntaxNode): CaptureMatch[] {
  const out: CaptureMatch[] = [];
  for (const oce of rootNode.descendantsOfType('object_creation_expression')) {
    const identity = synthesizeJavaTypeIdentity(oce);
    if (identity === undefined) continue;
    const body = oce.namedChildren.find((c) => c.type === 'class_body');
    if (body === undefined) continue;
    out.push({
      '@declaration.class': nodeToCapture('@declaration.class', body),
      '@declaration.name': syntheticCapture('@declaration.name', body, identity.name),
      '@declaration.is-synthetic': syntheticCapture('@declaration.is-synthetic', body, 'true'),
    });

    // Inheritance: the anonymous class extends/implements its constructed
    // type. Anchor the `@reference.inherits` on the `class_body` — its
    // range equals the anonymous Class scope, so the reference site's
    // enclosing class resolves to the SYNTHESIZED `Worker$N` def (anchoring
    // on the constructed-type node would sit OUTSIDE the anonymous scope
    // and mis-attribute the edge to the lexically enclosing class). The
    // synthetic `@reference.name` carries the base's simple name; a JDK
    // type with no repo def simply resolves to nothing (no edge). Without
    // this edge `mroFor(Worker$N)` is empty and the #2550 instance-
    // ownership gate suppressed TRUE bare calls to inherited methods
    // inside the anonymous body (empirically caught in review).
    const constructedType = oce.childForFieldName?.('type');
    const baseSimpleName =
      constructedType !== null && constructedType !== undefined
        ? javaBaseSimpleNameOf(constructedType)
        : undefined;
    if (baseSimpleName !== undefined) {
      out.push({
        '@reference.inherits': nodeToCapture('@reference.inherits', body),
        '@reference.name': syntheticCapture('@reference.name', body, baseSimpleName),
      });
    }

    // Receiver typeBinding: `Runnable handler = new Runnable() { ... }`
    // binds `handler` to the ANONYMOUS class (`Worker$1`), not the declared
    // interface — the instance is what `handler.run()` dispatches into, and
    // the declared type is frequently a JDK interface with no repo def.
    // Appended after the raw matches, so it overwrites the declared-type
    // binding the `@type-binding.annotation` query rule produced for the
    // same variable (pass-4 applies bindings in match order; last wins).
    const declarator = oce.parent;
    if (declarator !== null && declarator.type === 'variable_declarator') {
      const varName = declarator.childForFieldName?.('name');
      const declNode = declarator.parent ?? declarator;
      if (varName !== null && varName !== undefined) {
        out.push({
          '@type-binding.annotation': nodeToCapture('@type-binding.annotation', declNode),
          '@type-binding.name': nodeToCapture('@type-binding.name', varName),
          '@type-binding.type': syntheticCapture('@type-binding.type', oce, identity.name),
        });
      }
    }
  }

  // Enum constant bodies (`enum E { A { ... } }`) — javac's other
  // anonymous shape (#2555). Same synthesis, same body anchor; the
  // constant's class extends its HOST ENUM (javac semantics), so the
  // inherits reference names the enum — giving `mroFor(E$N) ∋ E` and
  // keeping bare calls from the body to the enum's own helpers alive
  // through the ownership gate's MRO arm.
  for (const constant of rootNode.descendantsOfType('enum_constant')) {
    const hostEnum = javaEnclosingEnumNameOf(constant);
    const bodyNode = constant.childForFieldName?.('body');
    const isBodied = bodyNode !== null && bodyNode !== undefined && bodyNode.type === 'class_body';
    const bodiedIdentity = synthesizeJavaTypeIdentity(constant);
    if (bodiedIdentity !== undefined && isBodied) {
      out.push({
        '@declaration.class': nodeToCapture('@declaration.class', bodyNode),
        '@declaration.name': syntheticCapture('@declaration.name', bodyNode, bodiedIdentity.name),
        '@declaration.is-synthetic': syntheticCapture(
          '@declaration.is-synthetic',
          bodyNode,
          'true',
        ),
      });
      if (hostEnum !== undefined) {
        out.push({
          '@reference.inherits': nodeToCapture('@reference.inherits', bodyNode),
          '@reference.name': syntheticCapture('@reference.name', bodyNode, hostEnum),
        });
      }
    }

    // Receiver dispatch (#2561): `E.CONST.method()` resolves through the
    // generic compound-receiver chain walk, which looks up each dotted
    // segment via the owning class scope's `typeBindings` map — the same
    // mechanism a field declaration uses (`private User user;` binds
    // `user` on the class scope). Binding the constant's own simple name
    // there — to its synthesized `E$N` class when bodied (MRO includes E,
    // so members inherited from the enum still resolve), or to the host
    // enum itself when body-less — makes `E.CONST.method()` resolve with
    // no changes to the shared receiver-binding machinery.
    //
    // A bodied constant binds ONLY to its `E$N` class, never the host enum:
    // if name synthesis fails on a malformed/error-recovery tree (`bodiedName`
    // undefined despite a real body), emit nothing rather than silently
    // misattributing an OVERRIDING constant's receiver to the enum's own
    // (non-overridden) method — a wrong edge is worse than no edge. Mirrors
    // the `object_creation_expression` branch, which skips on synthesis
    // failure. `hostEnum` is used only for genuinely body-less constants.
    const constantNameNode = constant.childForFieldName?.('name');
    const constantType = isBodied ? bodiedIdentity?.name : hostEnum;
    if (constantNameNode !== null && constantNameNode !== undefined && constantType !== undefined) {
      out.push({
        '@type-binding.annotation': nodeToCapture('@type-binding.annotation', constant),
        '@type-binding.name': nodeToCapture('@type-binding.name', constantNameNode),
        '@type-binding.type': syntheticCapture('@type-binding.type', constant, constantType),
      });
    }
  }
  return out;
}

/** Simple name of the enum_declaration enclosing an enum_constant, or
 *  undefined (grammar guarantees one exists in well-formed source). */
function javaEnclosingEnumNameOf(constant: SyntaxNode): string | undefined {
  let cursor: SyntaxNode | null = constant.parent;
  while (cursor !== null) {
    if (cursor.type === 'enum_declaration') {
      return cursor.childForFieldName?.('name')?.text ?? undefined;
    }
    cursor = cursor.parent;
  }
  return undefined;
}

/**
 * Synthesize `@reference.call.constructor` captures for explicit constructor
 * invocations — `super(...)` and `this(...)` (F38 #1928). tree-sitter-java
 * models these as `explicit_constructor_invocation` nodes, which the scope
 * query does not match, so the chained-constructor CALLS edges (subclass ctor →
 * superclass ctor; ctor → sibling overload) were silently dropped.
 *
 * The grammar gives no constructor *name* at the call site (the child is a bare
 * `(super)` / `(this)` token), so the target name is resolved structurally:
 *   - `this(...)`  → the enclosing type's own simple name (constructor symbols
 *                    are keyed by the declaring class name).
 *   - `super(...)` → the enclosing class's superclass simple-name tail (reusing
 *                    `javaBaseLookupNameNode` so qualified/generic supers reduce
 *                    to the bare class name, matching the EXTENDS synth). An
 *                    implicit `Object` super (no `superclass` field) has no
 *                    in-graph symbol, so it is skipped rather than emitting a
 *                    dangling reference.
 * Arity is attached so overloaded constructors disambiguate downstream, mirroring
 * the call-site arity synthesized for `new X(...)`.
 */
function synthesizeJavaExplicitConstructorReferences(root: SyntaxNode): CaptureMatch[] {
  const out: CaptureMatch[] = [];
  const stack: SyntaxNode[] = [root];
  while (stack.length > 0) {
    const node = stack.pop()!;
    if (node.type === 'explicit_constructor_invocation') {
      emitJavaExplicitConstructorRef(out, node);
    }
    for (let i = 0; i < node.namedChildCount; i++) {
      const child = node.namedChild(i);
      if (child !== null) stack.push(child);
    }
  }
  return out;
}

const TYPE_DECL_NODE_TYPES = new Set([
  'class_declaration',
  'enum_declaration',
  'record_declaration',
]);

function emitJavaExplicitConstructorRef(out: CaptureMatch[], node: SyntaxNode): void {
  const ctor = node.childForFieldName('constructor');
  if (ctor === null) return;

  const enclosingType = findEnclosingTypeDeclaration(node);
  if (enclosingType === null) return;

  let targetNameNode: SyntaxNode | null = null;
  if (ctor.type === 'this') {
    targetNameNode = enclosingType.childForFieldName('name');
  } else if (ctor.type === 'super') {
    // Only class_declaration carries a `superclass` field; enum/record cannot
    // declare an explicit superclass, so `super(...)` there has no resolvable
    // target symbol.
    const superclass = enclosingType.childForFieldName('superclass');
    if (superclass === null) return;
    for (const base of superclass.namedChildren) {
      if (base === null) continue;
      const nameNode = javaBaseLookupNameNode(base);
      if (nameNode !== null) {
        targetNameNode = nameNode;
        break;
      }
    }
  }
  if (targetNameNode === null) return;

  const argList = node.childForFieldName('arguments');
  const args =
    argList === null
      ? []
      : argList.namedChildren.filter(
          (c) => c !== null && c.type !== 'block_comment' && c.type !== 'line_comment',
        );

  out.push({
    '@reference.call.constructor': nodeToCapture('@reference.call.constructor', node),
    '@reference.name': nodeToCapture('@reference.name', targetNameNode),
    '@reference.arity': syntheticCapture('@reference.arity', node, String(args.length)),
  });
}

function findEnclosingTypeDeclaration(node: SyntaxNode): SyntaxNode | null {
  let cur: SyntaxNode | null = node.parent;
  while (cur !== null) {
    if (TYPE_DECL_NODE_TYPES.has(cur.type)) return cur;
    cur = cur.parent;
  }
  return null;
}

/**
 * Synthesize `@reference.inherits` captures from Java type heritage for the
 * authoritative registry-primary EXTENDS / IMPLEMENTS pre-pass (mirrors C++
 * `emitCppInheritanceCaptures`).
 *
 * Scope covers `class_declaration` (`superclass` extends + `interfaces`
 * implements clauses), `record_declaration` and `enum_declaration`
 * (`interfaces` implements clauses), and `interface_declaration`
 * (`extends_interfaces` clauses). Interface
 * inheritance was restored for registry-primary resolution in #1951. Record
 * graph nodes became canonical link targets in #2801 / PR #2871, so their
 * `implements` clauses must participate for interface dispatch (#2900).
 * Enums use the same tree-sitter `interfaces` field and participate as
 * class-like `Enum` graph nodes (#2918).
 *
 * Generic bases (`extends Box<T>`, `implements IFoo<T>`) and qualified bases
 * (`a.b.Base`, `a.b.Box<T>`, `a.b.IFoo<T>`) are normalized to their simple
 * lookup-name tails, consistent with C#/Rust and the V1 binding contract. The
 * EXTENDS-vs-IMPLEMENTS split is decided downstream from the resolved target's
 * symbol kind (`preEmitInheritanceEdges`): a superclass resolves to a class
 * (EXTENDS), an implemented interface resolves to an interface (IMPLEMENTS).
 * An `interface IA extends IB` base resolves to an Interface too, so it is
 * emitted as IMPLEMENTS. The synth only emits inheritance sites and lets the
 * resolved target decide the edge type.
 */
function synthesizeJavaInheritanceReferences(root: SyntaxNode): CaptureMatch[] {
  const out: CaptureMatch[] = [];
  const stack: SyntaxNode[] = [root];
  while (stack.length > 0) {
    const node = stack.pop()!;
    if (node.type === 'class_declaration') {
      const superclass = node.childForFieldName('superclass');
      if (superclass !== null) {
        for (const base of superclass.namedChildren) emitJavaInheritanceBase(out, base);
      }
    }
    if (
      node.type === 'class_declaration' ||
      node.type === 'record_declaration' ||
      node.type === 'enum_declaration'
    ) {
      // Records and enums cannot declare a superclass; all three declarations
      // expose implemented interfaces through the same tree-sitter field.
      const interfaces = node.childForFieldName('interfaces');
      if (interfaces !== null) {
        for (const typeList of interfaces.namedChildren) {
          if (typeList === null || typeList.type !== 'type_list') continue;
          for (const base of typeList.namedChildren) emitJavaInheritanceBase(out, base);
        }
      }
    } else if (node.type === 'interface_declaration') {
      // `interface IA extends IB, IC<T>` — the `extends_interfaces` clause is
      // NOT exposed via a tree-sitter field (unlike a class's `superclass` /
      // `interfaces`), so scan named children for it. It wraps a `type_list`
      // whose bases reuse `javaBaseLookupNameNode` (handles type_identifier /
      // generic_type / scoped_type_identifier). These resolve to Interface
      // targets, so `preEmitInheritanceEdges` emits them as IMPLEMENTS, at
      // parity with the legacy `interface_declaration` implements arm.
      for (let i = 0; i < node.namedChildCount; i++) {
        const extendsInterfaces = node.namedChild(i);
        if (extendsInterfaces === null || extendsInterfaces.type !== 'extends_interfaces') continue;
        for (const typeList of extendsInterfaces.namedChildren) {
          if (typeList === null || typeList.type !== 'type_list') continue;
          for (const base of typeList.namedChildren) emitJavaInheritanceBase(out, base);
        }
      }
    }
    // Named children only: every type/heritage node we care about is named,
    // so skipping unnamed punctuation tokens keeps the walk single-pass and
    // lighter on large files.
    for (let i = 0; i < node.namedChildCount; i++) {
      const child = node.namedChild(i);
      if (child !== null) stack.push(child);
    }
  }
  return out;
}

function emitJavaInheritanceBase(out: CaptureMatch[], base: SyntaxNode | null): void {
  if (base === null) return;
  const nameNode = javaBaseLookupNameNode(base);
  if (nameNode === null) return;
  out.push({
    '@reference.inherits': nodeToCapture('@reference.inherits', base),
    '@reference.name': nodeToCapture('@reference.name', nameNode),
  });
}

/** Resolve a Java base-type node to its bare simple-name identifier node. */
/** Simple name of a constructed/base type node, reusing the same node
 *  shapes `javaBaseLookupNameNode` handles (`Runnable`, `a.b.Base`,
 *  `Box<T>`). Returns undefined when the node is none of those. */
function javaBaseSimpleNameOf(typeNode: SyntaxNode): string | undefined {
  const nameNode = javaBaseLookupNameNode(typeNode);
  return nameNode === null ? undefined : nameNode.text;
}

function javaBaseLookupNameNode(node: SyntaxNode): SyntaxNode | null {
  switch (node.type) {
    case 'type_identifier':
      return node.isMissing || node.text.length === 0 ? null : node;
    case 'scoped_type_identifier': {
      // `java.io.Serializable` → trailing `type_identifier` (`Serializable`).
      const tail = node.lastNamedChild;
      return tail === null ? null : javaBaseLookupNameNode(tail);
    }
    case 'generic_type': {
      // `Box<String>` → recurse into the base type (`Box`).
      const first = node.firstNamedChild;
      return first === null ? null : javaBaseLookupNameNode(first);
    }
    case 'annotated_type': {
      // The final named child is the base type; preceding children are annotations.
      const type = node.lastNamedChild;
      return type === null ? null : javaBaseLookupNameNode(type);
    }
    default:
      return null;
  }
}

function resolveVarTypeBindings(matches: CaptureMatch[]): CaptureMatch[] {
  const returnTypes = new Map<string, string>();
  const varTypes = new Map<string, string>();
  const ambiguousReturns = new Set<string>();
  const ambiguousVars = new Set<string>();

  for (const m of matches) {
    if (
      m['@type-binding.return'] !== undefined &&
      m['@type-binding.type'] !== undefined &&
      m['@type-binding.name'] !== undefined
    ) {
      const name = m['@type-binding.name'].text;
      const type = m['@type-binding.type'].text;
      const existing = returnTypes.get(name);
      if (existing !== undefined && existing !== type) {
        ambiguousReturns.add(name);
        returnTypes.delete(name);
      } else if (!ambiguousReturns.has(name)) {
        returnTypes.set(name, type);
      }
    }
    if (
      m['@type-binding.annotation'] !== undefined &&
      m['@type-binding.type'] !== undefined &&
      m['@type-binding.name'] !== undefined
    ) {
      const name = m['@type-binding.name'].text;
      const t = m['@type-binding.type'].text;
      if (t !== 'var') {
        const existing = varTypes.get(name);
        if (existing !== undefined && existing !== t) {
          ambiguousVars.add(name);
          varTypes.delete(name);
        } else if (!ambiguousVars.has(name)) {
          varTypes.set(name, t);
        }
      }
    }
    if (
      m['@type-binding.constructor'] !== undefined &&
      m['@type-binding.type'] !== undefined &&
      m['@type-binding.name'] !== undefined
    ) {
      const name = m['@type-binding.name'].text;
      const type = m['@type-binding.type'].text;
      const existing = varTypes.get(name);
      if (existing !== undefined && existing !== type) {
        ambiguousVars.add(name);
        varTypes.delete(name);
      } else if (!ambiguousVars.has(name)) {
        varTypes.set(name, type);
      }
    }
  }

  const resolved: CaptureMatch[] = [];
  for (const m of matches) {
    if (m['@type-binding.call-result'] !== undefined && m['@type-binding.type'] !== undefined) {
      const methodName = m['@type-binding.type'].text;
      const resolvedType = returnTypes.get(methodName);
      if (resolvedType !== undefined) {
        const patched: Record<string, Capture> = { ...m };
        patched['@type-binding.type'] = { ...m['@type-binding.type']!, text: resolvedType };
        patched['@type-binding.annotation'] = m['@type-binding.call-result']!;
        delete patched['@type-binding.call-result'];
        resolved.push(patched);
        continue;
      }
    }
    if (m['@type-binding.alias'] !== undefined && m['@type-binding.type'] !== undefined) {
      const sourceName = m['@type-binding.type'].text;
      const resolvedType = varTypes.get(sourceName);
      if (resolvedType !== undefined) {
        const patched: Record<string, Capture> = { ...m };
        patched['@type-binding.type'] = { ...m['@type-binding.type']!, text: resolvedType };
        patched['@type-binding.annotation'] = m['@type-binding.alias']!;
        delete patched['@type-binding.alias'];
        resolved.push(patched);
        continue;
      }
    }
    if (m['@reference.arg-names'] !== undefined && m['@reference.parameter-types'] !== undefined) {
      try {
        const types: string[] = JSON.parse(m['@reference.parameter-types'].text);
        const names: string[] = JSON.parse(m['@reference.arg-names'].text);
        let patched = false;
        for (let i = 0; i < types.length; i++) {
          if (types[i] === '' && names[i] !== undefined && names[i] !== '') {
            const rt = varTypes.get(names[i]!);
            if (rt !== undefined) {
              types[i] = rt;
              patched = true;
            }
          }
        }
        if (patched) {
          const patchedMatch: Record<string, Capture> = { ...m };
          patchedMatch['@reference.parameter-types'] = {
            ...m['@reference.parameter-types']!,
            text: JSON.stringify(types),
          };
          delete patchedMatch['@reference.arg-names'];
          resolved.push(patchedMatch);
          continue;
        }
      } catch {
        // pass through
      }
    }
    resolved.push(m);
  }
  return resolved;
}

type SyntaxNode = ReturnType<ReturnType<typeof getJavaParser>['parse']>['rootNode'];

/** Infer a Java argument's static type from literal patterns. */
function inferArgType(argNode: SyntaxNode): string {
  switch (argNode.type) {
    case 'decimal_integer_literal':
    case 'hex_integer_literal':
    case 'octal_integer_literal':
    case 'binary_integer_literal':
      return 'int';
    case 'decimal_floating_point_literal':
    case 'hex_floating_point_literal':
      return 'double';
    case 'string_literal':
      return 'String';
    case 'character_literal':
      return 'char';
    case 'true':
    case 'false':
      return 'boolean';
    case 'null_literal':
      return 'null';
    case 'object_creation_expression': {
      const typeNode = argNode.childForFieldName('type');
      return typeNode?.text ?? '';
    }
    default:
      return '';
  }
}

/**
 * Resolve a Java function-like node from a query-captured node.
 *
 * The `@scope.function` / `@declaration.method` / `@declaration.constructor`
 * anchors all capture directly on the `method_declaration` /
 * `constructor_declaration` node (per JAVA_SCOPE_QUERY), so this is a type
 * guard against `FUNCTION_NODE_TYPES` — the threaded-node equivalent of the
 * old `findNodeAtRange(tree.rootNode, range, type)` root-walk, minus the
 * O(matches × rootChildren) traversal.
 */
function findFunctionNode(node: SyntaxNode | undefined): SyntaxNode | null {
  return nodeIfType(node, ...FUNCTION_NODE_TYPES);
}
