/**
 * Language Provider interface — the complete capability contract for a supported language.
 *
 * Each language implements this interface in a single file under `languages/`.
 * The pipeline accesses all per-language behavior through this interface.
 *
 * Design pattern: Strategy pattern with compile-time exhaustiveness.
 * The providers table in `languages/index.ts` uses `satisfies Record<SupportedLanguages, LanguageProvider>`
 * so adding a language to the enum without creating a provider is a compiler error.
 */

import type {
  SupportedLanguages,
  MroStrategy,
  CaptureMatch,
  BindingRef,
  TypeRef,
  Scope,
  ScopeId,
  ScopeKind,
  ScopeTree,
  ParsedImport,
  ParsedTypeBinding,
  SymbolDefinition,
  Callsite,
  WorkspaceIndex,
} from 'gitnexus-shared';
import type { LanguageTypeConfig } from './type-extractors/types.js';
import type { CallRouter } from './call-routing.js';
import type { CallExtractor } from './call-types.js';
import type { ClassExtractor } from './class-types.js';
import type { ExportChecker } from './export-detection.js';
import type { FieldExtractor } from './field-extractor.js';
import type { MethodExtractor } from './method-types.js';
import type { VariableExtractor } from './variable-types.js';
import type { ImportResolverFn } from './import-resolvers/types.js';
import type { SyntaxNode } from './utils/ast-helpers.js';
import type { NodeLabel } from 'gitnexus-shared';

// ── Shared type aliases ────────────────────────────────────────────────────
/** Tree-sitter query captures: capture name → AST node (or undefined if not captured). */
export type CaptureMap = Record<string, SyntaxNode | undefined>;

// ── Strategy tag types ─────────────────────────────────────────────────────
// NOTE: `MroStrategy` is defined in `gitnexus-shared` and re-exported above
// so `core/ingestion/model/resolve.ts` can consume it without importing from
// this file (which would pull in the full language-registry dependency graph).

/** Configuration for AST-based framework detection patterns. */
export interface AstFrameworkPatternConfig {
  framework: string;
  entryPointMultiplier: number;
  reason: string;
  patterns: string[];
}

/**
 * Everything a language needs to provide.
 * Required fields must be explicitly set; optional fields have defaults
 * applied by defineLanguage().
 */
interface LanguageProviderConfig {
  // ── Identity ──────────────────────────────────────────────────────
  readonly id: SupportedLanguages;
  /** File extensions that map to this language (e.g., ['.ts', '.tsx']) */
  readonly extensions: readonly string[];

  /** Entry-point function name patterns specific to this language.
   *  Merged with universal patterns at runtime for process detection scoring.
   *  Default: [] (only universal patterns apply). */
  readonly entryPointPatterns?: readonly RegExp[];

  /** AST-based framework detection patterns for this language.
   *  Used by detectFrameworkFromAST to identify framework entry points.
   *  Default: [] (no AST framework detection for this language). */
  readonly astFrameworkPatterns?: readonly AstFrameworkPatternConfig[];

  // ── Parser ────────────────────────────────────────────────────────
  /** Parse strategy: 'tree-sitter' (default) uses AST parsing via tree-sitter.
   *  'standalone' means the language has its own regex-based processor and
   *  should be skipped by the tree-sitter pipeline (e.g., COBOL, Markdown). */
  readonly parseStrategy?: 'tree-sitter' | 'standalone';
  /** Tree-sitter query strings for definitions, imports, calls, heritage.
   *  Required for tree-sitter languages; empty string for standalone processors. */
  readonly treeSitterQueries: string;

