'use strict'

const BB = require('bluebird')

const fs = BB.promisifyAll(require('graceful-fs'))
const gentlyRm = BB.promisify(require('../../utils/gently-rm.js'))
const log = require('npmlog')
const mkdirp = BB.promisify(require('mkdirp'))
const moduleName = require('../../utils/module-name.js')
const moduleStagingPath = require('../module-staging-path.js')
const move = BB.promisify(require('../../utils/move.js'))
const npa = require('npm-package-arg')
const npm = require('../../npm.js')
const packageId = require('../../utils/package-id.js')
const pacote = require('pacote')
const pacoteOpts = require('../../config/pacote')
const path = require('path')

module.exports = extract
function extract (staging, pkg, log) {
  log.silly('extract', packageId(pkg))
  const up = npm.config.get('unsafe-perm')
  const user = up ? null : npm.config.get('user')
  const group = up ? null : npm.config.get('group')
  const extractTo = moduleStagingPath(staging, pkg)
  const opts = pacoteOpts({
    uid: user,
    gid: group,
    integrity: pkg.package._integrity
  })
  return pacote.extract(
    pkg.package._resolved
    ? npa.resolve(pkg.package.name, pkg.package._resolved)
    : pkg.package._requested,
    extractTo,
    opts
  ).then(() => {
    if (pkg.package.bundleDependencies) {
      return readBundled(pkg, staging, extractTo)
    }
  }).then(() => {
    return gentlyRm(path.join(extractTo, 'node_modules'))
  })
}

function readBundled (pkg, staging, extractTo) {
  return BB.map(pkg.children, (child) => {
    if (child.error) {
      throw child.error
    } else {
      return stageBundledModule(pkg, child, staging, extractTo)
    }
  }, {concurrency: 10})
}

function getTree (pkg) {
  while (pkg.parent) pkg = pkg.parent
  return pkg
}

function warn (pkg, code, msg) {
  const tree = getTree(pkg)
  const err = new Error(msg)
  err.code = code
  tree.warnings.push(err)
}

function stageBundledModule (bundler, child, staging, parentPath) {
  const stageFrom = path.join(parentPath, 'node_modules', child.package.name)
  const stageTo = moduleStagingPath(staging, child)

  return BB.map(child.children, (child) => {
    if (child.error) {
      throw child.error
    } else {
      return stageBundledModule(bundler, child, staging, stageFrom)
    }
  }).then(() => {
    return finishModule(bundler, child, stageTo, stageFrom)
  })
}

function finishModule (bundler, child, stageTo, stageFrom) {
  // If we were the one's who bundled this module…
  if (child.fromBundle === bundler) {
    return mkdirp(path.dirname(stageTo)).then(() => {
      return move(stageFrom, stageTo)
    })
  } else {
    return fs.statAsync(stageFrom).then(() => {
      const bundlerId = packageId(bundler)
      if (!getTree(bundler).warnings.some((w) => {
        return w.code === 'EBUNDLEOVERRIDE'
      })) {
        warn(bundler, 'EBUNDLEOVERRIDE', `${bundlerId} had bundled packages that do not match the required version(s). They have been replaced with non-bundled versions.`)
      }
      log.verbose('bundle', `EBUNDLEOVERRIDE: Replacing ${bundlerId}'s bundled version of ${moduleName(child)} with ${packageId(child)}.`)
      return gentlyRm(stageFrom)
    }, () => {})
  }
}
