var Bluebird = require('bluebird');
var CronJob = require('./cronJob');
var Decode = require('jwt-decode');
var LogStream = require('webtask-log-stream');
var RandExp = require('randexp');
var Request = require('./issueRequest');
var Superagent = require('superagent');
var Webtask = require('./webtask');

var defaults = require('lodash.defaults');


/**
Sandbox node.js code.
@module sandboxjs
@typicalname Sandbox
*/
module.exports = Sandbox;
module.exports.PARSE_NEVER = 0;
module.exports.PARSE_ALWAYS = 1;
module.exports.PARSE_ON_ARITY = 2;



/**
 * Creates an object representing a user's webtask.io credentials
 *
 * @constructor
 * @param {Object} options - Options used to configure the profile
 * @param {String} options.url - The url of the webtask cluster where code will run
 * @param {String} options.container - The name of the container in which code will run
 * @param {String} options.token - The JWT (see: http://jwt.io) issued by webtask.io that grants rights to run code in the indicated container
 * @param {String} [options.onBeforeRequest] - An array of hook functions to be invoked with a prepared request
 */
function Sandbox (options) {
    var securityVersion = 'v1';

    this.url = options.url;
    this.container = options.container;
    this.logEndpoint = options.logEndpoint;
    this.token = options.token;
    this.csrfToken = options.csrfToken;
    this.onBeforeRequest = []
        .concat(options.onBeforeRequest)
        .filter(function(hook) { return typeof hook === 'function' } );

    this.headers = { 'Authorization': 'Bearer ' + this.token};
    if (this.csrfToken) {
        this.headers['X-CSRFToken'] = this.csrfToken;
    }

    try {
        var typ = Decode(options.token, { header: true }).typ;

        if (typ && typ.toLowerCase() === 'jwt') {
            securityVersion = 'v2';
        }
    } catch (_) {
        // Ignore jwt decoding failures and assume v1 opaque token
    }

    this.securityVersion = securityVersion;
}

/**
 * Create a clone of this sandbox instances with one or more different parameters
 *
 * @param {Object} options - Options used to configure the profile
 * @param {String} [options.url] - The url of the webtask cluster where code will run
 * @param {String} [options.container] - The name of the container in which code will run
 * @param {String} [options.token] - The JWT (see: http://jwt.io) issued by webtask.io that grants rights to run code in the indicated container
 * @param {String} [options.onBeforeRequest] - An array of hook functions to be invoked with a prepared request
 */
Sandbox.prototype.clone = function (options) {
    return new Sandbox({
        url: options.url || this.url,
        container: options.container || this.container,
        token: options.token || this.token,
        onBeforeRequest: options.onBeforeRequest || this.onBeforeRequest,
    });
};

/**
 * Create a Webtask from the given options
 *
 * @param {String} [codeOrUrl] - The code for the webtask or a url starting with http:// or https://
 * @param {Object} [options] - Options for creating the webtask
 * @param {Function} [cb] - Optional callback function for node-style callbacks
 * @returns {Promise} A Promise that will be fulfilled with the token
 */
Sandbox.prototype.create = function (codeOrUrl, options, cb) {
    if (typeof codeOrUrl !== 'string') {
        cb = options;
        options = codeOrUrl;
        codeOrUrl = typeof options.code === 'string'
            ? options.code
            : options.code_url;
    }

    if (typeof options === 'function') {
        cb = options;
        options = {};
    }

    if (!options) options = {};

    var fol = codeOrUrl.toLowerCase();

    if (fol.indexOf('http://') === 0 || fol.indexOf('https://') === 0) {
        options.code_url = codeOrUrl;
    } else {
        options.code = codeOrUrl;
    }

    var self = this;
    var token_options = defaults({}, options, { include_webtask_url: true });
    var promise = this.createToken(token_options)
        .then(function (result) {
            return token_options.include_webtask_url
                ? new Webtask(self, result.token, { meta: options.meta, webtask_url: result.webtask_url })
                : new Webtask(self, result, { meta: options.meta });
        });

    return cb ? promise.nodeify(cb) : promise;
};

/**
 * Create a Webtask from the given claims
 *
 * @param {Object} claims - Options for creating the webtask
 * @param {Function} [cb] - Optional callback function for node-style callbacks
 * @returns {Promise} A Promise that will be fulfilled with the token
 */
Sandbox.prototype.createRaw = function (claims, cb) {
    var self = this;

    var promise = this.createTokenRaw(claims)
        .then(function (token) {
            return new Webtask(self, token, { meta: claims.meta });
        });

    return cb ? promise.nodeify(cb) : promise;
};

