import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import type { Tree } from 'web-tree-sitter';
import { Language, Parser, Query } from 'web-tree-sitter';
import { debug, warn } from '../infrastructure/logger.js';
import { getNative, getNativePackageVersion, loadNative } from '../infrastructure/native.js';
import { ParseError, toErrorMessage } from '../shared/errors.js';
import type {
  EngineMode,
  ExtractorOutput,
  LanguageId,
  LanguageRegistryEntry,
  TypeMapEntry,
} from '../types.js';
import { disposeWasmWorkerPool, getWasmWorkerPool } from './wasm-worker-pool.js';
import type { WorkerAnalysisOpts } from './wasm-worker-protocol.js';

/** Default worker opts: run all analyses so output matches parseFilesFull. */
const FULL_ANALYSIS: WorkerAnalysisOpts = {
  ast: true,
  complexity: true,
  cfg: true,
  dataflow: true,
};

/** Extract-only opts: skip visitor walk for typeMap backfill / similar fast paths. */
const EXTRACT_ONLY: WorkerAnalysisOpts = {
  ast: false,
  complexity: false,
  cfg: false,
  dataflow: false,
};

// Re-export all extractors for backward compatibility
export {
  extractBashSymbols,
  extractClojureSymbols,
  extractCppSymbols,
  extractCSharpSymbols,
  extractCSymbols,
  extractCudaSymbols,
  extractDartSymbols,
  extractElixirSymbols,
  extractErlangSymbols,
  extractFSharpSymbols,
  extractGleamSymbols,
  extractGoSymbols,
  extractGroovySymbols,
  extractHaskellSymbols,
  extractHCLSymbols,
  extractJavaSymbols,
  extractJuliaSymbols,
  extractKotlinSymbols,
  extractLuaSymbols,
  extractObjCSymbols,
  extractOCamlSymbols,
  extractPHPSymbols,
  extractPythonSymbols,
  extractRSymbols,
  extractRubySymbols,
  extractRustSymbols,
  extractScalaSymbols,
  extractSoliditySymbols,
  extractSwiftSymbols,
  extractSymbols,
  extractVerilogSymbols,
  extractZigSymbols,
} from '../extractors/index.js';

import {
  extractBashSymbols,
  extractClojureSymbols,
  extractCppSymbols,
  extractCSharpSymbols,
  extractCSymbols,
  extractCudaSymbols,
  extractDartSymbols,
  extractElixirSymbols,
  extractErlangSymbols,
  extractFSharpSymbols,
  extractGleamSymbols,
  extractGoSymbols,
  extractGroovySymbols,
  extractHaskellSymbols,
  extractHCLSymbols,
  extractJavaSymbols,
  extractJuliaSymbols,
  extractKotlinSymbols,
  extractLuaSymbols,
  extractObjCSymbols,
  extractOCamlSymbols,
  extractPHPSymbols,
  extractPythonSymbols,
  extractRSymbols,
  extractRubySymbols,
  extractRustSymbols,
  extractScalaSymbols,
  extractSoliditySymbols,
  extractSwiftSymbols,
  extractSymbols,
  extractVerilogSymbols,
  extractZigSymbols,
} from '../extractors/index.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

function grammarPath(name: string): string {
  return path.join(__dirname, '..', '..', 'grammars', name);
}

let _initialized: boolean = false;

// Memoized parsers — avoids reloading WASM grammars on every createParsers() call
let _cachedParsers: Map<string, Parser | null> | null = null;

// Cached Language objects — WASM-backed, must be .delete()'d explicitly
let _cachedLanguages: Map<string, Language> | null = null;

// Query cache for JS/TS/TSX extractors (populated during createParsers)
const _queryCache: Map<string, Query> = new Map();

// Tracks whether ALL grammars have been loaded (vs. a lazy subset)
let _allParsersLoaded: boolean = false;

// In-flight grammar loads keyed by language id — prevents concurrent duplicate loads
const _loadingPromises: Map<string, Promise<void>> = new Map();

// Extensions that need typeMap backfill (type annotations only exist in TS/TSX)
const TS_BACKFILL_EXTS = new Set(['.ts', '.tsx']);

// Re-export for backward compatibility
export type { LanguageRegistryEntry } from '../types.js';

interface ParseEngineOpts {
  engine?: EngineMode;
  dataflow?: boolean;
  ast?: boolean;
}

interface ResolvedEngine {
  name: 'native' | 'wasm';
  native: any;
}

interface WasmExtractResult {
  symbols: ExtractorOutput;
  tree: Tree;
  langId: LanguageId;
}

// Shared patterns for all JS/TS/TSX (class_declaration excluded — name type differs)
const COMMON_QUERY_PATTERNS: string[] = [
  '(function_declaration name: (identifier) @fn_name) @fn_node',
  '(variable_declarator name: (identifier) @varfn_name value: (arrow_function) @varfn_value)',
  '(variable_declarator name: (identifier) @varfn_name value: (function_expression) @varfn_value)',
  '(method_definition name: (property_identifier) @meth_name) @meth_node',
  '(method_definition name: (private_property_identifier) @meth_name) @meth_node',
  '(import_statement source: (string) @imp_source) @imp_node',
  '(export_statement) @exp_node',
  '(call_expression function: (identifier) @callfn_name) @callfn_node',
  '(call_expression function: (member_expression) @callmem_fn) @callmem_node',
  '(call_expression function: (subscript_expression) @callsub_fn) @callsub_node',
  '(new_expression constructor: (identifier) @newfn_name) @newfn_node',
  '(new_expression constructor: (member_expression) @newmem_fn) @newmem_node',
  '(expression_statement (assignment_expression left: (member_expression) @assign_left right: (_) @assign_right)) @assign_node',
];

