'use strict';

var STATUS_CODES = require('http').STATUS_CODES;

var Track = /** @type Track */ require('./track');

var _ = require('lodash-node');
var accepts = require('accepts');
var cookieparser = require('cookieparser');
var libFastUrl = require('fast-url-parser');
var urlFastParse = libFastUrl.parse;
var urlFastFormat = libFastUrl.format;
var f = require('util').format;
var proxyAddr = require('proxy-addr');

/**
 * @class Connect
 * @extends Track
 *
 * @param {Server} app
 * @param {Logger} logger
 * @param {IncomingMessage} req
 * @param {ServerResponse} res
 * */
function Connect(app, logger, req, res) {

    /**
     * @public
     * @memberOf {Connect}
     * @property
     * @type {IncomingMessage}
     * */
    this.req = req;

    /**
     * @public
     * @memberOf {Connect}
     * @property
     * @type {ServerResponse}
     * */
    this.res = res;

    //  TODO give connect and track same signature
    Track.call(this, app, logger);

    /**
     * @public
     * @memberOf {Connect}
     * @property
     * @type {String}
     * */
    this.route = null;

    /**
     * @public
     * @memberOf {Connect}
     * @property
     * @type {Array<String>}
     * */
    this.matches = [];

    /**
     * @public
     * @memberOf {Connect}
     * @property
     * @type {Number}
     * */
    this.routeIndex = -1;

    /**
     * @private
     * @memberOf {Connect}
     * @property
     * @type {Object}
     * */
    this._url = null;
}

Connect.prototype = Object.create(Track.prototype);

/**
 * @public
 * @memberOf {Connect}
 * @getter
 * @type {Object}
 * */
Object.defineProperty(Connect.prototype, 'url', {
    get: function () {
        if (!this._url) {
            this._url = this._getUrlObj();
        }
        return this._url;
    }
});

/**
 * @public
 * @memberOf {Connect}
 * @method
 *
 * @constructs
 * */
Connect.prototype.constructor = Connect;

/**
 * @protected
 * @memberOf {Connect}
 * @method
 *
 * @returns {String}
 * */
Connect.prototype._createId = function () {
    return this.req.id;
};

/**
 * @public
 * @memberOf {Connect}
 * @method
 *
 * @returns {Boolean}
 * */
Connect.prototype.wasSent = function () {
    return this.res.headersSent || !this.req.socket.writable;
};

/**
 * @public
 * @memberOf {Connect}
 * @method
 *
 * @returns {Boolean}
 * */
Connect.prototype.isFlushed = function () {
    return Track.prototype.isFlushed.call(this) || this.wasSent();
};

/**
 * @public
 * @memberOf {Connect}
 * @method
 *
 * @param {Number} [statusCode]
 *
 * @returns {Number|Connect}
 * */
Connect.prototype.status = function (statusCode) {
    if (!arguments.length) {
        return this.res.statusCode;
    }

    this.res.statusCode = statusCode;

    return this;
};

/**
 * Читает заголовок запроса или ставит заголовок ответа
 *
 * @public
 * @memberOf {Connect}
 * @method
 *
 * @param {String} [name]
 * @param {*} [value]
 *
 * @returns {*}
 * */
Connect.prototype.header = function (name, value) {
    var req = this.req;
    var res = this.res;

    if (!arguments.length) {
        return req.headers;
    }

    if (name && typeof name === 'object') {
        _.forOwn(name, function (v, k) {
            res.setHeader(k, v);
        });

        return this;
    }

    if (arguments.length === 1) {
        return req.headers[String(name).toLowerCase()];
    }

    res.setHeader(name, value);

    return this;
};

/**
 * @public
 * @memberOf {Connect}
 * @method
 *
 * @param {String} [name]
 * @param {String} [value]
 * @param {Object} [opts]
 * */
Connect.prototype.cookie = function (name, value, opts) {
    var res = this.res;
    var setCookie;
    var l = arguments.length;

    if (!l) {
        return this._getCookies();
    }

    if (l === 1) {
        return this._getCookies()[name];
    }

    setCookie = res.getHeader('Set-Cookie');
    value = cookieparser.serialize(name, value, opts);

    if (!setCookie) {
        res.setHeader('Set-Cookie', value);
    } else if (!Array.isArray(setCookie)) {
        res.setHeader('Set-Cookie', [setCookie, value]);
    } else {
        setCookie[setCookie.length] = value;
    }

    return this;
};

/**
 * @public
 * @memberOf {Connect}
 * @method
 *
 * @param {String|Array<String>} types
 *
 * @returns {String}
 * */
Connect.prototype.acceptTypes = function (types) {
    return accepts(this.req).types(types);
};

/**
 * @public
 * @memberOf {Connect}
 * @method
 *
 * @param {String|Array<String>} encodings
 *
 * @returns {String}
 * */
Connect.prototype.acceptEncodings = function (encodings) {
    return accepts(this.req).encodings(encodings);
};

/**
 * @public
 * @memberOf {Connect}
 * @method
 *
 * @param {String|Array<String>} charsets
 *
 * @returns {String}
 * */
Connect.prototype.acceptCharsets = function (charsets) {
    return accepts(this.req).charsets(charsets);
};

/**
 * @public
 * @memberOf {Connect}
 * @method
 *
 * @param {String|Array<String>} languages
 *
 * @returns {String}
 * */
Connect.prototype.acceptLanguages = function (languages) {
    return accepts(this.req).languages(languages);
};

/**
 * @public
 * @memberOf {Connect}
 * @method
 *
 * @param {String} url
 * @param {Number} [status]
 * */
Connect.prototype.redirect = function (url, status) {
    var redirectUrl = urlFastParse(url, false, true);
    var pathname = decodeURIComponent(redirectUrl.pathname);

    if (!redirectUrl.host) {
        pathname = this._app.router.params.basePath + pathname;
    }

    //   make URL absolute
    redirectUrl = urlFastFormat({
        // inherit protocol from incoming request if needed
        protocol: redirectUrl.protocol || this.url.protocol,
        // inherit host from incoming request if needed
        host: redirectUrl.host || this.url.host,
        // may be encoded while parsing
        pathname: pathname,
        search: redirectUrl.search,
        hash: redirectUrl.hash
    });

    this.res.statusCode = status || 302;
    this.res.setHeader('Location', redirectUrl);

    if (this.acceptTypes(['html'])) {
        redirectUrl = _.escape(redirectUrl);
        this.res.setHeader('Content-Type', 'text/html; charset=utf-8');
        this.res.end(f('%s', redirectUrl, redirectUrl));
    } else {
        this.res.setHeader('Content-Type', 'text/plain; charset=utf-8');
        this.res.end(redirectUrl);
    }

    return this;
};

/**
 * @public
 * @memberOf {Connect}
 * @method
 *
 * @param {*} data
 * */
Connect.prototype.send = function (data) {
    if (data === void 0) {
        return this._sendUndefined();
    }

    if (typeof data === 'string') {
        return this._sendString(data);
    }

    if (Buffer.isBuffer(data)) {
        return this._sendBuffer(data);
    }

    if (data && typeof data === 'object' && typeof data.pipe === 'function') {
        return this._sendReadable(data);
    }

    if (data instanceof Error) {
        return this._sendError(data);
    }

    return this._sendJSON(data);
};

/**
 * @public
 * @memberOf {Connect}
 * @method
 *
 * @returns {String}
 * */
Connect.prototype.getIp = function () {
    return proxyAddr(this.req, this._app.params.trustProxy);
};

/**
 * @public
 * @memberOf {Connect}
 * @method
 *
 * @returns {String}
 * */
Connect.prototype.getIps = function () {
    return proxyAddr.all(this.req, this._app.params.trustProxy);
};

/**
 * @public
 * @memberOf {Connect}
 * @method
 *
 * @returns {String}
 * */
Connect.prototype.getHost = function () {
    var host = this.req.headers['x-forwarded-host'];

    if (host && this._isAddressTrusted()) {
        return host;
    }

    return this.req.headers.host || 'localhost';
};

/**
 * @public
 * @memberOf {Connect}
 * @method
 *
 * @returns {String}
 * */
Connect.prototype.getHostname = function () {
    var host = this.getHost();

    if (host.charAt(0) === '[') {
        return (host.match(/\[([^\[\]]+)]/) || [0, host])[1];
    }

    return (host.match(/([^:]+)/) || [0, host])[1];
};

/**
 * @public
 * @memberOf {Connect}
 * @method
 *
 * @returns {String}
 * */
Connect.prototype.getProtocol = function () {
    var headers = this.req.headers;
    var protocol = this.req.connection.encrypted ? 'https' : 'http';

    if (headers['x-forwarded-proto'] && this._isAddressTrusted()) {
        return headers['x-forwarded-proto'].split(/\s*,\s*/)[0];
    }

    return protocol;
};

//  aliases
Connect.prototype.acceptEncoding = Connect.prototype.acceptEncodings;
Connect.prototype.acceptType = Connect.prototype.acceptTypes;
Connect.prototype.acceptCharset = Connect.prototype.acceptCharsets;
Connect.prototype.acceptLanguage = Connect.prototype.acceptLanguages;

/**
 * @public
 * @memberOf {Connect}
 * @method
 *
 * @returns {Boolean}
 * */
Connect.prototype._isAddressTrusted = function () {
    return this._app.params.trustProxy(this.req.connection.remoteAddress);
};

/**
 * @protected
 * @memberOf {Connect}
 * @method
 *
 * @returns {Object}
 * */
Connect.prototype._getUrlObj = function $Connect$getUrlObj() {
    return urlFastParse(this._getUrlHref(), true);
};

/**
 * @protected
 * @memberOf {Connect}
 * @method
 *
 * @returns {String}
 * */
Connect.prototype._getUrlHref = function $Connect$getUrlHref() {
    return this.getProtocol() + '://' + this.getHost() + this.req.url;
};

/**
 * @protected
 * @memberOf {Connect}
 * @method
 *
 * @returns {Object}
 * */
Connect.prototype._getCookies = function () {
    if (!this.__cookies) {
        this.__cookies = cookieparser.parse(this.req.headers.cookie || '');
    }

    return this.__cookies;
};

