/**
 * Performs the REST API calls the ArrowDB Objects.
 *
 * @module rest
 *
 * @copyright
 * Copyright (c) 2012-2014 by Appcelerator, Inc. All Rights Reserved.
 *
 * @license
 * Licensed under the terms of the Apache Public License
 * Please see the LICENSE included with this distribution for details.
 */

const
	ArrowDBError = require('./arrowdbError'),
	fs = require('fs'),
	http = require('http'),
	messages = require('./messages'),
	request = require('request'),
	_ = require('lodash'),
	debug = require('debug')('arrowdb'),

	fileUploadParams = ['file', 'photo'], // http/https parameters that file upload uses
	excludedParameters = ['key', 'pretty_json', 'req', 'res']; // filtered out http/https parameters, that don't need to be in restOptions
	userLoginParams = ['_login', '_password', '_admin_']
	blankMethods = ['create', 'delete', 'get', 'update']
	manyMethods = ['createMany', 'deleteMany', 'updateMany']

/*
 * Public APIs
 */
module.exports.createArrowDBRequestFunction = createArrowDBRequestFunction;
module.exports.createArrowDBRESTRequestFunction = createArrowDBRESTRequestFunction;

/**
 * Internal function for making RESTful calls to ArrowDB API. It will put parameters
 * including ArrowDB key and `pretty_json` into query string or request body
 * properly, as well as dealing with the session cookie.
 *
 * For required format of ArrowDB API, it will also transform input JSON into correct format.
 *
 * @private
 *
 * @param {string} apiEntryPoint - The URL to use for API calls.
 * @param {object} appOptions - An object containing various options.
 * @param {string} [appOptions.apiEntryPoint] - The URL to use for all requests.
 * @param {boolean} [appOptions.prettyJson] - When truthy, sets the `pretty_json` REST option.
 * @param {string} arrowDBMethod - The Method used alongside the main component e.g. create in user/create
 * @param {string} httpMethod - The HTTP method to use for the request. Valid values include "GET", "POST", "PUT", and "DELETE".
 * @param {string} cookieString - The cookie to set for the session.
 * @param {object} restOptions - An object containing all needed parameters per request such as `username` and `password` for the user login request.
 * @param {function} callback - A function to call after the request completes.
 */
