'use strict';

const fs = require('fs');
const path = require('path');
const { execFile } = require('child_process');

const WALL_E_NATIVE_DEPENDENCIES = Object.freeze([
  'better-sqlite3',
  'sqlite-vec',
  'tree-sitter-bash',
]);

function safeRealpath(value) {
  try { return fs.realpathSync.native ? fs.realpathSync.native(value) : fs.realpathSync(value); } catch {}
  return '';
}

function inspectWalleNodeModules(cwd) {
  const root = path.resolve(cwd);
  const nodeModulesPath = path.join(root, 'node_modules');
  const rootRealpath = safeRealpath(root) || root;
  const expectedRealpath = path.join(rootRealpath, 'node_modules');
  let stat = null;
  try { stat = fs.lstatSync(nodeModulesPath); } catch {}
  const exists = !!stat;
  const symlink = !!(stat && stat.isSymbolicLink());
  const realpath = exists ? (safeRealpath(nodeModulesPath) || nodeModulesPath) : '';
  const ownedByWalleDir = !exists || realpath === expectedRealpath;
  return {
    path: nodeModulesPath,
    realpath,
    exists,
    symlink,
    ownedByWalleDir,
    shared: exists && !ownedByWalleDir,
  };
}

const NATIVE_CHECK_SCRIPT = `
const checked = ${JSON.stringify(WALL_E_NATIVE_DEPENDENCIES)};
function done(ok, payload) {
  const base = {
    ok,
    node: {
      execPath: process.execPath,
      version: process.version,
      modules: process.versions.modules || '',
      platform: process.platform,
      arch: process.arch
    }
  };
  process.stdout.write(JSON.stringify(Object.assign(base, payload || {})));
  process.exit(ok ? 0 : 42);
}
try {
  const Database = require('better-sqlite3');
  const db = new Database(':memory:');
  const quick = db.pragma('quick_check');
  let sqliteVec = 'not_checked';
  let treeSitterBash = 'not_checked';
  try {
    const vec = require('sqlite-vec');
    if (vec && typeof vec.load === 'function') {
      vec.load(db);
      sqliteVec = 'loaded';
    } else {
      sqliteVec = 'present';
    }
  } catch (err) {
    throw new Error('sqlite-vec failed: ' + err.message);
  } finally {
    try { db.close(); } catch {}
  }
  try {
    require('tree-sitter-bash');
    treeSitterBash = 'loaded';
  } catch (err) {
    throw new Error('tree-sitter-bash failed: ' + err.message);
  }
  done(true, {
    checked,
    sqlite: { quick_check: quick && quick[0] ? quick[0].quick_check : 'unknown' },
    sqlite_vec: sqliteVec,
    tree_sitter_bash: treeSitterBash
  });
} catch (err) {
  done(false, {
    checked,
    error: {
      name: err && err.name || 'Error',
      code: err && err.code || '',
      message: err && err.message || String(err || '')
    }
  });
}
`;

function execFileJson(file, args, options = {}) {
  const execFileImpl = options.execFileImpl || execFile;
  return new Promise((resolve) => {
    execFileImpl(file, args, {
      cwd: options.cwd,
      env: options.env || process.env,
      timeout: options.timeoutMs || 5000,
      maxBuffer: 1024 * 1024,
    }, (err, stdout, stderr) => {
      resolve({
        err,
        stdout: String(stdout || ''),
        stderr: String(stderr || ''),
        code: err && typeof err.code !== 'undefined' ? err.code : 0,
        signal: err && err.signal || null,
      });
    });
  });
}

function classifyNativeDependencyError(message, code = '') {
  const text = String(message || '');
  if (/NODE_MODULE_VERSION/i.test(text) || /compiled against a different Node\.js version/i.test(text)) {
    return 'native_dependency_abi_mismatch';
  }
  if (/Cannot find module/i.test(text) || code === 'MODULE_NOT_FOUND') {
    return 'native_dependency_missing';
  }
  if (/ERR_DLOPEN_FAILED|dlopen|invalid ELF|mach-o|was built for/i.test(text) || code === 'ERR_DLOPEN_FAILED') {
    return 'native_dependency_load_failed';
  }
  if (/timed out/i.test(text)) return 'native_dependency_check_timeout';
  return 'native_dependency_failed';
}

function parseJsonOutput(stdout) {
  const text = String(stdout || '').trim();
  if (!text) return null;
  try {
    return JSON.parse(text.split(/\r?\n/).filter(Boolean).pop());
  } catch {
    return null;
  }
}