/**
 * @protected
 * @memberOf {Connect}
 * @method
 */
Connect.prototype._sendUndefined = function () {
    return this._sendString(STATUS_CODES[this.res.statusCode]);
};

/**
 * @protected
 * @memberOf {Connect}
 * @method
 *
 * @param {String} data
 */
Connect.prototype._sendString = function (data) {
    var res = this.res;

    if (!res.getHeader('Content-Type')) {
        res.setHeader('Content-Type', 'text/plain');
    }

    res.removeHeader('Content-Length');
    res.setHeader('Content-Length', Buffer.byteLength(data));

    res.end(data);
};

/**
 * @protected
 * @memberOf {Connect}
 * @method
 *
 * @param {Buffer} data
 */
Connect.prototype._sendBuffer = function (data) {
    var res = this.res;

    if (!res.getHeader('Content-Type')) {
        res.setHeader('Content-Type', 'application/octet-stream');
    }

    res.removeHeader('Content-Length');
    res.setHeader('Content-Length', data.length);

    res.end(data);
};

/**
 * @protected
 * @memberOf {Connect}
 * @method
 *
 * @param {Stream.Readable} data
 */
Connect.prototype._sendReadable = function (data) {
    var res = this.res;

    if (!res.getHeader('Content-Type')) {
        res.setHeader('Content-Type', 'application/octet-stream');
    }

    // force flushing
    this._isFlushed = true;

    data.pipe(res);
};

/**
 * @protected
 * @memberOf {Connect}
 * @method
 *
 * @param {Error} data
 */
Connect.prototype._sendError = function (data) {
    if (typeof data.stack === 'string') {
        return this._sendString(data.stack);
    }

    return this._sendString(String(data));
};

/**
 * @protected
 * @memberOf {Connect}
 * @method
 *
 * @param {*} data
 */
Connect.prototype._sendJSON = function (data) {
    var outgoing = this.res;

    data = JSON.stringify(data);

    if (!outgoing.getHeader('Content-Type')) {
        outgoing.setHeader('Content-Type', 'application/json');
    }

    outgoing.removeHeader('Content-Length');
    outgoing.setHeader('Content-Length', Buffer.byteLength(data));

    outgoing.end(data);
};

module.exports = Connect;


