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')

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 || {})])

    // if the tagged version satisfies, then use that.
    var tagged = data['dist-tags'][npm.config.get('tag')]
    if (tagged &&
        data.versions[tagged] &&
        semver.satisfies(tagged, range, true)) {
      return addNamed(name, tagged, data.versions[tagged], cb)
    }

    // find the max satisfying version.
    var versions = Object.keys(data.versions || {})
    var ms = semver.maxSatisfying(versions, range, true)
    if (!ms) {
      if (range === '*' && versions.length) {
        return addNameTag(name, 'latest', data, cb)
      } else {
        return cb(installTargetsError(range, data))
      }
    }

    // if we don't have a registry connection, try to see if
    // there's a cached copy that will be ok.
    addNamed(name, ms, data.versions[ms], cb)
  }
}

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
}