function arrowDBRequest(apiEntryPoint, appOptions, arrowDBMethod, httpMethod, cookieString, restOptions, callback) {
	var reqBody = _.omit(restOptions, excludedParameters),
		theJar = request.jar(),
		cookie = null;
		headers = null;
		v2 = false;

	if (apiEntryPoint.includes('/v2/')) {
		v2 = true
	}

	// cookie may come from either relayed outside request, or cookieString
	if (restOptions.req) {
		if (restOptions.req.headers && restOptions.req.headers.cookie) {
			cookie = request.cookie(restOptions.req.headers.cookie);
			cookie && theJar.setCookie(cookie, apiEntryPoint);
		}

		if (restOptions.req.headers) {
			headers = restOptions.req.headers;
		}
		// Merge req.query and req.body into parameter JSON
		if (restOptions.req.query) {
			reqBody = _.defaults(_.clone(restOptions.req.query), reqBody);
		}
		if (restOptions.req.body) {
			reqBody = _.defaults(_.clone(restOptions.req.body), reqBody);
		}
	}
	if (cookieString){
		cookie = request.cookie(cookieString);
		cookie && theJar.setCookie(cookie, apiEntryPoint);
	}
	// cookieString can from either each requests or ArrowDB instance
	else if (restOptions.cookieString) {
		cookie = request.cookie(restOptions.cookieString || cookieString);
		cookie && theJar.setCookie(cookie, apiEntryPoint);
	}

	// pretty_json can be set as application level from appOptions
	if (appOptions.prettyJson && !restOptions.hasOwnProperty('pretty_json')) {
		if (typeof appOptions.prettyJson !== 'boolean') {
			return callback(new ArrowDBError(messages.ERR_WRONG_TYPE, {
				typeName: 'prettyJson'
			}));
		}
		reqBody.pretty_json = appOptions.prettyJson;
	}

	// response_json_depth can be set as application level from appOptions
	if (appOptions.responseJsonDepth && !restOptions.hasOwnProperty('response_json_depth')) {
		if (typeof appOptions.responseJsonDepth !== 'number') {
			return callback(new ArrowDBError(messages.ERR_WRONG_TYPE, {
				typeName: 'responseJsonDepth'
			}));
		}
		reqBody.response_json_depth = appOptions.responseJsonDepth;
	}

	var requestParam = null,
		preparedReqBody = {},
		hasFile = false;
		isUserLogin = false;

	Object.keys(reqBody).forEach(function (item) {
		
		var value = reqBody[item];

		if (value === undefined || value === null) {
			// if the value is undefined, just return now so that it's not added to the request
			return;
		} else if (fileUploadParams.indexOf(item) !== -1) {
			hasFile = true;
			if (typeof value === 'string') {
				value = fs.createReadStream(value);
			}
		} else if (userLoginParams.indexOf(item) !== -1) {
			isUserLogin = true;
		} else if (value !== null && typeof value === 'object') {
			value = JSON.stringify(value);
		} else {
			value = value.toString();
		}
		preparedReqBody[item] = value;
	});

	if (v2) {
		if (isUserLogin) {
			if (arrowDBMethod != "") {
				var o = {}
				var arrayFormat
				o[arrayFormat] = []
				o[arrayFormat].push(preparedReqBody);
				preparedReqBody = JSON.stringify(o[arrayFormat]);
			}else{
				preparedReqBody= JSON.stringify(preparedReqBody);
			}
		}
	}

	requestParam = {
		url: apiEntryPoint,
		method: httpMethod,
		jar: theJar,
		headers: headers
	};

	if (httpMethod === 'GET') {
		requestParam.qs = preparedReqBody;
	} else {
		// if there is any file upload needed, we need to use formData instead of form
		if (hasFile) {
			requestParam.formData = preparedReqBody;
		} else {
			requestParam.form = preparedReqBody;
		}
	}

	if (appOptions.proxy) {
		requestParam.proxy = appOptions.proxy;
	}

	debug('request %j',requestParam);

	request(requestParam, function (error, response, body) {
		debug('response - error=%o, body=%o',error,body);
		if (error) {
			return callback(new ArrowDBError(messages.ERR_REQUEST_FAILED_ERROR, { message: error.toString() }));
		}

		if (!response) {
			return callback(new ArrowDBError(messages.ERR_REQUEST_FAILED_NO_RESPONSE));
		}

		var parsedBody = body;

		// if the body is a string, try to json parse it
		if (body && typeof body === 'string') {
			try {
				parsedBody = JSON.parse(body);
			} catch (e) {}
		}

		var errmsg = parsedBody !== null && typeof parsedBody === 'object' && parsedBody.meta && parsedBody.meta.message,
			result = {
				statusCode: response.statusCode,
				reason: errmsg || http.STATUS_CODES[response.statusCode] || '',
				response: response,
				body: parsedBody
			};
		// if there was no explicit error, yet the request is a 4xx or 5xx then error
		if (response.statusCode >= 400) {
			return callback(new ArrowDBError(messages.ERR_REQUEST_UNSUCCESSFUL, result));
		}

		if (restOptions.res) {
			// if this is a relayed response, we will set response header to include cookie back from ArrowDB API
			var cookies = theJar.getCookies(apiEntryPoint);
			restOptions.res.setHeader('Set-Cookie', cookies.join('; '));
		} else {
			result.cookieString = theJar.getCookieString(apiEntryPoint);
		}

		callback(null, result);
	});
}

/**
 * Creates a function for invoking a ArrowDB API. The returned function is
 * expected to be set in the `ArrowDB` object prototype.
 *
 * For example, after creating new instance like:
 *
 *   var ArrowDB = require('arrowdb');
 *   var arrowDBApp = new ArrowDB('ARROWDB_APP_KEY');
 *
 * users will have methods like `arrowDBApp.usersLogin()` and `arrowDBApp.likesQuery()`.
 *
 * When calling `arrowDBApp.usersLogin()`, internally sdk transforms it to:
 *
 *   createArrowDBRequestFunction({
 *       arrowDBObjectKey: 'Users',
 *       arrowDBObjectName: 'users',
 *       arrowDBObjectMethodKey: 'Login',
 *       arrowDBObjectMethodName: 'login',
 *       httpMethod:'POST'
 *   }).
 *
 * Then, `createArrowDBRequestFunction()` will return a `function(restOptions, callback)`.
 * From user side we will get `arrowDBApp.usersLogin(restOptions, callback)`.
 *
 * @param {object} options - An object containing various options.
 * @param {string} options.arrowDBObjectKey - ArrowDB Object key listed under arrowDBObjects/xxx.js, like ACLs, Users, PushNotifications.
 * @param {string} options.arrowdbObjectURL - v1 or v2
 * @param {string} options.arrowDBObjectName - ArrowDB Object name that is used for entry point composition, like ACLs, users, push_notifications.
 * @param {string} options.arrowDBObjectMethodKey - The method key of ArrowDB Object listed inner arrowDBObjects/xxx.js, like count, remove, showMe, requestResetPassword.
 * @param {string} options.arrowDBObjectMethodName - The method name of ArrowDB Object that is used for entry point composition, like count, delete, show/me, request_reset_password.
 * @param {function} [options.arrowDBObjectMethodPreAction] - An ArrowDB Object method specific internal function to call before making the request.
 * @param {function} [options.arrowDBObjectMethodPostAction] - An ArrowDB Object method specific internal function to call after a request completes.
 * @param {string} options.httpMethod - The HTTP method to use for the request. Valid values include "GET", "POST", "PUT", and "DELETE".
 *
 * @returns {function(object, function(error, result))} A function that invokes the REST request.
 */