"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
const chokidar = require("chokidar");
const mime = require("mime");
const ALY = require("aliyun-sdk");
const path = require("path");
const fs = require("fs");
const url = require("url");
const crypto = require("crypto");
module.exports = function (options) {
    return __awaiter(this, void 0, void 0, function* () {
        //create OSS instance
        //options.oss.apiVersion = options.oss.apiVersion || '2013-10-15';
        if (!options.AccessKeySecret) {
            throw new Error('Please provide AccessKeySecret.');
        }
        if (!options.AccessKeyId) {
            throw new Error('Please provide AccessKeyId.');
        }
        options.oss.accessKeyId = options.AccessKeyId;
        options.oss.secretAccessKey = options.AccessKeySecret;
        options.oss.securityToken = options.oss.securityToken || '';
        options.oss.apiVersion = options.oss.apiVersion || '2013-10-15';
        if (!options.oss.endpoint && !options.oss.region) {
            throw new Error('Please provide oss endpoint or region.');
        }
        if (!options.oss.endpoint) {
            options.oss.endpoint = (options.oss.secure ? 'https' : 'http') + '://' + options.oss.region + (options.oss.internal ? '.internal' : '') + '.aliyuncs.com';
        }
        if (options.oss.secure === undefined) {
            options.cdn.secure = /https/.test(options.oss.endpoint);
        }
        let oss = new ALY.OSS(options.oss);
        //create cdn instance
        let cdn;
        if (options.cdn) {
            options.cdn.accessKeyId = options.AccessKeyId;
            options.cdn.secretAccessKey = options.AccessKeySecret;
            options.cdn.endpoint = options.cdn.endpoint || 'https://cdn.aliyuncs.com';
            options.cdn.apiVersion = options.cdn.apiVersion || '2014-11-11';
            cdn = new ALY.CDN(options.cdn);
        }
        const cwd = options.syncDir || '';
        let getBuckets = function () {
            return new Promise(function (resolve, reject) {
                oss.listBuckets(function (err, list) {
                    if (err) {
                        reject(err);
                    }
                    else {
                        resolve(list);
                    }
                });
            });
        };
        let buckets = yield getBuckets();
        let bucketsList = buckets.Buckets;
        let bucket;
        for (let i = 0, l = bucketsList.length; i < l; i++) {
            if (options.oss.bucket.toUpperCase() === bucketsList[i].Name.toUpperCase()) {
                bucket = bucketsList[i];
            }
        }
        if (!bucket) {
            throw new Error('Can not find your bucket. Pleas check the bucket name again.');
        }
        //get cdn refresh quota
        if (options.oss.autoRefreshCDN && cdn) {
            let getCDNRefreshQuota = function () {
                return new Promise(function (resolve) {
                    cdn.describeRefreshQuota(function (err, res) {
                        resolve(res);
                    });
                });
            };
            let res = yield getCDNRefreshQuota();
            console.log(res);
            options.cdn.refreshQuota = res.UrlRemain;
            options.debug && console.log('Refresh CDN file quota: ' + options.cdn.refreshQuota);
        }
        //oss use unix type of file type
        const prefix = cwd.replace(/\\/g, '/');
        let getObjects = function (cb) {
            let nextMarker = '';
            let bucketObjects = [];
            let getObjectsLoop = function () {
                if (typeof nextMarker === 'string') {
                    oss.listObjects({
                        Bucket: bucket.Name,
                        MaxKeys: 20,
                        Prefix: prefix,
                        Marker: nextMarker
                    }, function (listObjectsErr, ossObject) {
                        ossObject.Contents.forEach(function (o) {
                            options.debug && console.log('Found bucket file: ', o.Key);
                        });
                        if (listObjectsErr) {
                            throw new Error(listObjectsErr);
                        }
                        nextMarker = ossObject.NextMarker;
                        if (ossObject.NextMarker) {
                            options.debug && console.log('next marker: ' + ossObject.NextMarker);
                        }
                        else {
                            options.debug && console.log('Reach the end of the bucket.');
                        }
                        bucketObjects = bucketObjects.concat(ossObject.Contents);
                        getObjectsLoop();
                    });
                }
                else {
                    cb(bucketObjects);
                    options.debug && console.log('Scan oss bucket finish.');
                }
            };
            getObjectsLoop();
        };
        getObjects(function (bucketObjects) {
            //get all path of the bucket
            let bucketPaths = [];
            let localPaths = [];
            for (let i = 0; i < bucketObjects.length; i++) {
                bucketPaths.push(bucketObjects[i].Key);
            }
            const pathsArr = Array.isArray(options.syncFilter) ? options.syncFilter : options.syncFilter.split(' ');
            for (let i = 0; i < pathsArr.length; i++) {
                if (pathsArr[i].startsWith('!')) {
                    pathsArr[i] = pathsArr[i].replace('!', '');
                    pathsArr[i] = '!' + path.join(cwd, pathsArr[i]);
                }
                else {
                    pathsArr[i] = path.join(cwd, pathsArr[i]);
                }
            }
            //set watcher default config value
            options.watch = options.watch || {};
            let filesWatcher = chokidar.watch(pathsArr, options.watch);
            //upload or update file function
            let upsertFile = function (localFilePath) {
                let contentType = mime.lookup(localFilePath);
                let standerFilePath = localFilePath.replace(/\\/g, '/');
                fs.readFile(localFilePath, function (readFileErr, fileData) {
                    if (readFileErr) {
                        throw readFileErr;
                    }
                    const putConfig = {
                        Bucket: bucket.Name,
                        Body: fileData,
                        Key: standerFilePath,
                        ContentType: contentType,
                        AccessControlAllowOrigin: options.AccessControlAllowOrigin || '*',
                        CacheControl: options.CacheControl || 'no-cache',
                        Expires: options.Expires || null
                    };
                    if (options.contentEncoding) {
                        putConfig.ContentEncoding = options.contentEncoding;
                    }
                    oss.putObject(putConfig, function (putObjectErr) {
                        if (putObjectErr) {
                            console.error('error:', putObjectErr);
                            return putObjectErr;
                        }
                        console.log('upload success: ' + localFilePath);
                        if (bucketPaths.indexOf(standerFilePath) === -1) {
                            bucketPaths.push(standerFilePath);
                        }
                        if (localPaths.indexOf(standerFilePath) === -1) {
                            localPaths.push(standerFilePath);
                        }
                        //refresh cdn
                        if (options.oss.autoRefreshCDN && cdn) {
                            if (options.cdn.refreshQuota < 1) {
                                console.error('There is no refresh cdn url quota today.');
                                return;
                            }
                            let cdnDomain = '';
                            if (/^http/.test(options.cdn.domain)) {
                                cdnDomain = options.cdn.domain.replace(/^https?:?\/?\/?/, '');
                                options.cdn.secure === undefined && (options.cdn.secure = /^https/.test(options.cdn.domein));
                            }
                            else {
                                cdnDomain = options.cdn.domain;
                            }
                            let cdnObjectPath = url.format({
                                protocol: options.oss.secure ? 'https' : 'http',
                                hostname: cdnDomain,
                                pathname: standerFilePath
                            });
                            options.debug && console.log('Refreshing CDN file: ', cdnObjectPath);
                            cdn.refreshObjectCaches({
                                ObjectType: 'File',
                                ObjectPath: cdnObjectPath
                            }, function (refreshCDNErr) {
                                if (refreshCDNErr) {
                                    console.error('refresh cdn error: ', refreshCDNErr);
                                }
                                else {
                                    options.cdn.refreshQuota--;
                                    console.log('Refresh cdn file success: ', cdnObjectPath);
                                }
                            });
                        }
                    });
                });
            };
            //delete bucket file function
            let deleteFile = function (filePath) {
                let standerPath = filePath.replace(/\\/g, '/');
                oss.deleteObject({
                    Bucket: bucket.Name,
                    Key: standerPath
                }, function (err) {
                    if (err) {
                        console.log('error:', err);
                        return err;
                    }
                    let bucketIndex = bucketPaths.indexOf(standerPath);
                    if (bucketIndex !== -1) {
                        bucketPaths.splice(bucketIndex, 1);
                    }
                    let localIndex = localPaths.indexOf(standerPath);
                    if (localIndex !== -1) {
                        localPaths.splice(localIndex, 1);
                    }
                    console.log('delete success:' + standerPath);
                });
            };
            //add new files
            filesWatcher.on('add', function (localFilePath) {
                let standerFilePath = localFilePath.replace(/\\/g, '/');
                let bucketIndex = bucketPaths.indexOf(standerFilePath);
                if (bucketIndex === -1) {
                    options.debug && console.log('Bucket file not exist, uploading local file: ' + localFilePath);
                    upsertFile(localFilePath);
                }
                else {
                    if (localPaths.indexOf(standerFilePath) === -1) {
                        localPaths.push(standerFilePath);
                    }
                    fs.readFile(localFilePath, function (readFileErr, fileData) {
                        let fileMd5 = crypto.createHash('md5').update(fileData).digest('hex').toUpperCase();
                        for (let i = 0; i < bucketObjects.length; i++) {
                            if (bucketObjects[i].Key === standerFilePath) {
                                if (bucketObjects[i].ETag.replace(/"/g, '') !== fileMd5) {
                                    options.debug && console.log('ETag different, uploading local file: ' + localFilePath);
                                    upsertFile(localFilePath);
                                }
                            }
                        }
                    });
                }
            });
            //Initial scan complete.
            filesWatcher.on('ready', function () {
                //delete bucket object if local object is not exist.
                for (let i = 0; i < bucketPaths.length; i++) {
                    if (localPaths.indexOf(bucketPaths[i]) === -1) {
                        let filePath = bucketPaths[i];
                        options.debug && console.log('No this local file found, deleting: ', filePath);
                        deleteFile(filePath);
                    }
                }
                options.debug && console.log('Scanning local file finish.');
                if (options.keepWatching !== true) {
                    filesWatcher.close();
                    console.log('Sync files watcher closed.');
                    return;
                }
                console.log('Sync files watcher running...');
            });
            //modify file
            filesWatcher.on('change', function (filePath) {
                console.log('Local file change, uploading: ' + filePath);
                upsertFile(filePath);
            });
            //delete file
            filesWatcher.on('unlink', function (filePath) {
                console.log('Deleting bucket file: ' + filePath);
                deleteFile(filePath);
            });
        });
    });
};


'use strict';

// accepted
var B00000001 = parseInt('00000001', 2);
// rejected
var B00000010 = parseInt('00000010', 2);
// skipping
var B00000100 = parseInt('00000100', 2);
// updating
var B00001000 = parseInt('00001000', 2);
// skip cache
var B00010000 = parseInt('00010000', 2);
// finished
var B00100000 = parseInt('00100000', 2);

// accepted + rejected
var B00000011 = B00000010 | B00000001;
// skipping + rejected
var B00000110 = B00000100 | B00000010;

var Bluebird = /** @type Promise */ require('bluebird');
var Context = /** @type Context */ require('./context');
var Obus = /** @type Obus */ require('obus');

var maxStackSize = 1;

/**
 * @class Runtime
 * @extends Context
 *
 * @param {Unit} unit
 * @param {Track} track
 * @param {Runtime} [parent]
 * @param {Object} [args]
 * @param {Function} done
 *
 * @constructs
 * */
function Runtime(unit, track, parent, args, done) {
    // Create lite context to provide an interface to check execution parameters
    var context = new Context.Lite().
        // add default context params
        addParams(unit.params).
        // add track's params
        addParams(track.params).
        // add local args
        addParams(args);

    /**
     * Runtime identity is a part of cacheKey and memorization key
     *
     * @public
     * @memberOf {Runtime}
     * @property
     * @type {String}
     * */
    this.identity = unit.identify(track, context);

    /**
     * Invoking unit
     *
     * @public
     * @memberOf {Runtime}
     * @property
     * @type {Unit}
     * */
    this.unit = unit;

    /**
     * Request handling runtime
     *
     * @public
     * @memberOf {Runtime}
     * @property
     * @type {Track}
     * */
    this.track = track;

    /**
     * The dependant Runtime
     *
     * @public
     * @memberOf {Runtime}
     * @property
     * @type {Runtime}
     * */
    this.parent = parent;

    /**
     * Finish listener
     *
     * @public
     * @memberOf {Runtime}
     * @property
     * @type {Function}
     * */
    this.done = done;

    /**
     * The number of dependencies remaining to resolve
     *
     * @public
     * @memberOf {Runtime}
     * @property
     * @type {Number}
     * */
    this.pathsLeft = 0;

    /**
     * The array of dependency identities
     *
     * @public
     * @memberOf {Runtime}
     * @property
     * @type {Array}
     * */
    this.keys = [];

    /**
     * @public
     * @memberOf {Runtime}
     * @property
     * @type {Date}
     * */
    this.creationDate = 0;

    /**
     * @public
     * @memberOf {Runtime}
     * @property
     * @type {*}
     * */
    this.value = undefined;

    /**
     * The status of current runtime
     *
     * @public
     * @memberOf {Runtime}
     * @property
     * @type {Number}
     * */
    this.statusBits = 0;

    /**
     * Runtime context
     *
     * @public
     * @memberOf {Runtime}
     * @property
     * @type {Context}
     * */
    this.context = context;

    /**
     * @public
     * @memberOf {Runtime}
     * @property
     * @type {Array<Runtime>}
     * */
    this.listeners = [];

    /**
     * @public
     * @memberOf {Runtime}
     * @property
     * @type {String}
     * */
    this.cacheKey = unit.app.params.name;
}

/**
 * @public
 * @static
 * @memberOf {Runtime}
 * @method
 *
 * @param {Unit} unit
 * @param {Track} track
 * @param {Object} args
 * @param {Function} done
 * */
Runtime.startRun = function $Runtime$startRun(unit, track, args, done) {
    // All request unit calls
    var runtimeCache = track.calls;
    // Host application
    var app = unit.app;
    var deps;
    var l;
    var pos = 0;
    var identity;
    var existingRuns;
    var stack = new Array(maxStackSize);
    var runtime = new this(unit, track, null, args, done);
    var unitName;
    var logger;

    stack[pos] = runtime;

    do {
        // unpack stack item
        runtime = stack[pos];
        unit = runtime.unit;
        identity = runtime.identity;
        unitName = unit.name;

        // check for allocated runs object
        if (runtimeCache.hasOwnProperty(unitName)) {
            // The unit was already called
            existingRuns = runtimeCache[unitName];
        } else {
            // First time unit call
            existingRuns = runtimeCache[unitName] = {};
        }

        // check for existing execution
        if (existingRuns.hasOwnProperty(identity)) {
            // execution exist, set finish listener for existing execution
            $Runtime$addListener(existingRuns[identity], runtime);
            continue;
        }

        // memorize new execution by identity
        existingRuns[identity] = runtime;

        // bind unit's logger to track and execution identity
        logger = unit.logger.bind(track.id);

        // Now we can complete runtime initialization

        // Upgrade ContextLite to Context
        runtime.context = new Context(runtime.context.params, logger);

        // is not it so extraneous?
        logger.debug('Running...');

        // Set runtime creation date
        runtime.creationDate = new Date();

        // Set `skip cache` bit if maxAge less than 0 or not a number
        runtime.statusBits = B00010000 * !(unit.maxAge > 0);

        deps = unit.deps;
        l = runtime.pathsLeft = deps.length;

        if (l === 0) {
            // no deps, immediately execute runtime
            $Runtime$execute(runtime);
            continue;
        }

        // Allocate an array for deps identities
        runtime.keys = new Array(l);

        // deps raises child runtimes
        while (l) {
            l -= 1;
            unitName = deps[l];
            args = unit.depsArgs[unitName](track, runtime.context);
            stack[pos] = new this(app.getUnit(unitName), track, runtime, args, $Runtime$doneChild);
            pos += 1;
        }

        // auto adjust initial stack size, for future runs
        maxStackSize = Math.max(pos, maxStackSize);
        /*eslint no-plusplus: 0*/
    } while (pos--);
};

/**
 * @public
 * @memberOf {Runtime}
 * @method
 *
 * @constructs
 * */
Runtime.prototype.constructor = Runtime;

/**
 * @public
 * @memberOf {Runtime}
 * @method
 *
 * @returns {*}
 * */
Runtime.prototype.valueOf = function () {
    return this.value;
};

/**
 * @public
 * @memberOf {Runtime}
 * @method
 *
 * @returns {Boolean}
 * */
Runtime.prototype.isAccepted = function () {
    return (this.statusBits & B00000001) > 0;
};

/**
 * @public
 * @memberOf {Runtime}
 * @method
 *
 * @returns {Boolean}
 * */
Runtime.prototype.isRejected = function () {
    return (this.statusBits & B00000010) > 0;
};

/**
 * @public
 * @memberOf {Runtime}
 * @method
 *
 * @returns {Boolean}
 * */
Runtime.prototype.isResolved = function () {
    return (this.statusBits & B00000011) > 0;
};

function $Runtime$addListener(parent, runtime) {
    if (parent.statusBits & B00100000) {
        // has `finished` bit
        (0, runtime.done)(parent, runtime.parent);
    } else {
        // intermediate yet
        parent.listeners.push(runtime);
    }
}

function $Runtime$fbind(func, runtime) {
    return function $Runtime$bound(err, res) {
        return func(runtime, err, res);
    };
}

function $Runtime$doneChild(runtime, parent) {
    var name;

    //  parent has `skipping` bit
    //  NOTE: it is possible if one dependency flushes the track, and flush was bubbled to parent,
    //  then, next dependency tries to finish execution, this guard needed to prevent multiple runtime finishing
    if (parent.statusBits & B00000100) {
        return;
    }

    if (runtime.statusBits & B00010000) {
        // runtime has `skip cache` bit
        parent.statusBits |= B00010000;
    }

    if (runtime.statusBits & B00000100) {
        // runtime has `skipping` bit
        // set `skipping` bit to parent
        parent.statusBits |= B00000100;
        $Runtime$finish(parent);
        return;
    }

    name = runtime.unit.name;

    if (runtime.statusBits & B00000001) {
        //  runtime has `accepted` bit
        //  set `updating` bit to parent if runtime has `updating` bit
        parent.statusBits |= runtime.statusBits & B00001000;
        // add children identity to parent as part for cache key
        parent.keys[parent.unit.depsIndex[name]] = runtime.identity;
        // add children execution result to parent's context
        Obus.add(parent.context.result, parent.unit.depsMap[name], runtime.value);
    } else {
        //  has `rejected` bit
        //  set parent skip cache
        parent.statusBits |= B00010000;
        // add children execution fail reason to parent's context
        Obus.add(parent.context.errors, parent.unit.depsMap[name], runtime.value);
    }

    // reduce the counter of resolved dependencies
    parent.pathsLeft -= 1;

    if (parent.pathsLeft === 0) {
        // all the dependencies was resolved
        $Runtime$execute(parent);
    }
}

function $Runtime$execute(runtime) {
    if (runtime.statusBits & B00010000) {
        // has `skip cache` bit
        $Runtime$callUnitWithNoCacheUpdating(runtime);
        return;
    }

    // init cache key
    runtime.cacheKey += '-' + runtime.unit.name + '-' + runtime.identity + '-' + runtime.keys.join('-');

    if (runtime.statusBits & B00001000) {
        // has `updating` bit
        $Runtime$callUnit(runtime);
        return;
    }

    // Try to get cached result for runtime
    runtime.unit.cache.get(runtime.cacheKey, $Runtime$fbind($Runtime$onCacheGot, runtime));
}

function $Runtime$callUnit(runtime) {
    return Bluebird.attempt($Runtime$callUnitMain, runtime).
        done(function (res) {
            // Tear down unit.main() call
            $Runtime$afterMainCalled(runtime, res, B00000001);
        }, function (err) {
            // unit.main() was not successfully completed
            runtime.context.logger.error(err);
            // Tear down unit.main() call
            $Runtime$afterMainCalled(runtime, err, B00000010);
        });
}

function $Runtime$callUnitWithNoCacheUpdating(runtime) {
    // Same as $Runtime$callUnit, but does not tries to update cache with returned result
    return Bluebird.attempt($Runtime$callUnitMain, runtime).
        done(function (res) {
            $Runtime$afterMainCalled2(runtime, res, B00000001);
        }, function (err) {
            runtime.context.logger.error(err);
            $Runtime$afterMainCalled2(runtime, err, B00000010);
        });
}

function $Runtime$callUnitMain(runtime) {
    return runtime.unit.main(runtime.track, runtime.context);
}

function $Runtime$onCacheGot(runtime, err, res) {
    if (res) {
        runtime.context.logger.debug('Found in cache');
        runtime.value = res.value;
        // set `accepted` bit
        runtime.statusBits |= B00000001;
        $Runtime$finish(runtime);
        return;
    }

    if (err) {
        runtime.context.logger.warn(err);
    } else {
        // value was not found in cache
        runtime.context.logger.debug('Outdated');
    }

    // set `updating` bit, schedule update
    runtime.statusBits |= B00001000;

    $Runtime$callUnit(runtime);
}

function $Runtime$finish(runtime) {
    var i;
    var l;
    var listener;
    var listeners = runtime.listeners;
    var timePassed = new Date() - runtime.creationDate;

    // set `finished` bit
    runtime.statusBits |= B00100000;

    if (runtime.statusBits & B00000001) {
        //  has `accepted` bit
        runtime.context.logger.debug('Accepted in %dms', timePassed);
    } else if (runtime.statusBits & B00000010) {
        //  has `rejected` bit
        runtime.context.logger.debug('Rejected in %dms', timePassed);
    } else {
        // has no both `accepted` and `rejected` bits
        runtime.context.logger.debug('Skipping in %dms', timePassed);
    }

    // call runtime.done without context
    (0, runtime.done)(runtime, runtime.parent);

    for (i = 0, l = listeners.length; i < l; i += 1) {
        listener = listeners[i];
        // call listener.done without context
        (0, listener.done)(runtime, listener.parent);
    }
}

function $Runtime$afterMainCalled(runtime, value, statusBitMask) {
    // set `skipping` bit if track is flushed + `updating` + statusBitMask bits
    runtime.statusBits |= B00000100 * runtime.track.isFlushed() | B00001000 | statusBitMask;
    // assign value to a runtime
    runtime.value = value;

    if ((runtime.statusBits & B00000110) === 0) {
        // has no `skipping` and/or `rejected` bits
        runtime.unit.cache.set(runtime.cacheKey, {value: value},
            runtime.unit.maxAge, $Runtime$fbind($Runtime$onCacheSet, runtime));
    }

    $Runtime$finish(runtime);
}

function $Runtime$afterMainCalled2(runtime, value, statusBitMask) {
    // set `skipping` bit if track is flushed + `updating` + statusBitMask bits
    runtime.statusBits |= B00000100 * runtime.track.isFlushed() | B00001000 | statusBitMask;
    // assign value to a runtime
    runtime.value = value;

    $Runtime$finish(runtime);
}

function $Runtime$onCacheSet(runtime, err) {
    // Just noop function, cache set issues
    if (err) {
        runtime.context.logger.warn(err);
    } else {
        runtime.context.logger.debug('Updated');
    }
}

module.exports = Runtime;


const debug = require('debug')('great')
const setupGetService = require('./utils/getService')

const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)))