function normalizeNativeHealth(raw, fallbackMessage = '') {
  const payload = raw && typeof raw === 'object' ? raw : {};
  if (payload.ok) {
    return {
      ok: true,
      code: 'ok',
      message: 'Wall-E native dependencies are loadable.',
      checked: payload.checked || [],
      node: payload.node || {},
      sqlite: payload.sqlite || {},
      sqlite_vec: payload.sqlite_vec || '',
      tree_sitter_bash: payload.tree_sitter_bash || '',
      rebuild_attempted: false,
    };
  }

  const error = payload.error || {};
  const message = error.message || fallbackMessage || 'Wall-E native dependency check failed.';
  const code = classifyNativeDependencyError(message, error.code);
  return {
    ok: false,
    code,
    message,
    checked: payload.checked || WALL_E_NATIVE_DEPENDENCIES,
    node: payload.node || {},
    error: {
      name: error.name || 'Error',
      code: error.code || '',
      message,
    },
    rebuild_hint: `npm rebuild ${WALL_E_NATIVE_DEPENDENCIES.join(' ')} --loglevel=warn`,
    rebuild_attempted: false,
  };
}

async function checkWalleNativeDeps({
  walleDir,
  nodePath = process.execPath,
  execFileImpl = execFile,
  timeoutMs = 5000,
} = {}) {
  const cwd = walleDir || path.resolve(__dirname, '..', '..', 'wall-e');
  const nodeModules = inspectWalleNodeModules(cwd);
  const result = await execFileJson(nodePath, ['-e', NATIVE_CHECK_SCRIPT], {
    cwd,
    execFileImpl,
    timeoutMs,
  });
  const payload = parseJsonOutput(result.stdout);
  if (payload) {
    return {
      ...normalizeNativeHealth(payload, result.stderr || result.err?.message),
      node_modules: nodeModules,
    };
  }
  return {
    ...normalizeNativeHealth(null, result.stderr || result.err?.message || 'Native dependency check produced no JSON output.'),
    node_modules: nodeModules,
  };
}

function shouldRebuildNativeDeps(health) {
  if (health?.node_modules?.shared) return false;
  return Boolean(health && !health.ok && [
    'native_dependency_abi_mismatch',
    'native_dependency_load_failed',
    'native_dependency_missing',
  ].includes(health.code));
}

function dependencyPackagePath(cwd, dependency) {
  return path.join(cwd, 'node_modules', ...dependency.split('/'), 'package.json');
}

function missingNativeDependencies(cwd) {
  return WALL_E_NATIVE_DEPENDENCIES.filter(dep => !fs.existsSync(dependencyPackagePath(cwd, dep)));
}

function normalizeNpmExecPath(value) {
  if (!value) return null;
  const resolved = path.resolve(value);
  if (path.basename(resolved) === 'npx-cli.js') {
    return path.join(path.dirname(resolved), 'npm-cli.js');
  }
  return resolved;
}