/**
 * Shortcut to create a Webtask and get its url from the given options
 *
 * @param {Object} options - Options for creating the webtask
 * @param {Function} [cb] - Optional callback function for node-style callbacks
 * @returns {Promise} A Promise that will be fulfilled with the token
 */
Sandbox.prototype.createUrl = function (options, cb) {
    var promise = this.create(options)
        .get('url');

    return cb ? promise.nodeify(cb) : promise;
};

/**
 * Shortcut to create and run a Webtask from the given options
 *
 * @param {String} [codeOrUrl] - The code for the webtask or a url starting with http:// or https://
 * @param {Object} [options] - Options for creating the webtask
 * @param {Function} [cb] - Optional callback function for node-style callbacks
 * @returns {Promise} A Promise that will be fulfilled with the token
 */
Sandbox.prototype.run = function (codeOrUrl, options, cb) {
    if (typeof options === 'function') {
        cb = options;
        options = {};
    }

    if (!options) options = {};

    var promise = this.create(codeOrUrl, options)
        .call('run', options);

    return cb ? promise.nodeify(cb, {spread: true}) : promise;
};

/**
 * Create a webtask token - A JWT (see: http://jwt.io) with the supplied options
 *
 * @param {Object} options - Claims to make for this token (see: https://webtask.io/docs/api_issue)
 * @param {Function} [cb] - Optional callback function for node-style callbacks
 * @returns {Promise} A Promise that will be fulfilled with the token
 */
Sandbox.prototype.createToken = function (options, cb) {
    if (!options) options = {};

    var self = this;
    var promise = new Bluebird(function (resolve, reject) {
        var params = {
            ten: options.container || self.container,
            dd: options.issuanceDepth || 0,
        };

        if (options.exp !== undefined && options.nbf !== undefined
            && options.exp <= options.nbf) {
            return reject('The `nbf` parameter cannot be set to a later time than `exp`.');
        }

        if (options.host)
            params.host = options.host;
        if (options.code_url)
            params.url = options.code_url;
        if (typeof options.code === 'string')
            params.code = options.code;
        if (options.secrets && Object.keys(options.secrets).length > 0)
            params.ectx = options.secrets;
        if (options.secret && Object.keys(options.secret).length > 0)
            params.ectx = options.secret;
        if (options.params && Object.keys(options.params).length > 0)
            params.pctx = options.params;
        if (options.param && Object.keys(options.param).length > 0)
            params.pctx = options.param;
        if (options.meta && Object.keys(options.meta).length > 0)
            params.meta = options.meta;
        if (options.nbf !== undefined)
            params.nbf = options.nbf;
        if (options.exp !== undefined)
            params.exp = options.exp;
        if (options.merge || options.mergeBody)
            params.mb = 1;
        if ((options.parse || options.parseBody) !== undefined)
            // This can be a numeric value from the PARSE_* enumeration
            // or a boolean that will be normalized to 0 or 1.
            params.pb = +(options.parse || options.parseBody);
        if (!options.selfRevoke)
            params.dr = 1;
        if (options.name)
            params.jtn = options.name;

        try {
            if (options.tokenLimit)
                addLimits(options.tokenLimit, Sandbox.limits.token);
            if (options.containerLimit)
                addLimits(options.containerLimit, Sandbox.limits.container);
        } catch (err) {
            return reject(err);
        }

        return resolve(self.createTokenRaw(params, { include_webtask_url: options.include_webtask_url }));

        function addLimits(limits, spec) {
            for (var l in limits) {
                var limit = parseInt(limits[l], 10);

                if (!spec[l]) {
                    throw new Error('Unsupported limit type `' + l
                        + '`. Supported limits are: '
                        + Object.keys(spec).join(', ') + '.');
                }

                if (isNaN(limits[l]) || Math.floor(+limits[l]) !== limit
                    || limit < 1) {
                        throw new Error('Unsupported limit value for `' + l
                            + '` limit. All limits must be positive integers.');
                }

                params[spec[l]] = limit;
            }
        }
    });

    return cb ? promise.nodeify(cb) : promise;
};

/**
 * Run a prepared Superagent request through any configured
 * onBeforeRequest hooks.
 *
 * This can be useful for enablying proxies for server-side
 * consumers of sandboxjs.
 *
 * @param {Superagent.Request} request - Instance of a superagent request
 * @param {function} [cb] - Node-style callback function
 * @returns {Promise} - A promise representing the fulfillment of the request
 */
Sandbox.prototype.issueRequest = function (request, cb) {
    request.set(this.headers)
    const transformedRequest = this.onBeforeRequest
        .reduce(function(request, hook) {
            return hook(request, this);
        }, request);
    const promise = Request(transformedRequest);

    return cb ? promise.nodeify(cb) : promise;
};


