#!/usr/bin/env node

/**
 * Module dependencies.
 */

// Interface heavily inspired by the yarn package manager:
// https://yarnpkg.com/

require('./util');
const program = require('commander');
const inquirer = require('inquirer');

// Make things colorful!
require('colors');
const emoji = require('node-emoji');

const fs = require('fs-extra');
const path = require('path');
const dir = path.resolve(__dirname, '..');
const spawn = require('cross-spawn');
const semver = require('semver');
const resolve = require('resolve');
const { plugins, iteratePlugins, isInternal } = require('../plugins');

function existsInNodeModules(name) {
  try {
    resolve.sync(name, { basedir: dir });

    return true;
  } catch (e) {
    return false;
  }
}

function versionMatch(name, version) {
  try {
    let matched = false;

    resolve.sync(name, {
      basedir: dir,
      packageFilter: pkg => {
        if (pkg && pkg.version && semver.satisfies(pkg.version, version)) {
          matched = true;
        }

        return pkg;
      },
    });

    return matched;
  } catch (e) {
    return false;
  }
}

const EXTERNAL = /^\w[a-z\-0-9.]+$/; // Match "react", "path", "fs", "lodash.random", etc.

function reconcilePackages({ quiet = false, upgradeRemote = false }) {
  const fetchable = [];
  const local = [];
  const upgradable = [];

  if (!quiet) {
    console.log();
    console.log(' +local     (l)  packages in your project');
    console.log(' +external  (e)  packages are external');
    console.log(' +outofdate (oe) packages are external but are out of date');
    console.log(' +missing   (m)  packages are not found');
    console.log();
  }

  for (let i in plugins) {
    let section = iteratePlugins(plugins[i]);

    for (let j in section) {
      let { name, version } = section[j];

      let namespaced = name.charAt(0) === '@';
      let dep = name
        .split('/')
        .slice(0, namespaced ? 2 : 1)
        .join('/');

      // Ignore relative modules, which aren't installed by NPM
      if (!dep.match(EXTERNAL) && !namespaced) {
        return;
      }

      if (isInternal(dep)) {
        if (!quiet) {
          console.log(` l  ${name}`);
        }

        local.push({ name, version });
        continue;
      }

      if (!existsInNodeModules(dep)) {
        if (!quiet) {
          console.log(` m  ${name}`);
        }
        fetchable.push({ name, version });
      } else if (!versionMatch(dep, version)) {
        // A plugin was found, yet the current version does not match the
        // current version installed. We should warn if upgradeRemote is
        // not enabled that it is currently not supported.
        if (!upgradeRemote) {
          if (!quiet) {
            console.warn(` oe ${name} (package upgrade may be required)`.bgRed);
          }

          continue;
        }

        console.log(` oe ${name} (package upgrade may be required)`);

        upgradable.push({ name, version });
      } else {
        if (!quiet) {
          console.log(` e  ${name}`);
        }

        if (upgradeRemote) {
          upgradable.push({ name, version });
        }
      }
    }
  }

  if (!quiet) {
    console.log();
  }

  return { local, fetchable, upgradable };
}

async function reconcileRemotePlugins({ dryRun, upgradeRemote }) {
  console.log(`\n['1/2'] ${emoji.get('mag')} Reconciling plugins...`.yellow);
  const { fetchable, upgradable } = reconcilePackages({ upgradeRemote });

  console.log(`['2/2'] ${emoji.get('truck')} Fetching plugins...\n`.yellow);

  if (fetchable.length > 0) {
    console.log(
      `$ yarn add --ignore-scripts --ignore-workspace-root-check ${fetchable.map(
        ({ name, version }) => `${name}@${version}`.cyan
      )}`
    );

    if (!dryRun) {
      let args = [
        'add',
        '--ignore-scripts',
        '--ignore-workspace-root-check',
        ...fetchable.map(({ name, version }) => `${name}@${version}`),
      ];

      let output = spawn.sync('yarn', args, {
        stdio: ['ignore', 'pipe', 'inherit'],
      });

      if (output.status) {
        throw new Error(
          'Could not install external plugins, errors occured during install'
        );
      }

      console.log(output.stdout.toString());
    }
  }

  if (upgradable.length > 0) {
    console.log(
      `$ yarn upgrade ${upgradable.map(
        ({ name, version }) => `${name}@${version}`.cyan
      )}`
    );

    if (!dryRun) {
      let args = [
        'upgrade',
        ...upgradable.map(({ name, version }) => `${name}@${version}`),
      ];

      let output = spawn.sync('yarn', args, {
        stdio: ['ignore', 'pipe', 'inherit'],
      });

      if (output.status) {
        throw new Error(
          'Could not install external plugins, errors occured during install'
        );
      }

      console.log(output.stdout.toString());
    }
  }

  return { upgradable, fetchable };
}