// JS: class name is (identifier)
const JS_CLASS_PATTERN: string = '(class_declaration name: (identifier) @cls_name) @cls_node';

// TS/TSX: class name is (type_identifier), plus interface and type alias
const TS_EXTRA_PATTERNS: string[] = [
  '(class_declaration name: (type_identifier) @cls_name) @cls_node',
  '(interface_declaration name: (type_identifier) @iface_name) @iface_node',
  '(type_alias_declaration name: (type_identifier) @type_name) @type_node',
];

/**
 * Load a single language grammar and cache the parser + language + query.
 * Uses in-flight deduplication so concurrent callers awaiting the same grammar
 * share a single load rather than producing orphaned WASM instances.
 * Assumes Parser.init() has already been called and _cachedParsers/_cachedLanguages exist.
 */
async function loadLanguage(entry: LanguageRegistryEntry): Promise<void> {
  if (_cachedParsers!.has(entry.id)) return;
  const inflight = _loadingPromises.get(entry.id);
  if (inflight) return inflight;
  const p = doLoadLanguage(entry).finally(() => _loadingPromises.delete(entry.id));
  _loadingPromises.set(entry.id, p);
  return p;
}

async function doLoadLanguage(entry: LanguageRegistryEntry): Promise<void> {
  try {
    const lang = await Language.load(grammarPath(entry.grammarFile));
    const parser = new Parser();
    parser.setLanguage(lang);
    _cachedParsers!.set(entry.id, parser);
    _cachedLanguages!.set(entry.id, lang);
    if (entry.extractor === extractSymbols && !_queryCache.has(entry.id)) {
      const isTS = entry.id === 'typescript' || entry.id === 'tsx';
      const patterns = isTS
        ? [...COMMON_QUERY_PATTERNS, ...TS_EXTRA_PATTERNS]
        : [...COMMON_QUERY_PATTERNS, JS_CLASS_PATTERN];
      _queryCache.set(entry.id, new Query(lang, patterns.join('\n')));
    }
  } catch (e: unknown) {
    if (entry.required)
      throw new ParseError(`Required parser ${entry.id} failed to initialize`, {
        file: entry.grammarFile,
        cause: e as Error,
      });
    warn(
      `${entry.id} parser failed to initialize: ${(e as Error).message}. ${entry.id} files will be skipped.`,
    );
    _cachedParsers!.set(entry.id, null);
  }
}

async function initParserRuntime(): Promise<void> {
  if (!_initialized) {
    await Parser.init();
    _initialized = true;
  }
  if (!_cachedParsers) _cachedParsers = new Map();
  if (!_cachedLanguages) _cachedLanguages = new Map();
}

/**
 * Load only the WASM grammars needed for the given file paths.
 * Grammars already in cache are reused. This avoids the ~500ms cold-start
 * penalty of loading all 23+ grammars when only 1-2 are needed (e.g. incremental rebuilds).
 */
async function ensureParsersForFiles(filePaths: string[]): Promise<Map<string, Parser | null>> {
  await initParserRuntime();
  const needed = new Set<LanguageRegistryEntry>();
  for (const fp of filePaths) {
    const ext = path.extname(fp).toLowerCase();
    const entry = _extToLang.get(ext);
    if (entry && !_cachedParsers!.has(entry.id)) needed.add(entry);
  }
  for (const entry of needed) {
    await loadLanguage(entry);
  }
  return _cachedParsers!;
}

/**
 * Load ALL WASM grammars. Used by full builds and feature modules (CFG, dataflow, complexity)
 * that may process files of any language.
 */
export async function createParsers(): Promise<Map<string, Parser | null>> {
  if (_cachedParsers && _allParsersLoaded) return _cachedParsers;

  await initParserRuntime();
  for (const entry of LANGUAGE_REGISTRY) {
    if (!_cachedParsers!.has(entry.id)) {
      await loadLanguage(entry);
    }
  }
  _allParsersLoaded = true;
  return _cachedParsers!;
}

/**
 * Dispose all cached WASM parsers and queries to free WASM linear memory.
 * Call this between repeated builds in the same process (e.g. benchmarks)
 * to prevent memory accumulation that can cause segfaults.
 */
function disposeMapEntries(entries: Iterable<[string, any]>, label: string): void {
  for (const [id, item] of entries) {
    if (item && typeof item.delete === 'function') {
      try {
        item.delete();
      } catch (e: unknown) {
        debug(`Failed to dispose ${label} ${id}: ${(e as Error).message}`);
      }
    }
  }
}

export async function disposeParsers(): Promise<void> {
  if (_cachedParsers) {
    disposeMapEntries(_cachedParsers, 'parser');
    _cachedParsers = null;
  }
  disposeMapEntries(_queryCache, 'query');
  _queryCache.clear();
  if (_cachedLanguages) {
    disposeMapEntries(_cachedLanguages, 'language');
    _cachedLanguages = null;
  }
  _initialized = false;
  _allParsersLoaded = false;
  _loadingPromises.clear();
  await disposeWasmWorkerPool();
}

export function getParser(parsers: Map<string, Parser | null>, filePath: string): Parser | null {
  const ext = path.extname(filePath);
  const entry = _extToLang.get(ext);
  if (!entry) return null;
  return parsers.get(entry.id) || null;
}

/**
 * Backfill missing AST-analysis data (astNodes, dataflow, def.complexity,
 * def.cfg) via the WASM worker pool for files that were parsed by the native
 * engine but are missing one or more analyses.
 *
 * Historically this function populated `symbols._tree` so the main-thread
 * visitor walk in `ast-analysis/engine.ts` could run. After the worker-isolation
 * refactor (#965), the worker runs every visitor itself and returns pre-computed
 * analysis data — `_tree` is never set on the main thread.
 *
 * Name is preserved for caller compatibility; the function now ensures
 * *analysis data* rather than *trees*.
 *
 * `needsFn` (optional): when provided, only files for which it returns true are
 * re-parsed. Without it the function falls back to "any WASM-parseable file
 * without _tree", which was the source of #1036 — a single file missing one
 * analysis triggered a full-build re-parse of every WASM-parseable file.
 */