/**
 * Create a webtask token - A JWT (see: http://jwt.io) with the supplied claims
 *
 * @param {Object} claims - Claims to make for this token (see: https://webtask.io/docs/api_issue)
 * @param {Object} [options] - Optional options. Currently only options.include_webtask_url is supported.
 * @param {Function} [cb] - Optional callback function for node-style callbacks
 * @returns {Promise} A Promise that will be fulfilled with the token
 */
Sandbox.prototype.createTokenRaw = function (claims, options, cb) {
    if (typeof options === 'function') {
        cb = options;
        options = undefined;
    }
    var request = Superagent
        .post(this.url + '/api/tokens/issue')
    
    request.send(claims);

    var promise = this.issueRequest(request)
            .then(function (res) {
                return (options && options.include_webtask_url)
                    ? { token: res.text, webtask_url: res.header['location'] }
                    : res.text;
            });

    return cb ? promise.nodeify(cb) : promise;
};

/**
 * Create a stream of logs from the webtask container
 *
 * Note that the logs will include messages from our infrastructure.
 *
 * @param {Object} options - Streaming options overrides
 * @param {String} [options.container] - The container for which you would like to stream logs. Defaults to the current profile's container.
 * @returns {Stream} A stream that will emit 'data' events with container logs
 */
Sandbox.prototype.createLogStream = function (options) {
    if (!options) options = {};
    const logEndpoint = this.logEndpoint || '/api/logs/tenant/';
    var url = this.url + logEndpoint
        + (options.container || this.container)
    if (this.token) {
        url += '?key=' + encodeURIComponent(this.token);
    }
    return LogStream(url);
};

Sandbox.prototype._createCronJob = function (job) {
    return new CronJob(this, job);
};

/**
 * Read a named webtask
 *
 * @param {Object} options - Options
 * @param {String} [options.container] - Set the webtask container. Defaults to the profile's container.
 * @param {String} options.name - The name of the webtask.
 * @param {Boolean} [options.decrypt] - Decrypt the webtask's secrets.
 * @param {Boolean} [options.fetch_code] - Fetch the code associated with the webtask.
 * @param {Function} [cb] - Optional callback function for node-style callbacks.
 * @return {Promise} A Promise that will be fulfilled with an array of Webtasks
 */
Sandbox.prototype.getWebtask = function (options, cb) {
    if (!options) options = {};

    var promise;

    if (!options.name) {
        var err = new Error('Missing required option: `options.name`');
        err.statusCode = 400;

        promise = Bluebird.reject(err);
    } else {
        var url = this.url + '/api/webtask/'
            + (options.container || this.container) + '/' + options.name;
        var request = Superagent
            .get(url)
            .accept('json');
        var self = this;

        if (options.decrypt) request.query({ decrypt: options.decrypt });
        if (options.fetch_code) request.query({ fetch_code: options.fetch_code });

        promise = this.issueRequest(request)
            .get('body')
            .then(function (data) {
                return new Webtask(self, data.token, { name: data.name, secrets: data.secrets, code: data.code, meta: data.meta, webtask_url: data.webtask_url });
            });
    }

    return cb ? promise.nodeify(cb) : promise;
};

/**
 * Create a named webtask
 *
 * @param {Object} options - Options
 * @param {String} [options.container] - Set the webtask container. Defaults to the profile's container.
 * @param {String} options.name - The name of the webtask.
 * @param {String} [options.secrets] - Set the webtask secrets.
 * @param {String} [options.meta] - Set the webtask metadata.
 * @param {String} [options.host] - Set the webtask hostname.
 * @param {Function} [cb] - Optional callback function for node-style callbacks.
 * @return {Promise} A Promise that will be fulfilled with an array of Webtasks
 */
Sandbox.prototype.createWebtask = function (options, cb) {
    if (!options) options = {};

    var promise;
    var err;

    if (typeof options.name !== 'string') {
        err = new Error('The `name` option is required and must be a string');
        err.statusCode = 400;

        promise = Bluebird.reject(err);
    }

    if (options.code && typeof options.code !== 'string') {
        err = new Error('The `code` option must be a string');
        err.statusCode = 400;

        promise = Bluebird.reject(err);
    }

    if (options.url && typeof options.url !== 'string') {
        err = new Error('The `url` option must be a string');
        err.statusCode = 400;

        promise = Bluebird.reject(err);
    }

    if (options.code && options.url) {
        err = new Error('Either the `code` or `url` option can be specified, but not both');
        err.statusCode = 400;

        promise = Bluebird.reject(err);
    }

    if (!err) {
        var url = this.url + '/api/webtask/'
            + (options.container || this.container) + '/' + options.name;
        var payload = {};
        if (options.secrets) payload.secrets = options.secrets;
        if (options.code) payload.code = options.code;
        if (options.url) payload.url = options.url;
        if (options.meta) payload.meta = options.meta;
        if (options.host) payload.host = options.host;
        var request = Superagent
            .put(url)   
        
        request.send(payload)

        var self = this;
        promise = this.issueRequest(request)
            .then(function (res) {
                return new Webtask(self, res.body.token, { name: res.body.name, container: options.container || self.container, meta: res.body.meta, webtask_url: res.body.webtask_url });
            });
    }

    return cb ? promise.nodeify(cb) : promise;
};


