File: /var/dev/farhangmoaser/web/models/user.js
"use strict";
var md5 = require('md5');
var path = require('path');
var redis = require(path.join(BASEDIR, 'connectors/redis.js'));
var conf = require(path.join(BASEDIR, 'config'));
var SubscriptionModel = require('./subscription');
var redclient;
redis.connect(function(client) {
	redclient = client;
});
class UserModel {
	/**
	 * Constructor
	 * @param  {Object} data values for table fields.
	 *                       At least on of `key` or `id` must be set.
	 */
	constructor(conn, data) {
		this.data = data;
		this.conn = conn;
		this.subscriptions = [];
		
		this.cacheKey = '';
		this.hitData = {
			hitCount: 0,
			lastVisit: (new Date()).getTime(),
			lastReset: (new Date()).getTime(),
		};
		if(data.hasOwnProperty('id'))
			this.cacheKey = 'userdata-id:'+data.id;
		// else if(data.hasOwnProperty('email')) this.cacheKey = 'userdata-email:'+data.email;
	}
	field(key, value) {
		if(arguments.length==0) return;
		if(arguments.length==1)
			return this.data[key];
		
		if(value===undefined)
			delete this.data[key];
		else
			this.data[key] = value;
	}
	fields(){
		return this.data;
	}
	generateActivationKey() {
		let key = '';
		var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
		for (var i=0; i<32; i++)
			key += possible.charAt(Math.floor(Math.random() * possible.length));
		this.field('activationKey', key);
	}
	save(updateVisit) {
		updateVisit = typeof updateVisit !== 'undefined' ?  updateVisit : false;
		if(this.data.hasOwnProperty('id')) return this.update(updateVisit);
		else return this.add();
	}
	add() {
		var self = this;
		return new Promise(function(resolve, reject){
			self.conn.query(`INSERT INTO user SET ?`, self.data, function(err, result) {
				if (err) return reject(err.message);
				self.data.id = result.insertId;
				resolve(self);
			});
		});
	}
	update(updateVisit) {
		var self = this;
		return new Promise(function(resolve, reject){
			var visited = updateVisit ? ', visited=CURRENT_TIMESTAMP' : '';
			self.data.gender = self.data.gender ? self.data.gender : 0;
			self.data.hitCount = self.data.hitCount ? self.data.hitCount : 0;
			self.data.hitLimit = self.data.hitLimit ? self.data.hitLimit : 0;
			self.data.hitPeriod = self.data.hitPeriod ? self.data.hitPeriod : 0;
			self.conn.query(`UPDATE user SET ? ${visited} WHERE ?`, [self.data, {id: self.data.id}], function(err, rows, fields){
				if (err) return reject(err.message);
				resolve(self);
			});
		});
	}
	load(nocache=false) {
		if(nocache)
			return this.loadFromDB();
		else
			return new Promise((resolve, reject) =>
				this.loadFromCache().then(
					resolve,
					error => this.loadFromDB().then(resolve, reject))
			);
	}
	loadFromDB() {
		return new Promise((resolve, reject) => {
			this.loadUserData().then(
				(user) => {
					if(!user) return resolve(false);
					this.loadSubscriptions().then(
						(user) => {
							this.loadSubscriptions().then(
								() => {
									/* you may want to log the error(s) but we can cotinue without saving to cache*/
									this.saveToCache().then(resolve, error => resolve(this));
								}, error => reject(error));
						}, error => reject(error));
				}, error => reject(error));
		}, error => reject(error));
	}
	loadUserData() {
		return new Promise((resolve, reject) => {
			let params = {};
			if(this.data.hasOwnProperty('id')) params.id = this.data.id;
			else if(this.data.hasOwnProperty('email')) params.email = this.data.email;
			else if(this.data.hasOwnProperty('activationKey')) params.activationKey = this.data.activationKey;
			else return reject('At least on of the id and email fields must be provided.');
			this.conn.query(`SELECT * FROM user WHERE ?`, params, (err, rows, fields) => {
				if (err) return reject(err.message);
				if(!rows.length) return resolve(false);
				this.data = rows[0];
				this.cacheKey = 'userdata-id:'+this.data.id;
				if(!redclient) return resolve(this);
				redclient.get(this.cacheKey, (err, raw) => {
					if(err) return resolve(this);
					try {
						let cacheData = JSON.parse(raw);
						if((new Date()).getTime() - cacheData.timestamp > conf.cacheLifeSpan.userData*1000)
							this.hitData = {
								hitCount: 0,
								lastVisit: (new Date()).getTime(),
								lastReset: (new Date()).getTime(),
							};
						else
							// we've loaded user data from db and we are going to load the subscriptions from db too. so only the hit data is relevent.
							this.hitData = cacheData.hitData;
					}catch(e) {
						//preventing process termination. we can continue without reading cache data.
					}
					resolve(this);
				});
			});
		});
	}
	loadSubscriptions() {
		return new Promise((resolve, reject) => {
			if(!this.data.id) return reject('not a valid user data');
			SubscriptionModel.getActiveSubscriptionsOf(this.conn, this.data.id).then(
				subscriptions => {
					this.subscriptions = subscriptions;
					resolve(this);
				},
				errmsg => reject(errmsg)
			);
		});
	}
	loadFromCache() {
		return new Promise((resolve, reject) => {
			if(!redclient || !this.cacheKey) reject('redis client inaccessible.');
			redclient.get(this.cacheKey, (error, raw) => {
				if(error || !raw) return reject('redis read error.');
				try {
					let cacheData = JSON.parse(raw);
					if((new Date()).getTime() - cacheData.timestamp > conf.cacheLifeSpan.userData*1000)
						return reject('cache outdated.');
					this.data = cacheData.data;
					this.subscriptions = cacheData.subscriptions;
					this.hitData = cacheData.hitData;
					resolve(this);
				}catch(e) {
					reject('redis parse error.');
				}
			});
		});
	}
	saveToCache() {
		return new Promise((resolve, reject) => {
			if(redclient) {
				return redclient.set(
					this.cacheKey,
					JSON.stringify({
						data: this.data,
						subscriptions: this.subscriptions,
						timestamp: (new Date()).getTime(),
						hitData: this.hitData,
					}),
					(err) => {
						if(err) return reject('cache error')
						resolve(this);
					});
			}
			reject('redis not available.');
		});
	}
	resetHitCounter() {
		this.hitData = {
			hitCount: 0,
			lastVisit: (new Date()).getTime(),
			lastReset: (new Date()).getTime()
		};
		return this.saveToCache();
	}
	delete() {
		var self = this;
		return new Promise(function(resolve, reject){
			if(!self.data.hasOwnProperty('id'))
				reject('user id is required');
			self.conn.query(`DELETE FROM user WHERE ?`, {id: self.data.id}, function(err, rows, fields){
				if(err) return reject(err.message);
				resolve(self);
			});
		});
	}
	/**
	 * Increments the hit counter.
	 * IMPORTANT: Hence the hit limitations is a business rule we do not consider it in the model.
	 * @return {Promise} Result of the operation
	 */
	touch() {
		return new Promise((resolve, reject) => {
			if(!this.cacheKey || !redclient) return reject('cache inaccessible.');
			this.hitData.hitCount++;
			this.hitData.lastVisit = (new Date()).getTime();
			this.saveToCache().then(
				() => resolve(this),
				err => reject(err)
			);
		});
	}
	static list(conn, query, limit, offset, orderBy) {
		if(!orderBy) orderBy = 'newest';
		var orderByStr = '';
		switch(orderBy){
			case 'newest':
				orderByStr = 'ORDER BY `updated` DESC, id DESC';
				break;
			case 'oldest':
				orderByStr = 'ORDER BY `updated` ASC, id ASC';
				break;
			case 'displayname:a-z':
				orderByStr = 'ORDER BY `sortable` ASC, `id` ASC';
				break;
			case 'displayname:z-a':
				orderByStr = 'ORDER BY `sortable` DESC, `id` DESC';
				break;
			case 'email:a-z':
				orderByStr = 'ORDER BY `email` ASC, `id` ASC';
				break;
			case 'email:z-a':
				orderByStr = 'ORDER BY `email` DESC, `id` DESC';
				break;
		}
		limit = limit || 100;
		offset = offset || 0;
		return new Promise(function(resolve, reject){
			var params = [offset, limit];
			var where = '';
			if(Object.keys(query).length) {
				where = 'WHERE !!';
				params.unshift(query);
			}
			var template = conn.format(`SELECT * FROM \`user\` ${where} ${orderByStr} limit ?, ?`, params);
			conn.query(template, query, function(err, rows, fields){
				if (err) return reject(err.message);
				resolve(rows);
			});
		});
	}
	static count(conn, query) {
		return new Promise(function(resolve, reject){
			var where = '';
			if(Object.keys(query).length)
				where = 'WHERE !!';
			var template = conn.format(`SELECT COUNT(*) AS sum FROM \`user\` ${where}`, query);
			conn.query(template, query, function(err, rows, fields){
				if (err) return reject(err.message);
				resolve(rows[0].sum);
			});
		});
	}
	static authenticate(conn, email, password) {
		return new Promise(function(resolve, reject){
			if(!email || !password) return reject('Email and password required.');
			email = email.toLowerCase();
			conn.query(`SELECT * FROM user WHERE ?`, {email: email}, function(err, rows, fields){
				if(err) return reject(err.message);
				if(!rows.length)
					return reject('Unknown email address.');
				var row = rows[0];
				if(md5(password) != row.password)
					return reject('Incorrect password.');
				(new UserModel(conn, {id: row.id})).load().then(resolve, reject);
			});
		});
	}
}
module.exports = UserModel;