export async function ensureWasmTrees(
  fileSymbols: Map<string, any>,
  rootDir: string,
  needsFn?: (relPath: string, symbols: any) => boolean,
): Promise<void> {
  // Collect files that still need analysis data and are parseable by WASM.
  const pending: Array<{ relPath: string; absPath: string; symbols: any }> = [];
  for (const [relPath, symbols] of fileSymbols) {
    if (symbols._tree) continue; // legacy path — leave existing trees alone
    if (!_extToLang.has(path.extname(relPath).toLowerCase())) continue;
    if (needsFn && !needsFn(relPath, symbols)) continue;
    pending.push({ relPath, absPath: path.join(rootDir, relPath), symbols });
  }
  if (pending.length === 0) return;

  const pool = getWasmWorkerPool();
  for (const { relPath, absPath, symbols } of pending) {
    let code: string;
    try {
      code = fs.readFileSync(absPath, 'utf-8');
    } catch (e: unknown) {
      debug(`ensureWasmTrees: cannot read ${relPath}: ${(e as Error).message}`);
      continue;
    }
    const output = await pool.parse(absPath, code, FULL_ANALYSIS);
    if (!output) continue; // worker crashed or returned null — skip silently
    mergeAnalysisData(symbols, output);
  }
}

/**
 * Merge pre-computed analysis data from a worker result onto existing symbols.
 * Only fills gaps — never overwrites fields the caller already populated.
 * Used to patch native-parsed symbols with worker-produced astNodes / dataflow /
 * per-definition complexity and cfg.
 */
function mergeAnalysisData(symbols: any, worker: ExtractorOutput): void {
  if (!symbols._langId && worker._langId) symbols._langId = worker._langId;
  if (!symbols._lineCount && worker._lineCount) symbols._lineCount = worker._lineCount;
  if (!Array.isArray(symbols.astNodes) && Array.isArray(worker.astNodes)) {
    symbols.astNodes = worker.astNodes;
  }
  if (!symbols.dataflow && worker.dataflow) symbols.dataflow = worker.dataflow;
  if (worker.typeMap && worker.typeMap.size > 0) {
    if (!symbols.typeMap || !(symbols.typeMap instanceof Map)) {
      symbols.typeMap = new Map(worker.typeMap);
    } else {
      for (const [k, v] of worker.typeMap) {
        if (!symbols.typeMap.has(k)) symbols.typeMap.set(k, v);
      }
    }
  }
  const existingDefs: any[] = Array.isArray(symbols.definitions) ? symbols.definitions : [];
  const workerDefs: any[] = Array.isArray(worker.definitions) ? worker.definitions : [];
  // Index existing defs by (kind, name, line) — mirrors engine.ts matching key.
  const byKey = new Map<string, any>();
  for (const d of existingDefs) byKey.set(`${d.kind}|${d.name}|${d.line}`, d);
  for (const wd of workerDefs) {
    const existing = byKey.get(`${wd.kind}|${wd.name}|${wd.line}`);
    if (!existing) continue;
    if (!existing.complexity && wd.complexity) existing.complexity = wd.complexity;
    if ((!existing.cfg || !Array.isArray(existing.cfg.blocks)) && wd.cfg?.blocks) {
      existing.cfg = wd.cfg;
    }
  }
}

/**
 * Check whether the required WASM grammar files exist on disk.
 */
export function isWasmAvailable(): boolean {
  return LANGUAGE_REGISTRY.filter((e) => e.required).every((e) =>
    fs.existsSync(grammarPath(e.grammarFile)),
  );
}

/**
 * Return the set of lowercase file extensions whose WASM grammar is actually
 * installed on disk. Used to scope engine-parity backfill to files that WASM
 * can recover — languages without an installed grammar are skipped by both
 * engines, so they don't represent a native-engine drop.
 *
 * Cached on first call; the grammars directory is shipped immutable.
 */
let _installedWasmExts: Set<string> | null = null;
export function getInstalledWasmExtensions(): Set<string> {
  if (_installedWasmExts) return _installedWasmExts;
  const exts = new Set<string>();
  for (const entry of LANGUAGE_REGISTRY) {
    if (fs.existsSync(grammarPath(entry.grammarFile))) {
      for (const ext of entry.extensions) exts.add(ext.toLowerCase());
    }
  }
  _installedWasmExts = exts;
  return exts;
}

/**
 * Lowercase file extensions covered by the native Rust addon.
 *
 * Mirrors `LanguageKind::from_extension` in
 * `crates/codegraph-core/src/parser_registry.rs`. Used to classify why the
 * native orchestrator dropped a file: extensions outside this set are a
 * legitimate parser limit (no Rust extractor exists), while extensions inside
 * it indicate a real native bug (parse/read/extract failure).
 *
 * Keep this list in sync with the Rust enum — the native addon is a separate
 * npm package, so JS has no runtime way to discover its language coverage.
 */
