var progress = require('request-progress');
var request = require('request');
var Q = require('q');
var mout = require('mout');
var retry = require('retry');
var createError = require('./createError');
var createWriteStream = require('fs-write-stream-atomic');
var destroy = require('destroy');

var errorCodes = [
    'EADDRINFO',
    'ETIMEDOUT',
    'ECONNRESET',
    'ESOCKETTIMEDOUT',
    'ENOTFOUND'
];

function download(url, file, options) {
    var operation;
    var deferred = Q.defer();
    var progressDelay = 8000;

    options = mout.object.mixIn(
        {
            retries: 5,
            factor: 2,
            minTimeout: 1000,
            maxTimeout: 35000,
            randomize: true,
            progressDelay: progressDelay,
            gzip: true
        },
        options || {}
    );

    // Retry on network errors
    operation = retry.operation(options);

    operation.attempt(function() {
        Q.fcall(fetch, url, file, options)
            .then(function(response) {
                deferred.resolve(response);
            })
            .progress(function(status) {
                deferred.notify(status);
            })
            .fail(function(error) {
                // Save timeout before retrying to report
                var timeout = operation._timeouts[0];

                // Reject if error is not a network error
                if (errorCodes.indexOf(error.code) === -1) {
                    return deferred.reject(error);
                }

                // Next attempt will start reporting download progress immediately
                progressDelay = 0;

                // This will schedule next retry or return false
                if (operation.retry(error)) {
                    deferred.notify({
                        retry: true,
                        delay: timeout,
                        error: error
                    });
                } else {
                    deferred.reject(error);
                }
            });
    });

    return deferred.promise;
}

function fetch(url, file, options) {
    var deferred = Q.defer();

    var contentLength;
    var bytesDownloaded = 0;

    var reject = function(error) {
        deferred.reject(error);
    };

    var req = progress(request(url, options), {
        delay: options.progressDelay
    })
        .on('response', function(response) {
            contentLength = Number(response.headers['content-length']);

            var status = response.statusCode;

            if (status < 200 || status >= 300) {
                return deferred.reject(
                    createError('Status code of ' + status, 'EHTTP')
                );
            }

            var writeStream = createWriteStream(file);
            var errored = false;

            // Change error listener so it cleans up writeStream before exiting
            req.removeListener('error', reject);
            req.on('error', function(error) {
                errored = true;
                destroy(req);
                destroy(writeStream);

                // Wait for writeStream to cleanup after itself...
                // TODO: Maybe there's a better way?
                setTimeout(function() {
                    deferred.reject(error);
                }, 50);
            });

            writeStream.on('finish', function() {
                if (!errored) {
                    destroy(req);
                    deferred.resolve(response);
                }
            });

            req.pipe(writeStream);
        })
        .on('data', function(data) {
            bytesDownloaded += data.length;
        })
        .on('progress', function(state) {
            deferred.notify(state);
        })
        .on('error', reject)
        .on('end', function() {
            // Check if the whole file was downloaded
            // In some unstable connections the ACK/FIN packet might be sent in the
            // middle of the download
            // See: https://github.com/joyent/node/issues/6143
            if (contentLength && bytesDownloaded < contentLength) {
                req.emit(
                    'error',
                    createError(
                        'Transfer closed with ' +
                            (contentLength - bytesDownloaded) +
                            ' bytes remaining to read',
                        'EINCOMPLETE'
                    )
                );
            }
        });

    return deferred.promise;
}

module.exports = download;
