
module.exports = get

var fs = require("graceful-fs")
  , assert = require("assert")
  , path = require("path")
  , mkdir = require("mkdirp")
  , chownr = require("chownr")
  , url = require("url")

/**
 * options:
 *
 * timeout: request timeouts
 * follow:  follow redirects
 * staleOk: stale results are OK
 * stat:    results of checking for cached metadata
 * data:    the cached metadata
 */
function get (uri, options, cb) {
  assert(uri, "must have URL to fetch")
  assert(cb, "must have callback")
  if (!options) options = {}

  var parsed = url.parse(uri)
  assert(parsed.protocol, "must have a URL that starts with npm:, http:, or https:")

  var cache = this.cacheFile(uri) + "/.cache.json"

  // /-/all is special.
  // It uses timestamp-based caching and partial updates,
  // because it is a monster.
  if (parsed.pathname === "/-/all") {
    return requestAll.call(this, uri, cache, cb)
  }

  // If the GET is part of a write operation (PUT or DELETE), then
  // skip past the cache entirely, but still save the results.
  if (uri.match(/\?write=true$/)) {
    return get_.call(this, uri, cache, options, cb)
  }

  fs.stat(cache, function (er, stat) {
    if (!er) fs.readFile(cache, function (er, data) {
      try { data = JSON.parse(data) }
      catch (ex) { data = null }
      options.stat = stat
      options.data = data
      get_.call(this, uri, cache, options, cb)
    }.bind(this))
    else {
      get_.call(this, uri, cache, options, cb)
    }
  }.bind(this))
}

function requestAll (uri, cache, cb) {
  this.log.info("get", cache)
  mkdir(path.dirname(cache), function (er) {
    if (er) return cb(er)
    fs.readFile(cache, function (er, data) {
      if (er) return requestAll_.call(this, uri, 0, {}, cache, cb)
      try {
        data = JSON.parse(data)
      } catch (ex) {
        fs.writeFile(cache, "{}", function (er) {
          if (er) return cb(new Error("Broken cache."))
          return requestAll_.call(this, uri, 0, {}, cache, cb)
        }.bind(this))
      }
      var t = +data._updated || 0
      requestAll_.call(this, uri, t, data, cache, cb)
    }.bind(this))
  }.bind(this))
}

function requestAll_ (uri, c, data, cache, cb) {
  // use the cache and update in the background if it's not too old
  if (Date.now() - c < 60000) {
    cb(null, data)
    cb = function () {}
  }

  if (c === 0) {
    this.log.warn("", "Building the local index for the first time, please be patient")
    uri = url.resolve(uri, "/-/all")
  }
  else {
    uri = url.resolve(uri, "/-/all/since?stale=update_after&startkey=" + c)
  }

  this.request('GET', uri, null, function (er, updates, _, res) {
    if (er) return cb(er, data)
    var headers = res.headers
      , updated = updates._updated || Date.parse(headers.date)
    Object.keys(updates).forEach(function (p) {
      data[p] = updates[p]
    })
    data._updated = updated
    fs.writeFile( cache, JSON.stringify(data)
                , function (er) {
      delete data._updated
      return cb(er, data)
    })
  })
}

function get_ (uri, cache, options, cb) {
  var staleOk = options.staleOk === undefined ? false : options.staleOk
    , follow  = options.follow
    , data    = options.data
    , stat    = options.stat
    , etag

  var timeout = options.timeout === undefined ? -1 : options.timeout
  timeout = Math.min(timeout, this.conf.get('cache-max') || 0)
  timeout = Math.max(timeout, this.conf.get('cache-min') || -Infinity)
  if (process.env.COMP_CWORD !== undefined &&
      process.env.COMP_LINE  !== undefined &&
      process.env.COMP_POINT !== undefined) {
    timeout = Math.max(timeout, 60000)
  }

  if (data && data._etag) etag = data._etag

  if (timeout && timeout > 0 && options.stat && options.data) {
    if ((Date.now() - stat.mtime.getTime())/1000 < timeout) {
      this.log.verbose("registry.get", uri, "not expired, no request")
      delete data._etag
      return cb(null, data, JSON.stringify(data), {statusCode:304})
    }
    if (staleOk) {
      this.log.verbose("registry.get", uri, "staleOk, background update")
      delete data._etag
      process.nextTick(cb.bind( null, null, data, JSON.stringify(data)
                              , {statusCode: 304} ))
      cb = function () {}
    }
  }

  this.request('GET', uri, { etag : etag, follow : follow }, function (er, remoteData, raw, response) {
    // if we get an error talking to the registry, but we have it
    // from the cache, then just pretend we got it.
    if (er && cache && data && !data.error) {
      er = null
      response = {statusCode: 304}
    }

    if (response) {
      this.log.silly("registry.get", "cb", [response.statusCode, response.headers])
      if (response.statusCode === 304 && etag) {
        remoteData = data
        this.log.verbose("etag", uri+" from cache")
      }
    }

    data = remoteData
    if (!data) {
      er = er || new Error("failed to fetch from registry: " + uri)
    }

    if (er) return cb(er, data, raw, response)

    // just give the write the old college try.  if it fails, whatever.
    function saved () {
      delete data._etag
      cb(er, data, raw, response)
    }

    saveToCache.call(this, cache, data, saved)
  }.bind(this))
}

function saveToCache (cache, data, saved) {
  if (this._cacheStat) {
    var cs = this._cacheStat
    return saveToCache_.call(this, cache, data, cs.uid, cs.gid, saved)
  }
  fs.stat(this.conf.get('cache'), function (er, st) {
    if (er) {
      return fs.stat(process.env.HOME || "", function (er, st) {
        // if this fails, oh well.
        if (er) return saved()
        this._cacheStat = st
        return saveToCache.call(this, cache, data, saved)
      }.bind(this))
    }
    this._cacheStat = st || { uid: null, gid: null }
    return saveToCache.call(this, cache, data, saved)
  }.bind(this))
}

function saveToCache_ (cache, data, uid, gid, saved) {
  mkdir(path.dirname(cache), function (er, made) {
    if (er) return saved()
    fs.writeFile(cache, JSON.stringify(data), function (er) {
      if (er || uid === null || gid === null) {
        return saved()
      }
      chownr(made || cache, uid, gid, saved)
    })
  })
}