  /**
   * Optional source-text transform that runs **before** tree-sitter parses the file.
   *
   * Used to elide language constructs that confuse the grammar without affecting
   * source-position fidelity — e.g., Unreal Engine reflection macros (`UCLASS`,
   * `UFUNCTION`, `MODULENAME_API`) in C++ headers that prevent the parser from
   * recognising class/function names correctly.
   *
   * **Length / position preservation:** the returned string MUST have the same
   * JavaScript `.length` as the input AND preserve every newline (`\n`/`\r`)
   * position byte-for-byte. Implementations replace elided characters with
   * ASCII spaces while leaving newlines untouched. With this contract:
   *
   *   - tree-sitter's reported `startPosition.row`/`startPosition.column`
   *     match the original file exactly (line/column come from newline counts)
   *   - `startIndex`/`endIndex` byte offsets match the original file exactly
   *     **when the elided range is pure ASCII** (UTF-16 `.length` equals UTF-8
   *     byte length only for ASCII).
   *
   * Implementations targeting languages where elided ranges may contain
   * non-ASCII content must therefore preserve byte length, not just `.length`,
   * if downstream code uses `startIndex` to slice the original UTF-8 bytes.
   * The current C++ UE-macro preprocessor relies on the practical fact that
   * UE reflection macros and module-export tokens are ASCII-only.
   *
   * Must be a pure function — same input always yields the same output. Called
   * once per file, on every code path that re-parses (parsing-processor, import
   * processor, heritage processor, call processor, parse worker).
   *
   * Default: undefined (no preprocessing — `file.content` is parsed verbatim).
   */
  readonly preprocessSource?: (sourceText: string, filePath: string) => string;

  // ── Core (required) ───────────────────────────────────────────────
  /** Type extraction: declarations, initializers, for-loop bindings */
  readonly typeConfig: LanguageTypeConfig;
  /** Export detection: is this AST node a public/exported symbol? */
  readonly exportChecker: ExportChecker;
  /** Import resolution: resolves raw import path to file system path */
  readonly importResolver: ImportResolverFn;

  // ── Calls & Imports (optional) ────────────────────────────────────
  /** Call routing for languages that express imports/heritage as calls (e.g., Ruby).
   *  Default: no routing (all calls are normal call expressions). */
  readonly callRouter?: CallRouter;
  /** Language-specific transformation of raw import path text before resolution.
   *  Called after sanitization. E.g., Kotlin appends wildcard suffixes.
   *  Default: undefined (no preprocessing). */
  readonly importPathPreprocessor?: (cleaned: string, importNode: SyntaxNode) => string;

  // ── Enclosing owner resolution ─────────────────────────────────
  /** Resolve a container node during enclosing-owner tree walks.
   *  Called when a CLASS_CONTAINER_TYPES node is found while walking up.
   *  - Return a different SyntaxNode to remap the container (e.g., Ruby
   *    singleton_class → enclosing class/module).
   *  - Return null to skip this container and keep walking up.
   *  - Omit (undefined) to use the container node as-is (default).
   *  Default: undefined (no remapping). */
  readonly resolveEnclosingOwner?: (node: SyntaxNode) => SyntaxNode | null;

  // ── Enclosing function resolution ───────────────────────────────
  /** Resolve the enclosing function name + label from an AST ancestor node
   *  that is NOT a standard FUNCTION_NODE_TYPE.  For languages where the
   *  function body is a sibling of the signature (e.g. Dart: function_body ↔
   *  function_signature are siblings under program/class_body), the default
   *  parent walk cannot find the enclosing function.  This hook lets the
   *  language provider inspect each ancestor and return the resolved result.
   *  Return null to continue the default walk.
   *  Default: undefined (standard parent walk only). */
  readonly enclosingFunctionFinder?: (
    ancestorNode: SyntaxNode,
  ) => { funcName: string; label: NodeLabel } | null;

  // ── Template constraint extraction (SFINAE / `requires`) ────────────
  /**
   * Extract a per-language template-constraint payload for a templated
   * function / method definition. Used by `parsing-processor` to
   * disambiguate same-name same-arity overloads whose distinguishing
   * signal is their template constraints rather than their parameter
   * types — the canonical C++ SFINAE case (issue #1579):
   *
   *   template<class T, std::enable_if_t<is_integral_v<T>, int> = 0>
   *   void process(T);   // overload A
   *
   *   template<class T, std::enable_if_t<is_floating_point_v<T>, int> = 0>
   *   void process(T);   // overload B
   *
   * Both overloads' `parameterTypes` collapse to `['T']`, so without a
   * constraint fingerprint in the graph node ID they merge into one
   * Function node and the resolver only ever sees one candidate to
   * narrow. The hook's return value is stamped onto the node's ID via
   * `templateConstraintsIdTag()` AND stored on the node's
   * `templateConstraints` property so `resolveDefGraphId` can look up
   * the right overload by re-hashing the def's constraints at resolve
   * time.
   *
   * Returns the opaque payload (any JSON-serializable shape — the
   * producing adapter owns it; shared code MUST NOT inspect) or
   * `undefined` when no constraints exist / the node isn't a templated
   * function. Languages without SFINAE / concept semantics leave this
   * undefined and the disambiguation is a pass-through.
   */
  readonly extractTemplateConstraints?: (definitionNode: SyntaxNode) => unknown;