/**
 * Remove a named webtask from the webtask container
 *
 * @param {Object} options - Options
 * @param {String} [options.container] - Set the webtask container. Defaults to the profile's container.
 * @param {String} options.name - The name of the cron job.
 * @param {Function} [cb] - Optional callback function for node-style callbacks.
 * @return {Promise} A Promise that will be fulfilled with an array of Webtasks
 */
Sandbox.prototype.removeWebtask = function (options, cb) {
    if (!options) options = {};

    var promise;

    if (!options.name) {
        var err = new Error('Missing required option: `options.name`');
        err.statusCode = 400;

        promise = Bluebird.reject(err);
    } else {
        var url = this.url + '/api/webtask/'
            + (options.container || this.container) + '/' + options.name;
        var request = Superagent
            .del(url)

        promise = this.issueRequest(request)
            .return(true);
    }

    return cb ? promise.nodeify(cb) : promise;
};

/**
 * Update an existing webtask's code, secrets or other claims
 *
 * Note that this method should be used with caution as there is the potential
 * for a race condition where another agent updates the webtask between the time
 * that the webtask details and claims are resolved and when the webtask
 * update is issued.
 *
 * @param {Object} options - Options
 * @param {String} options.name - Name of the webtask to update
 * @param {String} [options.code] - Updated code for the webtask
 * @param {String} [options.url] - Updated code URL for the webtask
 * @param {String} [options.secrets] - If `false`, remove existing secrets, if an object update secrets, otherwise preserve
 * @param {String} [options.params] - If `false`, remove existing params, if an object update params, otherwise preserve
 * @param {String} [options.host] - If `false`, remove existing host, if a string update host, otherwise preserve
 * @param {Function} [cb] - Optional callback function for node-style callbacks.
 * @return {Promise} A Promise that will be fulfilled with an instance of Webtask representing the updated webtask
 */
Sandbox.prototype.updateWebtask = function (options, cb) {
    if (!options) options = {};

    var self = this;
    var err;
    var promise;

    if (typeof options.name !== 'string') {
        err = new Error('The `name` option is required and must be a string');
        err.statusCode = 400;

        promise = Bluebird.reject(err);
    }

    if (options.code && typeof options.code !== 'string') {
        err = new Error('The `code` option must be a string');
        err.statusCode = 400;

        promise = Bluebird.reject(err);
    }

    if (options.url && typeof options.url !== 'string') {
        err = new Error('The `url` option must be a string');
        err.statusCode = 400;

        promise = Bluebird.reject(err);
    }

    if (options.code && options.url) {
        err = new Error('Either the `code` or `url` option can be specified, but not both');
        err.statusCode = 400;

        promise = Bluebird.reject(err);
    }

    if (!err) {
        promise = this.getWebtask({ name: options.name })
            .then(onWebtask);

    }

    return cb ? promise.nodeify(cb) : promise;


    function onWebtask(webtask) {
        return webtask.inspect({ decrypt: options.secrets !== false, fetch_code: options.code !== false, meta: options.meta !== false })
            .then(onInspection);


        function onInspection(claims) {
            var newClaims = {
                ten: claims.ten,
                jtn: claims.jtn,
            };

            ['nbf', 'exp', 'dd', 'dr', 'ls', 'lm', 'lh', 'ld', 'lw', 'lo', 'lts', 'ltm', 'lth', 'ltd', 'ltw', 'lto']
                .forEach(function (claim) {
                    if (claims[claim]) {
                        newClaims[claim] = claims[claim];
                    }
                });

            if (options.host !== false && (options.host || claims.host)) {
                newClaims.host = options.host || claims.host;
            }
            if (typeof options.parseBody !== 'undefined' || typeof options.parse !== 'undefined' || claims.pb) {
                newClaims.pb = +(options.parseBody || options.parse || claims.pb);
            }
            if (typeof options.mergeBody !== 'undefined' || typeof options.merge !== 'undefined' || claims.mb) {
                newClaims.mb = +(options.mergeBody || options.merge || claims.mb);
            }
            if (options.secrets !== false && (options.secrets || claims.ectx)) {
                newClaims.ectx = options.secrets || claims.ectx;
            }
            if (options.params !== false && (options.params || claims.pctx)) {
                newClaims.pctx = options.params || claims.pctx;
            }
            if (options.meta !== false && (options.meta || claims.meta)) {
                newClaims.meta = options.meta || claims.meta;
            }
            if (options.url) {
                newClaims.url = options.url;
            }
            if (options.code) {
                newClaims.code = options.code;
            }

            return self.createRaw(newClaims, { include_webtask_url: options.include_webtask_url });
        }
    }
};