export const NATIVE_SUPPORTED_EXTENSIONS: ReadonlySet<string> = new Set([
  '.js',
  '.jsx',
  '.mjs',
  '.cjs',
  '.ts',
  '.tsx',
  '.py',
  '.pyi',
  '.tf',
  '.hcl',
  '.go',
  '.rs',
  '.java',
  '.cs',
  '.rb',
  '.rake',
  '.gemspec',
  '.php',
  '.phtml',
  '.c',
  '.h',
  '.cpp',
  '.cc',
  '.cxx',
  '.hpp',
  '.cu',
  '.cuh',
  '.kt',
  '.kts',
  '.swift',
  '.scala',
  '.sh',
  '.bash',
  '.ex',
  '.exs',
  '.lua',
  '.dart',
  '.zig',
  '.hs',
  '.ml',
  '.mli',
  '.fs',
  '.fsx',
  '.fsi',
  '.m',
  '.gleam',
  '.jl',
  '.clj',
  '.cljs',
  '.cljc',
  '.erl',
  '.hrl',
  '.groovy',
  '.gvy',
  '.r',
  '.sol',
  '.v',
  '.sv',
]);

/**
 * Classification for a file the native orchestrator dropped.
 * - `unsupported-by-native`: extension has no Rust extractor (legitimate parser limit).
 * - `native-extractor-failure`: extension is supported by native but the file was
 *   still dropped — points at a real bug (read error, parse failure, extractor crash).
 */
export type NativeDropReason = 'unsupported-by-native' | 'native-extractor-failure';

export interface NativeDropClassification {
  /** Per-reason → per-extension → list of relative paths that hit that bucket. */
  byReason: Record<NativeDropReason, Map<string, string[]>>;
  /** Total file count per reason. */
  totals: Record<NativeDropReason, number>;
}

/**
 * Group the missing files (relative paths) by drop reason and extension so the
 * caller can log per-extension counts and a sample path. Pure function — no
 * I/O, safe to unit-test independently of the build pipeline.
 */
export function classifyNativeDrops(relPaths: Iterable<string>): NativeDropClassification {
  const byReason: Record<NativeDropReason, Map<string, string[]>> = {
    'unsupported-by-native': new Map(),
    'native-extractor-failure': new Map(),
  };
  const totals: Record<NativeDropReason, number> = {
    'unsupported-by-native': 0,
    'native-extractor-failure': 0,
  };
  for (const rel of relPaths) {
    const ext = path.extname(rel).toLowerCase();
    const reason: NativeDropReason = NATIVE_SUPPORTED_EXTENSIONS.has(ext)
      ? 'native-extractor-failure'
      : 'unsupported-by-native';
    const bucket = byReason[reason];
    let list = bucket.get(ext);
    if (!list) {
      list = [];
      bucket.set(ext, list);
    }
    list.push(rel);
    totals[reason]++;
  }
  return { byReason, totals };
}

/**
 * Render `{ ext → paths[] }` as `ext (n: sample.ext, ...)` slices for log lines.
 * Caps at 3 sample paths per extension and 6 extensions total to keep warnings
 * readable when many languages are dropped at once. Extensions are sorted by
 * descending file count so the loudest offender shows up first; ties keep
 * insertion order. Pure function — safe to unit-test independently.
 */
export function formatDropExtensionSummary(buckets: Map<string, string[]>): string {
  const MAX_EXTS = 6;
  const MAX_SAMPLES = 3;
  const entries = Array.from(buckets.entries()).sort((a, b) => b[1].length - a[1].length);
  const shown = entries.slice(0, MAX_EXTS).map(([ext, paths]) => {
    const sample = paths.slice(0, MAX_SAMPLES).join(', ');
    const more = paths.length > MAX_SAMPLES ? `, +${paths.length - MAX_SAMPLES} more` : '';
    return `${ext} (${paths.length}: ${sample}${more})`;
  });
  if (entries.length > MAX_EXTS) {
    shown.push(`+${entries.length - MAX_EXTS} more extension(s)`);
  }
  return shown.join('; ');
}

// ── Unified API ──────────────────────────────────────────────────────────────

function resolveEngine(opts: ParseEngineOpts = {}): ResolvedEngine {
  const pref = opts.engine || 'auto';
  if (pref === 'wasm') return { name: 'wasm', native: null };
  if (pref === 'native' || pref === 'auto') {
    const native = loadNative();
    if (native) return { name: 'native', native };
    if (pref === 'native') {
      getNative(); // throws with detailed error + install instructions
    }
  }
  return { name: 'wasm', native: null };
}

/**
 * Patch native engine output in-place for the few remaining semantic transforms.
 * With #[napi(js_name)] on Rust types, most fields already arrive as camelCase.
 * This only handles:
 *  - _lineCount compat for builder.js
 *  - Backward compat for older native binaries missing js_name annotations
 *  - dataflow argFlows/mutations bindingType -> binding wrapper
 */
/** Patch definition fields for backward compat with older native binaries. */
function patchDefinitions(definitions: any[]): void {
  for (const d of definitions) {
    if (d.endLine === undefined && d.end_line !== undefined) {
      d.endLine = d.end_line;
    }
  }
}

/** Patch import fields for backward compat with older native binaries. */
function patchImports(imports: any[]): void {
  for (const i of imports) {
    if (i.typeOnly === undefined) i.typeOnly = i.type_only;
    if (i.wildcardReexport === undefined) i.wildcardReexport = i.wildcard_reexport;
    if (i.pythonImport === undefined) i.pythonImport = i.python_import;
    if (i.goImport === undefined) i.goImport = i.go_import;
    if (i.rustUse === undefined) i.rustUse = i.rust_use;
    if (i.javaImport === undefined) i.javaImport = i.java_import;
    if (i.csharpUsing === undefined) i.csharpUsing = i.csharp_using;
    if (i.rubyRequire === undefined) i.rubyRequire = i.ruby_require;
    if (i.phpUse === undefined) i.phpUse = i.php_use;
    if (i.cInclude === undefined) i.cInclude = i.c_include;
    if (i.kotlinImport === undefined) i.kotlinImport = i.kotlin_import;
    if (i.swiftImport === undefined) i.swiftImport = i.swift_import;
    if (i.scalaImport === undefined) i.scalaImport = i.scala_import;
    if (i.bashSource === undefined) i.bashSource = i.bash_source;
    if (i.dynamicImport === undefined) i.dynamicImport = i.dynamic_import;
  }
}