const handleAction = (action, resources, actionHandlers) => {
  if (action) {
    const { type, payload = {}, meta = {} } = action
    const handler = actionHandlers[type]

    if (typeof handler === 'function') {
      return handler({ type, payload, meta }, resources)
    }
  }

  return { status: 'noaction' }
}

/**
 * Setup and return dispatch function. The dispatch function will call the
 * relevant action handler.
 * @param {Object} resources - Object with actions, schemas, services, and middlewares
 * @returns {function} Dispatch function, accepting an action as only argument
 */
function setupDispatch ({ actions: actionHandlers = {}, schemas = {}, services = {}, middlewares = [], identOptions = {} }) {
  const getService = setupGetService(schemas, services)

  let dispatch = async (action) => {
    debug('Dispatch: %o', action)
    return handleAction(
      action,
      {
        schemas,
        services,
        dispatch,
        identOptions,
        getService
      },
      actionHandlers
    )
  }

  if (middlewares.length > 0) {
    dispatch = compose(...middlewares)(dispatch)
  }

  return dispatch
}

module.exports = setupDispatch


const later = require('later')

const nextDate = (dates, allowNow) => {
  if (Array.isArray(dates) && dates[0]) {
    return (allowNow || dates[0].getTime() > Date.now()) ? dates[0] : dates[1]
  }
  return null
}

/**
 * Get next time for a schedule. Will never return the current time, even if it
 * is valid for the schedule, unless `allowNow` is true.
 * @param {Object} schedule - The schedule
 * @param {boolean} allowNow - True to allow now as next Date
 * @returns {Date} The next Date
 */
function nextSchedule (schedule, allowNow = false) {
  if (schedule) {
    try {
      const dates = later.schedule(schedule).next(2)
      return nextDate(dates, allowNow)
    } catch (error) {
      throw TypeError('Invalid schedule definition')
    }
  }
  return null
}

module.exports = nextSchedule


const debug = require('debug')('great')
const appendToAction = require('../utils/appendToAction')
const createError = require('../utils/createError')
const createUnknownServiceError = require('../utils/createUnknownServiceError')

const prepareData = (payload) => {
  const { type, id } = payload

  if (type && id) {
    // Delete one action -- return as data
    return [{ id, type }]
  } else {
    // Filter away null values
    return [].concat(payload.data)
      .filter((item) => !!item)
  }
}

/**
 * Delete several items from a service, based on the given payload.
 * @param {Object} payload - Payload from action object
 * @param {Object} resources - Object with getService
 * @returns {Object} Response object with any data returned from the service
 */
async function deleteFn (action, { getService } = {}) {
  debug('Action: DELETE')
  const { type, id, service: serviceId, endpoint } = action.payload

  const service = (typeof getService === 'function') ? getService(type, serviceId) : null
  if (!service) {
    return createUnknownServiceError(type, serviceId, 'DELETE')
  }

  const data = prepareData(action.payload)
  if (data.length === 0) {
    return createError(`No items to delete from service '${service.id}'`, 'noaction')
  }

  const endpointDebug = (endpoint) ? `endpoint '${endpoint}'` : `endpoint matching ${type} and ${id}`
  debug('DELETE: Delete from service \'%s\' at %s.', service.id, endpointDebug)

  const { response } = await service.send(appendToAction(action, { data }))

  return (response.status === 'ok') ? { status: 'ok' } : response
}

module.exports = deleteFn


const debug = require('debug')('great')
const createUnknownServiceError = require('../utils/createUnknownServiceError')

/**
 * Normalize and map a request to an action, and map and serialize its response.
 * @param {Object} action - The action object
 * @param {Object} resources - Object with getService and dispatch
 * @returns {Object} Response object
 */
async function request (action, { getService, dispatch }) {
  debug('Action: REQUEST')

  const {
    type,
    service: serviceId = null,
    endpoint
  } = action.payload

  const service = getService(type, serviceId)
  if (!service) {
    return createUnknownServiceError(type, serviceId, 'GET')
  }

  const endpointDebug = (endpoint) ? `endpoint '${endpoint}'` : `endpoint matching type '${type}'`
  debug('REQUEST: Fetch from service %s at %s', service.id, endpointDebug)

  const { response } = await service.receive(action, dispatch)

  return response
}

module.exports = request


const util = require('util')
const getField = require('../utils/getField')
const createError = require('../utils/createError')
const createUnknownServiceError = require('../utils/createUnknownServiceError')

const preparePropKeys = ({
  id = 'id',
  roles = 'roles',
  tokens = 'tokens'
} = {}) => ({
  id, roles, tokens
})

const prepareParams = (ident, keys) =>
  (ident.id) ? { [keys.id]: ident.id }
    : (ident.withToken) ? { [keys.tokens]: ident.withToken }
      : null