/**
 * List named webtasks from the webtask container
 *
 * @param {Object} options - Options
 * @param {String} [options.container] - Set the webtask container. Defaults to the profile's container.
 * @param {Boolean} [options.fetch_code] - Include the webtask's code in the listing response.
 * @param {Function} [cb] - Optional callback function for node-style callbacks.
 * @return {Promise} A Promise that will be fulfilled with an array of Webtasks
 */
Sandbox.prototype.listWebtasks = function (options, cb) {
    if (!options) options = {};

    var url = this.url + '/api/webtask/'
        + (options.container || this.container);
    var request = Superagent
        .get(url)
        .accept('json');

    if (options.offset) request.query({ offset: options.offset });
    if (options.limit) request.query({ limit: options.limit });
    if (options.meta) {
        for (var m in options.meta) {
            request.query({ meta: m + ':' + options.meta[m] });
        }
    }
    if (options.fetch_code) request.query({ fetch_code: true });

    var self = this;
    var promise = this.issueRequest(request)
        .get('body')
        .map(function (webtask) {
            return new Webtask(self, webtask.token, {
                code: webtask.code,
                name: webtask.name,
                meta: webtask.meta,
                webtask_url: webtask.webtask_url,
            });
        });

    return cb ? promise.nodeify(cb) : promise;
};

/**
 * Create a cron job from an already-existing webtask
 *
 * @param {Object} options - Options for creating a cron job
 * @param {String} [options.container] - The container in which the job will run. Defaults to the current profile's container.
 * @param {String} options.name - The name of the cron job.
 * @param {String} [options.token] - The webtask token that will be used to run the job.
 * @param {String} options.schedule - The cron schedule that will be used to determine when the job will be run.
 * @param {String} options.tz - The cron timezone (IANA timezone).
 * @param {String} options.meta - The cron metadata (set of string key value pairs).
 * @param {Function} [cb] - Optional callback function for node-style callbacks.
 * @returns {Promise} A Promise that will be fulfilled with a {@see CronJob} instance.
 */
Sandbox.prototype.createCronJob = function (options, cb) {
    options = defaults(options, { container: this.container });

    var payload = {
        schedule: options.schedule,
    };
    if (options.token) payload.token = options.token;

    if (options.state) {
        payload.state = options.state;
    }
    if (options.meta) {
        payload.meta = options.meta;
    }
    if (options.tz) {
        payload.tz = options.tz;
    }

    var request = Superagent
        .put(this.url + '/api/cron/' + options.container + '/' + options.name)

    request.send(payload)
        .accept('json');

    var promise = this.issueRequest(request)
        .get('body')
        .then(this._createCronJob.bind(this));

    return cb ? promise.nodeify(cb) : promise;
};

/**
 * Remove an existing cron job
 *
 * @param {Object} options - Options for removing the cron job
 * @param {String} [options.container] - The container in which the job will run. Defaults to the current profile's container.
 * @param {String} options.name - The name of the cron job.
 * @param {Function} [cb] - Optional callback function for node-style callbacks.
 * @returns {Promise} A Promise that will be fulfilled with the response from removing the job.
 */
Sandbox.prototype.removeCronJob = function (options, cb) {
    options = defaults(options, { container: this.container });

    var request = Superagent
        .del(this.url + '/api/cron/' + options.container + '/' + options.name)

    request.accept('json');

    var promise = this.issueRequest(request)
        .get('body');

    return cb ? promise.nodeify(cb) : promise;
};

/**
 * Set an existing cron job's state
 *
 * @param {Object} options - Options for updating the cron job's state
 * @param {String} [options.container] - The container in which the job will run. Defaults to the current profile's container.
 * @param {String} options.name - The name of the cron job.
 * @param {String} options.state - The new state of the cron job.
 * @param {Function} [cb] - Optional callback function for node-style callbacks.
 * @returns {Promise} A Promise that will be fulfilled with the response from removing the job.
 */