/** Normalize native typeMap array to a Map instance.
 *  Uses first-wins semantics at equal confidence to match the WASM/JS extractor. */
function patchTypeMap(r: any): void {
  if (!r.typeMap) {
    r.typeMap = new Map();
  } else if (!(r.typeMap instanceof Map)) {
    const map = new Map<string, TypeMapEntry>();
    for (const e of r.typeMap as Array<{ name: string; typeName: string }>) {
      if (!map.has(e.name)) {
        map.set(e.name, { type: e.typeName, confidence: (e as any).confidence ?? 0.9 });
      }
    }
    r.typeMap = map;
  }
}

/** Wrap bindingType into binding object for dataflow argFlows and mutations. */
function patchDataflow(dataflow: any): void {
  if (dataflow.argFlows) {
    for (const f of dataflow.argFlows) {
      f.binding = f.bindingType ? { type: f.bindingType } : null;
    }
  }
  if (dataflow.mutations) {
    for (const m of dataflow.mutations) {
      m.binding = m.bindingType ? { type: m.bindingType } : null;
    }
  }
}

function patchNativeResult(r: any): ExtractorOutput {
  // lineCount: napi(js_name) emits "lineCount"; older binaries may emit "line_count"
  r.lineCount = r.lineCount ?? r.line_count ?? null;
  r._lineCount = r.lineCount;

  if (r.definitions) patchDefinitions(r.definitions);
  if (r.imports) patchImports(r.imports);
  patchTypeMap(r);
  if (r.dataflow) patchDataflow(r.dataflow);

  return r;
}

/**
 * Declarative registry of all supported languages.
 * Adding a new language requires only a new entry here + its extractor function.
 */
export const LANGUAGE_REGISTRY: LanguageRegistryEntry[] = [
  {
    id: 'javascript',
    extensions: ['.js', '.jsx', '.mjs', '.cjs'],
    grammarFile: 'tree-sitter-javascript.wasm',
    extractor: extractSymbols,
    required: true,
  },
  {
    id: 'typescript',
    extensions: ['.ts'],
    grammarFile: 'tree-sitter-typescript.wasm',
    extractor: extractSymbols,
    required: true,
  },
  {
    id: 'tsx',
    extensions: ['.tsx'],
    grammarFile: 'tree-sitter-tsx.wasm',
    extractor: extractSymbols,
    required: true,
  },
  {
    id: 'hcl',
    extensions: ['.tf', '.hcl'],
    grammarFile: 'tree-sitter-hcl.wasm',
    extractor: extractHCLSymbols,
    required: false,
  },
  {
    id: 'python',
    extensions: ['.py', '.pyi'],
    grammarFile: 'tree-sitter-python.wasm',
    extractor: extractPythonSymbols,
    required: false,
  },
  {
    id: 'go',
    extensions: ['.go'],
    grammarFile: 'tree-sitter-go.wasm',
    extractor: extractGoSymbols,
    required: false,
  },
  {
    id: 'rust',
    extensions: ['.rs'],
    grammarFile: 'tree-sitter-rust.wasm',
    extractor: extractRustSymbols,
    required: false,
  },
  {
    id: 'java',
    extensions: ['.java'],
    grammarFile: 'tree-sitter-java.wasm',
    extractor: extractJavaSymbols,
    required: false,
  },
  {
    id: 'csharp',
    extensions: ['.cs'],
    grammarFile: 'tree-sitter-c_sharp.wasm',
    extractor: extractCSharpSymbols,
    required: false,
  },
  {
    id: 'ruby',
    extensions: ['.rb', '.rake', '.gemspec'],
    grammarFile: 'tree-sitter-ruby.wasm',
    extractor: extractRubySymbols,
    required: false,
  },
  {
    id: 'php',
    extensions: ['.php', '.phtml'],
    grammarFile: 'tree-sitter-php.wasm',
    extractor: extractPHPSymbols,
    required: false,
  },
  {
    id: 'c',
    extensions: ['.c', '.h'],
    grammarFile: 'tree-sitter-c.wasm',
    extractor: extractCSymbols,
    required: false,
  },
  {
    id: 'cpp',
    extensions: ['.cpp', '.cc', '.cxx', '.hpp'],
    grammarFile: 'tree-sitter-cpp.wasm',
    extractor: extractCppSymbols,
    required: false,
  },
  {
    id: 'kotlin',
    extensions: ['.kt', '.kts'],
    grammarFile: 'tree-sitter-kotlin.wasm',
    extractor: extractKotlinSymbols,
    required: false,
  },
  {
    id: 'swift',
    extensions: ['.swift'],
    grammarFile: 'tree-sitter-swift.wasm',
    extractor: extractSwiftSymbols,
    required: false,
  },
  {
    id: 'scala',
    extensions: ['.scala'],
    grammarFile: 'tree-sitter-scala.wasm',
    extractor: extractScalaSymbols,
    required: false,
  },
  {
    id: 'bash',
    extensions: ['.sh', '.bash'],
    grammarFile: 'tree-sitter-bash.wasm',
    extractor: extractBashSymbols,
    required: false,
  },
  {
    id: 'elixir',
    extensions: ['.ex', '.exs'],
    grammarFile: 'tree-sitter-elixir.wasm',
    extractor: extractElixirSymbols,
    required: false,
  },
  {
    id: 'lua',
    extensions: ['.lua'],
    grammarFile: 'tree-sitter-lua.wasm',
    extractor: extractLuaSymbols,
    required: false,
  },
  {
    id: 'dart',
    extensions: ['.dart'],
    grammarFile: 'tree-sitter-dart.wasm',
    extractor: extractDartSymbols,
    required: false,
  },
  {
    id: 'zig',
    extensions: ['.zig'],
    grammarFile: 'tree-sitter-zig.wasm',
    extractor: extractZigSymbols,
    required: false,
  },
  {
    id: 'haskell',
    extensions: ['.hs'],
    grammarFile: 'tree-sitter-haskell.wasm',
    extractor: extractHaskellSymbols,
    required: false,
  },
  {
    id: 'ocaml',
    extensions: ['.ml'],
    grammarFile: 'tree-sitter-ocaml.wasm',
    extractor: extractOCamlSymbols,
    required: false,
  },
  {
    id: 'ocaml-interface',
    extensions: ['.mli'],
    grammarFile: 'tree-sitter-ocaml_interface.wasm',
    extractor: extractOCamlSymbols,
    required: false,
  },
  {
    id: 'fsharp',
    extensions: ['.fs', '.fsx'],
    grammarFile: 'tree-sitter-fsharp.wasm',
    extractor: extractFSharpSymbols,
    required: false,
  },
  {
    id: 'fsharp-signature',
    extensions: ['.fsi'],
    grammarFile: 'tree-sitter-fsharp_signature.wasm',
    extractor: extractFSharpSymbols,
    required: false,
  },
  {
    id: 'gleam',
    extensions: ['.gleam'],
    grammarFile: 'tree-sitter-gleam.wasm',
    extractor: extractGleamSymbols,
    required: false,
  },
  {
    id: 'clojure',
    extensions: ['.clj', '.cljs', '.cljc'],
    grammarFile: 'tree-sitter-clojure.wasm',
    extractor: extractClojureSymbols,
    required: false,
  },
  {
    id: 'julia',
    extensions: ['.jl'],
    grammarFile: 'tree-sitter-julia.wasm',
    extractor: extractJuliaSymbols,
    required: false,
  },
  {
    id: 'r',
    extensions: ['.r', '.R'],
    grammarFile: 'tree-sitter-r.wasm',
    extractor: extractRSymbols,
    required: false,
  },
  {
    id: 'erlang',
    extensions: ['.erl', '.hrl'],
    grammarFile: 'tree-sitter-erlang.wasm',
    extractor: extractErlangSymbols,
    required: false,
  },
  {
    id: 'solidity',
    extensions: ['.sol'],
    grammarFile: 'tree-sitter-solidity.wasm',
    extractor: extractSoliditySymbols,
    required: false,
  },
  {
    id: 'objc',
    extensions: ['.m'],
    grammarFile: 'tree-sitter-objc.wasm',
    extractor: extractObjCSymbols,
    required: false,
  },
  {
    id: 'cuda',
    extensions: ['.cu', '.cuh'],
    grammarFile: 'tree-sitter-cuda.wasm',
    extractor: extractCudaSymbols,
    required: false,
  },
  {
    id: 'groovy',
    extensions: ['.groovy', '.gvy'],
    grammarFile: 'tree-sitter-groovy.wasm',
    extractor: extractGroovySymbols,
    required: false,
  },
  {
    id: 'verilog',
    extensions: ['.v', '.sv'],
    grammarFile: 'tree-sitter-verilog.wasm',
    extractor: extractVerilogSymbols,
    required: false,
  },
];