  // ── Labels ────────────────────────────────────────────────────────
  /** Override the default node label for definition.function captures.
   *  Return null to skip (C/C++ duplicate), a different label to reclassify
   *  (e.g., 'Method' for Kotlin), or defaultLabel to keep as-is.
   *  Default: undefined (standard label assignment). */
  readonly labelOverride?: (functionNode: SyntaxNode, defaultLabel: NodeLabel) => NodeLabel | null;

  // ── MRO ───────────────────────────────────────────────────────────
  /** MRO strategy for multiple inheritance resolution.
   *  Default: 'first-wins'. */
  readonly mroStrategy?: MroStrategy;

  // ── Language-specific extraction hooks ────────────────────────────
  /** Call extractor for extracting call site information (calledName, callForm,
   *  receiverName, argCount, mixed chains) from @call / @call.name captures.
   *  Produced by createCallExtractor() with a per-language CallExtractionConfig.
   *  Default: undefined — if unset, no calls are extracted for this language.
   *  All tree-sitter providers MUST supply this. */
  readonly callExtractor?: CallExtractor;
  /** Field extractor for extracting field/property definitions from class/struct
   *  declarations. Produces FieldInfo[] with name, type, visibility, static,
   *  readonly metadata. Default: undefined (no field extraction). */
  readonly fieldExtractor?: FieldExtractor;
  /** Method extractor for extracting method/function definitions from class/struct/interface
   *  declarations. Produces MethodInfo[] with name, parameters, visibility, isAbstract,
   *  isFinal, annotations metadata. Default: undefined (no method extraction). */
  readonly methodExtractor?: MethodExtractor;
  /** Variable extractor for extracting metadata from module/file-scoped variable,
   *  constant, and static declarations. Produces VariableInfo with type, visibility,
   *  isConst, isStatic, isMutable metadata. Default: undefined (no variable extraction). */
  readonly variableExtractor?: VariableExtractor;
  /** Class/type extractor for deriving canonical qualified names for class-like symbols.
   *  Uses the same provider-driven strategy pattern as method/field extraction so
   *  namespace/package/module rules stay language-specific. */
  readonly classExtractor?: ClassExtractor;
  /** Extract a semantic description for a definition node (e.g., PHP Eloquent
   *  property arrays, relation method descriptions).
   *  Default: undefined (no description extraction). */
  readonly descriptionExtractor?: (
    nodeLabel: NodeLabel,
    nodeName: string,
    captureMap: CaptureMap,
  ) => string | undefined;
  /** Detect if a file contains framework route definitions (e.g., Laravel routes.php).
   *  When true, the worker extracts routes via the language's route extraction logic.
   *  Default: undefined (no route files). */
  readonly isRouteFile?: (filePath: string) => boolean;

  // ── Noise filtering ────────────────────────────────────────────────
  /** Built-in/stdlib names that should be filtered from the call graph for this language.
   *  Default: undefined (no language-specific filtering). */
  readonly builtInNames?: ReadonlySet<string>;

  // ══════════════════════════════════════════════════════════════════════════
  //  Scope-based resolution hooks (RFC #909 — Ring 1 #911)
  //
  //  All hooks below are OPTIONAL with safe defaults so existing providers
  //  continue to compile unchanged. Ring 2 (#919–#925) wires these into the
  //  central `ScopeExtractor` + finalize pipeline; Ring 3 per-language
  //  tickets implement the ones each language needs.
  //
  //  See: https://www.notion.so/346dc50b6ed281cfaacbe480bf231d50 §5.2
  // ══════════════════════════════════════════════════════════════════════════