function createArrowDBRequestFunction(options) {

	if (!options || !options.arrowdbObjectURL || !options.arrowDBObjectKey || !options.arrowDBObjectName  || !options.httpMethod) {
		throw new ArrowDBError(messages.ERR_MISS_REQUIRED_PARAMETER, {
			parameter: 'in createArrowDBRequestFunction'
		});
	}
	return function (restOptions, callback) {
		// parameter offset
		if (typeof restOptions === 'function') {i
			callback = restOptions;
			restOptions = null;
		}
		if (typeof callback !== 'function') {
			callback = function () {};
		}

		restOptions || (restOptions = {});
		function prepareArrowDBRequest() {

			// check required app key
			if (!this.appKey) {
				return callback(new ArrowDBError(messages.ERR_MISS_REQUIRED_PARAMETER, {
					parameter: 'appKey'
				}));
			}
			if (typeof this.appKey !== 'string') {
				return callback(new ArrowDBError(messages.ERR_WRONG_TYPE, {
					typeName: 'app key'
				}));
			}

			// check required app options
			if (!this.appOptions) {
				return callback(new ArrowDBError(messages.ERR_MISS_REQUIRED_PARAMETER, {
					parameter: 'appOptions'
				}));
			}

			if (typeof this.appOptions !== 'object') {
				return callback(new ArrowDBError(messages.ERR_WRONG_TYPE, {
					typeName: 'app options'
				}));
			}

			// determine the arrowdb method
			var arrowDBMethod = options.arrowDBObjectMethodName;

			if (arrowDBMethod !== null && typeof arrowDBMethod === 'object') {
				var dynamicMethod = arrowDBMethod.entry;
				if (Array.isArray(arrowDBMethod.variables)) {
					for (var i = 0, l = arrowDBMethod.variables.length; i < l; i++) {
						var variable = arrowDBMethod.variables[i];
						if (!restOptions[variable]) {
							return callback(new ArrowDBError(messages.ERR_MISS_REQUIRED_PARAMETER, {
								parameter: variable
							}));
						}
						dynamicMethod = dynamicMethod.replace(variable, restOptions[variable]);
					}
				}
				arrowDBMethod = dynamicMethod;
			}

			// recheck if arrowDBMethod is an object and if so, remove unnecessary parameters
			if (arrowDBMethod !== null && typeof arrowDBMethod === 'object' && Array.isArray(options.arrowDBObjectMethodName.variables)) {
				options.arrowDBObjectMethodName.variables.forEach(function (variable) {
					delete restOptions[variable];
				});
			}

			// remove arrowDBMethod for BAAS V2 to comply with new API's
			if (blankMethods.indexOf(arrowDBMethod) !== -1) {
				arrowDBMethod = ""
			}
			// manage arrowDBMethod for BAAS V2 to comply with new API's
			if (manyMethods.indexOf(arrowDBMethod) !== -1) {
				arrowDBMethod = arrowDBMethod.replace('Many','');
			}

			slashNeeded = "";
			if (arrowDBMethod != ""){
				slashNeeded = "/"
			}
			var apiEntryPoint = this.appOptions.apiEntryPoint + '/'+ options.arrowdbObjectURL + '/' + options.arrowDBObjectName + slashNeeded + arrowDBMethod
			// extend to support path overload for predefined calls
			if (restOptions.path){
				apiEntryPoint += '/' + restOptions.path
				delete restOptions.path
			}
			apiEntryPoint += '?key=' + this.appKey;

			console.log(apiEntryPoint)
			// console.log('arrowDBObjectName: %s', options.arrowDBObjectName);
			// console.log('arrowDBMethod: %s', arrowDBMethod);
			// console.log('httpMethod: %s', options.httpMethod);
			// console.log('appKey: %s', this.appKey);
			// console.log('appOptions: %j', this.appOptions);
			// console.log('restOptions: %j', restOptions);
			// console.log('sessionCookieString: %s', this.sessionCookieString);

			// allow the value to just be a cookie value (as the session) and not a full cookie string
			if (this.sessionCookieString && this.sessionCookieString.indexOf('=') < 0) {
				this.sessionCookieString = 'arrowDBSessionID='+encodeURIComponent(this.sessionCookieString);
			}

			arrowDBRequest(apiEntryPoint, this.appOptions, arrowDBMethod, options.httpMethod, this.sessionCookieString, restOptions, function (error, result) {
				if (typeof options.arrowDBObjectMethodPostAction === 'function') {
					// if the post action has 3 args (err, response, callback), then it's async
					if (options.arrowDBObjectMethodPostAction.length > 2) {
						options.arrowDBObjectMethodPostAction.call(this, error, result, function (err, res) {
							callback(err !== undefined ? err : error, res !== undefined ? res : result);
						});
						return;
					}
					options.arrowDBObjectMethodPostAction.call(this, error, result);
				}
				callback(error, result);
			}.bind(this));
		}

		if (typeof options.arrowDBObjectMethodPreAction === 'function') {
			// if the post action has 2 args (restOptions, callback), then it's async
			if (options.arrowDBObjectMethodPreAction.length > 1) {
				options.arrowDBObjectMethodPreAction.call(this, restOptions, function (err) {
					if (err) {
						return callback(err);
					}
					prepareArrowDBRequest.call(this);
				}.bind(this));
				return;
			}
			options.arrowDBObjectMethodPreAction.call(this, options, restOptions);
		}

		prepareArrowDBRequest.call(this);
	};
}