const _extToLang: Map<string, LanguageRegistryEntry> = new Map();
for (const entry of LANGUAGE_REGISTRY) {
  for (const ext of entry.extensions) {
    _extToLang.set(ext, entry);
  }
}

export const SUPPORTED_EXTENSIONS: Set<string> = new Set(_extToLang.keys());

/**
 * WASM-based typeMap backfill for TS/TSX files parsed by the native engine.
 * Serves two purposes:
 * 1. Compatibility with older native binaries that don't emit typeMap (< 3.2.0).
 * 2. Workaround for native parser scope-collision bugs — when the same variable
 *    name appears at multiple scopes, native type extraction can produce
 *    incorrect results. WASM's JS-based extractor handles scope traversal
 *    more accurately. TODO: Remove purpose (2) once the Rust extractor handles
 *    nested scopes correctly.
 *
 * Uses tree-sitter AST extraction instead of regex to avoid false positives from
 * matches inside comments and string literals.
 */
async function backfillTypeMap(
  filePath: string,
  source?: string,
): Promise<{ typeMap: Map<string, TypeMapEntry>; backfilled: boolean }> {
  let code = source;
  if (!code) {
    try {
      code = fs.readFileSync(filePath, 'utf-8');
    } catch (e) {
      debug(`backfillTypeMap: failed to read ${filePath}: ${toErrorMessage(e)}`);
      return { typeMap: new Map(), backfilled: false };
    }
  }
  const pool = getWasmWorkerPool();
  // Extract-only — no visitor walk, we only need the typeMap from this pass.
  const output = await pool.parse(filePath, code, EXTRACT_ONLY);
  if (!output || output.typeMap.size === 0) {
    return { typeMap: new Map(), backfilled: false };
  }
  return { typeMap: output.typeMap, backfilled: true };
}

/**
 * WASM extraction helper: picks the right extractor based on file extension.
 */
function wasmExtractSymbols(
  parsers: Map<string, Parser | null>,
  filePath: string,
  code: string,
): WasmExtractResult | null {
  const parser = getParser(parsers, filePath);
  if (!parser) return null;

  let tree: Tree | null;
  try {
    tree = parser.parse(code);
  } catch (e: unknown) {
    warn(`Parse error in ${filePath}: ${(e as Error).message}`);
    return null;
  }
  if (!tree) return null;

  const ext = path.extname(filePath);
  const entry = _extToLang.get(ext);
  if (!entry) return null;
  const query = _queryCache.get(entry.id) ?? undefined;
  // Query (web-tree-sitter) is structurally compatible with TreeSitterQuery at runtime
  let symbols: ExtractorOutput | null;
  try {
    symbols = entry.extractor(tree as any, filePath, query as any);
  } catch (e: unknown) {
    warn(`Extractor error in ${filePath}: ${(e as Error).message}`);
    // Free WASM tree to prevent memory leak — web-tree-sitter trees are backed
    // by WASM linear memory and are not garbage-collected automatically.
    if (typeof (tree as any).delete === 'function') (tree as any).delete();
    return null;
  }
  return symbols ? { symbols, tree, langId: entry.id } : null;
}

