module.exports = CachingRegistryClient

var path = require('path')
var fs = require('graceful-fs')
var url = require('url')
var assert = require('assert')
var inherits = require('util').inherits

var RegistryClient = require('npm-registry-client')
var npm = require('../npm.js')
var log = require('npmlog')
var getCacheStat = require('./get-stat.js')
var cacheFile = require('npm-cache-filename')
var mkdirp = require('mkdirp')
var rimraf = require('rimraf')
var chownr = require('chownr')
var writeFile = require('write-file-atomic')
var parseJSON = require('../utils/parse-json')

function CachingRegistryClient (config) {
  RegistryClient.call(this, adaptConfig(config))

  this._mapToCache = cacheFile(config.get('cache'))

  // swizzle in our custom cache invalidation logic
  this._request = this.request
  this.request = this._invalidatingRequest
  this.get = get
}
inherits(CachingRegistryClient, RegistryClient)

CachingRegistryClient.prototype._invalidatingRequest = function (uri, params, cb) {
  var client = this
  this._request(uri, params, function () {
    var args = arguments

    var method = params.method
    if (method !== 'HEAD' && method !== 'GET') {
      var invalidated = client._mapToCache(uri)
      // invalidate cache
      //
      // This is irrelevant for commands that do etag / last-modified caching,
      // but ls and view also have a timed cache, so this keeps the user from
      // thinking that it didn't work when it did.
      // Note that failure is an acceptable option here, since the only
      // result will be a stale cache for some helper commands.
      log.verbose('request', 'invalidating', invalidated, 'on', method)
      return rimraf(invalidated, function () {
        cb.apply(undefined, args)
      })
    }

    cb.apply(undefined, args)
  })
}

function get (uri, params, cb) {
  assert(typeof uri === 'string', 'must pass registry URI to get')
  assert(params && typeof params === 'object', 'must pass params to get')
  assert(typeof cb === 'function', 'must pass callback to get')

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

  var cacheBase = cacheFile(npm.config.get('cache'))(uri)
  var cachePath = path.join(cacheBase, '.cache.json')

  // 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$/)) {
    log.verbose('get', 'GET as part of write; not caching result')
    return get_.call(this, uri, cachePath, params, cb)
  }

  var client = this
  fs.stat(cachePath, function (er, stat) {
    if (!er) {
      fs.readFile(cachePath, function (er, data) {
        data = parseJSON.noExceptions(data)

        params.stat = stat
        params.data = data

        get_.call(client, uri, cachePath, params, cb)
      })
    } else {
      get_.call(client, uri, cachePath, params, cb)
    }
  })
}

function get_ (uri, cachePath, params, cb) {
  var staleOk = params.staleOk === undefined ? false : params.staleOk
  var timeout = params.timeout === undefined ? -1 : params.timeout
  var data = params.data
  var stat = params.stat
  var etag
  var lastModified

  timeout = Math.min(timeout, npm.config.get('cache-max') || 0)
  timeout = Math.max(timeout, npm.config.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) {
    if (data._etag) etag = data._etag
    if (data._lastModified) lastModified = data._lastModified

    if (stat && timeout && timeout > 0) {
      if ((Date.now() - stat.mtime.getTime()) / 1000 < timeout) {
        log.verbose('get', uri, 'not expired, no request')
        delete data._etag
        delete data._lastModified
        return cb(null, data, JSON.stringify(data), { statusCode: 304 })
      }

      if (staleOk) {
        log.verbose('get', uri, 'staleOk, background update')
        delete data._etag
        delete data._lastModified
        process.nextTick(
          cb.bind(null, null, data, JSON.stringify(data), { statusCode: 304 })
        )
        cb = function () {}
      }
    }
  }

  var options = {
    etag: etag,
    lastModified: lastModified,
    follow: params.follow,
    auth: params.auth
  }
  this.request(uri, options, 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 && cachePath && data && !data.error) {
      er = null
      response = { statusCode: 304 }
    }

    if (response) {
      log.silly('get', 'cb', [response.statusCode, response.headers])
      if (response.statusCode === 304 && (etag || lastModified)) {
        remoteData = data
        log.verbose(etag ? 'etag' : 'lastModified', 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)

    saveToCache(cachePath, data, saved)

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

    function saveToCache (cachePath, data, saved) {
      log.verbose('get', 'saving', data.name, 'to', cachePath)
      getCacheStat(function (er, st) {
        mkdirp(path.dirname(cachePath), function (er, made) {
          if (er) return saved()

          writeFile(cachePath, JSON.stringify(data), function (er) {
            if (er) return saved()

            chownr(made || cachePath, st.uid, st.gid, saved)
          })
        })
      })
    }
  })
}

function adaptConfig (config) {
  return {
    proxy: {
      http: config.get('proxy'),
      https: config.get('https-proxy'),
      localAddress: config.get('local-address')
    },
    ssl: {
      certificate: config.get('cert'),
      key: config.get('key'),
      ca: config.get('ca'),
      strict: config.get('strict-ssl')
    },
    retry: {
      retries: config.get('fetch-retries'),
      factor: config.get('fetch-retry-factor'),
      minTimeout: config.get('fetch-retry-mintimeout'),
      maxTimeout: config.get('fetch-retry-maxtimeout')
    },
    userAgent: config.get('user-agent'),
    log: log,
    defaultTag: config.get('tag'),
    couchToken: config.get('_token')
  }
}