const wrapOk = (data, ident) => ({ status: 'ok', data, access: { status: 'granted', ident } })

const prepareResponse = (response, params, propKeys) => {
  const { data } = response

  if (data && data[0]) {
    const completeIdent = {
      id: getField(data[0], propKeys.id),
      roles: getField(data[0], propKeys.roles),
      tokens: getField(data[0], propKeys.tokens)
    }
    return wrapOk(data[0], completeIdent)
  } else {
    return createError(`Could not find ident with params ${util.inspect(params)}`, 'notfound')
  }
}

/**
* Get an ident item from service, based on the meta.ident object on the action.
* @param {Object} action - Action object
* @param {Object} resources - Object with getService and identOptions
* @returns {Object} Response object with ident item as data
 */
async function getIdent ({ payload, meta }, { getService, identOptions = {} }) {
  if (!meta.ident) {
    return createError('GET_IDENT: The request has no ident', 'noaction')
  }

  const { type } = identOptions
  if (!type) {
    return createError('GET_IDENT: Integreat is not set up with authentication', 'noaction')
  }

  const service = getService(type)
  if (!service) {
    return createUnknownServiceError(type, null, 'GET_IDENT')
  }

  const propKeys = preparePropKeys(identOptions.props)
  const params = prepareParams(meta.ident, propKeys)
  if (!params) {
    return createError('GET_IDENT: The request has no ident with id or withToken', 'noaction')
  }

  const { response } = await service.send({
    type: 'GET',
    payload: { type, ...params },
    meta: { ident: { root: true } }
  })

  return prepareResponse(response, payload, propKeys)
}

module.exports = getIdent


const R = require('ramda')
const createService = require('./service')
const schema = require('./schema')
const setupMapping = require('./mapping')
const setupDispatch = require('./dispatch')
const builtinActions = require('./actions')

const version = '0.7.2'

/**
 * Return an Integreat instance with a dispatch method.
 * Use the dispatch method for sending actions to services, for retrieving typed
 * items and updating data.
 * @param {Object} defs - Services, schemas, mappings, and auths
 * @param {Object} resources - Object with adapters, authenticators, filters, transformers, and actions
 * @param {Array} middlewares - Array of middlewares
 * @returns {Object} Integration object with the dispatch method
 */
function integreat (
  {
    schemas: typeDefs,
    services: serviceDefs,
    mappings = [],
    auths: authDefs = [],
    ident: identOptions = {}
  },
  {
    adapters = {},
    authenticators = {},
    filters = {},
    transformers = {},
    actions = {}
  } = {},
  middlewares = []
) {
  if (!serviceDefs || !typeDefs) {
    throw new TypeError('Call integreat with at least services and schemas')
  }

  // Merge custom actions with built-in actions
  actions = { ...builtinActions, ...actions }

  // Setup schemas object from type defs
  const schemas = R.compose(
    R.indexBy(R.prop('id')),
    R.map(schema)
  )(typeDefs)

  const pluralTypes = Object.keys(schemas)
    .reduce((plurals, type) => ({ ...plurals, [schemas[type].plural]: type }), {})

  // Setup auths object from auth defs
  const auths = authDefs
    .reduce((auths, def) => (def)
      ? {
        ...auths,
        [def.id]: {
          authenticator: authenticators[def && def.authenticator],
          options: def.options,
          authentication: null
        }
      }
      : auths,
    {})

  // Setup services object from service defs.
  const services = R.compose(
    R.indexBy(R.prop('id')),
    R.map(createService({
      adapters,
      auths,
      transformers,
      schemas,
      setupMapping: setupMapping({ filters, transformers, schemas, mappings })
    }))
  )(serviceDefs)

  // Return Integreat instance
  return {
    version,
    schemas,
    services,
    identType: identOptions.type,

    /**
     * Function for dispatching actions to Integreat. Will be run through the
     * chain of middlewares before the relevant action handler is called.
     * @param {Object} action - The action to dispatch
     * @returns {Promise} Promise of result object
     */
    dispatch: setupDispatch({
      actions,
      services,
      schemas,
      middlewares,
      identOptions
    }),

    /**
     * Adds the `listener` function to the service's emitter for events with the
     * given `eventName` name.
     */
    on (eventName, serviceId, listener) {
      const service = services[serviceId]
      if (service && service.on) {
        service.on(eventName, listener)
      }
    },

    /**
     * Return schema type from its plural form.
     */
    typeFromPlural (plural) {
      return pluralTypes[plural]
    }
  }
}

module.exports = integreat


const later = require('later')
const nextSchedule = require('./nextSchedule')

const cleanSchedule = ({ schedules, exceptions }) => ({ schedules, exceptions })
const wrapSimpleSchedule = (schedule) => ({ schedules: [].concat(schedule) })

const parseStringDef = (def) => {
  const schedule = later.parse.text(def)
  if (schedule.error !== -1) {
    throw new Error('Invalid schedule string')
  }
  return cleanSchedule(schedule)
}

const parseSchedule = (def) => {
  if (typeof def === 'string') {
    return parseStringDef(def)
  } else if (def) {
    return (def.schedules || def.exceptions)
      ? cleanSchedule(def)
      : wrapSimpleSchedule(def)
  }

  return null
}

/**
 * Create a queuable action from a schedule definition.
 * @param {Object} def - A schedule definition
 * @returns {object} An action object
 */
function scheduleToAction (def) {
  if (!def) {
    return null
  }

  const id = def.id || null
  const schedule = parseSchedule(def.schedule)
  const nextTime = nextSchedule(schedule, true)

  return {
    ...def.action,
    meta: {
      id,
      schedule,
      queue: (nextTime) ? nextTime.getTime() : true
    }
  }
}

module.exports = scheduleToAction


const { doAuth, getScheme } = require('./authorizeScheme')
const getField = require('../utils/getField')

const doAuthRoleFromField = (field, data, ident) => {
  const role = getField(data, field)
  const roles = ident.roles || []
  return roles.includes(role)
}

const doAuthIdentFromField = (field, data, ident) => {
  const identId = getField(data, field)
  return (ident && ident.id === identId)
}

const authorizeWithScheme = (item, scheme, ident, requireAuth) => {
  if (scheme) {
    if (scheme.roleFromField) {
      return doAuthRoleFromField(scheme.roleFromField, item, ident)
    } else if (scheme.identFromField) {
      return doAuthIdentFromField(scheme.identFromField, item, ident)
    }
  }

  return doAuth(scheme, ident, requireAuth)
}

/**
 * Authorize data in Integreat's internal data format, according to the setting
 * of the relevant schema(s). The provided `access` object may be coming from
 * `authorizeRequest()`, and if this is refused already, this method will refuse
 * right away.
 *
 * Will return an access object, with status `granted` when all items are
 * successfully authorized, and `refused` when none is. In the case that only
 * some of the items are granted, the status will pe `partially`, and an
 * `itemStatus` property on the access object will hold an array of each item'
 * status, in the same order as the data array.
 *
 * @param {Object} item - A data item to authorize against
 * @param {Object} access - The access object to authorize against
 * @param {Object} options - Dataypes, action, and requireAuth
 */
function authorizeItem (item, access, { schemas, action, requireAuth }) {
  const { ident, status } = access

  if (status === 'refused') {
    return false
  }

  if (!item || (ident && ident.root)) {
    return true
  }

  const schema = schemas[item.type]
  const scheme = getScheme(schema, action)

  return authorizeWithScheme(item, scheme, ident, requireAuth)
}

module.exports = authorizeItem


const debug = require('debug')('great')
const appendToAction = require('../utils/appendToAction')
const createUnknownServiceError = require('../utils/createUnknownServiceError')

const hasCollectionEndpoint = (endpoints) =>
  endpoints.some((endpoint) => endpoint.match && endpoint.match.scope === 'members')

const getIndividualItems = async (ids, action, getService) => {
  const responses = await Promise.all(ids.map((id) => get(appendToAction(action, { id }), { getService })))
  if (responses.some((response) => response.status !== 'ok' && response.status !== 'notfound')) {
    return { status: 'error', error: `One or more of the requests for ids ${ids} failed.` }
  }
  return { status: 'ok', data: responses.map((response) => response.data && response.data[0]) }
}

const getIdFromPayload = ({ id }) =>
  (Array.isArray(id) && id.length === 1) ? id[0] : id

/**
 * Get several items from a service, based on the given action object.
 * @param {Object} action - payload and ident from the action object
 * @param {Object} resources - Object with getService
 * @returns {array} Array of data from the service
 */
async function get (action, { getService } = {}) {
  const {
    type,
    service: serviceId = null,
    onlyMappedValues = false,
    endpoint
  } = action.payload

  const service = (typeof getService === 'function')
    ? getService(type, serviceId) : null
  if (!service) {
    return createUnknownServiceError(type, serviceId, 'GET')
  }

  const id = getIdFromPayload(action.payload)

  // Do individual gets for array of ids, if there is no collection scoped endpoint
  if (Array.isArray(id) && !hasCollectionEndpoint(service.endpoints)) {
    return getIndividualItems(id, action, getService)
  }

  const endpointDebug = (endpoint) ? `endpoint '${endpoint}'` : `endpoint matching type '${type}' and id '${id}'`
  debug('GET: Fetch from service %s at %s', service.id, endpointDebug)

  const { response } = await service.send(appendToAction(action, { id, onlyMappedValues }))

  return response
}

module.exports = get


const createError = require('../utils/createError')

/**
 * Send the request with the adapter and return the response. Will serialize any
 * data in the request, and normalize any data in the response. The access
 * object on the request must have status granted, and will be returned on the
 * response object.
 *
 * @param {Object} request - The request object
 * @param {Object} options - adapter and serviceId
 * @returns {Object} The normalized response object
 */
function sendRequest ({ adapter, serviceId }) {
  return async ({ request, response, connection }) => {
    if (response) {
      return response
    }

    try {
      response = await adapter.send(request, connection)
      return { ...response, access: request.access }
    } catch (error) {
      return createError(`Error retrieving from service '${serviceId}'. ${error}`)
    }
  }
}

module.exports = sendRequest


const cast = require('./cast')
const castQueryParams = require('./castQueryParams')
const mapAny = require('map-any')