Sandbox.prototype.setCronJobState = function (options, cb) {
    options = defaults(options, { container: this.container });

    var request = Superagent
        .put(this.url + '/api/cron/' + options.container + '/' + options.name + '/state')

    request.send({
            state: options.state,
        })
        .accept('json');

    var promise = this.issueRequest(request)
        .get('body');

    return cb ? promise.nodeify(cb) : promise;
};

/**
 * List cron jobs associated with this profile
 *
 * @param {Object} [options] - Options for listing cron jobs.
 * @param {String} [options.container] - The container in which the job will run. Defaults to the current profile's container.
 * @param {Function} [cb] - Optional callback function for node-style callbacks.
 * @returns {Promise} A Promise that will be fulfilled with an Array of {@see CronJob} instances.
 */
Sandbox.prototype.listCronJobs = function (options, cb) {
    if (typeof options === 'function') {
        cb = options;
        options = null;
    }

    if (!options) options = {};

    options = defaults(options, { container: this.container });

    var request = Superagent
        .get(this.url + '/api/cron/' + options.container)
        .accept('json');

    if (options.offset) request.query({ offset: options.offset });
    if (options.limit) request.query({ limit: options.limit });
    if (options.meta) {
        for (var m in options.meta) {
            request.query({ meta: m + ':' + options.meta[m] });
        }
    }

    var promise = this.issueRequest(request)
        .get('body')
        .map(this._createCronJob.bind(this));

    return cb ? promise.nodeify(cb) : promise;
};

/**
 * Get a CronJob instance associated with an existing cron job
 *
 * @param {Object} options - Options for retrieving the cron job.
 * @param {String} [options.container] - The container in which the job will run. Defaults to the current profile's container.
 * @param {String} options.name - The name of the cron job.
 * @param {Function} [cb] - Optional callback function for node-style callbacks.
 * @returns {Promise} A Promise that will be fulfilled with a {@see CronJob} instance.
 */
Sandbox.prototype.getCronJob = function (options, cb) {
    options = defaults(options, { container: this.container });

    var request = Superagent
        .get(this.url + '/api/cron/' + options.container + '/' + options.name)
        .accept('json');

    var promise = this.issueRequest(request)
        .get('body')
        .then(this._createCronJob.bind(this));

    return cb ? promise.nodeify(cb) : promise;
};


/**
 * Get the historical results of executions of an existing cron job.
 *
 * @param {Object} options - Options for retrieving the cron job.
 * @param {String} [options.container] - The container in which the job will run. Defaults to the current profile's container.
 * @param {String} options.name - The name of the cron job.
 * @param {String} [options.offset] - The offset to use when paging through results.
 * @param {String} [options.limit] - The limit to use when paging through results.
 * @param {Function} [cb] - Optional callback function for node-style callbacks.
 * @returns {Promise} A Promise that will be fulfilled with an Array of cron job results.
 */
Sandbox.prototype.getCronJobHistory = function (options, cb) {
    options = defaults(options, { container: this.container });

    var request = Superagent
        .get(this.url + '/api/cron/' + options.container + '/' + options.name + '/history')
        .accept('json');

    if (options.offset) request.query({offset: options.offset});
    if (options.limit) request.query({limit: options.limit});

    var promise = this.issueRequest(request)
        .get('body')
        .map(function (result) {
            var auth0HeaderRx = /^x-auth0/;

            result.scheduled_at = new Date(result.scheduled_at);
            result.started_at = new Date(result.started_at);
            result.completed_at = new Date(result.completed_at);

            for (var header in result.headers) {
                if (auth0HeaderRx.test(header)) {
                    try {
                        result.headers[header] = JSON.parse(result.headers[header]);
                    } catch (__) {
                        // Do nothing
                    }
                }
            }

            return result;
        });

    return cb ? promise.nodeify(cb) : promise;
};

/**
 * Inspect an existing webtask token to resolve code and/or secrets
 *
 * @param {Object} options - Options for inspecting the webtask.
 * @param {Boolean} options.token - The token that you would like to inspect.
 * @param {Boolean} [options.decrypt] - Decrypt the webtask's secrets.
 * @param {Boolean} [options.fetch_code] - Fetch the code associated with the webtask.
 * @param {Function} [cb] - Optional callback function for node-style callbacks.
 * @returns {Promise} A Promise that will be fulfilled with the resolved webtask data.
 */
