const cp = require("child_process");
const ora = require("ora");
const path = require("path");
const yargs = require("yargs");
const util = require("util");
const { hashElement } = require("folder-hash");
const promptForOTP = require("./prompt-for-otp");

const PUBLISHABLE_PACKAGES = [
  "babel-plugin-react-compiler",
  "eslint-plugin-react-compiler",
  "react-compiler-healthcheck",
];
const TIME_TO_RECONSIDER = 3_000;

function _spawn(command, args, options, cb) {
  const child = cp.spawn(command, args, options);
  child.on("close", (exitCode) => {
    cb(null, exitCode);
  });
  return child;
}
const spawnHelper = util.promisify(_spawn);

function execHelper(command, options, streamStdout = false) {
  return new Promise((resolve, reject) => {
    const proc = cp.exec(command, options, (error, stdout) =>
      error ? reject(error) : resolve(stdout.trim())
    );
    if (streamStdout) {
      proc.stdout.pipe(process.stdout);
    }
  });
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function getDateStringForCommit(commit) {
  let dateString = await execHelper(
    `git show -s --no-show-signature --format=%cd --date=format:%Y%m%d ${commit}`
  );

  // On CI environment, this string is wrapped with quotes '...'s
  if (dateString.startsWith("'")) {
    dateString = dateString.slice(1, 9);
  }

  return dateString;
}

/**
 * Script for publishing PUBLISHABLE_PACKAGES to npm. By default, this runs in tarball mode, meaning
 * the script will only print out what the contents of the files included in the npm tarball would
 * be.
 *
 * Please run this first (ie `yarn npm:publish`) and double check the contents of the files that
 * will be pushed to npm.
 *
 * If it looks good, you can run `yarn npm:publish --for-real` to really publish to npm. You must
 * have 2FA enabled first and the script will prompt you to enter a 2FA code before proceeding.
 * There's a small annoying delay before the packages are actually pushed to give you time to panic
 * cancel. In this mode, we will bump the version field of each package's package.json, and git
 * commit it. Then, the packages will be published to npm.
 *
 * Optionally, you can add the `--debug` flag to `yarn npm:publish --debug --for-real` to run all
 * steps, but the final npm publish step will have the `--dry-run` flag added to it. This will make
 * the command only report what it would have done, instead of actually publishing to npm.
 */
async function main() {
  const argv = yargs(process.argv.slice(2))
    .option("packages", {
      description: "which packages to publish, defaults to all",
      choices: PUBLISHABLE_PACKAGES,
      default: PUBLISHABLE_PACKAGES,
    })
    .option("for-real", {
      alias: "frfr",
      description:
        "whether to publish to npm (npm publish) or dryrun (npm publish --dry-run)",
      type: "boolean",
      default: false,
    })
    .option("debug", {
      description:
        "If enabled, will always run npm commands in dry run mode irregardless of the for-real flag",
      type: "boolean",
      default: false,
    })
    .help("help")
    .parseSync();

  const { packages, forReal, debug } = argv;

  if (debug === false) {
    const currBranchName = await execHelper("git rev-parse --abbrev-ref HEAD");
    const isPristine = (await execHelper("git status --porcelain")) === "";
    if (currBranchName !== "main" || isPristine === false) {
      throw new Error(
        "This script must be run from the `main` branch with no uncommitted changes"
      );
    }
  }

  let pkgNames = packages;
  if (Array.isArray(packages) === false) {
    pkgNames = [packages];
  }
  const spinner = ora(
    `Preparing to publish ${
      forReal === true ? "(for real)" : "(dry run)"
    } [debug=${debug}]`
  ).info();

  spinner.info("Building packages");
  for (const pkgName of pkgNames) {
    const command = `yarn workspace ${pkgName} run build`;
    spinner.start(`Running: ${command}\n`);
    try {
      await execHelper(command);
    } catch (e) {
      spinner.fail(e.toString());
      throw e;
    }
    spinner.succeed(`Successfully built ${pkgName}`);
  }
  spinner.stop();

  if (forReal === false) {
    spinner.info("Dry run: Report tarball contents");
    for (const pkgName of pkgNames) {
      console.log(`\n========== ${pkgName} ==========\n`);
      spinner.start(`Running npm pack --dry-run\n`);
      try {
        await spawnHelper("npm", ["pack", "--dry-run"], {
          cwd: path.resolve(__dirname, `../../packages/${pkgName}`),
          stdio: "inherit",
        });
      } catch (e) {
        spinner.fail(e.toString());
        throw e;
      }
      spinner.stop(`Successfully packed ${pkgName} (dry run)`);
    }
    spinner.succeed(
      "Please confirm contents of packages before publishing. You can run this command again with --for-real to publish to npm"
    );
  }

  if (forReal === true) {
    const otp = await promptForOTP();
    const commit = await execHelper(
      "git show -s --no-show-signature --format=%h",
      {
        cwd: path.resolve(__dirname, ".."),
      }
    );
    const dateString = await getDateStringForCommit(commit);

    for (const pkgName of pkgNames) {
      const pkgDir = path.resolve(__dirname, `../../packages/${pkgName}`);
      const { hash } = await hashElement(pkgDir, {
        encoding: "hex",
        folders: { exclude: ["node_modules"] },
        files: { exclude: [".DS_Store"] },
      });
      const truncatedHash = hash.slice(0, 7);
      const newVersion = `0.0.0-experimental-${truncatedHash}-${dateString}`;

      spinner.start(`Bumping version: ${pkgName}`);
      try {
        await execHelper(
          `yarn version --new-version ${newVersion} --no-git-tag-version`,
          {
            cwd: pkgDir,
          }
        );
        await execHelper(
          `git add package.json && git commit -m "Bump version to ${newVersion}"`,
          {
            cwd: pkgDir,
          }
        );
      } catch (e) {
        spinner.fail(e.toString());
        throw e;
      }
      spinner.succeed(
        `Bumped ${pkgName} to ${newVersion} and added a git commit`
      );
    }

    if (debug === false) {
      spinner.info(
        `🚨🚨🚨 About to publish to npm in ${
          TIME_TO_RECONSIDER / 1000
        } seconds. You still have time to kill this script!`
      );
      await sleep(TIME_TO_RECONSIDER);
    }

    for (const pkgName of pkgNames) {
      const pkgDir = path.resolve(__dirname, `../../packages/${pkgName}`);
      console.log(`\n========== ${pkgName} ==========\n`);
      spinner.start(`Publishing ${pkgName} to npm\n`);

      const opts = debug === true ? ["publish", "--dry-run"] : ["publish"];
      try {
        await spawnHelper(
          "npm",
          [...opts, "--registry=https://registry.npmjs.org", `--otp=${otp}`],
          {
            cwd: pkgDir,
            stdio: "inherit",
          }
        );
        console.log("\n");
      } catch (e) {
        spinner.fail(e.toString());
        throw e;
      }
      spinner.succeed(`Successfully published ${pkgName} to npm`);
    }

    console.log("\n\n✅ All done, please push version bump commits to GitHub");
  }
}

main();