const expandField = (val) => (typeof val === 'string') ? { type: val } : val
const expandFields = (vals) => Object.keys(vals)
  .reduce((newVals, key) => ({ ...newVals, [key]: expandField(vals[key]) }), {})

const prepareDefaultAttrs = (attributes, attrDefs) => Object.keys(attrDefs || {})
  .reduce((attrs, key) => ({
    ...attrs,
    [key]: (attributes[key].default === undefined) ? null : attributes[key].default
  }), {})

const prepareDefaultRels = (relationships) => Object.keys(relationships)
  .reduce((rels, key) => {
    const def = relationships[key].default
    if (def !== undefined) {
      rels[key] = (def === null) ? null : { id: def, type: relationships[key].type }
    }
    return rels
  }, {})

/**
 * Create a schema with the given id and service.
 * @param {Object} def - Object with id, plural, service, attributes, and relationships
 * @returns {Object} The created schema
 */
function schema ({
  id,
  plural,
  service,
  attributes: attrDefs,
  relationships: relDefs,
  access,
  internal = false
}) {
  const attributes = {
    ...expandFields(attrDefs || {}),
    id: { type: 'string' },
    type: { type: 'string' },
    createdAt: { type: 'date' },
    updatedAt: { type: 'date' }
  }
  const relationships = expandFields(relDefs || {})

  const defaultAttrs = prepareDefaultAttrs(attributes, attrDefs)
  const defaultRels = prepareDefaultRels(relationships, relDefs)

  const castFn = cast({ id, attributes, relationships, defaultAttrs, defaultRels })

  return {
    id,
    plural: plural || `${id}s`,
    service,
    internal,
    attributes,
    relationships,
    access,

    /**
     * Will cast the given data according to the type. Attributes will be
     * coerced to the right format, relationships will be expanded to
     * relationship objects, and object properties will be moved from
     * `attributes` or be set with defaults.
     * @param {Object} data - The data to cast
     * @param {boolean} onlyMappedValues - Will use defaults if true
     * @returns {Object} Returned data in the format expected from the schema
     */
    cast (data, { onlyMappedValues = false } = {}) {
      return mapAny((data) => castFn(data, { onlyMappedValues }), data)
    },

    /**
     * Will create a query object for a relationship, given the relationship id
     * and a data item to get field values from.
     *
     * If the relationship is set up with a query definition object, each prop
     * of this object references field ids in the given data item. The returned
     * query object will have these ids replaced with actual field values.
     *
     * @param {string} relId - The id of a relationship
     * @param {Object} data - A data item to get field values from.
     * @returns {Object} Query object
     */
    castQueryParams (relId, data) {
      return castQueryParams(relId, data, { relationships })
    }
  }
}

module.exports = schema


const { compose, map, mergeDeepWith } = require('ramda')
const { mapTransform, transform, filter, compare, set, fwd, rev } = require('map-transform')
const is = require('@sindresorhus/is')
const { preparePipeline, prepareRevPipeline } = require('../utils/preparePipeline')
const { normalizeFieldMapping } = require('./normalize')

const hasFieldMappings = (id, attributes, relationships) =>
  id || Object.keys(attributes).length > 0 || Object.keys(relationships).length > 0

const prepareRelationship = (normalize, createPipeline) => (relationship) =>
  (relationship.mapping) ? createPipeline(relationship.mapping, undefined, relationship.path).pipeline : normalize(relationship)

function prepareMapping ({ attributes: { id = null, type = null, ...attributes } = {}, relationships = {}, toService = {} }, transformers, createPipeline) {
  if (hasFieldMappings(id, attributes, relationships)) {
    const normalize = normalizeFieldMapping(transformers)
    return {
      id: (id) ? normalize(id) : null,
      type: (type) ? rev(type) : null,
      attributes: map(normalize, attributes),
      relationships: map(prepareRelationship(normalize, createPipeline), relationships),
      ...map(normalize, toService)
    }
  } else {
    return {
      id: 'id',
      type: rev('type'),
      attributes: 'attributes',
      relationships: 'relationships'
    }
  }
}

const createCompareFilter = ([path, value]) => {
  try {
    return [filter(compare(path, JSON.parse(value)))]
  } catch (error) {
    return []
  }
}
const prepareQualifier = (qualifier) => (qualifier) ? createCompareFilter(qualifier.split('=')) : []

const prepareTypeQualifier = ({ attributes = {}, relationships = {}, type }) => (is.emptyObject(attributes) && is.emptyObject(relationships))
  ? [filter(compare('type', type))]
  : []

const concatOrRight = (left, right) => (Array.isArray(left)) ? left.concat(right) : right

const ensureArray = (data) => (Array.isArray(data)) ? data : ((data) ? [data] : [])

const overrideMappingProps = (mapping, overrideType, prependPath) => (mapping)
  ? {
    ...mapping,
    type: overrideType || mapping.type,
    path: [prependPath, mapping.path].filter(Boolean).join('.')
  }
  : undefined

const lookupMapping = (mapping, mappings) => (mappings[mapping]) ? { ...mappings[mapping], id: mapping } : null

const expandMapping = (mapping, mappings, overrideType, prependPath) =>
  overrideMappingProps(
    (typeof mapping === 'string') ? lookupMapping(mapping, mappings) : mapping,
    overrideType,
    prependPath
  )

const validateAndLookupSchema = (type, schemas) => {
  if (!type) {
    throw new TypeError("Can't create mapping without type")
  }
  const schema = schemas[type]
  if (!schema) {
    throw new TypeError(`Can't create mapping with unknown type '${type}'`)
  }
  return schema
}

const transformFwd = compose(fwd, transform)
const transformRev = compose(rev, transform)
const filterFwd = compose(fwd, filter)
const filterRev = compose(rev, filter)

const createPipelines = (mapping, transformers, filters) => {
  const {
    transform: transformDef = null,
    transformTo: transformToDef = null,
    filterFrom = null,
    filterTo = null
  } = mapping

  const transformPipeline = preparePipeline(transformDef, transformers)

  return {
    transformFwd: transformPipeline.map(transformFwd),
    transformRev: prepareRevPipeline(
      transformToDef,
      transformPipeline,
      transformers
    ).map(transformRev),
    filterFrom: preparePipeline(filterFrom, filters).map(filterFwd),
    filterTo: preparePipeline(filterTo, filters).map(filterRev)
  }
}

const concatPipeline = (mapping, schema, { createPipelineFn, transformers, filters }) => {
  const pipelines = createPipelines(mapping, transformers, filters)

  return [
    mapping.path,
    ...prepareQualifier(mapping.qualifier),
    ...prepareTypeQualifier(mapping),
    prepareMapping(mapping, transformers, createPipelineFn),
    transform(schema.cast),
    ...pipelines.transformFwd,
    ...pipelines.transformRev,
    ...pipelines.filterFrom,
    ...pipelines.filterTo,
    filter(item => typeof item !== 'undefined')
  ]
}

const createPipeline = (filters, transformers, schemas, mappings) => {
  const createPipelineFn = (mapping, overrideType, path) => {
    mapping = expandMapping(mapping, mappings, overrideType, path)
    if (!mapping) {
      return {}
    }
    const { id = null, type } = mapping

    const schema = validateAndLookupSchema(type, schemas)
    const pipeline = concatPipeline(mapping, schema, {
      createPipelineFn,
      transformers,
      filters
    })

    return { id, type, schema, pipeline }
  }

  return createPipelineFn
}

/**
 * Return item mapper object with fromService and toService.
 * @param {Object} resources - filters, transformers, and schemas
 * @returns {Object} Item mapping def
 */
function mapping ({
  filters,
  transformers,
  schemas = {},
  mappings: mappingsArr = []
} = {}) {
  const mappings = mappingsArr.reduce(
    (mappings, def) => ({ ...mappings, [def.id]: def }),
    {}
  )
  const createPipelineFn = createPipeline(filters, transformers, schemas, mappings)

  return (mapping, overrideType) => {
    const { id, type, schema, pipeline } = createPipelineFn(mapping, overrideType)
    if (!pipeline) {
      return null
    }

    const mapper = mapTransform([
      fwd('data'),
      ...pipeline,
      rev(set('data'))
    ])

    return {
      id,
      type,
      schema,

      /**
       * Map data from a service with attributes and relationships.
       * @param {Object} data - The service item to map from
       * @param {Object} options - onlyMappedValues
       * @returns {Object} Target item
       */
      fromService (data, { onlyMappedValues = true } = {}) {
        return data
          ? ensureArray(
            (onlyMappedValues) ? mapper.onlyMappedValues(data) : mapper(data)
          )
          : []
      },

      /**
       * Map data to a service with attributes and relationships.
       * @param {Object} data - The data item to map
       * @param {Object} target - Optional object to map to data on
       * @returns {Object} Mapped data
       */
      toService (data, target = null) {
        const mapped = mapper.rev.onlyMappedValues(data)
        return (
          (target
            ? Array.isArray(target)
              ? [...target].concat(mapped)
              : mergeDeepWith(concatOrRight, target, mapped)
            : mapped) || null
        )
      }
    }
  }
}

module.exports = mapping


const { flatten } = require('ramda')
const mapAny = require('map-any')

const removeDataProp = ({ data, ...response }) => response

const mapWithEndpoint = (responseMapper, response, actionType) => {
  if (responseMapper || actionType.startsWith('GET') || actionType === 'REQUEST') {
    return (responseMapper) ? responseMapper(response) : response
  } else {
    return {}
  }
}

/**
 * Map the data going _to_ the service. Everything is handled by the mappings,
 * but this method make sure that the right types are mapped.
 *
 * @param {Object} data - The data to map
 * @param {Object} options - mappings, params, onlyMappedValues, and endpoint
 * @returns {Object[]} Array of mapped items
 */
