var path = require('path');
var url = require('url');
var async = require('async');
var request = require('request');
var replay = require('request-replay');
var createError = require('./util/createError');
var Cache = require('./util/Cache');

function lookup(name, callback) {
    var data;
    var that = this;
    var registry = this._config.registry.search;
    var total = registry.length;
    var index = 0;

    // If no registry entries were passed..
    if (!total) {
        return callback();
    }

    // Lookup package in series in each registry
    // endpoint until we got the data
    async.doUntil(
        function(next) {
            var remote = url.parse(registry[index]);
            var lookupCache = that._lookupCache[remote.host];

            // If force flag is disabled we check the cache
            if (!that._config.force) {
                lookupCache.get(name, function(err, value) {
                    data = value;

                    // Don't proceed with making a request if we got an error,
                    // a value from the cache or if the offline flag is enabled
                    if (err || data || that._config.offline) {
                        return next(err);
                    }

                    doRequest.call(that, name, index, function(err, entry) {
                        if (err || !entry) {
                            return next(err);
                        }

                        data = entry;

                        // Store in cache
                        lookupCache.set(name, entry, getMaxAge(entry), next);
                    });
                });
                // Otherwise, we totally bypass the cache and
                // make only the request
            } else {
                doRequest.call(that, name, index, function(err, entry) {
                    if (err || !entry) {
                        return next(err);
                    }

                    data = entry;

                    // Store in cache
                    lookupCache.set(name, entry, getMaxAge(entry), next);
                });
            }
        },
        function() {
            // Until the data is unknown or there's still registries to test
            return !!data || ++index === total;
        },
        function(err) {
            // If some of the registry entries failed, error out
            if (err) {
                return callback(err);
            }

            callback(null, data);
        }
    );
}

function doRequest(name, index, callback) {
    var req;
    var msg;
    var requestUrl =
        this._config.registry.search[index] +
        '/packages/' +
        encodeURIComponent(name);
    var remote = url.parse(requestUrl);
    var headers = {};
    var that = this;

    if (this._config.userAgent) {
        headers['User-Agent'] = this._config.userAgent;
    }

    req = replay(
        request.get(
            requestUrl,
            {
                headers: headers,
                ca: this._config.ca.search[index],
                strictSSL: this._config.strictSsl,
                timeout: this._config.timeout,
                json: true
            },
            function(err, response, body) {
                // If there was an internal error (e.g. timeout)
                if (err) {
                    return callback(
                        createError(
                            'Request to ' +
                                requestUrl +
                                ' failed: ' +
                                err.message,
                            err.code
                        )
                    );
                }

                // If not found, try next
                if (response.statusCode === 404) {
                    return callback();
                }

                // Abort if there was an error (range different than 2xx)
                if (response.statusCode < 200 || response.statusCode > 299) {
                    return callback(
                        createError(
                            'Request to ' +
                                requestUrl +
                                ' failed with ' +
                                response.statusCode,
                            'EINVRES'
                        )
                    );
                }

                // Validate response body, since we are expecting a JSON object
                // If the server returns an invalid JSON, it's still a string
                if (typeof body !== 'object') {
                    return callback(
                        createError(
                            'Response of request to ' +
                                requestUrl +
                                ' is not a valid json',
                            'EINVRES'
                        )
                    );
                }

                var data;
                if (body.url) {
                    data = {
                        type: 'alias',
                        url: body.url
                    };
                }
                callback(null, data);
            }
        )
    );

    if (this._logger) {
        req.on('replay', function(replay) {
            msg =
                'Request to ' +
                requestUrl +
                ' failed with ' +
                replay.error.code +
                ', ';
            msg += 'retrying in ' + (replay.delay / 1000).toFixed(1) + 's';
            that._logger.warn('retry', msg);
        });
    }
}

function getMaxAge(entry) {
    // If type is alias, make it 5 days
    if (entry.type === 'alias') {
        return 5 * 24 * 60 * 60 * 1000;
    }

    // Otherwise make it 5 minutes
    return 5 * 60 * 60 * 1000;
}

function initCache() {
    this._lookupCache = this._cache.lookup || {};

    // Generate a cache instance for each registry endpoint
    this._config.registry.search.forEach(function(registry) {
        var cacheDir;
        var host = url.parse(registry).host;

        // Skip if there's a cache for the same host
        if (this._lookupCache[host]) {
            return;
        }

        if (this._config.cache) {
            cacheDir = path.join(
                this._config.cache,
                encodeURIComponent(host),
                'lookup'
            );
        }

        this._lookupCache[host] = new Cache(cacheDir, {
            max: 250,
            // If offline flag is passed, we use stale entries from the cache
            useStale: this._config.offline
        });
    }, this);
}

function clearCache(name, callback) {
    var lookupCache = this._lookupCache;
    var remotes = Object.keys(lookupCache);

    if (typeof name === 'function') {
        callback = name;
        name = null;
    }

    if (name) {
        async.forEach(
            remotes,
            function(remote, next) {
                lookupCache[remote].del(name, next);
            },
            callback
        );
    } else {
        async.forEach(
            remotes,
            function(remote, next) {
                lookupCache[remote].clear(next);
            },
            callback
        );
    }
}

function resetCache() {
    var remote;

    for (remote in this._lookupCache) {
        this._lookupCache[remote].reset();
    }
}

lookup.initCache = initCache;
lookup.clearCache = clearCache;
lookup.resetCache = resetCache;

module.exports = lookup;