/**
 * Parse a single file and return normalized symbols.
 */
export async function parseFileAuto(
  filePath: string,
  source: string,
  opts: ParseEngineOpts = {},
): Promise<ExtractorOutput | null> {
  const { native } = resolveEngine(opts);

  if (native) {
    const result = native.parseFile(filePath, source, true, true);
    if (!result) return null;
    const patched = patchNativeResult(result);
    // Always backfill typeMap for TS/TSX from WASM — native parser's type
    // extraction can produce incorrect scope-collision results. Non-TS files
    // are skipped to stay consistent with the batch path (backfillTypeMapBatch).
    if (TS_BACKFILL_EXTS.has(path.extname(filePath))) {
      const { typeMap, backfilled } = await backfillTypeMap(filePath, source);
      if (backfilled) {
        patched.typeMap = typeMap;
        patched._typeMapBackfilled = true;
      }
    }
    return patched;
  }

  // WASM path — dispatch to isolated worker
  const pool = getWasmWorkerPool();
  return pool.parse(filePath, source, FULL_ANALYSIS);
}

/** Backfill typeMap via WASM for TS/TSX files parsed by the native engine. */
async function backfillTypeMapBatch(
  needsTypeMap: { filePath: string; relPath: string }[],
  result: Map<string, ExtractorOutput>,
): Promise<void> {
  const tsFiles = needsTypeMap.filter(({ filePath }) =>
    TS_BACKFILL_EXTS.has(path.extname(filePath)),
  );
  if (tsFiles.length === 0) return;

  const pool = getWasmWorkerPool();
  for (const { filePath, relPath } of tsFiles) {
    let code: string;
    try {
      code = fs.readFileSync(filePath, 'utf-8');
    } catch (e) {
      debug(`batchExtract: cannot read ${filePath}: ${toErrorMessage(e)}`);
      continue;
    }
    const output = await pool.parse(filePath, code, EXTRACT_ONLY);
    if (!output || output.typeMap.size === 0) continue;
    const symbols = result.get(relPath);
    if (!symbols) continue;
    symbols.typeMap = output.typeMap;
    symbols._typeMapBackfilled = true;
  }
}

/**
 * Parse files via WASM engine, returning a Map<relPath, symbols>.
 *
 * Each file is dispatched to the WASM worker pool. The worker parses, extracts,
 * and runs all AST analyses (complexity, CFG, dataflow, ast-store) in its own
 * thread, returning fully pre-computed ExtractorOutput. V8 fatal errors from
 * tree-sitter WASM (#965) kill only the worker — the pool skips the file and
 * restarts the worker for the next one.
 *
 * `_tree` is NEVER set by this path. All downstream analyses operate on the
 * pre-computed `astNodes` / `dataflow` / `def.complexity` / `def.cfg` fields.
 */
async function parseFilesWasm(
  filePaths: string[],
  rootDir: string,
): Promise<Map<string, ExtractorOutput>> {
  const result = new Map<string, ExtractorOutput>();
  const pool = getWasmWorkerPool();
  for (const filePath of filePaths) {
    if (!_extToLang.has(path.extname(filePath).toLowerCase())) continue;
    let code: string;
    try {
      code = fs.readFileSync(filePath, 'utf-8');
    } catch (err: unknown) {
      warn(`Skipping ${path.relative(rootDir, filePath)}: ${(err as Error).message}`);
      continue;
    }
    const output = await pool.parse(filePath, code, FULL_ANALYSIS);
    if (output) {
      const relPath = path.relative(rootDir, filePath).split(path.sep).join('/');
      result.set(relPath, output);
    }
  }
  return result;
}

/**
 * Files at or below this count use the inline parse path (no worker spawn).
 *
 * Sized for typical engine-parity drops: a handful of fixture files in one
 * or two languages (the recurring HCL case is 4 files). Above this, the
 * worker-pool's IPC + crash-isolation cost (#965) is amortized over enough
 * parse work to be worth paying; below it, the ~1–2s cold-start dominates.
 */
const INLINE_BACKFILL_THRESHOLD = 16;

/**
 * Inline WASM parse (no worker) for small file batches.
 *
 * Used by the engine-parity backfill path when the native engine drops a
 * handful of files (typically test fixtures). The worker pool's per-call
 * IPC + grammar-init overhead can cost 1–2s on slow CI runners — for a
 * 4-file backfill, that dwarfs the ~10ms of actual parse work.
 *
 * Returns symbols with `_tree` set so `runAnalyses` can run AST/CFG/dataflow
 * visitors via the unified walker (mirrors how WASM-engine results behaved
 * before the worker pool was introduced).
 */
async function parseFilesWasmInline(
  filePaths: string[],
  rootDir: string,
): Promise<Map<string, ExtractorOutput>> {
  const result = new Map<string, ExtractorOutput>();
  if (filePaths.length === 0) return result;
  const parsers = await ensureParsersForFiles(filePaths);
  for (const filePath of filePaths) {
    if (!_extToLang.has(path.extname(filePath).toLowerCase())) continue;
    let code: string;
    try {
      code = fs.readFileSync(filePath, 'utf-8');
    } catch (err: unknown) {
      warn(`Skipping ${path.relative(rootDir, filePath)}: ${(err as Error).message}`);
      continue;
    }
    const extracted = wasmExtractSymbols(parsers, filePath, code);
    if (!extracted) continue;
    const relPath = path.relative(rootDir, filePath).split(path.sep).join('/');
    const symbols = extracted.symbols as ExtractorOutput & { _tree?: unknown; _langId?: string };
    symbols._tree = extracted.tree;
    symbols._langId = extracted.langId;
    result.set(relPath, symbols);
  }
  return result;
}

