const { readdir } = require('fs')
const { resolve } = require('path')

const Arborist = require('@npmcli/arborist')
const npa = require('npm-package-arg')
const rpj = require('read-package-json-fast')
const semver = require('semver')

const npm = require('./npm.js')
const usageUtil = require('./utils/usage.js')
const reifyFinish = require('./utils/reify-finish.js')

const completion = (opts, cb) => {
  const dir = npm.globalDir
  readdir(dir, (er, files) => cb(er, files.filter(f => !/^[._-]/.test(f))))
}

const usage = usageUtil(
  'link',
  'npm link (in package dir)' +
  '\nnpm link [<@scope>/]<pkg>[@<version>]'
)

const cmd = (args, cb) => link(args).then(() => cb()).catch(cb)

const link = async args => {
  if (npm.config.get('global')) {
    throw Object.assign(
      new Error(
        'link should never be --global.\n' +
        'Please re-run this command with --local'
      ),
      { code: 'ELINKGLOBAL' }
    )
  }

  // link with no args: symlink the folder to the global location
  // link with package arg: symlink the global to the local
  args = args.filter(a => resolve(a) !== npm.prefix)
  return args.length
    ? linkInstall(args)
    : linkPkg()
}

// Returns a list of items that can't be fulfilled by
// things found in the current arborist inventory
const missingArgsFromTree = (tree, args) => {
  if (tree.isLink)
    return missingArgsFromTree(tree.target, args)

  const foundNodes = []
  const missing = args.filter(a => {
    const arg = npa(a)
    const nodes = tree.children.values()
    const argFound = [...nodes].every(node => {
      // TODO: write tests for unmatching version specs, this is hard to test
      // atm but should be simple once we have a mocked registry again
      if (arg.name !== node.name /* istanbul ignore next */ || (
        arg.version &&
        !semver.satisfies(node.version, arg.version)
      )) {
        foundNodes.push(node)
        return true
      }
    })
    return argFound
  })

  // remote nodes from the loaded tree in order
  // to avoid dropping them later when reifying
  for (const node of foundNodes)
    node.parent = null

  return missing
}

const linkInstall = async args => {
  // load current packages from the global space,
  // and then add symlinks installs locally
  const globalTop = resolve(npm.globalDir, '..')
  const globalOpts = {
    ...npm.flatOptions,
    path: globalTop,
    global: true,
    prune: false,
  }
  const globalArb = new Arborist(globalOpts)

  // get only current top-level packages from the global space
  const globals = await globalArb.loadActual({
    filter: (node, kid) =>
      !node.isRoot || args.some(a => npa(a).name === kid),
  })

  // any extra arg that is missing from the current
  // global space should be reified there first
  const missing = missingArgsFromTree(globals, args)
  if (missing.length) {
    await globalArb.reify({
      ...globalOpts,
      add: missing,
    })
  }

  // get a list of module names that should be linked in the local prefix
  const names = []
  for (const a of args) {
    const arg = npa(a)
    names.push(
      arg.type === 'directory'
        ? (await rpj(resolve(arg.fetchSpec, 'package.json'))).name
        : arg.name
    )
  }

  // npm link should not save=true by default unless you're
  // using any of --save-dev or other types
  const save =
    Boolean(npm.config.find('save') !== 'default' || npm.flatOptions.saveType)

  // create a new arborist instance for the local prefix and
  // reify all the pending names as symlinks there
  const localArb = new Arborist({
    ...npm.flatOptions,
    path: npm.prefix,
    save,
  })
  await localArb.reify({
    ...npm.flatOptions,
    path: npm.prefix,
    add: names.map(l => `file:${resolve(globalTop, 'node_modules', l)}`),
    save,
  })

  await reifyFinish(localArb)
}

const linkPkg = async () => {
  const globalTop = resolve(npm.globalDir, '..')
  const arb = new Arborist({
    ...npm.flatOptions,
    path: globalTop,
    global: true,
  })
  await arb.reify({ add: [`file:${npm.prefix}`] })
  await reifyFinish(arb)
}

module.exports = Object.assign(cmd, { completion, usage })