  // ── Parse phase (per-capture interpretation) ───────────────────────

  /**
   * Emit scope captures from raw source, **pre-grouped per tree-sitter
   * query match**. Tree-sitter-based providers run a scope query
   * (embedded as a string constant in each language's `query.ts`) and
   * emit one `CaptureMatch` per query match; standalone providers
   * (COBOL) emit matches from a regex tagger. The return shape is
   * parser-agnostic: the central `ScopeExtractor` consumes
   * `CaptureMatch[]` without knowing which parser produced them.
   *
   * **Pre-grouping is the provider's job.** The extractor expects each
   * `CaptureMatch` to correspond to one logical match — e.g., an import
   * statement match carries `@import.statement` + `@import.source` +
   * `@import.name` keyed under their capture names. Providers MUST
   * preserve the tree-sitter match boundaries so the extractor's topic
   * routing (scope / declaration / import / type-binding / reference)
   * lands on coherent records.
   *
   * Required for any provider participating in scope-based resolution
   * (the sole resolution path).
   *
   * **Sync return.** Tree-sitter query execution and COBOL's regex
   * tagger are both synchronous; no current or foreseeable provider
   * needs async work inside this hook. The sync signature lets
   * `parse-worker.ts` (#920) invoke it inline in its already-sync
   * per-file loop without cascading `async` through the batch pipeline.
   *
   * Default: undefined (no scope-based captures emitted for this language).
   */
  readonly emitScopeCaptures?: (
    sourceText: string,
    filePath: string,
    /**
     * Optional pre-parsed tree-sitter Tree the caller has already
     * produced (e.g. from the parse phase's AST cache). When supplied,
     * the provider SHOULD skip its own `parser.parse(sourceText)` and
     * run its capture query against the supplied tree directly. Typed
     * as `unknown` here to avoid leaking the tree-sitter dependency
     * into the provider contract — the provider casts at use site.
     * Cache miss (parameter omitted or undefined) is always safe and
     * MUST trigger a fresh parse.
     */
    cachedTree?: unknown,
    /**
     * Optional metadata about how `sourceText` was produced.
     *
     * Most providers ignore this and treat `sourceText` as full file content.
     * Vue uses it to distinguish:
     *   - `full-file`: full `.vue` SFC source
     *   - `pre-extracted-script`: worker-preprocessed bare `<script>` content
     *
     * Default: `{ sourceKind: 'full-file' }`.
     */
    sourceMeta?: {
      readonly sourceKind?: 'full-file' | 'pre-extracted-script';
    },
  ) => readonly CaptureMatch[];

  /**
   * Snapshot the capture-time side-channel state that this provider's
   * `emitScopeCaptures` just populated for `filePath` into module-level maps,
   * returning a plain JSON-serializable value (or `undefined` when there is
   * nothing to carry).
   *
   * Called in the parse worker IMMEDIATELY after `emitScopeCaptures` runs for
   * a file (see `parse-worker.ts`), and the result is stored on the produced
   * `ParsedFile.captureSideChannel`. Scope-resolution on the main thread reuses
   * that serialized `ParsedFile` and skips re-extraction (#1983), so this hook
   * is how the worker-computed marks survive the worker→main boundary and the
   * disk store WITHOUT a main-thread re-parse. The main thread restores them
   * via the matching `ScopeResolver.applyCaptureSideChannel` hook.
   *
   * MUST return plain data (objects / arrays / primitives) so it round-trips
   * through `JSON.stringify` + the parsedfile-store interning reviver.
   *
   * Default: undefined (provider has no capture-time module-level side effects).
   */
  readonly collectCaptureSideChannel?: (filePath: string) => unknown;