/**
 * Backfill helper: small batches use the inline (main-thread) path; larger
 * batches keep the worker-pool isolation against tree-sitter WASM crashes
 * (#965). Threshold matches typical engine-parity drop sizes (a few fixture
 * files in one or two languages).
 */
export async function parseFilesWasmForBackfill(
  filePaths: string[],
  rootDir: string,
): Promise<Map<string, ExtractorOutput>> {
  if (filePaths.length <= INLINE_BACKFILL_THRESHOLD) {
    return parseFilesWasmInline(filePaths, rootDir);
  }
  return parseFilesWasm(filePaths, rootDir);
}

/**
 * Parse multiple files in bulk and return a Map<relPath, symbols>.
 */
export async function parseFilesAuto(
  filePaths: string[],
  rootDir: string,
  opts: ParseEngineOpts = {},
): Promise<Map<string, ExtractorOutput>> {
  const { native } = resolveEngine(opts);

  if (!native) return parseFilesWasm(filePaths, rootDir);

  const result = new Map<string, ExtractorOutput>();
  // Always extract all analysis data (dataflow + AST nodes) during native parse.
  // This eliminates the need for any downstream WASM re-parse or native standalone calls.
  const nativeResults = native.parseFilesFull
    ? native.parseFilesFull(filePaths, rootDir)
    : native.parseFiles(filePaths, rootDir, true, true);
  const needsTypeMap: { filePath: string; relPath: string }[] = [];
  const nativeParsed = new Set<string>();
  for (const r of nativeResults) {
    if (!r) continue;
    nativeParsed.add(r.file);
    const patched = patchNativeResult(r);
    const relPath = path.relative(rootDir, r.file).split(path.sep).join('/');
    result.set(relPath, patched);
    // Always backfill TS/TSX type maps from WASM — the native parser's type
    // extraction can produce incorrect results when the same variable name
    // appears at multiple scopes (e.g. `node: TreeSitterNode` in one function
    // vs `node: NodeRow` in another). The WASM JS extractor handles scope
    // traversal order more accurately.
    if (TS_BACKFILL_EXTS.has(path.extname(r.file))) {
      needsTypeMap.push({ filePath: r.file, relPath });
    }
  }
  if (needsTypeMap.length > 0) {
    await backfillTypeMapBatch(needsTypeMap, result);
  }

  // Engine parity: native may silently drop files whose extensions are in
  // SUPPORTED_EXTENSIONS (because a WASM grammar exists) but whose Rust
  // extractor/grammar is missing or fails. WASM handles these — fall back so
  // both engines process the same file set (#967). Restrict to installed WASM
  // grammars so we don't warn about files that neither engine can parse.
  const installedExts = getInstalledWasmExtensions();
  const dropped = filePaths.filter(
    (f) => !nativeParsed.has(f) && installedExts.has(path.extname(f).toLowerCase()),
  );
  if (dropped.length > 0) {
    warn(`Native engine dropped ${dropped.length} file(s); falling back to WASM for parity`);
    const wasmResults = await parseFilesWasmForBackfill(dropped, rootDir);
    for (const [relPath, symbols] of wasmResults) {
      result.set(relPath, symbols);
    }
  }

  return result;
}

/**
 * Report which engine is active.
 */
export function getActiveEngine(opts: ParseEngineOpts = {}): {
  name: 'native' | 'wasm';
  version: string | null;
  binaryVersion: string | null;
} {
  const { name, native } = resolveEngine(opts);
  const binaryVersion: string | null =
    native && typeof native.engineVersion === 'function' ? native.engineVersion() : null;
  // The display version prefers the platform package.json so the "Using native
  // engine (vX)" log matches the npm release the user installed. The Rust
  // orchestrator's check_version_mismatch compares against CARGO_PKG_VERSION
  // (the binary's own value), so build_meta writes must use `binaryVersion`,
  // not this display value — see pipeline.ts and finalize.ts (#1066).
  let version: string | null = binaryVersion;
  if (native) {
    try {
      version = getNativePackageVersion() ?? version;
    } catch (e: unknown) {
      debug(`getNativePackageVersion failed: ${(e as Error).message}`);
    }
  }
  return { name, version, binaryVersion };
}

/**
 * Create a native ParseTreeCache for incremental parsing.
 * Returns null if the native engine is unavailable (WASM fallback).
 */
export function createParseTreeCache(): any {
  const native = loadNative();
  if (!native?.ParseTreeCache) return null;
  return new native.ParseTreeCache();
}

/**
 * Parse a file incrementally using the cache, or fall back to full parse.
 */
export async function parseFileIncremental(
  cache: any,
  filePath: string,
  source: string,
  opts: ParseEngineOpts = {},
): Promise<ExtractorOutput | null> {
  if (cache) {
    const result = cache.parseFile(filePath, source);
    if (!result) return null;
    const patched = patchNativeResult(result);
    // Always backfill typeMap for TS/TSX from WASM (see parseFileAuto comment).
    if (TS_BACKFILL_EXTS.has(path.extname(filePath))) {
      const { typeMap, backfilled } = await backfillTypeMap(filePath, source);
      if (backfilled) {
        patched.typeMap = typeMap;
        patched._typeMapBackfilled = true;
      }
    }
    return patched;
  }
  return parseFileAuto(filePath, source, opts);
}