// This traverses the local plugins and installs any dependencies listed there,
// this only is really needed for plugins that are installed via docker because
// core plugins will have their dependencies already included in core.
async function reconcilePluginDeps({ dryRun, upgradeRemote }) {
  try {
    let startTime = new Date();

    // Locate any external plugins and install them.
    const results = await reconcileRemotePlugins({
      dryRun,
      upgradeRemote,
    });

    let status;
    if (dryRun) {
      status = '[dry-run] success'.green;
    } else {
      status = 'success'.green;
    }

    let message;
    if (results.upgradable.length === 0 && results.fetchable.length === 0) {
      message = 'Already up-to-date.';
    } else if (results.upgradable.length === 0) {
      message = `Fetched ${results.fetchable.length} new plugins.`;
    } else if (results.fetchable.length === 0) {
      message = `Upgraded ${results.upgradable.length} new plugins.`;
    } else {
      message = `Fetched ${results.fetchable.length} new plugins, upgraded ${
        results.upgradable.length
      } plugins.`;
    }

    console.log(`\n${status} ${message}`);

    let endTime = new Date();

    let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(
      2
    );
    console.log(`✨ Done in ${totalTime}s.`);
  } catch (err) {
    console.error(err);
    process.exit(1);
  }
}

async function createSeedPlugin() {
  const pluginsDir = path.resolve(__dirname, '..', 'plugins');

  function pluginNameExists(pluginName) {
    const pluginNames = fs.readdirSync(pluginsDir);
    return !!pluginNames.filter(pn => pn === pluginName).length;
  }

  let answers = await inquirer.prompt([
    {
      type: 'input',
      name: 'pluginName',
      message: 'Plugin Name:',
      validate: input => {
        if (pluginNameExists(input)) {
          return 'Please, choose another name. That name already exists';
        }

        if (input && input.length > 0) {
          return true;
        }

        return 'Plugin Name is required.';
      },
    },
    {
      type: 'confirm',
      name: 'server',
      message: 'Is this plugin extending the server capabilities?',
    },
    {
      type: 'confirm',
      name: 'client',
      message: 'Is this plugin extending the client capabilities?',
    },
    {
      type: 'confirm',
      name: 'addPluginsJson',
      message: 'Should we add it to the plugins.json?',
    },
  ]);

  //==============================================================================
  // Creating plugin seed
  //==============================================================================

  const seedPlugin = path.join(__dirname, 'templates/plugin');
  const newPluginPath = path.join(pluginsDir, answers.pluginName);

  if (fs.existsSync(seedPlugin)) {
    if (answers.server && answers.client) {
      // This is a server-side and client-side plugin!, let's copy the template
      fs.copySync(seedPlugin, newPluginPath);
    } else {
      fs.copySync(seedPlugin, newPluginPath, {
        filter: p => {
          // Allowing plugin folder and files with no subfolders
          const rootRx = /plugin$|plugin\/[^/]*(\.).{2,3}/gim;
          if (
            rootRx.test(p) &&
            (fs.lstatSync(p).isDirectory() || fs.lstatSync(p).isFile())
          ) {
            return true;
          }

          // If it's a client-side plugin, copying client folder
          if (answers.client) {
            return /client/.test(p);
          }

          // If it's a server-side plugin, copying server folder
          if (answers.server) {
            return /server/.test(p);
          }
        },
      });
    }

    // Let's add this to the plugins.json
    if (answers.addPluginsJson) {
      const pluginsJson = path.resolve(__dirname, '..', 'plugins.json');

      fs
        .readJson(pluginsJson)
        .then(j => {
          // This is a client-side plugin, let's push this.
          if (answers.client) {
            j.client.push(answers.pluginName);

            const output = JSON.stringify(j, null, 2);
            fs.writeFileSync(pluginsJson, output);
          }

          // This is a server-side plugin, let's push this.
          if (answers.server) {
            j.server.push(answers.pluginName);

            const output = JSON.stringify(j, null, 2);
            fs.writeFileSync(pluginsJson, output);
          }
        })
        .catch(err => {
          console.error(err);
        });
    }

    console.log(
      `✨ Yay! Plugin created! Find your plugin: ${
        answers.pluginName
      } in the ./plugins folder`
    );
  }
}

//==============================================================================
// Setting up the program command line arguments.
//==============================================================================

program
  .command('create')
  .description('creates a seed plugin')
  .action(createSeedPlugin);

program
  .command('list')
  .description('')
  .action(reconcilePackages);

program
  .command('reconcile')
  .description('reconciles dependencies by downloading external plugins')
  .option('-u, --upgrade-remote', 'upgrades remote dependencies')
  .option(
    '-d, --dry-run',
    'does not actually change anything on the filesystem acts only as a simulation'
  )
  .action(reconcilePluginDeps);

program.parse(process.argv);

// If there is no command listed, output help.
if (!process.argv.slice(2).length) {
  program.outputHelp();
}
