import { COMPLEXITY_THRESHOLDS } from './constants';

export const SOURCE_EXTENSIONS: Record<string, string[]> = {
  cobol: ['.cbl', '.cob', '.cpy', '.pco'],
  cobol_as400: ['.cbl', '.cob', '.cpy', '.pco', '.txt', '.src'],
  pli: ['.pli', '.pl1', '.plinc'],
  rpg: ['.rpg', '.rpgle', '.sqlrpgle'],
  fortran: ['.f', '.f90', '.f95', '.for'],
  java: ['.java'],
  python: ['.py'],
  csharp: ['.cs'],
  php: ['.php', '.phtml', '.php3', '.php4', '.php5', '.phps'],
  php_legacy: ['.php', '.phtml', '.php3', '.php4', '.php5'],
  javascript: ['.js', '.jsx', '.mjs', '.cjs'],
  typescript: ['.ts', '.tsx', '.mts', '.cts'],
  ruby: ['.rb', '.rake', '.gemspec', '.ru'],
  go: ['.go'],
  rust: ['.rs'],
  swift: ['.swift'],
  kotlin: ['.kt', '.kts'],
  scala: ['.scala', '.sc'],
  cpp: ['.cpp', '.cxx', '.cc', '.c++', '.hpp', '.hxx', '.h'],
  c: ['.c', '.h'],
  vb: ['.vb', '.vbs'],
  vbnet: ['.vb'],
  vb6: ['.bas', '.cls', '.frm', '.ctl', '.pag', '.dob', '.dsr', '.vbs'],
  perl: ['.pl', '.pm', '.t'],
  groovy: ['.groovy', '.gvy', '.gy', '.gsh'],
  r: ['.r', '.R'],
  matlab: ['.m'],
  pascal: ['.pas', '.pp', '.p'],
  delphi: ['.pas', '.dpr', '.dpk'],
  abap: ['.abap'],
  pl_sql: ['.sql', '.pls', '.plb'],
  jcl: ['.jcl', '.proc'],
  natural: ['.nsp', '.nsa'],
  mumps: ['.m', '.mps'],
  crystal: ['.cr'],
  elixir: ['.ex', '.exs'],
  clojure: ['.clj', '.cljs', '.cljc'],
  haskell: ['.hs', '.lhs'],
  lua: ['.lua'],
  dart: ['.dart'],
  // Wizard ID aliases for already-supported languages
  sap: ['.abap'],
  oracle: ['.sql', '.pls', '.plb', '.pks', '.pkb', '.fnc', '.prc', '.trg', '.vw'],
  tsql: ['.sql', '.tsql'],
  db2: ['.sql'],
  // Legacy languages
  rexx: ['.rexx', '.rex', '.exec'],
  clist: ['.clist', '.clt'],
  assembly: ['.asm', '.s', '.S', '.inc'],
  ada: ['.adb', '.ads'],
  powerbuilder: ['.sra', '.sru', '.srw', '.srd', '.srs'],
  progress: ['.p', '.w', '.i'],
  coldfusion: ['.cfm', '.cfc', '.cfml'],
  clipper: ['.prg', '.ch'],
  visualfoxpro: ['.prg', '.vcx', '.scx', '.frx'],
  lotusscript: ['.ls', '.lss'],
  informix: ['.4gl'],
  sas: ['.sas'],
  pick: ['.b'],
  uniface: ['.unv'],
  apl: ['.apl', '.dyalog'],
  smalltalk: ['.st'],
  objective_c: ['.m', '.mm'],
  actionscript: ['.as'],
  basic: ['.bas', '.qb'],
  tcl: ['.tcl', '.tk'],
  // Modern languages missing from original list
  fsharp: ['.fs', '.fsi', '.fsx'],
  julia: ['.jl'],
  nim: ['.nim', '.nims'],
  zig: ['.zig'],
  vlang: ['.v'],
  odin: ['.odin'],
  wasm: ['.wat'],
  sql: ['.sql'],
  graphql: ['.graphql', '.gql'],
  powershell: ['.ps1', '.psm1', '.psd1'],
  bash: ['.sh', '.bash'],
  scheme: ['.scm', '.ss', '.rkt'],
  lisp: ['.lisp', '.lsp', '.cl'],
  erlang: ['.erl', '.hrl'],
  ocaml: ['.ml', '.mli'],
  elm: ['.elm'],
  purescript: ['.purs'],
  verilog: ['.v', '.sv', '.svh'],
  vhdl: ['.vhd', '.vhdl'],
  solidity: ['.sol'],
  move: ['.move'],
  coffeescript: ['.coffee'],
  haxe: ['.hx', '.hxml'],
  // Variant aliases (point to same extensions as their base language)
  java_legacy: ['.java'],
  perl_legacy: ['.pl', '.pm', '.t'],
  dotnet_legacy: ['.cs', '.vb'],
  rpg_free: ['.rpgle', '.sqlrpgle'],
  node: ['.js', '.mjs', '.cjs'],
  // Additional languages
  apex: ['.cls', '.trigger', '.page', '.component'],
  ballerina: ['.bal'],
  classic_asp: ['.asp', '.asa'],
  coq: ['.coq'],
  d: ['.d'],
  forth: ['.fth', '.4th'],
  gdscript: ['.gd'],
  gleam: ['.gleam'],
  glsl: ['.glsl', '.vert', '.frag', '.geom', '.comp'],
  hack: ['.hack', '.hh'],
  idris2: ['.idr'],
  lean4: ['.lean'],
  mojo: ['.mojo'],
  nix: ['.nix'],
  plc: ['.st', '.scl'],
  prolog: ['.pro', '.P'],
  reason: ['.re', '.rei'],
  spss: ['.sps'],
  wolfram: ['.wl', '.wls'],
  xslt: ['.xsl', '.xslt'],
  tibco_bw: ['.process', '.substvar', '.xsd', '.wsdl'],
};

// Manifest / package-manager files to look for, per source language.
// Patterns starting with "*" are suffix-matched (e.g. "*.csproj").
export const MANIFEST_FILES: Record<string, string[]> = {
  php: ['composer.json'],
  php_legacy: ['composer.json'],
  javascript: ['package.json'],
  typescript: ['package.json'],
  python: ['requirements.txt', 'pyproject.toml', 'setup.cfg', 'Pipfile'],
  java: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
  kotlin: ['build.gradle', 'build.gradle.kts', 'pom.xml'],
  scala: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
  groovy: ['build.gradle', 'build.gradle.kts', 'pom.xml'],
  ruby: ['Gemfile'],
  go: ['go.mod'],
  rust: ['Cargo.toml'],
  dart: ['pubspec.yaml'],
  elixir: ['mix.exs'],
  csharp: ['*.csproj', 'packages.config', 'Directory.Packages.props'],
  vb: ['*.vbproj', 'packages.config'],
  vbnet: ['*.vbproj', 'packages.config'],
  vb6: ['*.vbp', '*.vbg'],
  swift: ['Package.swift'],
  perl: ['cpanfile', 'Makefile.PL'],
  haskell: ['*.cabal', 'package.yaml'],
  lua: ['rockspec'],
  clojure: ['project.clj', 'deps.edn'],
  crystal: ['shard.yml'],
  // Wizard ID aliases
  sap: [],
  oracle: [],
  tsql: [],
  db2: [],
  cpp: ['CMakeLists.txt', 'conanfile.txt', 'conanfile.py', 'vcpkg.json', 'meson.build'],
  c: ['CMakeLists.txt', 'Makefile'],
  delphi: ['*.dproj', '*.groupproj'],
  bash: [],
  pl_sql: [],
  plsql: [],
  cobol: [],
  rpg: [],
  // Legacy languages
  ada: ['*.gpr'],
  coldfusion: ['box.json'],
  objective_c: ['Podfile', 'Package.swift'],
  // Modern languages
  fsharp: ['*.fsproj', 'paket.dependencies'],
  julia: ['Project.toml'],
  nim: ['*.nimble'],
  zig: ['build.zig.zon'],
  vlang: ['v.mod'],
  erlang: ['rebar.config', '*.app.src'],
  ocaml: ['dune-project', '*.opam'],
  elm: ['elm.json'],
  purescript: ['spago.yaml', 'spago.dhall'],
  solidity: ['foundry.toml'],
  move: ['Move.toml'],
  coffeescript: ['package.json'],
  haxe: ['*.hxml'],
};

/** Max source files to fetch & analyze per project. Dependency mapping needs broad
 *  coverage of program files, not a small arbitrary sample. */
export const MAX_ANALYZE_FILES = 150;

/** Order source files so dependency-bearing program files are analyzed first within the
 *  cap (programs carry COPY/CALL/EXEC; copybooks/includes rarely do; assets never). */
export function prioritizeSourceFiles<T extends { path: string }>(files: T[]): T[] {
  const rank = (p: string): number => {
    const lower = p.toLowerCase();
    if (/\.(cbl|cob|pco|pli|pl1|rpgle|rpg|sqlrpgle|cls|trigger)$/.test(lower)) return 0;
    if (/\.(cpy|inc|copy)$/.test(lower)) return 1;
    return 2;
  };
  return [...files].sort((a, b) => rank(a.path) - rank(b.path));
}

/** Kind of a detected dependency. Drives how it is migrated/priced/proposed. */
export type DepKind =
  | 'copybook'        // COBOL COPY / EXEC SQL INCLUDE — internal data structure → target class/record
  | 'called-program'  // static CALL 'PROG' — internal module → target method/service
  | 'cics-program'    // EXEC CICS LINK/XCTL PROGRAM(...)
  | 'cics-map'        // BMS map/mapset (SEND/RECEIVE MAP)
  | 'sql-table'       // DB2 table referenced by embedded SQL
  | 'dataset'         // file/VSAM dataset (SELECT ... ASSIGN TO)
  | 'external-lib';   // real third-party package (manifest / import)

export interface DepRef {
  name: string;
  kind: DepKind;
}

/** True only for kinds that are genuine third-party packages with a target+version. */
export function isExternalDep(kind: DepKind): boolean {
  return kind === 'external-lib';
}

function dedupeDeps(list: DepRef[]): DepRef[] {
  const seen = new Set<string>();
  const out: DepRef[] = [];
  for (const d of list) {
    if (!d.name) continue;
    const key = `${d.kind}:${d.name.toUpperCase()}`;
    if (seen.has(key)) continue;
    seen.add(key);
    out.push(d);
  }
  return out;
}