  /**
   * Interpret a raw `@import.statement` capture group into a `ParsedImport`.
   * The central finalize algorithm resolves `ParsedImport.targetRaw` to a
   * concrete file via `resolveImportTarget` and materializes the final
   * `ImportEdge` with `targetModuleScope` / `targetDefId` filled in.
   *
   * Required when `emitScopeCaptures` is implemented.
   */
  readonly interpretImport?: (captures: CaptureMatch) => ParsedImport | null;

  /**
   * What is the implicit receiver on a Function scope? For instance methods
   * this is `self`/`this`; for standalone functions it is `null`. Consulted
   * by `Registry.lookup` Step 2 via the `resolveTypeRef` helper.
   *
   * Required for any language with method dispatch (OO semantics).
   *
   * Default: undefined (treated as `null` — no implicit receiver).
   */
  readonly receiverBinding?: (functionScope: Scope) => TypeRef | null;

  /**
   * Interpret a raw type-binding capture (parameter annotation, `self`,
   * assignment with constructor RHS, …) into a `ParsedTypeBinding`. The
   * central extractor attaches the resulting `TypeRef` to the appropriate
   * scope's `typeBindings` map.
   *
   * Default: undefined (falls back to `{ boundName: captures.name, rawTypeName: captures.type, source: 'annotation' }`).
   */
  readonly interpretTypeBinding?: (captures: CaptureMatch) => ParsedTypeBinding | null;

  /**
   * Override the `ScopeKind` assigned to a scope capture. Use when the
   * capture name alone can't resolve the kind (e.g., tree-sitter captures
   * a `block` that is semantically an `Expression` in this language).
   *
   * Default: undefined (the central extractor uses the capture name's
   * suffix — `@scope.function` → `'Function'`, etc.).
   */
  readonly resolveScopeKind?: (captures: CaptureMatch) => ScopeKind | null;

  /**
   * Override where a declaration's name becomes visible. By default the name
   * is bound in the innermost enclosing scope; return a different `ScopeId`
   * to hoist it (JS `var` → enclosing function scope; Ruby `def` inside
   * `begin` → enclosing class scope).
   *
   * Return `null` to delegate to the central default (innermost enclosing
   * scope). This matches the `X | null` convention used by the other optional
   * hooks and supports partial overrides — e.g., a JS provider can return a
   * hoisted scope for `var` declarations and `null` for `let`/`const`, without
   * re-implementing the default lookup.
   *
   * **Purity:** must be a pure function of its inputs — same parameters must
   * yield the same `ScopeId` (or `null`) across invocations. No closure over
   * mutable state. Required so scope-tree construction stays deterministic
   * across re-parses.
   *
   * Default: undefined (the central extractor uses `innermostScope.id`).
   */
  readonly bindingScopeFor?: (
    declCapture: CaptureMatch,
    innermostScope: Scope,
    scopeTree: ScopeTree,
  ) => ScopeId | null;

  // ── Finalize phase (cross-file + materialization) ──────────────────

  /**
   * Resolve a `ParsedImport.targetRaw` expression to a concrete file path in
   * the workspace. Language-specific resolution: Python relative imports,
   * JS package.json + node_modules, Go module paths, Java classpath,
   * COBOL COPY paths. Ports today's per-language import resolver.
   *
   * Required when `emitScopeCaptures` is implemented. Ring 2 PKG #922
   * provides the adapter that bridges today's resolver shape to this hook.
   */
  readonly resolveImportTarget?: (
    parsedImport: ParsedImport,
    workspaceIndex: WorkspaceIndex,
  ) => string | null;

  /**
   * Enumerate the exported names of a file — used by the finalize algorithm
   * to expand `import * from M` into individual `BindingRef`s with
   * `origin: 'wildcard'`.
   *
   * Default: undefined (central finalize walks the target file's
   * `ExportMap.keys()`).
   */
  readonly expandsWildcardTo?: (
    targetFile: string,
    workspaceIndex: WorkspaceIndex,
  ) => readonly string[];

