#!/usr/bin/env node
import * as yargs from 'yargs';
import { execSync } from 'child_process';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { URL } from 'url';
import { join } from 'path';

import { parse } from 'semver';

import * as version from '@lerna/version/index';
import * as publish from '@lerna/publish/index';

const lernaJsonPath = join(__dirname, '../lerna.json');
const originalLernaJson = readFileSync(lernaJsonPath);

function hideFromGitIndex(uncommittedFiles: string[]) {
  execSync(`git update-index --assume-unchanged ${uncommittedFiles.join(' ')}`);

  return () =>
    execSync(
      `git update-index --no-assume-unchanged ${uncommittedFiles.join(' ')}`
    );
}

(async () => {
  const options = parseArgs();

  if (options.clearLocalRegistry) {
    execSync('yarn local-registry clear');
  }

  const currentLatestVersion = execSync('npm view nx version')
    .toString()
    .trim();

  const parsedVersion = parse(options.version);
  const parsedCurrentLatestVersion = parse(currentLatestVersion);

  const distTag =
    parsedVersion?.prerelease.length > 0
      ? 'next'
      : parsedVersion?.major < parsedCurrentLatestVersion.major
      ? 'previous'
      : 'latest';

  const buildCommand = 'yarn build';
  console.log(`> ${buildCommand}`);
  execSync(buildCommand, {
    stdio: [0, 1, 2],
  });

  if (options.local) {
    updateLernaJsonVersion(currentLatestVersion);
  }

  if (options.local) {
    // Force all projects to be not private
    const projects = JSON.parse(
      execSync('npx lerna list --json --all').toString()
    );
    for (const proj of projects) {
      if (proj.private) {
        console.log('Publishing private package locally:', proj.name);
        const packageJsonPath = join(proj.location, 'package.json');
        const original = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
        writeFileSync(
          packageJsonPath,
          JSON.stringify({ ...original, private: false })
        );
      }
    }
  }

  if (!options.local && process.env.NPM_TOKEN) {
    execSync('npx nx run-many --target=artifacts', {
      stdio: [0, 1, 2],
    });
  }

  const versionOptions = {
    bump: options.version ? options.version : undefined,
    conventionalCommits: true,
    conventionalPrerelease: options.tag === 'next',
    preid: options.preid,
    forcePublish: true,
    createRelease: 'github',
    tagVersionPrefix: '',
    exact: true,
    gitRemote: options.gitRemote,
    gitTagVersion: !process.env.NPM_TOKEN,
    message: 'chore(misc): publish %v',
    loglevel: options.loglevel ?? 'info',
    yes: !!process.env.NPM_TOKEN,
  };

  if (options.local) {
    versionOptions.conventionalCommits = false;
    delete versionOptions.createRelease;
    versionOptions.gitTagVersion = false;
    versionOptions.loglevel = options.loglevel ?? 'error';
    versionOptions.yes = true;
    versionOptions.bump = options.version ? options.version : 'minor';
  }

  if (options.local) {
    /**
     * Hide changes from Lerna
     */
    const uncommittedFiles = execSync('git diff --name-only --relative HEAD .')
      .toString()
      .split('\n')
      .filter((i) => i.length > 0)
      .filter((f) => existsSync(f));
    const unhideFromGitIndex = hideFromGitIndex(uncommittedFiles);

    process.on('exit', unhideFromGitIndex);
    process.on('SIGTERM', unhideFromGitIndex);
    process.on('SIGINT', unhideFromGitIndex);
  }

  const publishOptions: Record<string, boolean | string | undefined> = {
    gitReset: false,
    distTag: distTag,
  };

  if (!options.local && !process.env.NPM_TOKEN) {
    execSync('git status --ahead-behind');

    await version(versionOptions);
    console.log(
      'Check github: https://github.com/nrwl/nx/actions/workflows/publish.yml'
    );
  } else if (!options.skipPublish) {
    await publish({ ...versionOptions, ...publishOptions });
  } else {
    await version(versionOptions);
    console.warn('Not Publishing because --dryRun was passed');
  }

  if (options.local) {
    restoreOriginalLernaJson();
  }
})();

function parseArgs() {
  const parsedArgs = yargs
    .scriptName('yarn nx-release')
    .wrap(144)
    .strictOptions()
    .version(false)
    .command(
      '$0 [version]',
      'This script is for publishing Nx both locally and publically'
    )
    .option('skipPublish', {
      type: 'boolean',
      description: 'Skips the actual publishing for testing out versioning',
    })
    .option('clearLocalRegistry', {
      type: 'boolean',
      description:
        'Clear existing versions in the local registry so that you can republish the same version',
      default: true,
    })
    .option('local', {
      type: 'boolean',
      description: 'Publish Nx locally, not to actual NPM',
      alias: 'l',
      default: true,
    })
    .option('force', {
      type: 'boolean',
      description: "Don't use this unless you really know what it does",
      hidden: true,
    })
    .positional('version', {
      type: 'string',
      description:
        'The version to publish. This does not need to be passed and can be inferred.',
    })
    .option('gitRemote', {
      type: 'string',
      description:
        'Alternate git remote name to publish tags to (useful for testing changelog)',
      default: 'origin',
    })
    .option('loglevel', {
      type: 'string',
      description: 'Log Level',
      choices: ['error', 'info', 'debug'],
    })
    .example(
      '$0',
      `By default, this will locally publish a minor version bump as latest. Great for local development. Most developers should only need this.`
    )
    .example(
      '$0 --local false 2.3.4-beta.0',
      `This will really publish a new version to npm as next.`
    )
    .example(
      '$0 --local false 2.3.4',
      `Given the current latest major version on npm is 2, this will really publish a new version to npm as latest.`
    )
    .example(
      '$0 --local false 1.3.4-beta.0',
      `Given the current latest major version on npm is 2, this will really publish a new version to npm as previous.`
    )
    .group(
      ['local', 'clearLocalRegistry'],
      'Local Publishing Options for most developers'
    )
    .group(
      ['gitRemote', 'force'],
      'Real Publishing Options for actually publishing to NPM'
    )
    .check((args) => {
      const registry = getRegistry();
      const registryIsLocalhost = registry.hostname === 'localhost';
      if (!args.local) {
        if (!process.env.GH_TOKEN) {
          throw new Error('process.env.GH_TOKEN is not set');
        }
        if (!args.force && registryIsLocalhost) {
          throw new Error(
            'Registry is still set to localhost! Run "yarn local-registry disable" or pass --force'
          );
        }
      } else {
        if (!args.force && !registryIsLocalhost) {
          throw new Error('--local was passed and registry is not localhost');
        }
      }

      return true;
    })
    .parseSync();

  parsedArgs.tag ??= parsedArgs.local ? 'latest' : 'next';

  return parsedArgs;
}

function updateLernaJsonVersion(version: string) {
  const json = JSON.parse(readFileSync(lernaJsonPath).toString());

  json.version = version;

  writeFileSync(lernaJsonPath, JSON.stringify(json));
}

function restoreOriginalLernaJson() {
  writeFileSync(lernaJsonPath, originalLernaJson);
}

function getRegistry() {
  return new URL(execSync('npm config get registry').toString().trim());
}
