const chokidar = require(`chokidar`);
const _ = require(`lodash`);
const del = require(`del`);
const fs = require(`fs-extra`);
const path = require(`path`);
const findWorkspaceRoot = require(`find-yarn-workspace-root`);

const { publishPackagesLocallyAndInstall } = require(`./local-npm-registry`);
const { checkDepsChanges } = require(`./utils/check-deps-changes`);
const { getDependantPackages } = require(`./utils/get-dependant-packages`);
const {
  setDefaultSpawnStdio,
  promisifiedSpawn,
} = require(`./utils/promisified-spawn`);
const { traversePackagesDeps } = require(`./utils/traverse-package-deps`);

let numCopied = 0;

const quit = () => {
  console.log(`Copied ${numCopied} files`);
  process.exit();
};

const MAX_COPY_RETRIES = 3;

/*
 * non-existent packages break on('ready')
 * See: https://github.com/paulmillr/chokidar/issues/449
 */
async function watch(
  root,
  packages,
  { scanOnce, quiet, forceInstall, monoRepoPackages, localPackages }
) {
  setDefaultSpawnStdio(quiet ? `ignore` : `inherit`);
  // determine if in yarn workspace - if in workspace, force using verdaccio
  // as current logic of copying files will not work correctly.
  const yarnWorkspaceRoot = findWorkspaceRoot();
  if (yarnWorkspaceRoot && process.env.NODE_ENV !== `test`) {
    console.log(`Yarn workspace found.`);
    forceInstall = true;
  }

  let afterPackageInstallation = false;
  let queuedCopies = [];

  const realCopyPath = (arg) => {
    const { oldPath, newPath, quiet, resolve, reject, retry = 0 } = arg;
    fs.copy(oldPath, newPath, (err) => {
      if (err) {
        if (retry >= MAX_COPY_RETRIES) {
          console.error(err);
          reject(err);
          return;
        } else {
          setTimeout(
            () => realCopyPath({ ...arg, retry: retry + 1 }),
            500 * Math.pow(2, retry)
          );
          return;
        }
      }

      // When the medusa binary is copied over, it is not setup with the executable
      // permissions that it is given when installed via yarn.
      // This fixes the issue where after running meduas-dev, running `yarn medusa develop`
      // fails with a permission issue.
      if (/(bin\/meduas.js|medusa(-cli)?\/cli.js)$/.test(newPath)) {
        fs.chmodSync(newPath, `0755`);
      }

      numCopied += 1;
      if (!quiet) {
        console.log(`Copied ${oldPath} to ${newPath}`);
      }
      resolve();
    });
  };

  const copyPath = (oldPath, newPath, quiet, packageName) =>
    new Promise((resolve, reject) => {
      const argObj = { oldPath, newPath, quiet, packageName, resolve, reject };
      if (afterPackageInstallation) {
        realCopyPath(argObj);
      } else {
        queuedCopies.push(argObj);
      }
    });

  const runQueuedCopies = () => {
    afterPackageInstallation = true;
    queuedCopies.forEach((argObj) => realCopyPath(argObj));
    queuedCopies = [];
  };

  const clearJSFilesFromNodeModules = async () => {
    const packagesToClear = queuedCopies.reduce((acc, { packageName }) => {
      if (packageName) {
        acc.add(packageName);
      }
      return acc;
    }, new Set());

    await Promise.all(
      [...packagesToClear].map(
        async (packageToClear) =>
          await del([
            `node_modules/${packageToClear}/**/*.{js,js.map}`,
            `!node_modules/${packageToClear}/node_modules/**/*.{js,js.map}`,
            `!node_modules/${packageToClear}/src/**/*.{js,js.map}`,
          ])
      )
    );
  };
  // check packages deps and if they depend on other packages from monorepo
  // add them to packages list
  const { seenPackages, depTree } = traversePackagesDeps({
    root,
    packages: _.uniq(localPackages),
    monoRepoPackages,
  });

  const allPackagesToWatch = packages
    ? _.intersection(packages, seenPackages)
    : seenPackages;

  const ignoredPackageJSON = new Map();
  const ignorePackageJSONChanges = (packageName, contentArray) => {
    ignoredPackageJSON.set(packageName, contentArray);

    return () => {
      ignoredPackageJSON.delete(packageName);
    };
  };

  if (forceInstall) {
    try {
      if (allPackagesToWatch.length > 0) {
        await publishPackagesLocallyAndInstall({
          packagesToPublish: allPackagesToWatch,
          root,
          localPackages,
          ignorePackageJSONChanges,
          yarnWorkspaceRoot,
        });
      } else {
        // run `yarn`
        const yarnInstallCmd = [`yarn`];

        console.log(`Installing packages from public NPM registry`);
        await promisifiedSpawn(yarnInstallCmd);
        console.log(`Installation complete`);
      }
    } catch (e) {
      console.log(e);
    }

    process.exit();
  }

  if (allPackagesToWatch.length === 0) {
    console.error(`There are no packages to watch.`);
    return;
  }

  const cleanToWatch = allPackagesToWatch.map((pkgName) => {
    if (pkgName.startsWith(`@medusajs`)) {
      return pkgName.split("/")[1];
    }
    return pkgName;
  });

  const ignored = [
    /[/\\]node_modules[/\\]/i,
    /\.git/i,
    /\.DS_Store/,
    /[/\\]__tests__[/\\]/i,
    /[/\\]__mocks__[/\\]/i,
    /\.npmrc/i,
  ].concat(
    cleanToWatch.map((p) => new RegExp(`${p}[\\/\\\\]src[\\/\\\\]`, `i`))
  );
  const watchers = _.uniq(
    cleanToWatch
      .map((p) => path.join(root, `/packages/`, p))
      .filter((p) => fs.existsSync(p))
  );

  let allCopies = [];
  const packagesToPublish = new Set();
  let isInitialScan = true;
  let isPublishing = false;

  const waitFor = new Set();
  let anyPackageNotInstalled = false;

  const watchEvents = [`change`, `add`];

  chokidar
    .watch(watchers, {
      ignored: [(filePath) => _.some(ignored, (reg) => reg.test(filePath))],
    })
    .on(`all`, async (event, filePath) => {
      if (!watchEvents.includes(event)) {
        return;
      }

      const [pack] = filePath
        .split(/packages[/\\]/)
        .pop()
        .split(/[/\\]/);

      const sourcePkg = JSON.parse(
        fs.readFileSync(path.join(root, `/packages/`, pack, `package.json`))
      );
      const packageName = sourcePkg.name;

      const prefix = path.join(root, `/packages/`, pack);

      // Copy it over local version.
      // Don't copy over the medusa bin file as that breaks the NPM symlink.
      if (_.includes(filePath, `dist/medusa-cli.js`)) {
        return;
      }

      const relativePackageFile = path.relative(prefix, filePath);

      const newPath = path.join(
        `./node_modules/${packageName}`,
        relativePackageFile
      );

      if (relativePackageFile === `package.json`) {
        // package.json files will change during publish to adjust version of package (and dependencies), so ignore
        // changes during this process
        if (isPublishing) {
          return;
        }

        // Compare dependencies with local version

        const didDepsChangedPromise = checkDepsChanges({
          newPath,
          packageName,
          monoRepoPackages,
          root,
          isInitialScan,
          ignoredPackageJSON,
        });

        if (isInitialScan) {
          // normally checkDepsChanges would be sync,
          // but because it also can do async GET request
          // to unpkg if local package is not installed
          // keep track of it to make sure all of it
          // finish before installing

          waitFor.add(didDepsChangedPromise);
        }

        const {
          didDepsChanged,
          packageNotInstalled,
        } = await didDepsChangedPromise;

        if (packageNotInstalled) {
          anyPackageNotInstalled = true;
        }

        if (didDepsChanged) {
          if (isInitialScan) {
            waitFor.delete(didDepsChangedPromise);
            // handle dependency change only in initial scan - this is for sure doable to
            // handle this in watching mode correctly - but for the sake of shipping
            // this I limit more work/time consuming edge cases.

            // Dependency changed - now we need to figure out
            // the packages that actually need to be published.
            // If package with changed dependencies is dependency of other
            // medusa package - like for example `medusa-plugin-page-creator`
            // we need to publish both `medusa-plugin-page-creator` and `medusa`
            // and install `medusa` in example site project.
            getDependantPackages({
              packageName,
              depTree,
              packages,
            }).forEach((packageToPublish) => {
              // scheduling publish - we will publish when `ready` is emitted
              // as we can do single publish then
              packagesToPublish.add(packageToPublish);
            });
          }
        }

        // don't ever copy package.json as this will mess up any future dependency
        // changes checks
        return;
      }

      const localCopies = [copyPath(filePath, newPath, quiet, packageName)];

      // If this is from "cache-dir" also copy it into the site's .cache
      if (_.includes(filePath, `cache-dir`)) {
        const newCachePath = path.join(
          `.cache/`,
          path.relative(path.join(prefix, `cache-dir`), filePath)
        );
        localCopies.push(copyPath(filePath, newCachePath, quiet));
      }

      allCopies = allCopies.concat(localCopies);
    })
    .on(`ready`, async () => {
      // wait for all async work needed to be done
      // before publishing / installing
      await Promise.all(Array.from(waitFor));

      if (isInitialScan) {
        isInitialScan = false;
        if (packagesToPublish.size > 0) {
          isPublishing = true;
          await publishPackagesLocallyAndInstall({
            packagesToPublish: Array.from(packagesToPublish),
            root,
            localPackages,
            ignorePackageJSONChanges,
          });
          packagesToPublish.clear();
          isPublishing = false;
        } else if (anyPackageNotInstalled) {
          // run `yarn`
          const yarnInstallCmd = [`yarn`];

          console.log(`Installing packages from public NPM registry`);
          await promisifiedSpawn(yarnInstallCmd);
          console.log(`Installation complete`);
        }

        await clearJSFilesFromNodeModules();
        runQueuedCopies();
      }

      // all files watched, quit once all files are copied if necessary
      Promise.all(allCopies).then(() => {
        if (scanOnce) {
          quit();
        }
      });
    });
}

module.exports = watch;