function mapFromService () {
  return ({ response, request, responseMapper, mappings }) => {
    if (response.status !== 'ok') {
      return response
    }

    const type = request.params.type || Object.keys(mappings)
    const { onlyMappedValues, unmapped = false } = request.params

    if (unmapped) {
      return response
    }

    const { data, status = response.status, error, paging, params } =
      mapWithEndpoint(responseMapper, response, request.action)

    if (status !== 'ok') {
      return removeDataProp({ ...response, status, error })
    }

    const mapType = (type) => (mappings[type])
      ? mappings[type].fromService({ ...request, data }, { onlyMappedValues })
      : []

    return {
      ...response,
      status,
      ...((paging) ? { paging } : {}),
      ...((params) ? { params } : {}),
      data: (data) ? flatten(mapAny(mapType, type)) : undefined
    }
  }
}

module.exports = mapFromService


var watchfd = require('watchfd'),
EventEmitter = require('events').EventEmitter,
util = require('util'),
fs = require('fs');

module.exports = function(log,options,cb){
  var tails = {},
  q = [],
  watch,
  args = Array.prototype.slice.call(arguments);

  // load args from args array
  log = args.shift();
  options = args.shift() || {};
  cb = args.pop();

  //support optional callback
  if(typeof options == 'function') {
    if(!cb) cb = options;
    options = {};
  }

  //start watching
  var tailer = new TailFd(log,options);

  if(cb) {
    tailer.on('line',function(line,tailInfo){
      cb(line,tailInfo);
    });
  }

  return tailer;
};

module.exports.tail = module.exports;

function TailFd(log,options){
  this.startWatching(log,options);
}

util.inherits(TailFd,EventEmitter);

_ext(TailFd.prototype,{
  q:[],
  tails:{},
  watch:null,
  startWatching:function(log,options){
    var z = this,
    first = 1,
    noent = 0,
    watch = this.watch = watchfd.watch(log,options,function(stat,prev,data){
      //
      // TODO
      // test refactor from stat.ino to +data.fd
      //
      if(!z.tails[stat.ino]) {

        z.tails[stat.ino] = tailDescriptor(data);
        z.tails[stat.ino].pos = stat.size;
        // if this is the first time i have picked up any file attached to this fd
        if (first) {

          first = 0;
          //apply hard start
          if(typeof options.start != 'undefined') {
            z.tails[stat.ino].pos = options.start>stat.size?stat.size:options.start;
          } else if(noent) {
            z.tails[stat.ino].pos = 0;
          }

          //apply offset
          if(options.offset) {
            z.tails[stat.ino].pos -= options.offset;
          }

          //dont let offset take read to invalid range
          if(z.tails[stat.ino].pos > stat.size) {
            z.tails[stat.ino].pos = stat.size;
          } else if (z.tails[stat.ino].pos < 0) {
            z.tails[stat.ino].pos = 0;
          }
        } else {
          //new file descriptor at this file path but not the first one.
          //an unlink+create or rename opperation happened.
          //
          // TODO
          // if A file was moved to this path i should still start from 0.
          // i better have flood control for this case because fs.read will seriously read all of that data from the file
          //
          z.tails[stat.ino].pos = 0;
        }
        z.tails[stat.ino].linePos = z.tails[stat.ino].pos;
        z.tails[stat.ino].fd = data.fd;
      }

      z.tails[stat.ino].stat = stat;
      z.tails[stat.ino].changed = 1;
      z.readChangedFile(z.tails[stat.ino]);
    });

    watch.on('noent',function(){
      // if the file didnt exist when the watch started and its the first time start should be 0.
      noent = 1;
    });

    watch.on('unlink',function(stat,prev,data){
      if(z.tails[stat.ino]) {
        z.tails[stat.ino].stat = stat;
        z.tails[stat.ino].changed = 1;
      }
      z.emit.apply(z,arguments);
    });

    watch.on('timeout',function(stat,data){
      if(z.tails[stat.ino]) {
        delete z.tails[stat.ino];
        //cleanup queue will be in queue process.
      }
      z.emit.apply(z,arguments);
    });

    this.on('data',function(buffer,tailInfo){

      tailInfo.buf = tailInfo.buf.toString()+buffer.toString();

      var lines = tailInfo.buf.split(options.delimiter||"\n");
      var b;

      tailInfo.buf = lines.pop();

      tailInfo.linePos = tailInfo.pos-tailInfo.buf.length;

      for(var i=0,j=lines.length;i<j;++i) {
        // binary length. =/ not efficient this way but i dont want to emit lines as buffers right now
        b = new Buffer(lines[i]+(options.delimiter||"\n"));

        if(!tailInfo.linePos) tailInfo.linePos = 0;

        if(tailInfo.linePos > tailInfo.pos) {
          console.log('i have a bug! tailinfo line position in source file is greater than the position in the source file!');
          console.log('linePos:',tailInfo.linePos-b.length,'pos:',tailInfo.pos);
          tailInfo.linePos = tailInfo.pos;
        }
        // copy tailinfo with line position so events can be handles async and preserve state
        z.emit('line',lines[i],_ext({},tailInfo));
      }

      // in order to make last line length reflect the binary length buf has to be packed into a Buffer.
      tailInfo.buf = new Buffer(tailInfo.buf);

      if(tailInfo.buf.length >= z.maxLineLength){
        z.emit('line-part',tailInfo.buf,tailInfo);
        tailInfo.linePos += tailInfo.buf.length;
        tailInfo.buf = '';
      }

      if(tailInfo.linePos < (tailInfo.pos-tailInfo.buf.length)) {
        console.log('i have a bug! tailinfo line position in source file is less than the ammount of data i should have sent!');
      }

    });

    //10k max per read
    this.maxBytesPerRead = options.maxBytesPerRead || 10240;
    // 1mb max per line
    this.maxLineLength = options.maxLineLength || 102400;
    // 3 read attempts per range
    this.readAttempts = options.readAttempts||3;
  },
  //this emits the data events on the watcher emitter for all fds
  readChangedFile:function(tailInfo){
    var z = this;
    if(tailInfo) {
      z.q.push(tailInfo);
    }

    var ti;
    //for all changed fds fire readStream
    for(var i = 0;i < z.q.length;++i) {
      ti = z.q[i];

      if(ti.reading) {
        //still reading file
        continue;
      }

      if (!z.tails[ti.stat.ino]) {
        //remove timed out file tail from q
        z.q.splice(i,1);
        --i;
        continue;
      }

      //truncated
      if(ti.stat.size < ti.pos) {
        ti.pos = 0;
      }

      var len = ti.stat.size-ti.pos;
      //remove from queue because im doing this work.
      z.q.splice(i,1);
      --i;

      z.readTail(ti,len);
    }
  },
  readTail:function(tailInfo,len) {
    var z = this;
    if(len){
      tailInfo.reading = 1;
      //retry attempts per range.
      var attempts = [];

      var readJob = function(len){
        fs.read(tailInfo.fd, new Buffer(len), 0, len, tailInfo.pos, function(err,bytesRead,buffer) {
          if(err) {
            attempts.push(err);
            //
            // after configured number of attempts emit range-unreadable and move to next
            //
            if(attempts.length >= (z.readAttempts || 3)) {

              z.emit('range-unreadable',attempts,tailInfo.pos,len,tailInfo);
              // skip range
              tailInfo.pos += len;
              attempts = [];
            }
            done();
            return;
          }

          tailInfo.pos += bytesRead;
          attempts = [];
          //
          // TODO
          // provide a stream event for each distinct file descriptor
          // i cant stream multiple file descriptor's data through the same steam object because mixing the data makes it not make sense.
          // this cannot emit data events here? because to be a stream the above case has to make sense.
          //
          z.emit('data',buffer,tailInfo);
          done();
        });
      },
      done = function(){
        //
        //if paused i should not continue to buffer data events.
        //

        if(!len || z.watch.paused){
          tailInfo.reading = 0;
          if(z.watch.paused && len){
            // if i am paused mid read requeue remaining.
            z.q.push(tailInfo);
            //console.log('requeued remaining read because im paused');
          }
          return;
        }

        var toRead = z.maxBytesPerRead;
        if(len < toRead) {
          toRead = len;
        }
        len -= toRead;

        readJob(toRead);
      }
      ;
      done();

    }
  },
  //
  // return the total line buffer length from all active tails
  //
  lineBufferLength:function(){
    var z = this;
    var l = 0;
    Object.keys(z.tails).forEach(function(k) {
      l += (z.tails[k].buf||'').length;
    });
    return l;
  },
  //
  // streamy methods
  //
  pause:function() {
    this.watch.pause();
  },
  resume:function(){
    this.watch.resume();
    // i may have been stopped mid read so changes may still need to be read.
    this.readChangedFile();
  },
  destroy:function(){
    this.close();
  },
  destroySoon:function(){
    this.close();
  },
  close:function(){
    this.readable = false;
    this.emit('close');
    this.watch.close();
  },
  writable:false,
  readable:true
});

function tailDescriptor(data){
  var o = {
    stat:null,
    pos:0,
    linePos:0,
    fd:data.fd,
    buf:''
  };

  return o;
}

function _ext(o,o2){
  for(var i in o2) if(o2.hasOwnProperty(i)) o[i] = o2[i];
  return o;
}


/**
 * Prepare pipeline by replacing keys with functions or function objects from
 * the collection object, and remove anything that is not functions or function
 * objects.
 * @param {array} pipeline - The pipeline definition
 * @param {Object} collection - Object with pipeline functions
 * @returns {array} Prepared pipeline
 */
function preparePipeline (pipeline, collection = {}) {
  pipeline = [].concat(pipeline)

  const replaceWithFunction = (key) => (typeof key === 'string') ? collection[key] : key
  const isFunctionOrObject = (obj) => obj && ['function', 'object'].includes(typeof obj)

  return pipeline
    .map(replaceWithFunction)
    .filter(isFunctionOrObject)
}

/**
 * Prepare reverse pipeline by either running the `revPipeline` through the
 * regular preparePipeline() or – if `revPipeline` is not set - pick `.rev()`
 * functions from the `fwdPipeline`.
 * @param {array} revPipeline - The reverse pipeline definition or undefined
 * @param {array} fwdPipeline - A pipeline to pick `.rev()` from (already prepared)
 * @param {Object} collection - Object with pipeline functions
 * @returns {array} Prepared reverse pipeline
 */
function prepareRevPipeline (revPipeline, fwdPipeline, collection) {
  return (revPipeline)
    ? preparePipeline(revPipeline, collection)
    : fwdPipeline.map((fn) => (fn.rev) ? fn.rev : null).filter(Boolean)
}