function npmCliCandidates(nodePath = process.execPath) {
  const binDir = path.dirname(nodePath);
  return [
    path.join(binDir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
    path.join(binDir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
    path.join(binDir, '..', 'libexec', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
    path.join(binDir, '..', '..', '..', '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
  ].map(candidate => path.resolve(candidate));
}

function resolveNpmRunner({ env = process.env, nodePath = process.execPath } = {}) {
  // Only trust the ambient npm_execpath when resolving npm for the SAME Node that
  // launched this process. When rebuilding native deps for a specific (possibly
  // different) target Node, that Node's own bundled npm must win — the ambient
  // npm_execpath points at whatever npm launched CTM (or the test runner) and
  // would rebuild against the wrong install/ABI.
  let resolvingForCurrentNode = true;
  try {
    resolvingForCurrentNode = path.resolve(nodePath) === path.resolve(process.execPath);
  } catch { resolvingForCurrentNode = true; }
  if (resolvingForCurrentNode) {
    const envPath = normalizeNpmExecPath(env.npm_execpath);
    if (envPath && fs.existsSync(envPath)) return { type: 'node-cli', path: envPath };
  }
  for (const candidate of npmCliCandidates(nodePath)) {
    if (fs.existsSync(candidate)) return { type: 'node-cli', path: candidate };
  }
  return { type: 'bin', path: 'npm' };
}

function npmChildEnv(nodePath = process.execPath) {
  const nodeDir = path.dirname(nodePath);
  return {
    ...process.env,
    PATH: [nodeDir, process.env.PATH || ''].filter(Boolean).join(path.delimiter),
    npm_config_update_notifier: 'false',
  };
}

async function runNpmForNode({
  cwd,
  args,
  nodePath = process.execPath,
  execFileImpl = execFile,
  timeoutMs = 120000,
} = {}) {
  const runner = resolveNpmRunner({ nodePath });
  const env = npmChildEnv(nodePath);
  if (runner.type === 'node-cli') {
    const result = await execFileJson(nodePath, [runner.path, ...args], {
      cwd,
      env,
      execFileImpl,
      timeoutMs,
    });
    return {
      ...result,
      command: `node ${runner.path} ${args.join(' ')}`,
      runner,
    };
  }

  const result = await execFileJson('npm', args, {
    cwd,
    env,
    execFileImpl,
    timeoutMs,
  });
  return {
    ...result,
    command: `npm ${args.join(' ')}`,
    runner,
  };
}

async function rebuildWalleNativeDeps({
  walleDir,
  nodePath = process.execPath,
  execFileImpl = execFile,
  timeoutMs = 120000,
} = {}) {
  const cwd = walleDir || path.resolve(__dirname, '..', '..', 'wall-e');

  // Backstop against the ABI ping-pong: never let a process running under a
  // non-pinned Node major rebuild the shared native binary (that flip is what
  // crash-loops the daemon). The entry guard normally prevents reaching here
  // under the wrong Node; this also covers direct/CLI rebuild callers.
  try {
    const { findPinRoot, readPin } = require('./node-pin-guard');
    const pinRoot = findPinRoot(__dirname);
    const pin = pinRoot ? readPin(pinRoot) : '';
    if (pin && pin.split('.')[0] !== process.versions.node.split('.')[0]) {
      return {
        ok: false,
        code: 'native_dependency_wrong_node_major',
        message: `Refusing to rebuild Wall-E native dependencies under Node v${process.versions.node}: this repo is pinned to v${pin}. Rebuilding here would flip the shared better-sqlite3 ABI and crash-loop the daemon. Re-run under the pinned Node ("$(bash ${path.join(pinRoot, 'bin', 'node-bin.sh')})") or follow the deliberate upgrade procedure.`,
        pinned: pin,
        running: process.versions.node,
      };
    }
  } catch { /* no pin guard available — non-owner/shipped context, proceed */ }

  const nodeModules = inspectWalleNodeModules(cwd);
  if (nodeModules.shared) {
    return {
      ok: false,
      code: 'native_dependency_shared_node_modules',
      message: `Refusing to rebuild Wall-E native dependencies because ${nodeModules.path} resolves outside this Wall-E checkout (${nodeModules.realpath}). Use the same Node runtime as the owner or install isolated Wall-E dependencies in this worktree.`,
      node_modules: nodeModules,
    };
  }

  const attempts = [];
  const missing = missingNativeDependencies(cwd);
  if (missing.length > 0) {
    const install = await runNpmForNode({
      cwd,
      nodePath,
      args: ['install', '--omit=dev', '--loglevel=warn'],
      execFileImpl,
      timeoutMs,
    });
    attempts.push(install);
    if (install.err) {
      const output = `${install.stdout}\n${install.stderr}`.trim();
      return {
        ok: false,
        code: 'native_dependency_install_failed',
        message: output || install.err.message || 'npm install failed',
        command: attempts.map(a => a.command).join(' && '),
        missing,
      };
    }
  }

  const result = await runNpmForNode({
    cwd,
    nodePath,
    args: ['rebuild', ...WALL_E_NATIVE_DEPENDENCIES, '--loglevel=warn'],
    execFileImpl,
    timeoutMs,
  });
  attempts.push(result);
  const output = `${result.stdout}\n${result.stderr}`.trim();
  if (result.err) {
    return {
      ok: false,
      code: 'native_dependency_rebuild_failed',
      message: output || result.err.message || 'npm rebuild failed',
      command: attempts.map(a => a.command).join(' && '),
      missing,
    };
  }
  return {
    ok: true,
    code: 'ok',
    message: output || 'rebuilt dependencies successfully',
    command: attempts.map(a => a.command).join(' && '),
    missing,
  };
}

module.exports = {
  WALL_E_NATIVE_DEPENDENCIES,
  checkWalleNativeDeps,
  classifyNativeDependencyError,
  inspectWalleNodeModules,
  normalizeNativeHealth,
  npmCliCandidates,
  rebuildWalleNativeDeps,
  resolveNpmRunner,
  shouldRebuildNativeDeps,
  _NATIVE_CHECK_SCRIPT: NATIVE_CHECK_SCRIPT,
};
