"use strict";

function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }

let localMode = process.env.LAMBDA_TASK_ROOT ? false : true;
global.blackoutLocalMode = localMode;

/**
 * Get a local user settings file, if exists
 */
if (localMode) {
	let d = 3; // Depth
	for (var i = 1; i <= d; i++) {
		try {
			global.blackoutUserSettings = require('../'.repeat(i) + '.user.json');
		} catch (err) {
			// A local settings file must not exist
		}
	}
}

// Low level
require("../proto/array");
require("../proto/string");

/**
 * Load larger NPM packages here in index.js to save time later
 * 
 */
const AWS = require("aws-sdk");
const randtoken = require('rand-token');
const sortBy = require('sort-on');
const md5 = require('md5');
const uuidV4 = require('uuid/v4');
const base62 = require('base-x')('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
const isemail = Promise.promisifyAll(require('isemail'));
const http = Promise.promisifyAll(require('request'));
const moment = require('moment');

/**
 * Instantiate AWS Clients
 * 
 */
AWS.config.update({ region: 'us-east-1' });

if (blackoutLocalMode && blackoutUserSettings) {

	if ('awsCredentialProfile' in blackoutUserSettings) AWS.config.credentials = new AWS.SharedIniFileCredentials({ profile: blackoutUserSettings.awsCredentialProfile });
}

//DynamoDB
const dynamodb = Promise.promisifyAll(new AWS.DynamoDB({ apiVersion: '2012-08-10' }));

//Simple Queue Service
const sqs = Promise.promisifyAll(new AWS.SQS({ region: 'us-east-1' }));

/**
 * Connect to other services
 */
const nodeRedis = require("redis");
const redlock = require('redlock');

/**
 * Prototype Promisification
 */
Promise.promisifyAll(nodeRedis.RedisClient.prototype);
Promise.promisifyAll(nodeRedis.Multi.prototype);

module.exports = {

	setup(event, lambdaCallback) {

		/**
   * Make modules available on blackout class before requiring our own modules
   * as they may depend on these.
   * 
   */
		this.AWS = AWS;
		this.dynamodb = dynamodb;
		this.sqs = sqs;
		this.randtoken = randtoken;
		this.nodeRedis = nodeRedis;
		this.redlockLib = redlock;
		this.sortBy = sortBy;
		this.md5 = md5;
		this.uuidV4 = uuidV4;
		this.base62 = base62;
		this.isemail = isemail;
		this.http = http;
		this.moment = moment;

		// Init variables for lambda function reuse

		// Core
		this.initCompleter();
		this.initErrors();

		// Add out own functions to node, or node components
		this.extendNode();

		// Services
		this.dynamo.init();
		this.db.init();
		this.settings.init();
		this.queue.init();
		this.genesis.init();
		this.playerLib.init();
		this.initLocking();
		this.genesis.init();

		// Setup
		this.event = event;
		this.callback = lambdaCallback;
		this.db.dynamo = this.dynamo;

		// Enable long stack traces
		Promise.config({
			longStackTraces: true
		});
	},

	connectRedis(callback) {

		// Running locally?
		let redisHost = 'redis-12618.c10.us-east-1-4.ec2.cloud.redislabs.com';
		let redisPort = '12618';
		let redisPass = 'Y4Ohanp$PlbHjz5om$I7tLeG';
		if (localMode && blackoutUserSettings && blackoutUserSettings.redis) {
			redisHost = blackoutUserSettings.redis.host;
			redisPort = blackoutUserSettings.redis.port;
			redisPass = blackoutUserSettings.redis.password;
		}
		console.log('Redis host', redisHost);

		this.redis = this.nodeRedis.createClient({

			//host: 'rugby.e5di5i.0001.use1.cache.amazonaws.com',
			//host: 'test.e5di5i.0001.use1.cache.amazonaws.com',

			/**
    * Instead of creating a private elasticache in VPC (or using our DB EC2 instance), I have opted for now to use a public server with RedisLabs
    * 
    * 1. Means we don't need to configure lambda to work in VPC
    * 2. Means need a NAT gateway to still access DynamoDB from lambda inside VPC
    * 3. Means we don't have to pay anything right now! We get 30MB for free
    */
			host: redisHost,
			port: redisPort,
			password: redisPass,

			/**
    * connect_timeout, milliseconds deprecated in favour or retry_strategy
    * 
    * I haven't been able to get retry_strategy to be valueable as a connect timeout yet
    * I have asked on here:
    * https://gitter.im/NodeRedis/node_redis
    * 
    * Give a decent timeout since we need redis for locking
    */
			connect_timeout: blackoutLocalMode ? 3000 : 10000,

			retry_strategy: function (options) {
				if (options.error) {
					// End reconnecting on a specific error and flush all commands with a individual error 
					throw new Error('Could not connect to redis (' + options.error.code + ')');
				} else if (options.total_retry_time > 1000 * 1) {
					// Milliseconds?
					// End reconnecting after a specific timeout and flush all commands with a individual error 
					throw new Error('Retry time exhausted');
				}
				if (options.times_connected > 10) {
					// End reconnecting with built in error 
					return undefined;
				}
				// reconnect after 
				return Math.min(options.attempt * 100, 3000);
			}

		});

		this.redis.on('error', err => {
			console.log("Redis Error " + err);
			callback(err);
		});

		this.redis.on('ready', () => {
			console.log("Redis Connected");
			this.db.redis = this.redis;

			callback();
		});

		this.setupRedlock(this.redis);
		this.redis = this.extendRedis(this.redis);
	},

	setupRedlock(redis) {
		this.redlock = new this.redlockLib(
		// you should have one client for each redis node
		// in your cluster
		[redis], //, client2, client3],
		{
			// the expected clock drift; for more details
			// see http://redis.io/topics/distlock
			driftFactor: 0.01, // time in ms

			// the max number of times Redlock will attempt
			// to lock a resource before erroring
			retryCount: 0, // 10 default | We'll set out own wait times on a case by case

			// the time in ms between attempts
			retryDelay: 200, // time in ms

			// the max time in ms randomly added to retries
			// to improve performance under high contention
			// see https://www.awsarchitectureblog.com/2015/03/backoff.html
			retryJitter: 200 // time in ms
		});
	},

	extendRedis(redis) {

		redis = Object.assign(redis, Promise.promisifyAll({

			/**
    * Redis->SET for maps
    * Serializes as JSON before setting
    */
			setmap(key, val, callback) {
				this.set(key, JSON.stringify(val), callback);
			},

			/**
    * Redis->MSET for maps
    * Serializes as JSON before setting
    * Supports: [key,val,key,val,...] or { key: val, key: val, ... }
    */
			msetmap(data, callback) {
				if (blackout.isArray(data)) {
					for (let [i, item] of data.entries()) {
						if (i % 2 === 1) {
							data[i] = JSON.stringify(item);
						}
					}
				} else {
					let arr = [];
					for (let key in data) {
						arr.push(key, JSON.stringify(data[key]));
					}
					data = arr;
				}
				this.mset(data, callback);
			},

			/**
    * Redis->SETEX for maps
    * Serializes as JSON before setting
    */
			setexmap(key, seconds, val) {
				this.setex(key, seconds, JSON.stringify(val));
			},

			/**
    * Redis->GET for maps
    * Parses from JSON before returning
    */
			getmap(key, callback) {
				var _this2 = this;

				return _asyncToGenerator(function* () {

					// Get error now so that we have a usable stack
					var errPre = new Error("A non-json string was returned when attempting to retrieve an object map from redis");

					try {

						let val = yield _this2.getAsync(key);

						if (blackout.isJson(val)) {
							callback(null, JSON.parse(val));
						} else {
							blackout.errorInternal(errPre);
						}
					} catch (err) {
						callback(err);
					}
				})();
			},

			/**
    * Redis->MGET (Get multiple keys) for maps
    * Parses from JSON before returning
    */
			mgetmap(keys, callback) {
				var _this3 = this;

				return _asyncToGenerator(function* () {
					try {

						let vals = yield _this3.mgetAsync(keys);

						for (let [index, val] of vals.entries()) {
							if (val) {
								vals[index] = JSON.parse(val);
							}
						}
						callback(null, vals);
					} catch (err) {
						callback(err);
					}
				})();
			},

			/**
    * Longest time to wait for a patch to complete
    */
			maxPatchWait: 3000,

			/**
    * This function updates an item *ATOMICALLY* in redis
    */
			patchmap(key, patch, callback) {
				var _this4 = this;

				let doPatch = (() => {
					var _ref = _asyncToGenerator(function* (wait = 400, timeWaited = 0) {

						try {

							// Watch the key
							_this4.watch(key);

							// Get item
							let data = yield _this4.getmapAsync(key);

							if (data) {

								data = blackout.patch(data, patch);

								//console.log('patched item',data);

								// Create a new command queue (returns a new redis client)
								let multi = _this4.multi();
								blackout.extendRedis(multi);

								// Set the new item
								multi.setmap(key, data); // Dont need to 'await', since we're just queueing
								let success = yield multi.execAsync();

								if (!success) {

									// If failed, try again soon
									yield blackout.sleep(wait);

									timeWaited += wait;

									if (timeWaited < _this4.maxPatchWait) {
										wait += Math.floor(Math.random() * 400);
										doPatch(wait, timeWaited);
									} else {
										blackout.errorInternal(new Error(`Failed to patch item (${key}) in cache after waiting ${timeWaited}ms.`));
									}
								} else {

									// If succeeded, callback
									callback();
								}
							} else {

								// If item didn't exist in cache, unwatch and we're done here
								_this4.unwatch();
								callback();
							}
						} catch (err) {
							callback(err);
						}
					});

					return function doPatch() {
						return _ref.apply(this, arguments);
					};
				})();

				doPatch();
			},

			/**
    * This function updates a hash using atomic functions
    */
			hpatch(cacheKey, patch, callback) {
				var _this5 = this;

				return _asyncToGenerator(function* () {

					// Make sure hash is actually in cache or we don't need to do anything
					let len = yield _this5.hlenAsync(cacheKey);
					if (len) {

						let awaits = [];

						for (let key in patch) {

							let val = patch[key];
							let actions = false;

							// Run any actions
							if (typeof val === 'object' && val.incr) {
								awaits.push(_this5.hincrbyAsync(cacheKey, key, val.incr));
								actions = true;
							}

							// Or set new value
							if (!actions) {
								awaits.push(_this5.hsetAsync(cacheKey, key, val));
							}
						}

						yield Promise.all(awaits);
					}
					callback();
				})();
			}

		}));

		return redis;
	},

	extendNode() {

		this.appStartTime = Date.now();

		console._log = console.log;
		let _this = this;

		Object.defineProperty(console, 'log', {
			enumerable: false,
			value: function () {
				let timeInSeconds = (Math.round((Date.now() - _this.appStartTime) / 1000 / 0.001) * 0.001).toString();
				let dot = timeInSeconds.indexOf('.');
				if (dot) timeInSeconds = timeInSeconds.substr(0, dot + 4);
				let spaces = 7 - timeInSeconds.length;
				let prefix = timeInSeconds + 's' + ' '.repeat(spaces) + ':  ';
				console._log(prefix, ...arguments);
			}
		});

		console.logObject = obj => {
			console.log(JSON.stringify(obj, null, 4));
		};
	},

	logPromises() {

		// Better local debugging for asynchronous flow
		Promise.config({
			// Enable long stack traces
			longStackTraces: true,
			// Enable monitoring
			monitoring: true
		});

		// Track duplicate events
		let chainedPromises = [];
		let fulfilledPromises = [];
		let rememberPromises = 7;

		// Events to follow
		let eventsToFollow = {
			promiseChained: chainedPromises,
			promiseFulfilled: fulfilledPromises
		};

		// Handle events
		for (let evt in eventsToFollow) {

			let promises = eventsToFollow[evt];

			// Note the event name is in camelCase, as per Node.js convention.
			process.on(evt, function (promise) {

				// For debugging this function, see how stacks are formed (as they may change over time)
				//console.log('promise',promise);

				// Check for trace
				if (promise._trace) {
					let lines = promise._trace.stack.split("\n");
					let promiseFunctionName,
					    l = 1,
					    lineFound;

					for (let line of lines) {
						if (promiseFunctionName) {
							let initial = line.indexOf('at /');
							if (initial >= 0) {
								let url = line.substr(initial + 4);
								console.log(`PROMISE DEBUGGER -> ${evt} -> ${promiseFunctionName}() called at /${url}`);
								break;
							}
						} else {
							let initial = line.indexOf('at Object.ret [as ');
							if (initial >= 0) {
								let start = initial + 'at Object.ret [as '.length;
								let end = line.indexOf(']');
								lineFound = l;
								let name = line.substr(start, end - start);
								let i = promises.indexOf(name);

								// Ensure promise hasn't been found recently, unless the line is the SAME
								// which would suggest this is a genuinely new promise.
								if (i < 0 || promises[i + 1] === lineFound) {
									promiseFunctionName = name;
								} else {
									break;
								}
							}
						}
						l++;
					}

					// Track logged promises so we don't log duplicates
					if (promiseFunctionName) {
						promises.push(promiseFunctionName, lineFound);
						if (promises.length > rememberPromises * 2) {
							promises.shift();
							promises.shift();
						}
					}
				}
			});
		}
	}

};
//# sourceMappingURL=setup.js.map