text: var path = require('path')
  var assert = require('assert')
  var fs = require('graceful-fs')
  var http = require('http')
  var log = require('npmlog')
  var semver = require('semver')
  var readJson = require('read-package-json')
  var url = require('url')
  var npm = require('../npm.js')
  var deprCheck = require('../utils/depr-check.js')
  var inflight = require('inflight')
  var addRemoteTarball = require('./add-remote-tarball.js')
  var cachedPackageRoot = require('./cached-package-root.js')
  var mapToRegistry = require('../utils/map-to-registry.js')
  var pulseTillDone = require('../utils/pulse-till-done.js')
  var packageId = require('../utils/package-id.js')
  var pickManifestFromRegistryMetadata = require('../utils/pick-manifest-from-registry-metadata.js')
  
  module.exports = addNamed
  
  function getOnceFromRegistry (name, from, next, done) {
    function fixName (err, data, json, resp) {
      // this is only necessary until npm/npm-registry-client#80 is fixed
      if (err && err.pkgid && err.pkgid !== name) {
        err.message = err.message.replace(
          new RegExp(': ' + err.pkgid.replace(/(\W)/g, '\\$1') + '$'),
          ': ' + name
        )
        err.pkgid = name
      }
      next(err, data, json, resp)
    }
  
    mapToRegistry(name, npm.config, function (er, uri, auth) {
      if (er) return done(er)
  
      var key = 'registry:' + uri
      next = inflight(key, next)
      if (!next) return log.verbose(from, key, 'already in flight; waiting')
      else log.verbose(from, key, 'not in flight; fetching')
  
      npm.registry.get(uri, { auth: auth }, pulseTillDone('fetchRegistry', fixName))
    })
  }
  
  function addNamed (name, version, data, cb_) {
    assert(typeof name === 'string', 'must have module name')
    assert(typeof cb_ === 'function', 'must have callback')
  
    var key = name + '@' + version
    log.silly('addNamed', key)
  
    function cb (er, data) {
      if (data && !data._fromHosted) data._from = key
      cb_(er, data)
    }
  
    if (semver.valid(version, true)) {
      log.verbose('addNamed', JSON.stringify(version), 'is a plain semver version for', name)
      addNameVersion(name, version, data, cb)
    } else if (semver.validRange(version, true)) {
      log.verbose('addNamed', JSON.stringify(version), 'is a valid semver range for', name)
      addNameRange(name, version, data, cb)
    } else {
      log.verbose('addNamed', JSON.stringify(version), 'is being treated as a dist-tag for', name)
      addNameTag(name, version, data, cb)
    }
  }
  
  function addNameTag (name, tag, data, cb) {
    log.info('addNameTag', [name, tag])
    var explicit = true
    if (!tag) {
      explicit = false
      tag = npm.config.get('tag')
    }
  
    getOnceFromRegistry(name, 'addNameTag', next, cb)
  
    function next (er, data, json, resp) {
      if (!er) er = errorResponse(name, resp)
      if (er) return cb(er)
  
      log.silly('addNameTag', 'next cb for', name, 'with tag', tag)
  
      engineFilter(data)
      if (data['dist-tags'] && data['dist-tags'][tag] &&
          data.versions[data['dist-tags'][tag]]) {
        var ver = data['dist-tags'][tag]
        return addNamed(name, ver, data.versions[ver], cb)
      }
      if (!explicit && Object.keys(data.versions).length) {
        return addNamed(name, '*', data, cb)
      }
  
      er = installTargetsError(tag, data)
      return cb(er)
    }
  }
  
  function engineFilter (data) {
    var npmv = npm.version
    var nodev = npm.config.get('node-version')
    var strict = npm.config.get('engine-strict')
  
    if (!nodev || npm.config.get('force')) return data
  
    Object.keys(data.versions || {}).forEach(function (v) {
      var eng = data.versions[v].engines
      if (!eng) return
      if (!strict) return
      if (eng.node && !semver.satisfies(nodev, eng.node, true) ||
          eng.npm && !semver.satisfies(npmv, eng.npm, true)) {
        delete data.versions[v]
      }
    })
  }
  
  function addNameVersion (name, v, data, cb) {
    var ver = semver.valid(v, true)
    if (!ver) return cb(new Error('Invalid version: ' + v))
  
    var response
  
    if (data) {
      response = null
      return next()
    }
  
    getOnceFromRegistry(name, 'addNameVersion', setData, cb)
  
    function setData (er, d, json, resp) {
      if (!er) {
        er = errorResponse(name, resp)
      }
      if (er) return cb(er)
      data = d && d.versions[ver]
      if (!data) {
        er = new Error('version not found: ' + name + '@' + ver)
        er.package = name
        er.statusCode = 404
        return cb(er)
      }
      response = resp
      next()
    }
  
    function next () {
      deprCheck(data)
      var dist = data.dist
  
      if (!dist) return cb(new Error('No dist in ' + packageId(data) + ' package'))
  
      if (!dist.tarball) {
        return cb(new Error(
        'No dist.tarball in ' + packageId(data) + ' package'
        ))
      }
  
      if ((response && response.statusCode !== 304) || npm.config.get('force')) {
        return fetchit()
      }
  
      // we got cached data, so let's see if we have a tarball.
      var pkgroot = cachedPackageRoot({ name: name, version: ver })
      var pkgtgz = path.join(pkgroot, 'package.tgz')
      var pkgjson = path.join(pkgroot, 'package', 'package.json')
      fs.stat(pkgtgz, function (er) {
        if (!er) {
          readJson(pkgjson, function (er, data) {
            if (er && er.code !== 'ENOENT' && er.code !== 'ENOTDIR') return cb(er)
  
            if (data) {
              if (!data.name) return cb(new Error('No name provided'))
              if (!data.version) return cb(new Error('No version provided'))
  
              // check the SHA of the package we have, to ensure it wasn't installed
              // from somewhere other than the registry (eg, a fork)
              if (data._shasum && dist.shasum && data._shasum !== dist.shasum) {
                return fetchit()
              }
            }
  
            if (er) return fetchit()
              else return cb(null, data)
          })
        } else return fetchit()
      })
  
      function fetchit () {
        mapToRegistry(name, npm.config, function (er, _, auth, ruri) {
          if (er) return cb(er)
  
          // Use the same protocol as the registry.  https registry --> https
          // tarballs, but only if they're the same hostname, or else detached
          // tarballs may not work.
          var tb = url.parse(dist.tarball)
          var rp = url.parse(ruri)
          if (tb.hostname === rp.hostname && tb.protocol !== rp.protocol) {
            tb.protocol = rp.protocol
            // If a different port is associated with the other protocol
            // we need to update that as well
            if (rp.port !== tb.port) {
              tb.port = rp.port
              delete tb.host
            }
            delete tb.href
          }
          tb = url.format(tb)
  
          // Only add non-shasum'ed packages if --forced. Only ancient things
          // would lack this for good reasons nowadays.
          if (!dist.shasum && !npm.config.get('force')) {
            return cb(new Error('package lacks shasum: ' + packageId(data)))
          }
  
          addRemoteTarball(tb, data, dist.shasum, auth, cb)
        })
      }
    }
  }
  
  function addNameRange (name, range, data, cb) {
    range = semver.validRange(range, true)
    if (range === null) {
      return cb(new Error(
        'Invalid version range: ' + range
      ))
    }
  
    log.silly('addNameRange', { name: name, range: range, hasData: !!data })
  
    if (data) return next()
  
    getOnceFromRegistry(name, 'addNameRange', setData, cb)
  
    function setData (er, d, json, resp) {
      if (!er) {
        er = errorResponse(name, resp)
      }
      if (er) return cb(er)
      data = d
      next()
    }
  
    function next () {
      log.silly(
        'addNameRange',
        'number 2', { name: name, range: range, hasData: !!data }
      )
      engineFilter(data)
  
      log.silly('addNameRange', 'versions'
               , [data.name, Object.keys(data.versions || {})])
  
      var versions = Object.keys(data.versions).filter(function (v) { return semver.valid(v) })
      var picked = pickManifestFromRegistryMetadata(range, npm.config.get('tag'), versions, data)
      if (picked) return addNamed(name, picked.resolvedTo, picked.manifest, cb)
      return cb(installTargetsError(range, data))
    }
  }
  
  function installTargetsError (requested, data) {
    var targets = Object.keys(data['dist-tags']).filter(function (f) {
      return (data.versions || {}).hasOwnProperty(f)
    }).concat(Object.keys(data.versions || {}))
  
    requested = data.name + (requested ? "@'" + requested + "'" : '')
  
    targets = targets.length
            ? 'Valid install targets:\n' + targets.join(', ') + '\n'
            : 'No valid targets found.\n' +
              'Perhaps not compatible with your version of node?'
  
    var er = new Error('No compatible version found: ' + requested + '\n' + targets)
    er.code = 'ETARGET'
    return er
  }
  
  function errorResponse (name, response) {
    var er
    if (response.statusCode >= 400) {
      er = new Error(http.STATUS_CODES[response.statusCode])
      er.statusCode = response.statusCode
      er.code = 'E' + er.statusCode
      er.pkgid = name
    }
    return er
  }
  