#!/usr/bin/env node

/**
 * We need to run a platform-specific `turbo`. The dependency _should_
 * have already been installed, but it's possible that it has not.
 */

const child_process = require('child_process');
const fs = require('fs');
const path = require('path');

// If we do not find the correct platform binary, should we attempt to install it?
const SHOULD_INSTALL = true;

// If we do not find the correct platform binary, should we trust calling an emulated variant?
const SHOULD_ATTEMPT_EMULATED = true;

// Relies on the fact that each tarball publishes the `package.json`.
// We can simply cd into the `turbo` directory and install there.
function installUsingNPM() {
  const turboPath = path.dirname(require.resolve('turbo/package'));

  // Erase "npm_config_global" so that "npm install --global turbo" works.
  // Otherwise this nested "npm install" will also be global, and the install
  // will deadlock waiting for the global installation lock.
  const env = { ...process.env, npm_config_global: undefined };

  child_process.execSync(
    `npm install --loglevel=error --prefer-offline --no-audit --progress=false`,
    { cwd: turboPath, stdio: "pipe", env }
  );
}

// npm has multiple places that we need to look for a package name.
function hasPackage(sourceObject, packageName) {
  return !!sourceObject[packageName] || !!sourceObject[`node_modules/${packageName}`];
}

// This provides logging messages as it progresses towards calculating the binary path.
function getBinaryPath() {
  // First we see if the user has configured a particular binary path.
  const TURBO_BINARY_PATH = process.env.TURBO_BINARY_PATH;
  if (TURBO_BINARY_PATH) {
    if (!fs.existsSync(TURBO_BINARY_PATH)) {
      console.error(`Turborepo was unable to find the executable specified by TURBO_BINARY_PATH:\n${TURBO_BINARY_PATH}`);
      console.error();
      console.error(`TURBO_BINARY_PATH is intended for development use-cases. You likely want to unset the environment variable.`);
      process.exit(1);
    } else {
      return TURBO_BINARY_PATH;
    }
  }

  const availablePlatforms = [
    'darwin',
    'linux',
    'windows',
  ];

  const availableArchs = [
    '64',
    'arm64',
  ];

  // We need to figure out which binary to hand the user.
  // The only place where the binary can be at this point is `require.resolve`-able
  // relative to this package as it should be installed as an optional dependency.

  let { platform, arch } = process;
  // Node uses `win32` but we want to use `windows` for consistency with
  // our package naming conventions (i.e. `turbo-windows-64`).
  if (platform === "win32") {
    platform = "windows";
  }
  const resolvedArch = arch === 'x64' ? '64' : arch;
  const ext = platform === 'windows' ? '.exe' : '';

  // Try all places in order until we get a hit.

  // 1. The package which contains the binary we _should_ be running.
  const correctBinary = availablePlatforms.includes(platform) && availableArchs.includes(resolvedArch) ? `turbo-${platform}-${resolvedArch}/bin/turbo${ext}` : null;
  if (correctBinary !== null) {
    try {
      return require.resolve(`${correctBinary}`);
    } catch (e) {}
  }

  // 2. Install the binary that they need just in time.
  if (SHOULD_INSTALL && correctBinary !== null) {
    console.warn('Turborepo did not find the correct binary for your platform.');
    console.warn('We will attempt to install it now.');

    try {
      installUsingNPM();
      const resolvedPath = require.resolve(`${correctBinary}`);
      console.warn('Installation has succeeded.');
      return resolvedPath;
    } catch (e) {
      console.warn('Installation has failed.');
    }
  }

  // 3. Both Windows and macOS ARM boxes can run x64 binaries. Attempt to run under emulation.
  const alternateBinary = (arch === "arm64" && ['darwin', 'windows'].includes(platform)) ? `turbo-${platform}-64/bin/turbo${ext}` : null;
  if (SHOULD_ATTEMPT_EMULATED && alternateBinary !== null) {
    try {
      const resolvedPath = require.resolve(`${alternateBinary}`);
      console.warn(`Turborepo detected that you're running:\n${platform} ${resolvedArch}.`);
      console.warn(`We were not able to find the binary at:\n${correctBinary}`);
      console.warn(`We found a possibly-compatible binary at:\n${alternateBinary}`);
      console.warn(`We will attempt to run that binary.`);
      return resolvedPath;
    } catch (e) {}
  }

  // We are not going to run `turbo` this invocation.
  // Let's give the best error message that we can.

  // Possible error scenarios:
  // - The user is on a platform/arch combination we do not support.
  // - We somehow got detection wrong and never attempted to run the _actual_ `correctBinary` or `alternateBinary`.
  // - The user doesn't have the correct packages installed for their platform.

  // Explain our detection attempt:
  console.error();
  console.error('***');
  console.error();
  console.error(`Turborepo failed to start.`);
  console.error();
  console.error(`Turborepo detected that you are running:\n${platform} ${resolvedArch}`);

  // Tell them if we support their platform at all.
  if (!availablePlatforms.includes(platform)) {
    console.error();
    console.error('Turborepo does not presently support your platform.');
    process.exit(1);
  } else if (!availableArchs.includes(resolvedArch)) {
    if (availablePlatforms.includes(platform)) {
      console.error();
      console.error('Turborepo supports your platform, but does not support your processor architecture.');
      process.exit(1);
    } else {
      console.error();
      console.error('Turborepo does not either of your platform or processor architecture.');
      process.exit(1);
    }
  }

  if (correctBinary !== null) {
    console.error();
    console.error('***');
    console.error();
    console.error(`We were not able to find the binary at:\n${correctBinary}`);
    console.error();
    console.error(`We looked for it at:`);
    console.error(require.resolve.paths(correctBinary).join('\n'));
  }
  if (alternateBinary !== null) {
    console.error();
    console.error('***');
    console.error();
    console.error(`Your platform (${platform}) can sometimes run x86 under emulation.`);
    console.error(`We did not find a possibly-compatible binary at:\n${alternateBinary}`);
    console.error();
    console.error(`We looked for it at:`);
    console.error(require.resolve.paths(alternateBinary).join('\n'));
  }

  // Investigate other failure modes.

  // Has the wrong platform's binaries available.
  const availableBinaries = availablePlatforms.map(platform => availableArchs.map(arch => `turbo-${platform}-${arch}/bin/turbo${platform === 'windows' ? '.exe' : ''}`)).flat();
  const definitelyWrongBinaries = availableBinaries.filter(binary => binary !== correctBinary && binary !== alternateBinary);
  const otherInstalled = definitelyWrongBinaries.filter(binaryPath => {
    try {
      return require.resolve(binaryPath);
    } catch (e) {}
  });

  console.error();
  console.error('***');
  console.error();

  if (otherInstalled.length > 0) {
    console.error('Turborepo checked to see if binaries for another platform are installed.');
    console.error('This typically indicates an error in sharing of pre-resolved node_modules across platforms.');
    console.error('One common reason for this is copying files to Docker.');
    console.error();
    console.error(`We found these unnecessary binaries:`);
    console.error(otherInstalled.join('\n'));
  } else {
    console.error(`We did not find any binaries on this system.`);
    console.error(`This can happen if you run installation with the --no-optional flag.`);
  }

  // Check to see if we have partially-populated dependencies in the npm lockfile.
  const MAX_LOOKUPS = 10;
  const availablePackages = availablePlatforms.map(platform => availableArchs.map(arch => `turbo-${platform}-${arch}`)).flat();

  try {
    // Attempt to find project root.
    const selfPath = require.resolve('turbo/package');

    let previous = null;
    let current = path.join(selfPath, '..', '..', 'package-lock.json');

    for (let i = 0; previous !== current && i < MAX_LOOKUPS; i++) {
      try {
        const lockfile = fs.readFileSync(current);
        const parsedLockfile = JSON.parse(lockfile);

        const sourceObject = parsedLockfile?.dependencies ?? parsedLockfile?.packages ?? {};

        // If we don't show up in the lockfile it's the wrong lockfile.
        if (hasPackage(sourceObject, 'turbo')) {
          // Check to see if all of `turbo-<PLATFORM>-<ARCH>` is included.
          const hasAllPackages = availablePackages.every(package => hasPackage(sourceObject, package));
          if (!hasAllPackages) {
            console.error();
            console.error('***');
            console.error();
            console.error(`Turborepo detected that your lockfile (${current}) does not enumerate all available platforms.`);
            console.error('This is likely a consequence of an npm issue: https://github.com/npm/cli/issues/4828.');

            // Let's build their repair command:
            let version = '';
            let environment = '';
            if (parsedLockfile?.packages[""]?.dependencies?.turbo) {
              version = `@${parsedLockfile.packages[""].dependencies.turbo}`;
              environment = ' --save-prod';
            } else if (parsedLockfile?.packages[""]?.devDependencies?.turbo) {
              version = `@${parsedLockfile.packages[""].devDependencies.turbo}`;
              environment = ' --save-dev';
            } else if (parsedLockfile?.packages[""]?.optionalDependencies?.turbo) {
              version = `@${parsedLockfile.packages[""].optionalDependencies.turbo}`;
              environment = ' --save-optional';
            }

            console.error();
            console.error('To resolve this issue for your repository, run:');
            console.error(`npm install turbo${version} --package-lock-only${environment} && npm install`);
            console.error();
            console.error(`You will need to commit the updated lockfile.`);
          }
          break;
        }
        break;
      } catch (e) {}

      let next = path.join(current, '..', '..', 'package-lock.json');
      previous = current;
      current = next;
    }
  } catch (e) {}

  console.error();
  console.error('***');
  console.error();
  console.error(`If you believe this is an error, please include this message in your report.`);

  process.exit(1);
}

// Run the binary we got.
try {
  child_process.execFileSync(
    getBinaryPath(),
    process.argv.slice(2),
    { stdio: "inherit" }
  );
} catch (e) {
  if (e && e.status) process.exit(e.status);
  throw e;
}