  /**
   * Decide the scope to which a `ParsedImport` attaches. Most languages
   * attach imports to the nearest enclosing `Module`/`Namespace` scope
   * (the default); some languages allow local imports (Python function-local
   * `from x import Y`, Rust fn-local `use`, TS dynamic `import()`) — return
   * a `Function`/`Block` scope id instead.
   *
   * Return `null` to delegate to the central default (nearest enclosing
   * `Module`/`Namespace`). This matches the `X | null` convention used by
   * the other optional hooks and supports partial overrides — a provider
   * that handles only specific import forms non-standardly can `return null`
   * for the common cases and let the central walk handle them.
   *
   * **Purity:** must be a pure function of its inputs — same parameters must
   * yield the same `ScopeId` (or `null`) across invocations. No closure over
   * mutable state. Required so scope-tree construction stays deterministic
   * across re-parses.
   *
   * Default: undefined (central finalize walks to the nearest enclosing
   * `Module` or `Namespace` scope).
   */
  readonly importOwningScope?: (
    parsedImport: ParsedImport,
    innermostScope: Scope,
    scopeTree: ScopeTree,
  ) => ScopeId | null;

  /**
   * Merge local declarations and imported bindings for a single (scope, name)
   * during finalize materialization of a scope's binding table. Language-
   * specific precedence: Python local hides import; TypeScript namespace
   * merging keeps both; Ruby constant resolution has its own rules.
   *
   * Default: undefined (central finalize uses local-first-then-imports,
   * deduping by `DefId`).
   */
  readonly mergeBindings?: (scope: Scope, bindings: readonly BindingRef[]) => readonly BindingRef[];

  // ── Reference-extraction phase ─────────────────────────────────────

  /**
   * Classify a `@reference.call` capture as free / member / constructor /
   * index. Preferred path is declarative via capture sub-tags
   * (`@reference.call.free`, etc.); this hook handles the languages where
   * call form can't be decided statically (Ruby bare `foo(x)` is free-or-
   * member until resolved).
   *
   * Default: undefined (central extractor reads capture sub-tag if present;
   * else treats as `'free'`).
   */
  readonly classifyCallForm?: (
    captures: CaptureMatch,
    enclosingScope: Scope,
  ) => 'free' | 'member' | 'constructor' | 'index';

  // ── Resolution phase (RFC §4v2) ────────────────────────────────────

  /** Order same-name type candidates when a language can index multiple
   * definitions for one logical type. Return null to keep shared ambiguity
   * handling. */
  readonly orderSameNameTypeCandidates?: (params: {
    readonly typeName: string;
    readonly callSiteFilePath: string;
    readonly candidates: readonly SymbolDefinition[];
  }) => readonly SymbolDefinition[] | null;

  /**
   * Is this callable definition compatible with the given call-site arity?
   * Language-specific rules: Python `*args`/`**kwargs`/defaults, JS default
   * params + rest, Kotlin vararg + defaults, Ruby optional/splat/block, Go
   * straight counts, Rust no-variadic-no-defaults.
   *
   * `'incompatible'` is a soft penalty (−0.15 per EvidenceWeights) and is
   * filtered only when at least one `'compatible'` candidate exists;
   * otherwise the incompatible candidate is kept with the penalty so the
   * call-site still links to a best-guess target.
   *
   * Default: undefined (treated as `'unknown'` — no signal either way).
   */
  readonly arityCompatibility?: (
    def: SymbolDefinition,
    callsite: Callsite,
  ) => 'compatible' | 'unknown' | 'incompatible';
}

/** Runtime type — same as LanguageProviderConfig but with defaults guaranteed present. */
export interface LanguageProvider extends Omit<LanguageProviderConfig, 'mroStrategy'> {
  readonly mroStrategy: MroStrategy;
  /** Check if a name is a built-in/stdlib function that should be filtered from the call graph. */
  readonly isBuiltInName: (name: string) => boolean;
}

const DEFAULTS: Pick<LanguageProvider, 'mroStrategy'> = {
  mroStrategy: 'first-wins',
};

/** Define a language provider — required fields must be supplied, optional fields get sensible defaults. */
export function defineLanguage(config: LanguageProviderConfig): LanguageProvider {
  const builtIns = config.builtInNames;
  return {
    ...DEFAULTS,
    ...config,
    isBuiltInName: builtIns ? (name: string) => builtIns.has(name) : () => false,
  };
}