module.exports = { preparePipeline, prepareRevPipeline }


const getField = require('../utils/getField')

/**
 * Cast query params according to type
 */
function castQueryParams (relId, data, { relationships }) {
  const relationship = relationships[relId]

  if (!relationship.query) {
    return {}
  }

  return Object.keys(relationship.query)
    .reduce((params, key) => {
      const value = getField(data, relationship.query[key])
      if (value === undefined) {
        throw new TypeError('Missing value for query param')
      }
      return { ...params, [key]: value }
    }, {})
}

module.exports = castQueryParams


const middleware = require('./middleware')
const schedule = require('./schedule')

/**
 * Set up Integreat queue interface.
 *
 * @param {Object} queue - A supported queue implementation
 * @returns {Object} An Integreat queue instance with setDispatch and middleware methods
 */
function setupQueue (queue) {
  let dispatch = null
  let subscribed = false

  return {
    queue,

    /**
     * Set dispatch function to use for dequeuing
     */
    setDispatch (dispatchFn) {
      dispatch = dispatchFn

      if (!subscribed && typeof dispatch === 'function') {
        queue.subscribe(dispatch)
        subscribed = true
      }
    },

    /**
     * Middleware interface for Integreat. Will push queuable actions to queue,
     * and pass the rest on to the next() function.
     *
     * @param {function} next - The next middleware
     * @returns {Object} A response object
     */
    middleware (next) {
      return middleware(next, queue)
    },

    /**
     * Schedule actions from the given defs.
     * Actions are enqueued with a timestamp, and are ran at the
     * set time.
     * @param {array} defs - An array of schedule definitions
     * @returns {array} Array of returned responses
     */
    async schedule (defs) {
      return schedule(defs, queue)
    }
  }
}

module.exports = setupQueue


const debug = require('debug')('great')
const createError = require('../utils/createError')

const prepareMeta = (keys, meta) => (keys)
  ? [].concat(keys)
    .filter((key) => key !== 'createdAt' && key !== 'updatedAt')
    .reduce((ret, key) =>
      ({ ...ret, [key]: meta[key] || null }), {})
  : prepareMeta(Object.keys(meta), meta)

/**
 * Get metadata for a service, based on the given action object.
 * @param {Object} payload - Payload from action object
 * @param {Object} resources - Object with getService
 * @returns {Promise} Promise of metdata
 */
async function getMeta ({ payload, meta }, { getService }) {
  debug('Action: GET_META')

  const { service: serviceId, endpoint, keys } = payload
  const id = `meta:${serviceId}`

  const service = getService(null, serviceId)
  if (!service) {
    debug(`GET_META: Service '${serviceId}' doesn't exist`)
    return createError(`Service '${serviceId}' doesn't exist`)
  }

  const type = service.meta
  const metaService = getService(type)
  if (!metaService) {
    return createError(`Service '${service.id}' doesn't support metadata (setting was '${service.meta}')`)
  }

  const endpointDebug = (endpoint) ? `endpoint '${endpoint}'` : `endpoint matching ${type} and ${id}`
  debug('GET_META: Get meta %s for service \'%s\' on service \'%s\' at %s',
    keys, service.id, metaService.id, endpointDebug)

  const { response } = await metaService.send({
    type: 'GET',
    payload: { keys, type, id, endpoint },
    meta: { ident: meta.ident }
  })

  if (response.status === 'ok') {
    const { data } = response
    const meta = prepareMeta(keys, data[0].attributes)
    return { ...response, data: { service: serviceId, meta } }
  } else {
    return response
  }
}

module.exports = getMeta


const is = require('@sindresorhus/is')
const debug = require('debug')('great')
const createUnknownServiceError = require('../utils/createUnknownServiceError')
const appendToAction = require('../utils/appendToAction')
const { mergeDeepWith } = require('ramda')

const mergeDiff = (left, right) =>
  (is.undefined(right) || (is.emptyArray(right) && is.nonEmptyArray(left)))
    ? left : right

const merge = (requestData, responseData) => {
  requestData = [].concat(requestData)
  if (!responseData || is.emptyArray(responseData)) {
    return requestData
  }
  responseData = [].concat(responseData)

  return requestData.map(
    (data, index) => (data) ? mergeDeepWith(mergeDiff, data, responseData[index]) : responseData[index]
  )
}

const mergeRequestAndResponseData = (response, requestData) => (response.status === 'ok')
  ? { ...response, data: merge(requestData, response.data) }
  : response

const extractType = (action, data) => action.payload.type || (data && data.type) || undefined

const extractId = (data) => (data && data.id) || undefined

/**
 * Set several items to a service, based on the given action object.
 * @param {Object} payload - Payload from action object
 * @param {Object} resources - Object with getService
 * @returns {Object} Response object with any data returned from the service
 */
async function set (action, { getService, schemas }) {
  debug('Action: SET')

  const { service: serviceId, data, endpoint, onlyMappedValues = true } = action.payload
  const type = extractType(action, data)
  const id = extractId(data)

  const service = getService(type, serviceId)
  if (!service) {
    return createUnknownServiceError(type, serviceId, 'SET')
  }

  const endpointDebug = (endpoint) ? `at endpoint '${endpoint}'` : ''
  debug('SET: Send to service %s %s', service.id, endpointDebug)

  const { response, authorizedRequestData } = await service.send(appendToAction(action, { id, type, onlyMappedValues }))

  return mergeRequestAndResponseData(response, authorizedRequestData)
}

module.exports = set


const mapAny = require('map-any')
const uuid = require('uuid/v4')
const formatValue = require('./formatValue')

const setDates = (attrs) => {
  attrs.createdAt = new Date(attrs.createdAt || attrs.updatedAt || new Date())
  attrs.updatedAt = new Date(attrs.updatedAt || attrs.createdAt)
}

const castAttributes = (dataAttrs = {}, typeAttrs, defaults) => {
  return Object.keys(dataAttrs)
    .reduce((attrs, key) => {
      if (typeAttrs.hasOwnProperty(key)) {
        const value = formatValue(dataAttrs[key], typeAttrs[key])
        if (value !== undefined) {
          attrs[key] = value
        }
      }
      return attrs
    }, defaults)
}

const setIfTruish = (rel, val, prop) => {
  if (val[prop]) {
    rel[prop] = val[prop]
  }
}

const populateRelObject = (id, type) => {
  id = (typeof id === 'object' && id !== null) ? id.id : id
  if (id === null || typeof id === 'undefined') {
    return null
  }
  return {
    id: String(id),
    type
  }
}

const castRel = (type) => (val) => {
  const rel = populateRelObject(val, type)

  if (rel !== null) {
    if (val.attributes || val.relationships) {
      rel.attributes = val.attributes || {}
      rel.relationships = val.relationships || {}
    }

    setIfTruish(rel, val, 'meta')
    setIfTruish(rel, val, 'isNew')
    setIfTruish(rel, val, 'isDeleted')
  }

  return rel
}

const filterIfArray = (arr) => (Array.isArray(arr)) ? arr.filter(Boolean) : arr

const flattenRelArray = (rel) => (Array.isArray(rel.id)) ? rel.id.map((id) => ({ ...rel, id })) : rel
const castRels = (val, type) => (val === null) ? null : filterIfArray(mapAny(castRel(type), flattenRelArray(val)))

const stripArrayNotation = (type) => (type.endsWith('[]')) ? type.substr(0, type.length - 2) : type

const castRelationships = (dataRels = {}, typeRels, defaults) => {
  return Object.keys(dataRels)
    .reduce((rels, key) => (typeRels[key] && dataRels[key] !== undefined)
      ? { ...rels, [key]: castRels(dataRels[key], stripArrayNotation(typeRels[key].type)) }
      : rels,
    defaults)
}

/**
 * Cast item
 */
function cast ({ id, attributes, relationships, defaultAttrs, defaultRels }) {
  return (data, { onlyMappedValues }) => {
    if (!data) {
      return undefined
    }

    const attrs = castAttributes(data.attributes, attributes, (onlyMappedValues) ? {} : { ...defaultAttrs })
    if (!onlyMappedValues) {
      setDates(attrs)
    }
    const rels = castRelationships(data.relationships, relationships, (onlyMappedValues) ? {} : { ...defaultRels })
    const castId = data.id || attrs.id || uuid()
    delete attrs.id

    const casted = {
      id: castId,
      type: id,
      attributes: attrs,
      relationships: rels
    }

    if (data.isNew) {
      casted.isNew = true
    }
    if (data.isDeleted) {
      casted.isDeleted = true
    }

    return casted
  }
}

module.exports = cast


const debug = require('debug')('great')
const createError = require('../utils/createError')

/**
 * Set metadata on a service, based on the given action object.
 * @param {Object} payload - Payload from action object
 * @param {Object} resources - Object with getService
 * @returns {Promise} Promise that will be resolved when metadata is set
 */
async function setMeta ({ payload, meta }, { getService }) {
  debug('Action: SET_META')

  const {
    service: serviceId,
    meta: metaAttrs,
    endpoint
  } = payload
  const id = `meta:${serviceId}`

  const service = getService(null, serviceId)
  if (!service) {
    debug(`SET_META: Service '${serviceId}' doesn't exist`)
    return createError(`Service '${serviceId}' doesn't exist`)
  }

  const type = service.meta
  const metaService = getService(type)
  if (!metaService) {
    debug(`SET_META: Service '${service.id}' doesn't support metadata (setting was '${service.meta}')`)
    return { status: 'noaction' }
  }

  const endpointDebug = (endpoint) ? `endpoint '${endpoint}'` : `endpoint matching ${type} and ${id}`
  debug('SET_META: Send metadata %o for service \'%s\' on service \'%s\' %s',
    metaAttrs, service.id, metaService.id, endpointDebug)

  const data = { id, type, attributes: metaAttrs }
  const { response } = await metaService.send({
    type: 'SET',
    payload: { keys: Object.keys(metaAttrs), type, id, data, endpoint, onlyMappedValues: true },
    meta: { ident: meta.ident }
  })
  return response
}

module.exports = setMeta