/**
 * Creates a function for invoking a REST call method. The returned function is
 * expected to be set in the `ArrowDB` object prototype.
 *
 * For example, after creating new instance like:
 *
 *   var ArrowDB = require('arrowdb');
 *   var arrowDBApp = new ArrowDB('ARROWDB_APP_KEY');
 *
 * users will have methods like `arrowDBApp.get()` and `arrowDBApp.post()`.
 *
 * When calling `arrowDBApp.get('/v1/users/login.json')`, internally the SDK
 * transforms it to:
 *
 *   createArrowDBRESTRequestFunction({methodPath: '/v1/users/login.json', httpMethod: 'GET'})
 *
 * Then, `createArrowDBRESTRequestFunction()` will return a `function(restOptions, callback)`.
 * From user side we will get arrowDBApp.get(methodPath, restOptions, callback).
 *
 * @param {string} httpMethod - The HTTP method to use for the request. Valid
 * values include "GET", "POST", "PUT", and "DELETE".
 *
 * @returns {function(string, object, function(error result))} A function that
 * invokes the REST request.
 */
function createArrowDBRESTRequestFunction(httpMethod) {
	if (!httpMethod) {
		throw new ArrowDBError(messages.ERR_MISS_REQUIRED_PARAMETER, {
			parameter: 'httpMethod'
		});
	}

	return function (methodPath, restOptions, callback) {
		// parameter offset
		if (typeof restOptions === 'function') {
			callback = restOptions;
			restOptions = null;
		}
		if (typeof callback !== 'function') {
			callback = function () {};
		}

		restOptions || (restOptions = {});

		// check required method path
		if (!methodPath) {
			return callback(new ArrowDBError(messages.ERR_MISS_REQUIRED_PARAMETER, {
				parameter: 'methodPath'
			}));
		}
		if (typeof methodPath !== 'string') {
			return callback(new ArrowDBError(messages.ERR_WRONG_TYPE, {
				typeName: 'method path'
			}));
		}

		// check required app key
		if (!this.appKey) {
			return callback(new ArrowDBError(messages.ERR_MISS_REQUIRED_PARAMETER, {
				parameter: 'appKey'
			}));
		}
		if (typeof this.appKey !== 'string') {
			return callback(new ArrowDBError(messages.ERR_WRONG_TYPE, {
				typeName: 'app key'
			}));
		}

		// check required app options
		if (!this.appOptions) {
			return callback(new ArrowDBError(messages.ERR_MISS_REQUIRED_PARAMETER, {
				parameter: 'appOptions'
			}));
		}
		if (typeof this.appOptions !== 'object') {
			return callback(new ArrowDBError(messages.ERR_WRONG_TYPE, {
				typeName: 'app options'
			}));
		}

		keyParam = '?key=';
		if (methodPath.includes('&')){
			keyParam = '&key=';
		}

		var apiEntryPoint = this.appOptions.apiEntryPoint + methodPath + keyParam + this.appKey;
		// if (this.sessionCookieString && this.sessionCookieString.indexOf('=') < 0) {
		// 	console.log('sessionCookieString: %s', this.sessionCookieString);
		// 	this.sessionCookieString = 'arrowDBSessionID='+encodeURIComponent(this.sessionCookieString);
		// }
		
		arrowDBRequest(apiEntryPoint, this.appOptions, this.arrowDBMethod, httpMethod, this.sessionCookieString, restOptions, callback);
	};
}