Sandbox.prototype.inspectToken = function (options, cb) {
    options = defaults(options, { container: this.container });

    var request = Superagent
        .get(this.url + '/api/tokens/inspect')
        .query({ token: options.token })
        .accept('json');

    if (options.decrypt) request.query({ decrypt: options.decrypt });
    if (options.fetch_code) request.query({ fetch_code: options.fetch_code });
    if (options.meta) request.query({ meta: options.meta });

    var promise = this.issueRequest(request)
        .get('body');

    return cb ? promise.nodeify(cb) : promise;
};

/**
 * Inspect an existing named webtask to resolve code and/or secrets
 *
 * @param {Object} options - Options for inspecting the webtask.
 * @param {Boolean} options.name - The named webtask that you would like to inspect.
 * @param {Boolean} [options.decrypt] - Decrypt the webtask's secrets.
 * @param {Boolean} [options.fetch_code] - Fetch the code associated with the webtask.
 * @param {Function} [cb] - Optional callback function for node-style callbacks.
 * @returns {Promise} A Promise that will be fulfilled with the resolved webtask data.
 */
Sandbox.prototype.inspectWebtask = function (options, cb) {
    options = defaults(options, { container: this.container });

    var promise = this.securityVersion === 'v2'
        ? this.getWebtask({ name: options.name, decrypt: options.decrypt, fetch_code: options.fetch_code })
        : this.getWebtask({ name: options.name })
            .call('inspect', { decrypt: options.decrypt, fetch_code: options.fetch_code, meta: options.meta });

    return cb ? promise.nodeify(cb) : promise;
};


/**
 * Revoke a webtask token
 *
 * @param {String} token - The token that should be revoked
 * @param {Function} [cb] - Optional callback function for node-style callbacks
 * @returns {Promise} A Promise that will be fulfilled with the token
 * @see https://webtask.io/docs/api_revoke
 */
Sandbox.prototype.revokeToken = function (token, cb) {
    var request = Superagent
        .post(this.url + '/api/tokens/revoke')

    request.query({ token: token });

    var promise = this.issueRequest(request);

    return cb ? promise.nodeify(cb) : promise;
};

/**
 * List versions of a given node module that are available on the platform
 *
 * @param {Object} options - Options
 * @param {String} options.name - Name of the node module
 * @param {Function} [cb] - Optional callback function for node-style callbacks
 * @returns {Promise} A Promise that will be fulfilled with the token
 */
Sandbox.prototype.listNodeModuleVersions = function (options, cb) {
    var request = Superagent
        .get(this.url + `/api/env/node/modules/${encodeURIComponent(options.name)}`);

    var promise = this.issueRequest(request)
        .get('body');

    return cb ? promise.nodeify(cb) : promise;
};

/**
 * Ensure that a set of modules are available on the platform
 *
 * @param {Object} options - Options
 * @param {Array} options.modules - Array of { name, version } pairs
 * @param {Boolean} options.reset - Trigger a rebuild of the modules (Requires administrative token)
 * @param {Function} [cb] - Optional callback function for node-style callbacks
 * @returns {Promise} A Promise that will be fulfilled with an array of { name, version, state } objects
*/
Sandbox.prototype.ensureNodeModules = function (options, cb) {
    var request = Superagent
        .post(this.url + '/api/env/node/modules')
        .send({ modules: options.modules })

    if (options.reset) {
        request.query({ reset: 1 });
    }

    var promise = this.issueRequest(request)
        .get('body');

    return cb ? promise.nodeify(cb) : promise;
};

/**
 * Update the storage associated to the a webtask
 *
 * @param {Object} options - Options
 * @param {String} [options.container] - Set the webtask container. Defaults to the profile's container.
 * @param {String} options.name - The name of the webtask.
 * @param {Object} storage - storage
 * @param {Object} storage.data - The data to be stored
 * @param {String} storage.etag - Pass in an optional string to be used for optimistic concurrency control to prevent simultaneous updates of the same data.
 * @param {Function} [cb] - Optional callback function for node-style callbacks.
 * @return {Promise} A Promise that will be fulfilled with an array of Webtasks
 */
Sandbox.prototype.updateStorage = function (storage, options, cb) {
    var promise;

    if (!options || !options.name) {
        var err = new Error('Missing required option: `options.name`');
        err.statusCode = 400;

        promise = Bluebird.reject(err);
    }
    else {
        var obj = {
            data: JSON.stringify(storage.data)
        }

        if (storage.etag) {
            obj.etag = storage.etag.toString();
        }

        var request = Superagent
            .put(this.url + '/api/webtask/' + (options.container || this.container) + '/' + options.name + '/data')
            .send(obj)
            .accept('json');

        promise = this.issueRequest(request)
            .get('body');
    }

    return cb ? promise.nodeify(cb) : promise;
};