export interface AnalyzedFile {
  path: string;
  name: string;
  size: number;
  lines: number;
  blankLines: number;
  commentLines: number;
  codeLines: number;
  complexity: number;
  nestingDepth: number;
  risk: 'high' | 'medium' | 'low';
  /** De-duplicated dependency names (backward-compat; derived from `deps`). */
  dependencies: string[];
  /** Typed dependencies with their kind. */
  deps: DepRef[];
  externalCallCount: number;
  procedures: number;
  sections: number;
}

// Extract a single external dependency name from a source line, language-aware.
export function extractInlineDep(lang: string, trimmed: string): string | null {
  switch (lang) {
    case 'java':
    case 'kotlin':
    case 'scala':
    case 'groovy': {
      const m = trimmed.match(/^import\s+(?:static\s+)?([\w][\w.]*)/);
      if (!m) return null;
      return m[1].split('.').slice(0, 3).join('.');
    }
    case 'python': {
      return (
        trimmed.match(/^from\s+([\w][\w.]*)\s+import/)?.[1]?.split('.')[0] ??
        trimmed.match(/^import\s+([\w][\w.]*)/)?.[1]?.split('.')[0] ??
        null
      );
    }
    case 'php':
    case 'php_legacy': {
      const useMatch = trimmed.match(/^use\s+([\w\\][\w\\]*)/)?.[1];
      if (useMatch) return useMatch.split('\\')[0];
      return (
        trimmed.match(/require(?:_once)?\s*\(\s*['"]([^.'"\/][^'"]*)['"]/)?.[1] ??
        trimmed.match(/include(?:_once)?\s*\(\s*['"]([^.'"\/][^'"]*)['"]/)?.[1] ??
        null
      );
    }
    case 'csharp': {
      return trimmed.match(/^using\s+([\w][\w.]*)/)?.[1]?.split('.')[0] ?? null;
    }
    case 'vb':
    case 'vbnet': {
      return trimmed.match(/^Imports\s+([\w][\w.]*)/i)?.[1]?.split('.')[0] ?? null;
    }
    case 'vb6': {
      // Declare Function/Sub ... Lib "libname" — DLL references
      const libMatch = trimmed.match(/\bDeclare\s+(?:Function|Sub)\s+\w+\s+Lib\s+"([^"]+)"/i);
      if (libMatch) return libMatch[1];
      // CreateObject("ProgID") — COM late binding
      const coMatch = trimmed.match(/\bCreateObject\s*\(\s*"([^"]+)"/i);
      if (coMatch) return coMatch[1].split('.')[0];
      // GetObject("", "ProgID") — COM
      const goMatch = trimmed.match(/\bGetObject\s*\([^,]*,\s*"([^"]+)"/i);
      if (goMatch) return goMatch[1].split('.')[0];
      return null;
    }
    case 'javascript':
    case 'typescript': {
      return (
        trimmed.match(/from\s+['"]([^.'"\/][^'"]*)['"]/)?.[1]?.split('/')[0] ??
        trimmed.match(/require\s*\(\s*['"]([^.'"\/][^'"]*)['"]\s*\)/)?.[1]?.split('/')[0] ??
        trimmed.match(/^import\s+['"]([^.'"\/][^'"]*)['"]/)?.[1]?.split('/')[0] ??
        null
      );
    }
    case 'go': {
      const single = trimmed.match(/^import\s+"([^"]+)"/)?.[1];
      if (single && !single.startsWith('.')) return single.split('/').slice(0, 3).join('/');
      const bare = trimmed.match(/^"([^"]+)"/)?.[1];
      if (bare && !bare.startsWith('.')) return bare.split('/').slice(0, 3).join('/');
      return null;
    }
    case 'rust': {
      return (
        trimmed.match(/^extern\s+crate\s+([\w]+)/)?.[1] ??
        trimmed.match(/^use\s+([\w]+)/)?.[1] ??
        null
      );
    }
    case 'swift': {
      return trimmed.match(/^import\s+([\w]+)/)?.[1] ?? null;
    }
    case 'dart': {
      const m = trimmed.match(/^import\s+['"]([^'"]+)['"]/);
      if (!m) return null;
      const path = m[1];
      if (path.startsWith('package:')) return path.slice(8).split('/')[0];
      if (path.startsWith('dart:') || path.startsWith('.')) return null;
      return path.split('/')[0];
    }
    case 'ruby': {
      return (
        trimmed.match(/^require\s+['"]([^.'"\/][^'"\/]*)['"]/)?.[1] ??
        trimmed.match(/^gem\s+['"]([^'"]+)['"]/)?.[1] ??
        null
      );
    }
    case 'elixir': {
      return (
        trimmed.match(/^(?:import|require|use|alias)\s+([\w.]+)/)?.[1]?.split('.')[0] ??
        null
      );
    }
    case 'haskell': {
      return trimmed.match(/^import\s+(?:qualified\s+)?([\w.]+)/)?.[1]?.split('.')[0] ?? null;
    }
    case 'lua': {
      return trimmed.match(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/)?.[1] ?? null;
    }
    case 'perl': {
      return (
        trimmed.match(/^use\s+([\w:]+)/)?.[1] ??
        trimmed.match(/^require\s+(['"]?)([\w:/]+)\1/)?.[2] ??
        null
      );
    }
    case 'r': {
      return (
        trimmed.match(/^library\s*\(\s*['"]?([\w.]+)['"]?\s*\)/)?.[1] ??
        trimmed.match(/^require\s*\(\s*['"]?([\w.]+)['"]?\s*\)/)?.[1] ??
        null
      );
    }
    case 'c':
    case 'cpp': {
      const m = trimmed.match(/^#include\s+[<"]([\w./]+)[>"]/);
      if (!m) return null;
      if (!m[1].includes('/')) return m[1];
      if (m[1].startsWith('.')) return null;
      return m[1].split('/')[0];
    }
    case 'clojure': {
      return (
        trimmed.match(/\(:require\s+\[?([\w.-]+)/)?.[1] ??
        trimmed.match(/\(:import\s+\[?([\w.]+)/)?.[1]?.split('.')[0] ??
        null
      );
    }
    case 'pascal':
    case 'delphi': {
      if (/\buses\b/i.test(trimmed)) {
        return trimmed.match(/\buses\s+([\w]+)/i)?.[1] ?? null;
      }
      return null;
    }
    case 'crystal': {
      return trimmed.match(/^require\s+['"]([^.'"\/][^'"]*)['"]/)?.[1] ?? null;
    }
    case 'ada': {
      return trimmed.match(/^with\s+([\w.]+)/i)?.[1]?.split('.')[0] ?? null;
    }
    case 'rexx': {
      // CALL external program or ADDRESS SYSTEM
      return trimmed.match(/^\s*call\s+([\w]+)/i)?.[1] ?? null;
    }
    case 'assembly': {
      return (
        trimmed.match(/^include\s+([\w./\\]+)/i)?.[1]?.split(/[/\\]/).pop() ??
        trimmed.match(/^extern\s+[\w,\s]*\b([\w]+)\b/i)?.[1] ??
        null
      );
    }
    case 'objective_c': {
      return (
        trimmed.match(/^#import\s+[<"]([\w/]+\.h)[>"]/)?.[1]?.split('/')[0] ??
        trimmed.match(/^@import\s+([\w.]+)/)?.[1]?.split('.')[0] ??
        null
      );
    }
    case 'actionscript': {
      return trimmed.match(/^import\s+([\w.]+)/)?.[1]?.split('.')[0] ?? null;
    }
    case 'coffeescript': {
      return (
        trimmed.match(/require\s+['"]([^.'"\/][^'"]*)['"]/)?.[1]?.split('/')[0] ??
        null
      );
    }
    case 'tcl': {
      return trimmed.match(/^package\s+require\s+([\w:.-]+)/i)?.[1] ?? null;
    }
    case 'fsharp': {
      return (
        trimmed.match(/^open\s+([\w.]+)/)?.[1]?.split('.')[0] ??
        trimmed.match(/^#r\s+"(?:nuget:\s*)?([\w.-]+)/)?.[1] ??
        null
      );
    }
    case 'julia': {
      return (
        trimmed.match(/^using\s+([\w.]+)/)?.[1]?.split('.')[0] ??
        trimmed.match(/^import\s+([\w.]+)/)?.[1]?.split('.')[0] ??
        null
      );
    }
    case 'nim': {
      return (
        trimmed.match(/^import\s+([\w/]+)/)?.[1]?.split('/')[0] ??
        trimmed.match(/^include\s+([\w/]+)/)?.[1]?.split('/')[0] ??
        null
      );
    }
    case 'zig': {
      return trimmed.match(/@import\s*\(\s*"([^."][^"]+)"/)?.[1] ?? null;
    }
    case 'powershell': {
      return (
        trimmed.match(/^Import-Module\s+(?:-Name\s+)?([\w.-]+)/i)?.[1] ??
        trimmed.match(/^#Requires\s+-Module\s+([\w.-]+)/i)?.[1] ??
        null
      );
    }
    case 'bash': {
      return (
        trimmed.match(/^(?:source|\.\s+)([\w/.~$-]+\.sh)/)?.[1]?.split('/').pop()?.replace('.sh', '') ??
        null
      );
    }
    case 'scheme': {
      return (
        trimmed.match(/^\(require\s+['"]?([\w-]+)/)?.[1] ??
        trimmed.match(/^\(import\s+\(?([\w-]+)/)?.[1] ??
        null
      );
    }
    case 'lisp': {
      return (
        trimmed.match(/^\(require\s+[:#'"]*([\w-]+)/i)?.[1]?.toLowerCase() ??
        trimmed.match(/^\(:require\s+([\w-]+)/i)?.[1]?.toLowerCase() ??
        null
      );
    }
    case 'erlang': {
      return (
        trimmed.match(/^-include_lib\s*\(\s*"([^/]+)\//)?.[1] ??
        trimmed.match(/^-include\s*\(\s*"([^"]+)"/)?.[1] ??
        null
      );
    }
    case 'ocaml': {
      return trimmed.match(/^open\s+([\w.]+)/)?.[1]?.split('.')[0] ?? null;
    }
    case 'elm': {
      return trimmed.match(/^import\s+([\w.]+)/)?.[1]?.split('.')[0] ?? null;
    }
    case 'purescript': {
      return trimmed.match(/^import\s+([\w.]+)/)?.[1]?.split('.')[0] ?? null;
    }
    case 'haxe': {
      return trimmed.match(/^import\s+([\w.]+)/)?.[1]?.split('.')[0] ?? null;
    }
    case 'solidity': {
      return (
        trimmed.match(/^import\s+['"]([^.'"\/][^'"]*)['"]/)?.[1]?.split('/')[0] ??
        trimmed.match(/from\s+['"]([^.'"\/][^'"]*)['"]/)?.[1]?.split('/')[0] ??
        null
      );
    }
    case 'sas': {
      return (
        trimmed.match(/^%include\s+['"]([^'"]+)['"]/i)?.[1]?.split('/').pop()?.replace(/\.\w+$/, '') ??
        trimmed.match(/^libname\s+([\w]+)/i)?.[1] ??
        null
      );
    }
    case 'progress': {
      return trimmed.match(/^using\s+([\w.*]+)/i)?.[1]?.split('.')[0] ?? null;
    }
    case 'coldfusion': {
      return (
        trimmed.match(/createObject\s*\(\s*['"][^'"]*['"]\s*,\s*['"]([^'"]+)['"]/i)?.[1]?.split('.')[0] ??
        trimmed.match(/<cfimport\s[^>]*prefix=["']([^"']+)["']/i)?.[1] ??
        null
      );
    }
    case 'lotusscript': {
      return (
        trimmed.match(/^Use\s+"([^"]+)"/i)?.[1] ??
        trimmed.match(/^%Include\s+"([^"]+)"/i)?.[1] ??
        null
      );
    }
    case 'clipper': {
      return (
        trimmed.match(/^#include\s+['"]([^'"]+)['"]/i)?.[1]?.replace(/\.\w+$/, '') ??
        null
      );
    }
    case 'powerbuilder': {
      // external function DLL calls
      return trimmed.match(/\bfunction\s+\S+\s+\S+\s+library\s+"([^"]+)"/i)?.[1] ?? null;
    }
    case 'visualfoxpro': {
      return (
        trimmed.match(/^set\s+procedure\s+to\s+([\w.\\\/]+)/i)?.[1]?.split(/[/\\]/).pop()?.replace(/\.\w+$/, '') ??
        trimmed.match(/^do\s+([\w]+)/i)?.[1] ??
        null
      );
    }
    case 'smalltalk': {
      return trimmed.match(/^Smalltalk\s+at:\s+#([\w]+)/)?.[1] ?? null;
    }
    case 'verilog': {
      return (
        trimmed.match(/^`include\s+["']([\w./]+)["']/)?.[1]?.split('/').pop()?.replace(/\.\w+$/, '') ??
        null
      );
    }
    case 'vhdl': {
      return (
        trimmed.match(/^use\s+([\w.]+)\.(all|[\w]+)/i)?.[1]?.split('.')[0] ??
        null
      );
    }
    case 'move': {
      return trimmed.match(/^use\s+([\w::]+)/)?.[1]?.split('::')[0] ?? null;
    }
    case 'apex': {
      return trimmed.match(/^(?:import|using)\s+([\w.]+)/i)?.[1]?.split('.')[0] ?? null;
    }
    case 'ballerina': {
      return trimmed.match(/^import\s+([\w./]+)/)?.[1]?.split('/')[0] ?? null;
    }
    case 'gleam': {
      return trimmed.match(/^import\s+([\w/]+)/)?.[1]?.split('/')[0] ?? null;
    }
    case 'nix': {
      return trimmed.match(/^import\s+([\w./]+)/)?.[1]?.split('/').pop()?.replace(/\.nix$/, '') ?? null;
    }
    case 'prolog': {
      return trimmed.match(/^:- use_module\s*\(\s*(?:library\()?([\w/]+)/)?.[1]?.split('/').pop() ?? null;
    }
    case 'reason': {
      return trimmed.match(/^open\s+([\w.]+)/)?.[1]?.split('.')[0] ?? null;
    }
    case 'gdscript': {
      return (
        trimmed.match(/^(?:preload|load)\s*\(\s*["']([^"']+)["']/)?.[1]?.split('/').pop()?.replace(/\.gd$/, '') ??
        null
      );
    }
    case 'classic_asp': {
      return (
        trimmed.match(/<!--\s*#include\s+(?:file|virtual)\s*=\s*["']([^"']+)["']/i)?.[1]?.split('/').pop()?.replace(/\.\w+$/, '') ??
        null
      );
    }
    case 'xslt': {
      return (
        trimmed.match(/xsl:import\s+href=["']([^"']+)["']/i)?.[1]?.split('/').pop()?.replace(/\.\w+$/, '') ??
        trimmed.match(/xsl:include\s+href=["']([^"']+)["']/i)?.[1]?.split('/').pop()?.replace(/\.\w+$/, '') ??
        null
      );
    }
    default: {
      return (
        trimmed.match(/require\s*\(\s*['"]([^.'"\/][^'"]*)['"]\s*\)/)?.[1] ??
        trimmed.match(/from\s+['"]([^.'"\/][^'"]*)['"]/)?.[1] ??
        trimmed.match(/^import\s+['"]([^.'"\/][^'"]*)['"]/)?.[1] ??
        trimmed.match(/^from\s+([\w][\w.]*)\s+import/)?.[1] ??
        trimmed.match(/^import\s+(?:static\s+)?([\w][\w.]*)/)?.[1] ??
        trimmed.match(/^using\s+([\w][\w.]*)/)?.[1] ??
        trimmed.match(/^use\s+([\w\\][\w\\]*)/)?.[1] ??
        null
      );
    }
  }
}

// Parse a manifest/package-manager file and return external dependency names.
export function parseManifestDeps(filePath: string, content: string): string[] {
  const deps: string[] = [];
  const name = (filePath.split('/').pop() ?? '').toLowerCase();

  // composer.json — PHP (Composer packages + PHP native extensions)
  if (name === 'composer.json') {
    try {
      const json = JSON.parse(content);
      for (const pkg of Object.keys(json.require ?? {})) {
        if (pkg !== 'php') deps.push(pkg); // keep ext-* — extensions matter for migration
      }
      for (const pkg of Object.keys(json['require-dev'] ?? {})) deps.push(pkg);
    } catch { /* malformed */ }
  }

  // package.json — JS/TS/Node
  else if (name === 'package.json') {
    try {
      const json = JSON.parse(content);
      deps.push(...Object.keys(json.dependencies ?? {}));
      deps.push(...Object.keys(json.devDependencies ?? {}));
      deps.push(...Object.keys(json.peerDependencies ?? {}));
    } catch { /* malformed */ }
  }

  // requirements.txt — Python
  else if (name === 'requirements.txt') {
    for (const line of content.split('\n')) {
      const t = line.trim();
      if (!t || t.startsWith('#') || t.startsWith('-r') || t.startsWith('--')) continue;
      const pkg = t.split(/[>=<!~\[; \t]/)[0].trim();
      if (pkg) deps.push(pkg);
    }
  }

  // pyproject.toml — Python (PEP 517/518)
  else if (name === 'pyproject.toml') {
    let inDeps = false;
    for (const line of content.split('\n')) {
      const t = line.trim();
      if (t === 'dependencies = [' || t === 'dependencies=[') { inDeps = true; continue; }
      if (inDeps) {
        if (t === ']') { inDeps = false; continue; }
        const pkg = t.replace(/['"]/g, '').split(/[>=<!~\[; \t]/)[0].trim();
        if (pkg && !pkg.startsWith('#')) deps.push(pkg);
      }
    }
    // Also handle inline array: dependencies = ["pkg>=1", "pkg2"]
    const inline = content.match(/^dependencies\s*=\s*\[([^\]]+)\]/m)?.[1];
    if (inline) {
      for (const item of inline.split(',')) {
        const pkg = item.replace(/['"]/g, '').trim().split(/[>=<!~\[; \t]/)[0].trim();
        if (pkg && !pkg.startsWith('#')) deps.push(pkg);
      }
    }
  }

  // setup.cfg — Python
  else if (name === 'setup.cfg') {
    let inInstallRequires = false;
    for (const line of content.split('\n')) {
      const t = line.trim();
      if (/^install_requires\s*=/.test(t)) { inInstallRequires = true; continue; }
      if (inInstallRequires) {
        if (/^\[/.test(t) || (t && !/^\s/.test(line))) { inInstallRequires = false; continue; }
        const pkg = t.split(/[>=<!~\[; \t]/)[0].trim();
        if (pkg) deps.push(pkg);
      }
    }
  }

  // Pipfile — Python
  else if (name === 'pipfile') {
    let inPackages = false;
    for (const line of content.split('\n')) {
      const t = line.trim();
      if (t === '[packages]' || t === '[dev-packages]') { inPackages = true; continue; }
      if (t.startsWith('[')) { inPackages = false; continue; }
      if (inPackages) {
        const m = t.match(/^([\w-]+)\s*=/);
        if (m) deps.push(m[1]);
      }
    }
  }

  // pom.xml — Java/Maven
  else if (name === 'pom.xml') {
    const artifactIds = [...content.matchAll(/<artifactId>([\w.-]+)<\/artifactId>/g)].map(m => m[1]);
    deps.push(...artifactIds.slice(1)); // skip index 0 (the project's own artifactId)
  }

  // build.gradle / build.gradle.kts — Gradle
  else if (name === 'build.gradle' || name === 'build.gradle.kts') {
    for (const line of content.split('\n')) {
      const t = line.trim();
      const m = t.match(
        /(?:implementation|api|compile|testImplementation|testCompile|runtimeOnly|compileOnly|annotationProcessor|kapt)\s*[\('"]([\w.-]+):([\w.-]+)/i
      );
      if (m) deps.push(m[2]); // artifact id
    }
  }

  // build.sbt — Scala/SBT
  else if (name === 'build.sbt') {
    for (const line of content.split('\n')) {
      const m = line.trim().match(/["'][\w.-]+["']\s*%%?\s*["']([\w.-]+)["']/);
      if (m) deps.push(m[1]);
    }
  }

  // Gemfile — Ruby
  else if (name === 'gemfile') {
    for (const line of content.split('\n')) {
      const t = line.trim();
      if (t.startsWith('#')) continue;
      const m = t.match(/^gem\s+['"]([^'"]+)['"]/);
      if (m) deps.push(m[1]);
    }
  }

  // go.mod — Go
  else if (name === 'go.mod') {
    let inRequire = false;
    for (const line of content.split('\n')) {
      const t = line.trim();
      if (t === 'require (') { inRequire = true; continue; }
      if (inRequire && t === ')') { inRequire = false; continue; }
      const raw = inRequire ? t : t.replace(/^require\s+/, '');
      const m = raw.match(/^([\w./\-]+)\s+v/);
      if (m && m[1].includes('/')) deps.push(m[1].split('/').slice(0, 3).join('/'));
    }
  }

  // Cargo.toml — Rust
  else if (name === 'cargo.toml') {
    let inDeps = false;
    for (const line of content.split('\n')) {
      const t = line.trim();
      if (/^\[(dependencies|dev-dependencies|build-dependencies)\]/.test(t)) { inDeps = true; continue; }
      if (t.startsWith('[')) { inDeps = false; continue; }
      if (inDeps) {
        const m = t.match(/^([\w-]+)\s*=/);
        if (m) deps.push(m[1]);
      }
    }
  }

  // pubspec.yaml — Dart/Flutter
  else if (name === 'pubspec.yaml') {
    let inDeps = false;
    for (const line of content.split('\n')) {
      const t = line.trim();
      if (t === 'dependencies:' || t === 'dev_dependencies:') { inDeps = true; continue; }
      if (/^\S+:/.test(line) && !line.startsWith(' ') && !line.startsWith('\t')) { inDeps = false; continue; }
      if (inDeps) {
        const m = t.match(/^([\w_-]+):/);
        if (m && m[1] !== 'flutter' && m[1] !== 'sdk') deps.push(m[1]);
      }
    }
  }

  // mix.exs — Elixir
  else if (name === 'mix.exs') {
    const skip = new Set(['ok', 'error', 'app', 'version', 'elixir', 'deps', 'env', 'test', 'dev']);
    for (const m of content.matchAll(/\{:([\w_]+),\s*["'~^><=]/g)) {
      if (!skip.has(m[1])) deps.push(m[1]);
    }
  }

  // Package.swift — Swift Package Manager
  else if (name === 'package.swift') {
    for (const m of content.matchAll(/\.package\s*\(.*?url:\s*"[^"]*\/([^"/]+?)(?:\.git)?"/g)) deps.push(m[1]);
    for (const m of content.matchAll(/\.product\s*\(\s*name:\s*"([^"]+)"/g)) deps.push(m[1]);
  }

  // cpanfile — Perl
  else if (name === 'cpanfile') {
    for (const line of content.split('\n')) {
      const m = line.trim().match(/^requires?\s+['"]?([\w:]+)/);
      if (m) deps.push(m[1]);
    }
  }

  // *.vbp — VB6 project file (Reference= COM libs, Object= OCX controls)
  else if (filePath.endsWith('.vbp')) {
    for (const line of content.split('\n')) {
      const t = line.trim();
      // Reference=*\G{CLSID}#version#lcid#path#description
      const refMatch = t.match(/^Reference=.*#([^#\r\n]+)$/);
      if (refMatch) { const desc = refMatch[1].trim(); if (desc) deps.push(desc); continue; }
      // Object={CLSID}#version#description; filename.ocx
      const objMatch = t.match(/^Object=\{[^}]+\}#[\d.]+#([^;#\r\n]+)/);
      if (objMatch) { const desc = objMatch[1].trim(); if (desc) deps.push(desc); }
    }
  }

  // *.csproj / *.vbproj — C# / VB.NET (MSBuild)
  else if (filePath.endsWith('.csproj') || filePath.endsWith('.vbproj')) {
    for (const m of content.matchAll(/PackageReference\s+Include="([^"]+)"/g)) deps.push(m[1]);
  }

  // packages.config — older C# NuGet
  else if (name === 'packages.config') {
    for (const m of content.matchAll(/package\s+id="([^"]+)"/g)) deps.push(m[1]);
  }

  // Directory.Packages.props / Directory.Build.props — centralized NuGet
  else if (name === 'directory.packages.props' || name === 'directory.build.props') {
    for (const m of content.matchAll(/PackageVersion\s+Include="([^"]+)"/g)) deps.push(m[1]);
  }

  // shard.yml — Crystal
  else if (name === 'shard.yml') {
    let inDeps = false;
    for (const line of content.split('\n')) {
      const t = line.trim();
      if (t === 'dependencies:' || t === 'development_dependencies:') { inDeps = true; continue; }
      if (/^\S/.test(line) && t.endsWith(':') && !['dependencies:', 'development_dependencies:'].includes(t)) { inDeps = false; continue; }
      if (inDeps) {
        const m = t.match(/^([\w_-]+):/);
        if (m) deps.push(m[1]);
      }
    }
  }

  // project.clj — Clojure/Leiningen
  else if (name === 'project.clj') {
    for (const m of content.matchAll(/\[([\w.-]+\/[\w.-]+)\s+"[^"]+"\]/g)) deps.push(m[1]);
  }

  // deps.edn — Clojure/deps.edn
  else if (name === 'deps.edn') {
    for (const m of content.matchAll(/([\w.-]+\/[\w.-]+)\s*\{/g)) deps.push(m[1]);
  }

  // *.cabal — Haskell
  else if (filePath.endsWith('.cabal')) {
    let inBuildDeps = false;
    for (const line of content.split('\n')) {
      const t = line.trim();
      if (/^build-depends:/i.test(t)) { inBuildDeps = true; }
      if (inBuildDeps) {
        const pkgs = t.replace(/^build-depends:\s*/i, '').split(',');
        for (const pkg of pkgs) {
          const pname = pkg.trim().split(/[\s>=<!]/)[0];
          if (pname && /^[\w-]+$/.test(pname)) deps.push(pname);
        }
        if (!t.endsWith(',') && !t.endsWith('\\')) inBuildDeps = false;
      }
    }
  }

  // *.fsproj — F# (MSBuild, same PackageReference format as C#)
  else if (filePath.endsWith('.fsproj')) {
    for (const m of content.matchAll(/PackageReference\s+Include="([^"]+)"/g)) deps.push(m[1]);
  }

  // paket.dependencies — F# / .NET Paket
  else if (name === 'paket.dependencies') {
    for (const line of content.split('\n')) {
      const m = line.trim().match(/^nuget\s+([\w.-]+)/i);
      if (m) deps.push(m[1]);
    }
  }

  // Project.toml — Julia
  else if (name === 'project.toml') {
    let inDeps = false;
    for (const line of content.split('\n')) {
      const t = line.trim();
      if (t === '[deps]') { inDeps = true; continue; }
      if (t.startsWith('[')) { inDeps = false; continue; }
      if (inDeps) {
        const m = t.match(/^([\w_]+)\s*=/);
        if (m) deps.push(m[1]);
      }
    }
  }

  // *.nimble — Nim
  else if (filePath.endsWith('.nimble')) {
    for (const line of content.split('\n')) {
      const m = line.trim().match(/^requires\s+"([\w_-]+)/i);
      if (m) deps.push(m[1]);
    }
  }

  // build.zig.zon — Zig
  else if (name === 'build.zig.zon') {
    for (const m of content.matchAll(/\.([\w_]+)\s*=\s*\.dependency\s*\(/g)) deps.push(m[1]);
  }

  // v.mod — V lang
  else if (name === 'v.mod') {
    for (const line of content.split('\n')) {
      const m = line.trim().match(/^deps:\s*\[|'([\w.-]+)'/);
      if (m?.[1]) deps.push(m[1]);
    }
    for (const m of content.matchAll(/'([\w./-]+)'/g)) deps.push(m[1]);
  }

  // rebar.config — Erlang
  else if (name === 'rebar.config') {
    for (const m of content.matchAll(/\{([\w_]+)\s*,\s*"[^"]*"/g)) deps.push(m[1]);
  }

  // *.app.src — Erlang OTP application
  else if (filePath.endsWith('.app.src')) {
    let inApps = false;
    for (const line of content.split('\n')) {
      const t = line.trim();
      if (t.includes('applications')) { inApps = true; continue; }
      if (inApps) {
        const m = t.match(/\b([\w_]+)\b/g);
        if (m) { deps.push(...m.filter(d => !['kernel', 'stdlib'].includes(d))); }
        if (t.includes(']')) inApps = false;
      }
    }
  }

  // dune-project — OCaml
  else if (name === 'dune-project') {
    for (const m of content.matchAll(/\(depends\s+([\w.-]+)/g)) deps.push(m[1]);
    for (const m of content.matchAll(/\(libraries\s+([\w.-]+)/g)) deps.push(m[1]);
  }

  // *.opam — OCaml OPAM
  else if (filePath.endsWith('.opam')) {
    let inDeps = false;
    for (const line of content.split('\n')) {
      const t = line.trim();
      if (/^depends:/.test(t)) { inDeps = true; }
      if (inDeps) {
        const m = t.match(/^"([\w-]+)"/);
        if (m) deps.push(m[1]);
        if (t === ']') inDeps = false;
      }
    }
  }

  // elm.json — Elm
  else if (name === 'elm.json') {
    try {
      const json = JSON.parse(content);
      deps.push(...Object.keys(json.dependencies ?? {}));
      deps.push(...Object.keys(json['test-dependencies'] ?? {}));
    } catch { /* malformed */ }
  }

  // spago.yaml — PureScript
  else if (name === 'spago.yaml') {
    let inDeps = false;
    for (const line of content.split('\n')) {
      const t = line.trim();
      if (t === 'dependencies:') { inDeps = true; continue; }
      if (inDeps && t.startsWith('-')) { deps.push(t.slice(1).trim().split(/\s/)[0]); continue; }
      if (inDeps && !/^\s/.test(line) && t) inDeps = false;
    }
  }

  // spago.dhall — PureScript (legacy)
  else if (name === 'spago.dhall') {
    for (const m of content.matchAll(/"([\w-]+)"/g)) deps.push(m[1]);
  }

  // foundry.toml — Solidity / Foundry
  else if (name === 'foundry.toml') {
    for (const line of content.split('\n')) {
      const m = line.trim().match(/^([\w-]+)\s*=\s*\{.*version/);
      if (m) deps.push(m[1]);
    }
  }

  // Move.toml — Move / Aptos / Sui
  else if (name === 'move.toml') {
    let inDeps = false;
    for (const line of content.split('\n')) {
      const t = line.trim();
      if (t === '[dependencies]') { inDeps = true; continue; }
      if (t.startsWith('[')) { inDeps = false; continue; }
      if (inDeps) {
        const m = t.match(/^([\w_-]+)\s*=/);
        if (m) deps.push(m[1]);
      }
    }
  }

  // *.hxml — Haxe build file
  else if (filePath.endsWith('.hxml')) {
    for (const line of content.split('\n')) {
      const m = line.trim().match(/^-lib\s+([\w-]+)/);
      if (m) deps.push(m[1]);
    }
  }

  // box.json — ColdFusion / CommandBox
  else if (name === 'box.json') {
    try {
      const json = JSON.parse(content);
      deps.push(...Object.keys(json.dependencies ?? {}));
      deps.push(...Object.keys(json.devDependencies ?? {}));
    } catch { /* malformed */ }
  }

  // vcpkg.json — C++ vcpkg package manager
  else if (name === 'vcpkg.json') {
    try {
      const json = JSON.parse(content);
      for (const dep of (json.dependencies ?? []) as Array<string | { name: string }>) {
        if (typeof dep === 'string') deps.push(dep);
        else if (dep && typeof dep === 'object' && dep.name) deps.push(dep.name);
      }
    } catch { /* malformed */ }
  }

  // conanfile.txt — C++ Conan 1.x
  else if (name === 'conanfile.txt') {
    let inRequires = false;
    for (const line of content.split('\n')) {
      const t = line.trim();
      if (t === '[requires]') { inRequires = true; continue; }
      if (t.startsWith('[')) { inRequires = false; continue; }
      if (inRequires && t && !t.startsWith('#')) {
        const pkg = t.split('/')[0].trim();
        if (pkg) deps.push(pkg);
      }
    }
  }

  // conanfile.py — C++ Conan 2.x
  else if (name === 'conanfile.py') {
    for (const m of content.matchAll(/requires\s*=\s*\[([^\]]+)\]/g)) {
      for (const item of m[1].split(',')) {
        const pkg = item.replace(/['"]/g, '').trim().split('/')[0].trim();
        if (pkg && !pkg.startsWith('#')) deps.push(pkg);
      }
    }
    for (const m of content.matchAll(/self\.requires\s*\(\s*["']([^/"']+)/g)) deps.push(m[1]);
  }

  // CMakeLists.txt — C++ CMake (find_package / FetchContent)
  else if (name === 'cmakelists.txt') {
    for (const m of content.matchAll(/find_package\s*\(\s*([\w-]+)/gi)) deps.push(m[1]);
    for (const m of content.matchAll(/FetchContent_Declare\s*\(\s*([\w-]+)/gi)) deps.push(m[1]);
    for (const m of content.matchAll(/pkg_check_modules\s*\([^)]*\s+([\w-]+)/gi)) deps.push(m[1]);
  }

  // *.dproj / *.groupproj — Delphi MSBuild project (GetIt / NuGet style)
  else if (filePath.endsWith('.dproj') || filePath.endsWith('.groupproj')) {
    for (const m of content.matchAll(/PackageReference\s+Include="([^"]+)"/g)) deps.push(m[1]);
    for (const m of content.matchAll(/<Requires>\s*<Package\s+Name="([^"]+)"/gi)) deps.push(m[1]);
  }

  return [...new Set(deps)].filter(Boolean);
}

// Returns paths from a flat file list that match the manifest patterns for the given language.
export function findManifestPaths(filePaths: string[], lang: string): string[] {
  const patterns = MANIFEST_FILES[lang.toLowerCase()] ?? [];
  return filePaths.filter(p => {
    const fname = (p.split('/').pop() ?? '').toLowerCase();
    return patterns.some((pattern: string) =>
      pattern.startsWith('*') ? fname.endsWith(pattern.slice(1)) : fname === pattern.toLowerCase()
    );
  }).slice(0, 6);
}

export function analyzeSourceFile(path: string, content: string, lang: string): AnalyzedFile {
  // Wizard dialect labels map to canonical analysis rules.
  const effectiveLang = lang === 'cobol_as400' ? 'cobol' : lang;
  const lines = content.split('\n');
  const totalLines = lines.length;
  let blankLines = 0;
  let commentLines = 0;
  let complexity = 1;
  let maxNesting = 0;
  let currentNesting = 0;
  let procedures = 0;
  let sections = 0;
  let externalCallCount = 0;
  const dependencies: string[] = [];

  for (const line of lines) {
    const trimmed = line.trim();
    if (!trimmed) { blankLines++; continue; }

    // ── COBOL ──────────────────────────────────────────────────────────────
    if (effectiveLang === 'cobol') {
      if (line.length > 6 && line[6] === '*') { commentLines++; continue; }
      const upper = trimmed.toUpperCase();
      if (upper.match(/\bIF\b/)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (upper.match(/\bEND-IF\b/)) currentNesting = Math.max(0, currentNesting - 1);
      if (upper.match(/\bEVALUATE\b/)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (upper.match(/\bEND-EVALUATE\b/)) currentNesting = Math.max(0, currentNesting - 1);
      if (upper.match(/\bPERFORM\b.*\bUNTIL\b/)) complexity++;
      if (upper.match(/\bPERFORM\b.*\bVARYING\b/)) complexity++;
      if (upper.match(/\bWHEN\b/)) complexity++;
      if (upper.match(/\bSECTION\s*\./)) sections++;
      if (upper.match(/^\d{0,6}\s+[\w-]+\s*\.\s*$/) && !upper.includes('SECTION')) procedures++;
      // COBOL dependencies (COPY/CALL/CICS/SQL/dataset) are extracted via the typed
      // content scan after this loop — see `cobolDeps` below.

    // ── RPG / RPGLE ────────────────────────────────────────────────────────
    } else if (lang === 'rpg') {
      if (trimmed.startsWith('//') || (line.length > 5 && line[5] === '*')) { commentLines++; continue; }
      const upper = trimmed.toUpperCase();
      if (upper.match(/\bIF\b/)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (upper.match(/\bENDIF\b/)) currentNesting = Math.max(0, currentNesting - 1);
      if (upper.match(/\bDOW\b|\bDOU\b|\bFOR\b/)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (upper.match(/\bENDDO\b|\bENDFOR\b/)) currentNesting = Math.max(0, currentNesting - 1);
      if (upper.match(/\bWHEN\b|\bOTHER\b/)) complexity++;
      const copyMatch = upper.match(/\/COPY\s+(?:[\w,.-]+,)?([\w.-]+)/);
      if (copyMatch) dependencies.push(copyMatch[1].trim());
      const includeMatch = upper.match(/\/INCLUDE\s+(?:[\w,.-]+,)?([\w.-]+)/);
      if (includeMatch) dependencies.push(includeMatch[1].trim());
      const callMatch = upper.match(/\bCALLP?\s+([\w]+)/);
      if (callMatch) { dependencies.push(callMatch[1]); externalCallCount++; }
      if (upper.match(/\bDCL-PROC\b/)) procedures++;
      if (upper.match(/\bBEGSR\b/)) sections++;

    // ── JCL ───────────────────────────────────────────────────────────────
    } else if (lang === 'jcl') {
      if (trimmed.startsWith('//*')) { commentLines++; continue; }
      const upper = trimmed.toUpperCase();
      const execMatch = upper.match(/\/\/\w*\s+EXEC\s+(?:PGM=([\w#$@]+)|PROC=([\w#$@]+)|([\w#$@]+))/);
      if (execMatch) {
        const prog = execMatch[1] ?? execMatch[2] ?? execMatch[3];
        if (prog) { dependencies.push(prog); externalCallCount++; }
      }
      procedures++;

    // ── PL/I ──────────────────────────────────────────────────────────────
    } else if (lang === 'pli') {
      if (trimmed.startsWith('/*') || trimmed.startsWith('*')) { commentLines++; continue; }
      const upper = trimmed.toUpperCase();
      if (upper.match(/\bIF\b/)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (upper.match(/\bEND\s*;/)) currentNesting = Math.max(0, currentNesting - 1);
      if (upper.match(/\bDO\b|\bSELECT\b/)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (upper.match(/\bWHEN\b|\bOTHERWISE\b/)) complexity++;
      const incMatch = upper.match(/%INCLUDE\s+([\w]+)/);
      if (incMatch) dependencies.push(incMatch[1]);
      const callMatch = upper.match(/\bCALL\s+([\w]+)/);
      if (callMatch) { dependencies.push(callMatch[1]); externalCallCount++; }
      if (upper.match(/\bPROCEDURE\b/)) procedures++;

    // ── NATURAL ───────────────────────────────────────────────────────────
    } else if (lang === 'natural') {
      if (trimmed.startsWith('*')) { commentLines++; continue; }
      const upper = trimmed.toUpperCase();
      if (upper.match(/\bIF\b/)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (upper.match(/\bEND-IF\b/)) currentNesting = Math.max(0, currentNesting - 1);
      if (upper.match(/\bFOR\b|\bREPEAT\b/)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (upper.match(/\bEND-FOR\b|\bEND-REPEAT\b/)) currentNesting = Math.max(0, currentNesting - 1);
      if (upper.match(/\bDECIDE\b/)) complexity++;
      const fetchMatch = upper.match(/\bFETCH\s+(?:RETURN\s+)?'?([\w-]+)/);
      if (fetchMatch) { dependencies.push(fetchMatch[1]); externalCallCount++; }
      const callnatMatch = upper.match(/\bCALLNAT\s+'([\w-]+)'/);
      if (callnatMatch) { dependencies.push(callnatMatch[1]); externalCallCount++; }
      const incMatch = upper.match(/\bINCLUDE\s+([\w-]+)/);
      if (incMatch) dependencies.push(incMatch[1]);
      if (upper.match(/\bDEFINE\s+SUBROUTINE\b/)) procedures++;

    // ── FORTRAN ───────────────────────────────────────────────────────────
    } else if (lang === 'fortran') {
      if (trimmed.startsWith('!') || /^[Cc]\s/.test(trimmed)) { commentLines++; continue; }
      const upper = trimmed.toUpperCase();
      if (upper.match(/\bIF\b|\bDO\b/)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (upper.match(/\bEND\s*IF\b|\bEND\s*DO\b|\bENDIF\b|\bENDDO\b/)) currentNesting = Math.max(0, currentNesting - 1);
      if (upper.match(/\bSELECT\s+CASE\b/)) complexity++;
      const useMatch = upper.match(/\bUSE\s+([\w_]+)/);
      if (useMatch) dependencies.push(useMatch[1]);
      const incMatch = upper.match(/\bINCLUDE\s+['"]?([\w._-]+)/);
      if (incMatch && !incMatch[1].startsWith('.')) dependencies.push(incMatch[1]);
      const callMatch = upper.match(/\bCALL\s+([\w_]+)/);
      if (callMatch) { dependencies.push(callMatch[1]); externalCallCount++; }
      if (upper.match(/\bSUBROUTINE\b|\bFUNCTION\b/)) procedures++;
      if (upper.match(/\bMODULE\b/)) sections++;

    // ── ABAP ──────────────────────────────────────────────────────────────
    } else if (lang === 'abap') {
      if (trimmed.startsWith('*')) { commentLines++; continue; }
      const upper = trimmed.toUpperCase();
      if (upper.match(/\bIF\b/)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (upper.match(/\bENDIF\b/)) currentNesting = Math.max(0, currentNesting - 1);
      if (upper.match(/\bLOOP\b|\bWHILE\b/)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (upper.match(/\bENDLOOP\b|\bENDWHILE\b/)) currentNesting = Math.max(0, currentNesting - 1);
      if (upper.match(/\bWHEN\b/)) complexity++;
      const callFuncMatch = upper.match(/\bCALL\s+FUNCTION\s+'([\w-]+)'/);
      if (callFuncMatch) { dependencies.push(callFuncMatch[1]); externalCallCount++; }
      const callTxMatch = upper.match(/\bCALL\s+TRANSACTION\s+'([\w-]+)'/);
      if (callTxMatch) { dependencies.push(callTxMatch[1]); externalCallCount++; }
      const incMatch = upper.match(/\bINCLUDE\s+([\w_]+)/);
      if (incMatch) dependencies.push(incMatch[1]);
      if (upper.match(/\bFORM\b/)) procedures++;
      if (upper.match(/\bCLASS\b.*\bDEFINITION\b/)) sections++;

    // ── MUMPS / M ──────────────────────────────────────────────────────────
    } else if (lang === 'mumps') {
      if (trimmed.startsWith(';')) { commentLines++; continue; }
      const upper = trimmed.toUpperCase();
      if (upper.match(/\bIF\b|\bFOR\b/)) complexity++;
      const doMatch = trimmed.match(/\bDO\s+\^([\w]+)/i);
      if (doMatch) { dependencies.push(doMatch[1]); externalCallCount++; }
      const extrinsicMatch = trimmed.match(/\$\$[\w]+\^([\w]+)/i);
      if (extrinsicMatch) { dependencies.push(extrinsicMatch[1]); externalCallCount++; }

    // ── VB6 / VBA ─────────────────────────────────────────────────────────
    } else if (lang === 'vb6') {
      if (trimmed.startsWith("'") || /^Rem\b/i.test(trimmed)) { commentLines++; continue; }
      const upper = trimmed.toUpperCase();
      if (/\bIF\b/.test(upper)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (/\bEND\s+IF\b/.test(upper)) currentNesting = Math.max(0, currentNesting - 1);
      if (/\bELSEIF\b/.test(upper)) complexity++;
      if (/\bSELECT\s+CASE\b/.test(upper)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (/\bEND\s+SELECT\b/.test(upper)) currentNesting = Math.max(0, currentNesting - 1);
      if (/\bCASE\b/.test(upper) && !/\bSELECT\s+CASE\b/.test(upper)) complexity++;
      if (/\bFOR\b|\bDO\b|\bWHILE\b/.test(upper)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (/\bNEXT\b|\bLOOP\b|\bWEND\b/.test(upper)) currentNesting = Math.max(0, currentNesting - 1);
      if (/\b(?:Sub|Function|Property\s+(?:Get|Set|Let))\b/i.test(trimmed)) procedures++;
      const dep = extractInlineDep('vb6', trimmed);
      if (dep) { dependencies.push(dep); if (/\bCreateObject\b|\bGetObject\b/i.test(trimmed)) externalCallCount++; }

    // ── REXX ──────────────────────────────────────────────────────────────
    } else if (lang === 'rexx') {
      if (trimmed.startsWith('/*') || trimmed.startsWith('--')) { commentLines++; continue; }
      if (trimmed.match(/\bIF\b|\bDO\b/i)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (trimmed.match(/\bEND\b/i)) currentNesting = Math.max(0, currentNesting - 1);
      if (trimmed.match(/\bWHEN\b|\bOTHERWISE\b/i)) complexity++;
      if (trimmed.match(/^[\w!?$@#]+\s*:/)) procedures++;
      const dep = extractInlineDep('rexx', trimmed);
      if (dep) { dependencies.push(dep); externalCallCount++; }

    // ── Ada ───────────────────────────────────────────────────────────────
    } else if (lang === 'ada') {
      if (trimmed.startsWith('--')) { commentLines++; continue; }
      if (trimmed.match(/\bif\b|\bcase\b|\bloop\b|\bfor\b|\bwhile\b/i)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (trimmed.match(/\bend\s+if\b|\bend\s+loop\b|\bend\s+case\b/i)) currentNesting = Math.max(0, currentNesting - 1);
      if (trimmed.match(/\bwhen\b/i)) complexity++;
      if (trimmed.match(/\bprocedure\b|\bfunction\b|\btask\b/i)) procedures++;
      if (trimmed.match(/\bpackage\b/i)) sections++;
      const dep = extractInlineDep('ada', trimmed);
      if (dep) dependencies.push(dep);

    // ── Assembly ──────────────────────────────────────────────────────────
    } else if (lang === 'assembly') {
      if (trimmed.startsWith(';') || trimmed.startsWith('//') || trimmed.startsWith('*')) { commentLines++; continue; }
      if (trimmed.match(/\bj[a-z]{1,3}\b|\bjmp\b|\bbr\b|\bbeq\b|\bbne\b/i)) complexity++;
      if (trimmed.match(/^[\w_.?$@]+\s*:/)) procedures++;
      if (trimmed.match(/\bsection\b|\bsegment\b|\b\.text\b|\b\.data\b/i)) sections++;
      const dep = extractInlineDep('assembly', trimmed);
      if (dep) dependencies.push(dep);

    // ── LotusScript (VB-like) ──────────────────────────────────────────────
    } else if (lang === 'lotusscript') {
      if (trimmed.startsWith("'") || trimmed.startsWith('%REM') || trimmed.startsWith('Rem ')) { commentLines++; continue; }
      const upper = trimmed.toUpperCase();
      if (/\bIF\b/.test(upper)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (/\bEND\s+IF\b/.test(upper)) currentNesting = Math.max(0, currentNesting - 1);
      if (/\bFOR\b|\bDO\b|\bWHILE\b/.test(upper)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (/\bNEXT\b|\bLOOP\b|\bWEND\b/.test(upper)) currentNesting = Math.max(0, currentNesting - 1);
      if (/\bSELECT\b/.test(upper)) complexity++;
      if (/\bSub\b|\bFunction\b|\bProperty\b/i.test(trimmed)) procedures++;
      const dep = extractInlineDep('lotusscript', trimmed);
      if (dep) dependencies.push(dep);

    // ── PowerBuilder ──────────────────────────────────────────────────────
    } else if (lang === 'powerbuilder') {
      if (trimmed.startsWith('//') || trimmed.startsWith('/*')) { commentLines++; continue; }
      const upper = trimmed.toUpperCase();
      if (/\bIF\b/.test(upper)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (/\bEND\s+IF\b/.test(upper)) currentNesting = Math.max(0, currentNesting - 1);
      if (/\bCHOOSE\s+CASE\b/.test(upper)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (/\bEND\s+CHOOSE\b/.test(upper)) currentNesting = Math.max(0, currentNesting - 1);
      if (/\bFOR\b|\bDO\b|\bWHILE\b/.test(upper)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (/\bNEXT\b|\bLOOP\b/.test(upper)) currentNesting = Math.max(0, currentNesting - 1);
      if (/\bFUNCTION\b|\bSUBROUTINE\b|\bEVENT\b/i.test(trimmed)) procedures++;
      const dep = extractInlineDep('powerbuilder', trimmed);
      if (dep) { dependencies.push(dep); externalCallCount++; }

    // ── Progress 4GL / ABL ────────────────────────────────────────────────
    } else if (lang === 'progress') {
      if (trimmed.startsWith('/*') || trimmed.startsWith('//')) { commentLines++; continue; }
      const upper = trimmed.toUpperCase();
      if (/\bIF\b/.test(upper)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (/\bEND\b/.test(upper)) currentNesting = Math.max(0, currentNesting - 1);
      if (/\bFOR\b|\bDO\b|\bREPEAT\b/.test(upper)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (/\bCASE\b/.test(upper)) complexity++;
      if (/\bPROCEDURE\b|\bFUNCTION\b|\bMETHOD\b/i.test(trimmed)) procedures++;
      if (/\bCLASS\b|\bINTERFACE\b/i.test(trimmed)) sections++;
      const dep = extractInlineDep('progress', trimmed);
      if (dep) dependencies.push(dep);

    // ── ColdFusion / CFML ─────────────────────────────────────────────────
    } else if (lang === 'coldfusion') {
      if (trimmed.startsWith('<!---') || trimmed.startsWith('//') || trimmed.startsWith('/*')) { commentLines++; continue; }
      if (/<CFIF\b/i.test(trimmed)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (/<\/CFIF>/i.test(trimmed)) currentNesting = Math.max(0, currentNesting - 1);
      if (/<CFELSEIF\b/i.test(trimmed)) complexity++;
      if (/<CFLOOP\b/i.test(trimmed)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (/<\/CFLOOP>/i.test(trimmed)) currentNesting = Math.max(0, currentNesting - 1);
      if (/<CFSWITCH\b/i.test(trimmed)) complexity++;
      if (/<CFCASE\b/i.test(trimmed)) complexity++;
      if (/<CFFUNCTION\b/i.test(trimmed) || /\bfunction\s+\w+\s*\(/i.test(trimmed)) procedures++;
      if (/<CFCOMPONENT\b/i.test(trimmed) || /\bcomponent\s*\{/i.test(trimmed)) sections++;
      const dep = extractInlineDep('coldfusion', trimmed);
      if (dep) { dependencies.push(dep); externalCallCount++; }

    // ── Clipper / xBase ───────────────────────────────────────────────────
    } else if (lang === 'clipper') {
      if (trimmed.startsWith('//') || trimmed.startsWith('&&') || trimmed.startsWith('*')) { commentLines++; continue; }
      const upper = trimmed.toUpperCase();
      if (/\bIF\b/.test(upper)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (/\bENDIF\b|\bEND\b/.test(upper)) currentNesting = Math.max(0, currentNesting - 1);
      if (/\bDO\s+WHILE\b/.test(upper)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (/\bENDDO\b/.test(upper)) currentNesting = Math.max(0, currentNesting - 1);
      if (/\bFOR\b/.test(upper)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (/\bNEXT\b/.test(upper)) currentNesting = Math.max(0, currentNesting - 1);
      if (/\bCASE\b/.test(upper)) complexity++;
      if (/\bFUNCTION\b|\bPROCEDURE\b/i.test(trimmed)) procedures++;
      const dep = extractInlineDep('clipper', trimmed);
      if (dep) dependencies.push(dep);

    // ── Visual FoxPro ─────────────────────────────────────────────────────
    } else if (lang === 'visualfoxpro') {
      if (trimmed.startsWith('*') || trimmed.startsWith('&&') || trimmed.startsWith('//')) { commentLines++; continue; }
      const upper = trimmed.toUpperCase();
      if (/\bIF\b/.test(upper)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (/\bENDIF\b/.test(upper)) currentNesting = Math.max(0, currentNesting - 1);
      if (/\bDO\s+WHILE\b|\bFOR\b|\bSCAN\b/.test(upper)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (/\bENDDO\b|\bENDFOR\b|\bENDSCAN\b/.test(upper)) currentNesting = Math.max(0, currentNesting - 1);
      if (/\bCASE\b/.test(upper)) complexity++;
      if (/\bFUNCTION\b|\bPROCEDURE\b/i.test(trimmed)) procedures++;
      const dep = extractInlineDep('visualfoxpro', trimmed);
      if (dep) { dependencies.push(dep); externalCallCount++; }

    // ── SAS ───────────────────────────────────────────────────────────────
    } else if (lang === 'sas') {
      if (trimmed.startsWith('*') || trimmed.startsWith('/*')) { commentLines++; continue; }
      const upper = trimmed.toUpperCase();
      if (/\bIF\b/.test(upper)) complexity++;
      if (/\bDO\b/.test(upper)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (/\bEND\b/.test(upper)) currentNesting = Math.max(0, currentNesting - 1);
      if (/\bWHEN\b/.test(upper)) complexity++;
      if (/^%MACRO\b/i.test(trimmed)) procedures++;
      if (/^DATA\b|^PROC\b/i.test(trimmed)) sections++;
      const dep = extractInlineDep('sas', trimmed);
      if (dep) dependencies.push(dep);

    // ── Erlang ────────────────────────────────────────────────────────────
    } else if (lang === 'erlang') {
      if (trimmed.startsWith('%')) { commentLines++; continue; }
      if (trimmed.match(/\bif\b|\bcase\b|\breceive\b|\btry\b/)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (trimmed.match(/\bend\b/)) currentNesting = Math.max(0, currentNesting - 1);
      if (trimmed.match(/^[\w_]+\s*\(/)) procedures++;
      if (trimmed.match(/^-module\b/)) sections++;
      const dep = extractInlineDep('erlang', trimmed);
      if (dep) dependencies.push(dep);

    // ── OCaml / F# (functional, expression-based) ─────────────────────────
    } else if (lang === 'ocaml' || lang === 'fsharp') {
      if (trimmed.startsWith('(*') || trimmed.startsWith('//')) { commentLines++; continue; }
      if (trimmed.match(/\bif\b|\bmatch\b|\bwhen\b|\btry\b/)) complexity++;
      if (trimmed.match(/\blet\s+(?:rec\s+)?[\w_]+\s*[^=]*=/)) procedures++;
      if (trimmed.match(/\bmodule\b|\btype\b/)) sections++;
      const dep = extractInlineDep(lang, trimmed);
      if (dep) dependencies.push(dep);

    // ── Julia ─────────────────────────────────────────────────────────────
    } else if (lang === 'julia') {
      if (trimmed.startsWith('#')) { commentLines++; continue; }
      if (trimmed.match(/\bif\b|\belseif\b|\bfor\b|\bwhile\b/)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (trimmed.match(/^end\b/)) currentNesting = Math.max(0, currentNesting - 1);
      if (trimmed.match(/^function\b|^macro\b/)) procedures++;
      if (trimmed.match(/^module\b|^struct\b/)) sections++;
      const dep = extractInlineDep('julia', trimmed);
      if (dep) dependencies.push(dep);

    // ── Nim ───────────────────────────────────────────────────────────────
    } else if (lang === 'nim') {
      if (trimmed.startsWith('#')) { commentLines++; continue; }
      if (trimmed.match(/\bif\b|\belif\b|\bfor\b|\bwhile\b|\bcase\b/)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (trimmed.match(/\bof\b/)) complexity++;
      if (trimmed.match(/^proc\b|^func\b|^method\b|^iterator\b/)) procedures++;
      if (trimmed.match(/^type\b|^object\b/)) sections++;
      const dep = extractInlineDep('nim', trimmed);
      if (dep) dependencies.push(dep);

    // ── PowerShell ────────────────────────────────────────────────────────
    } else if (lang === 'powershell') {
      if (trimmed.startsWith('#')) { commentLines++; continue; }
      if (trimmed.match(/\bif\b|\belseif\b|\bforeach\b|\bfor\b|\bwhile\b|\bswitch\b/i)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (trimmed === '}') currentNesting = Math.max(0, currentNesting - 1);
      if (trimmed.includes('{') && !trimmed.includes('}')) { currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (trimmed.match(/^function\b/i)) procedures++;
      const dep = extractInlineDep('powershell', trimmed);
      if (dep) dependencies.push(dep);

    // ── Bash / Shell ──────────────────────────────────────────────────────
    } else if (lang === 'bash') {
      if (trimmed.startsWith('#')) { commentLines++; continue; }
      if (trimmed.match(/\bif\b|\bfor\b|\bwhile\b|\bcase\b|\buntil\b/)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (trimmed.match(/^fi\b|^done\b|^esac\b/)) currentNesting = Math.max(0, currentNesting - 1);
      if (trimmed.match(/^\w[\w_]*\s*\(\s*\)/)) procedures++;
      const dep = extractInlineDep('bash', trimmed);
      if (dep) dependencies.push(dep);

    // ── Tcl ───────────────────────────────────────────────────────────────
    } else if (lang === 'tcl') {
      if (trimmed.startsWith('#')) { commentLines++; continue; }
      if (trimmed.match(/\bif\b|\bfor\b|\bforeach\b|\bwhile\b|\bswitch\b/)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (trimmed.match(/\bproc\b/)) procedures++;
      if (trimmed.match(/\bnamespace\b/)) sections++;
      const dep = extractInlineDep('tcl', trimmed);
      if (dep) dependencies.push(dep);

    // ── Scheme / Lisp ─────────────────────────────────────────────────────
    } else if (lang === 'scheme' || lang === 'lisp') {
      if (trimmed.startsWith(';')) { commentLines++; continue; }
      if (trimmed.match(/^\(if\b|\(cond\b|\(when\b|\(unless\b/)) complexity++;
      if (trimmed.match(/^\(do\b|\(loop\b|\(for\b/)) complexity++;
      if (trimmed.match(/^\(define\b|\(defun\b|\(defmethod\b|\(lambda\b/)) procedures++;
      if (trimmed.match(/^\(defpackage\b|\(in-package\b|\(ns\b/)) sections++;
      const dep = extractInlineDep(lang, trimmed);
      if (dep) dependencies.push(dep);

    // ── Elm / PureScript ──────────────────────────────────────────────────
    } else if (lang === 'elm' || lang === 'purescript') {
      if (trimmed.startsWith('--') || trimmed.startsWith('{-')) { commentLines++; continue; }
      if (trimmed.match(/\bif\b|\bcase\b|\bof\b/)) complexity++;
      if (trimmed.match(/^[\w_]+\s*[:=]/)) procedures++;
      if (trimmed.match(/^module\b/)) sections++;
      const dep = extractInlineDep(lang, trimmed);
      if (dep) dependencies.push(dep);

    // ── VHDL ─────────────────────────────────────────────────────────────
    } else if (lang === 'vhdl') {
      if (trimmed.startsWith('--')) { commentLines++; continue; }
      const upper = trimmed.toUpperCase();
      if (/\bIF\b/.test(upper)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (/\bEND\s+IF\b/.test(upper)) currentNesting = Math.max(0, currentNesting - 1);
      if (/\bCASE\b/.test(upper)) { complexity++; currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (/\bEND\s+CASE\b/.test(upper)) currentNesting = Math.max(0, currentNesting - 1);
      if (/\bWHEN\b/.test(upper)) complexity++;
      if (/\bPROCEDURE\b|\bFUNCTION\b/i.test(trimmed)) procedures++;
      if (/\bENTITY\b|\bARCHITECTURE\b|\bPACKAGE\b/i.test(trimmed)) sections++;
      const dep = extractInlineDep('vhdl', trimmed);
      if (dep) dependencies.push(dep);

    // ── Generic / C-style + all other languages ────────────────────────────
    } else {
      const isHashComment = ['python', 'ruby', 'perl', 'r', 'elixir', 'crystal', 'nim', 'julia', 'bash', 'powershell', 'tcl', 'coffeescript', 'sas'].includes(lang);
      if (trimmed.startsWith('//') || trimmed.startsWith('--')) { commentLines++; continue; }
      if (isHashComment && trimmed.startsWith('#')) { commentLines++; continue; }
      if (!isHashComment && trimmed.startsWith('#')) { commentLines++; continue; }

      if (trimmed.match(/\bif\b|\bfor\b|\bwhile\b|\bswitch\b|\bcatch\b|\bcase\b/i)) complexity++;
      if (trimmed.includes('{')) { currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); }
      if (trimmed.includes('}')) currentNesting = Math.max(0, currentNesting - 1);

      const dep = extractInlineDep(lang, trimmed);
      if (dep) dependencies.push(dep);

      if (trimmed.match(/\b(?:function|func|def|sub|method|proc|fn|fun)\b/i)) procedures++;
    }
  }

  // Typed COBOL dependency extraction. CICS/SQL commands span multiple lines, so
  // scan the whole text (the per-line pass only handles complexity/structure).
  const cobolDeps: DepRef[] = [];
  if (effectiveLang === 'cobol') {
    const src = content.toUpperCase();
    for (const m of src.matchAll(/\bCOPY\s+([\w-]+)/g)) cobolDeps.push({ name: m[1], kind: 'copybook' });
    // Static program calls only (literal name); dynamic CALL <var> is intentionally ignored.
    for (const m of src.matchAll(/\bCALL\s+'([\w-]+)'|\bCALL\s+"([\w-]+)"/g)) {
      cobolDeps.push({ name: m[1] ?? m[2], kind: 'called-program' });
    }
    // File/VSAM datasets: SELECT <file> ASSIGN TO <ddname>.
    for (const m of src.matchAll(/\bASSIGN\s+(?:TO\s+)?([\w-]+)/g)) {
      cobolDeps.push({ name: m[1], kind: 'dataset' });
    }
    for (const m of src.matchAll(/EXEC\s+CICS\b[\s\S]*?END-EXEC/g)) {
      const block = m[0];
      const prog = block.match(/\b(?:LINK|XCTL)\b[\s\S]*?PROGRAM\s*\(\s*'?([\w$#@-]+)/);
      if (prog) cobolDeps.push({ name: prog[1], kind: 'cics-program' });
      const mapset = block.match(/MAPSET\s*\(\s*'?([\w$#@-]+)/);
      if (mapset) cobolDeps.push({ name: mapset[1], kind: 'cics-map' });
      const map = block.match(/\bMAP\s*\(\s*'?([\w$#@-]+)/);
      if (map) cobolDeps.push({ name: map[1], kind: 'cics-map' });
    }
    for (const m of src.matchAll(/EXEC\s+SQL\b[\s\S]*?END-EXEC/g)) {
      const block = m[0];
      const inc = block.match(/\bINCLUDE\s+([\w$#@-]+)/);
      if (inc) cobolDeps.push({ name: inc[1], kind: 'copybook' });
      // DB2 tables referenced by embedded SQL (host vars start with ':' and are skipped).
      for (const t of block.matchAll(/\b(?:FROM|INTO|UPDATE|JOIN|DELETE\s+FROM)\s+([A-Z][\w$#@.-]+)/g)) {
        if (t[1] !== 'DUAL') cobolDeps.push({ name: t[1], kind: 'sql-table' });
      }
    }
  }

  // Final typed dependency list: COBOL uses the typed scan; other languages treat
  // every detected import/package as an external library.
  const deps: DepRef[] = effectiveLang === 'cobol'
    ? dedupeDeps(cobolDeps)
    : [...new Set(dependencies)].map((name) => ({ name, kind: 'external-lib' as const }));
  const depNames = deps.map((d) => d.name);
  const callCount = effectiveLang === 'cobol'
    ? deps.filter((d) => d.kind === 'called-program' || d.kind === 'cics-program').length
    : externalCallCount;

  const codeLines = totalLines - blankLines - commentLines;

  let risk: 'high' | 'medium' | 'low' = 'low';
  const { high, medium } = COMPLEXITY_THRESHOLDS;
  if (complexity > high.complexity || maxNesting > high.nestingDepth || codeLines > high.codeLines) risk = 'high';
  else if (complexity > medium.complexity || maxNesting > medium.nestingDepth || codeLines > medium.codeLines) risk = 'medium';

  return {
    path,
    name: path.split('/').pop() || path,
    size: content.length,
    lines: totalLines,
    blankLines,
    commentLines,
    codeLines,
    complexity,
    nestingDepth: maxNesting,
    risk,
    dependencies: depNames,
    deps,
    externalCallCount: callCount,
    procedures,
    sections,
  };
}

// Build the shared dependencies/complexity response fields from analyzed files + manifest deps.
export function buildAnalysisResponse(
  analyzedFiles: AnalyzedFile[],
  manifestDeps: string[],
  totalSourceFiles: number,
) {
  const totalLines = analyzedFiles.reduce((s, f) => s + f.lines, 0);
  const totalCodeLines = analyzedFiles.reduce((s, f) => s + f.codeLines, 0);
  const totalCommentLines = analyzedFiles.reduce((s, f) => s + f.commentLines, 0);
  const totalBlankLines = analyzedFiles.reduce((s, f) => s + f.blankLines, 0);

  const avgComplexity = analyzedFiles.length > 0
    ? analyzedFiles.reduce((s, f) => s + f.complexity, 0) / analyzedFiles.length : 0;
  const maxComplexity = analyzedFiles.length > 0 ? Math.max(...analyzedFiles.map(f => f.complexity)) : 0;
  const avgNesting = analyzedFiles.length > 0
    ? analyzedFiles.reduce((s, f) => s + f.nestingDepth, 0) / analyzedFiles.length : 0;
  const maxNesting = analyzedFiles.length > 0 ? Math.max(...analyzedFiles.map(f => f.nestingDepth)) : 0;
  const totalProcedures = analyzedFiles.reduce((s, f) => s + f.procedures, 0);
  const totalSections = analyzedFiles.reduce((s, f) => s + f.sections, 0);
  const totalExternalCalls = analyzedFiles.reduce((s, f) => s + f.externalCallCount, 0);

  // ── Typed dependency aggregation ────────────────────────────────────────────
  // Manifest packages are always genuine external libraries.
  const manifestTyped: DepRef[] = [...new Set(manifestDeps)].map((name) => ({ name, kind: 'external-lib' as const }));
  const allTyped = dedupeDeps([...analyzedFiles.flatMap((f) => f.deps), ...manifestTyped]);
  const countKind = (k: DepKind) => allTyped.filter((d) => d.kind === k).length;

  const LIST_CAP = 200;
  const total = allTyped.length;
  const list = allTyped.slice(0, LIST_CAP).map((d) => ({ name: d.name, kind: d.kind }));

  // Caller → callee graph (program calls only) + copybook fan-in (impact / ordering).
  const moduleOf = (f: AnalyzedFile) => f.name.replace(/\.\w+$/, '').toUpperCase();
  const edges: { from: string; to: string; kind: DepKind }[] = [];
  for (const f of analyzedFiles) {
    const from = moduleOf(f);
    for (const d of f.deps) {
      if (d.kind === 'called-program' || d.kind === 'cics-program') {
        edges.push({ from, to: d.name.toUpperCase(), kind: d.kind });
      }
    }
  }
  const fanIn = new Map<string, number>();
  for (const f of analyzedFiles) {
    for (const name of new Set(f.deps.filter((d) => d.kind === 'copybook').map((d) => d.name.toUpperCase()))) {
      fanIn.set(name, (fanIn.get(name) ?? 0) + 1);
    }
  }
  const copybookFanIn = [...fanIn.entries()]
    .map(([name, count]) => ({ name, count }))
    .sort((a, b) => b.count - a.count)
    .slice(0, 25);

  // Circular calls: A calls B and B calls A (on the program-call graph).
  const callMap = new Map<string, Set<string>>();
  for (const e of edges) {
    if (!callMap.has(e.from)) callMap.set(e.from, new Set());
    callMap.get(e.from)!.add(e.to);
  }
  let circularDeps = 0;
  for (const [from, tos] of callMap) {
    for (const to of tos) if (callMap.get(to)?.has(from)) circularDeps++;
  }
  circularDeps = Math.floor(circularDeps / 2);

  // A "self-contained" file has no outbound dependencies (not an error — many are).
  const selfContainedFiles = totalSourceFiles - analyzedFiles.filter((f) => f.deps.length > 0).length;

  return {
    // effectiveLoc = code lines only (no blanks/comments) — the basis used for pricing,
    // matching the engine's effLOC cost model. Routes may raise it to cover unanalyzed files.
    lineMetrics: { totalLines, totalCodeLines, totalCommentLines, totalBlankLines, effectiveLoc: totalCodeLines },
    complexity: {
      avgCyclomatic: Math.round(avgComplexity * 10) / 10,
      maxCyclomatic: maxComplexity,
      avgNesting: Math.round(avgNesting * 10) / 10,
      maxNesting,
      totalProcedures,
      totalSections,
      avgLines: analyzedFiles.length > 0 ? Math.round(totalLines / analyzedFiles.length) : 0,
      maxLines: analyzedFiles.length > 0 ? Math.max(...analyzedFiles.map(f => f.lines)) : 0,
    },
    dependencies: {
      total,
      shown: list.length,
      truncated: total > list.length,
      // Per-kind counts (labels now reflect what is actually counted).
      byKind: {
        copybook: countKind('copybook'),
        calledProgram: countKind('called-program'),
        cicsProgram: countKind('cics-program'),
        cicsMap: countKind('cics-map'),
        sqlTable: countKind('sql-table'),
        dataset: countKind('dataset'),
        externalLib: countKind('external-lib'),
      },
      copybooks: countKind('copybook'),
      externalCalls: totalExternalCalls,
      manifestPackages: manifestDeps.length,
      circularDeps,
      // Files with no outbound dependencies (renamed from the misleading "orphanFiles").
      selfContainedFiles,
      orphanFiles: selfContainedFiles,
      list,
      graph: { edges: edges.slice(0, 400), copybookFanIn },
    },
    risk: {
      high: analyzedFiles.filter(f => f.risk === 'high').length,
      medium: analyzedFiles.filter(f => f.risk === 'medium').length,
      low: analyzedFiles.filter(f => f.risk === 'low').length,
      files: analyzedFiles
        .sort((a, b) => b.complexity - a.complexity)
        .slice(0, 20)
        .map(f => ({
          name: f.name, path: f.path, lines: f.lines, codeLines: f.codeLines,
          complexity: f.complexity, nesting: f.nestingDepth, risk: f.risk,
          dependencies: f.dependencies.length, procedures: f.procedures, size: f.size,
        })),
    },
  };
}