/**
 * Read the storage associated to the a webtask
 *
 * @param {Object} options - Options
 * @param {String} [options.container] - Set the webtask container. Defaults to the profile's container.
 * @param {String} options.name - The name of the webtask.
 * @param {Function} [cb] - Optional callback function for node-style callbacks.
 * @return {Promise} A Promise that will be fulfilled with an array of Webtasks
 */
Sandbox.prototype.getStorage = function (options, cb) {
    var promise;

    if (!options || !options.name) {
        var err = new Error('Missing required option: `options.name`');
        err.statusCode = 400;

        promise = Bluebird.reject(err);
    }
    else {
        var request = Superagent
            .get(this.url + '/api/webtask/' + (options.container || this.container) + '/' + options.name + '/data')
            .accept('json');

        promise = this.issueRequest(request)
            .get('body')
            .then(function (result) {
                var storage = result;

                try {
                    storage.data = JSON.parse(result.data);
                } catch(e) {
                    // TODO: Log somewhere
                }

                return storage;
            });
    }

    return cb ? promise.nodeify(cb) : promise;
}

Sandbox.limits = {
    container: {
        second: 'ls',
        minute: 'lm',
        hour: 'lh',
        day: 'ld',
        week: 'lw',
        month: 'lo'
    },
    token: {
        second: 'lts',
        minute: 'ltm',
        hour: 'lth',
        day: 'ltd',
        week: 'ltw',
        month: 'lto',
    },
};

/**
 * Create a Sandbox instance from a webtask token
 *
 * @param {String} token - The webtask token from which the Sandbox profile will be derived.
 * @param {Object} options - The options for creating the Sandbox instance that override the derived values from the token.
 * @param {String} [options.url] - The url of the webtask cluster. Defaults to the public 'sandbox.auth0-extend.com' cluster.
 * @param {String} options.container - The container with which this Sandbox instance should be associated. Note that your Webtask token must give you access to that container or all operations will fail.
 * @param {String} options.token - The Webtask Token. See: https://webtask.io/docs/api_issue.
 * @returns {Sandbox} A {@see Sandbox} instance whose url, token and container were derived from the given webtask token.
 *
 * @alias module:sandboxjs.fromToken
 */
Sandbox.fromToken = function (token, options) {
    var config = defaults({}, options, Sandbox.optionsFromJwt(token));

    return Sandbox.init(config);
};

/**
 * Create a Sandbox instance
 *
 * @param {Object} options - The options for creating the Sandbox instance.
 * @param {String} [options.url] - The url of the webtask cluster. Defaults to the public 'sandbox.auth0-extend.com' cluster.
 * @param {String} options.container - The container with which this Sandbox instance should be associated. Note that your Webtask token must give you access to that container or all operations will fail.
 * @param {String} options.token - The Webtask Token. See: https://webtask.io/docs/api_issue.
 * @returns {Sandbox} A {@see Sandbox} instance.
 *
 * @alias module:sandboxjs.init
 */
Sandbox.init = function (options) {
    if (typeof options !== 'object') throw new Error('Expecting an options Object, got `' + typeof options + '`.');
    if (!options.container) throw new Error('A Sandbox instance cannot be created without a container.');
    if (typeof options.container !== 'string') throw new Error('Only String containers are supported, got `' + typeof options.container + '`.');

    defaults(options, {
        url: 'https://sandbox.auth0-extend.com',
    });

    return new Sandbox(options);
};

Sandbox.optionsFromJwt = function (jwt) {
    var claims = Decode(jwt);

    if (!claims) throw new Error('Unable to decode token `' + jwt + '` (https://jwt.io/#id_token=' + jwt + ').');

    // What does the
    var ten = claims.ten;

    if (!ten) throw new Error('Invalid token, missing `ten` claim `' + jwt + '` (https://jwt.io/#id_token=' + jwt + ').');

    if (Array.isArray(ten)) {
        ten = ten[0];
    } else {
        // Check if the `ten` claim is a RegExp
        var matches = ten.match(/\/(.+)\//);
        if (matches) {
            try {
                var regex = new RegExp(matches[1]);
                var gen = new RandExp(regex);

                // Monkey-patch RandExp to be deterministic
                gen.randInt = function (l) { return l; };

                ten = gen.gen();
            } catch (err) {
                throw new Error('Unable to derive containtainer name from `ten` claim `' + claims.ten + '`: ' + err.message + '.');
            }
        }
    }

    if (typeof ten !== 'string' || !ten) throw new Error('Expecting `ten` claim to be a non-blank string, got `' + typeof ten + '`, with value `' + ten + '`.');

    return {
        container: ten,
        token: jwt,
    };
};
