"use strict";

var db = require('./database'),
	async = require('async'),
	winston = require('winston'),
	fs = require('fs'),
	path = require('path'),

	User = require('./user'),
	Topics = require('./topics'),
	Posts = require('./posts'),
	Categories = require('./categories'),
	Groups = require('./groups'),
	Meta = require('./meta'),
	Plugins = require('./plugins'),
	Utils = require('../public/src/utils'),

	Upgrade = {},

	minSchemaDate = Date.UTC(2014, 9, 22),		// This value gets updated every new MINOR version
	schemaDate, thisSchemaDate,

	// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema
	latestSchema = Date.UTC(2015, 4, 11);

Upgrade.check = function(callback) {
	db.get('schemaDate', function(err, value) {
		if (err) {
			return callback(err);
		}

		if (!value) {
			db.set('schemaDate', latestSchema, function(err) {
				if (err) {
					return callback(err);
				}
				callback(null, true);
			});
			return;
		}

		callback(null, parseInt(value, 10) >= latestSchema);
	});
};

Upgrade.update = function(schemaDate, callback) {
	db.set('schemaDate', schemaDate, callback);
};

Upgrade.upgrade = function(callback) {
	var updatesMade = false;

	winston.info('Beginning database schema update');

	async.series([
		function(next) {
			// Prepare for upgrade & check to make sure the upgrade is possible
			db.get('schemaDate', function(err, value) {
				if(!value) {
					db.set('schemaDate', latestSchema, function(err) {
						next();
					});
					schemaDate = latestSchema;
				} else {
					schemaDate = parseInt(value, 10);
				}

				if (schemaDate >= minSchemaDate) {
					next();
				} else {
					next(new Error('upgrade-not-possible'));
				}
			});
		},
		function(next) {
			thisSchemaDate = Date.UTC(2014, 9, 31);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2014/10/31] Applying newbiePostDelay values');

				async.series([
					async.apply(Meta.configs.setOnEmpty, 'newbiePostDelay', '120'),
					async.apply(Meta.configs.setOnEmpty, 'newbiePostDelayThreshold', '3')
				], function(err) {
					if (err) {
						winston.error('[2014/10/31] Error encountered while Applying newbiePostDelay values');
						return next(err);
					}
					winston.info('[2014/10/31] Applying newbiePostDelay values done');
					Upgrade.update(thisSchemaDate, next);
				});
			} else {
				winston.info('[2014/10/31] Applying newbiePostDelay values skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2014, 10, 6, 18, 30);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2014/11/6] Updating topic authorship sorted set');

				async.waterfall([
					async.apply(db.getObjectField, 'global', 'nextTid'),
					function(nextTid, next) {
						var tids = [];
						for(var x=1,numTids=nextTid-1;x<numTids;x++) {
							tids.push(x);
						}
						async.filter(tids, function(tid, next) {
							db.exists('topic:' + tid, function(err, exists) {
								next(exists);
							});
						}, function(tids) {
							next(null, tids);
						});
					},
					function(tids, next) {
						async.eachLimit(tids, 100, function(tid, next) {
							Topics.getTopicFields(tid, ['uid', 'cid', 'tid', 'timestamp'], function(err, data) {
								if (!err && (data.cid && data.uid && data.timestamp && data.tid)) {
									db.sortedSetAdd('cid:' + data.cid + ':uid:' + data.uid + ':tid', data.timestamp, data.tid, next);
								} else {
									// Post was probably purged, skip record
									next();
								}
							});
						}, next);
					}
				], function(err) {
					if (err) {
						winston.error('[2014/11/6] Error encountered while Updating topic authorship sorted set');
						return next(err);
					}
					winston.info('[2014/11/6] Updating topic authorship sorted set done');
					Upgrade.update(thisSchemaDate, next);
				});
			} else {
				winston.info('[2014/11/6] Updating topic authorship sorted set skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2014, 10, 7);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2014/11/7] Renaming sorted set names');

				async.waterfall([
					function(next) {
						async.parallel({
							cids: function(next) {
								db.getSortedSetRange('categories:cid', 0, -1, next);
							},
							uids: function(next) {
								db.getSortedSetRange('users:joindate', 0, -1, next);
							}
						}, next);
					},
					function(results, next) {
						async.eachLimit(results.cids, 50, function(cid, next) {
							async.parallel([
								function(next) {
									db.exists('categories:' + cid + ':tid', function(err, exists) {
										if (err || !exists) {
											return next(err);
										}
										db.rename('categories:' + cid + ':tid', 'cid:' + cid + ':tids', next);
									});
								},
								function(next) {
									db.exists('categories:recent_posts:cid:' + cid, function(err, exists) {
										if (err || !exists) {
											return next(err);
										}
										db.rename('categories:recent_posts:cid:' + cid, 'cid:' + cid + ':pids', next);
									});
								},
								function(next) {
									async.eachLimit(results.uids, 50, function(uid, next) {
										db.exists('cid:' + cid + ':uid:' + uid + ':tid', function(err, exists) {
											if (err || !exists) {
												return next(err);
											}
											db.rename('cid:' + cid + ':uid:' + uid + ':tid', 'cid:' + cid + ':uid:' + uid + ':tids', next);
										});
									}, next);
								}
							], next);
						}, next);
					}
				], function(err) {
					if (err) {
						winston.error('[2014/11/7] Error encountered while renaming sorted sets');
						return next(err);
					}
					winston.info('[2014/11/7] Renaming sorted sets done');
					Upgrade.update(thisSchemaDate, next);
				});
			} else {
				winston.info('[2014/11/7] Renaming sorted sets skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2014, 10, 11);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2014/11/11] Upgrading permissions');

				async.waterfall([
					function(next) {
						db.getSortedSetRange('categories:cid', 0, -1, next);
					},
					function(cids, next) {
						function categoryHasPrivilegesSet(cid, privilege, next) {
							async.parallel({
								userPrivExists: function(next) {
									Groups.getMemberCount('cid:' + cid + ':privileges:' + privilege, next);
								},
								groupPrivExists: function(next) {
									Groups.getMemberCount('cid:' + cid + ':privileges:groups:' + privilege, next);
								}
							}, function(err, results) {
								if (err) {
									return next(err);
								}
								next(null, results.userPrivExists || results.groupPrivExists);
							});
						}

						function upgradePrivileges(cid, groups, next) {
							var privs = ['find', 'read', 'topics:reply', 'topics:create'];

							async.each(privs, function(priv, next) {
								categoryHasPrivilegesSet(cid, priv, function(err, privilegesSet) {
									if (err || privilegesSet) {
										return next(err);
									}

									async.eachLimit(groups, 50, function(group, next) {
										if (group) {
											if (group === 'guests' && (priv === 'topics:reply' || priv === 'topics:create')) {
												return next();
											}
											Groups.join('cid:' + cid + ':privileges:groups:' + priv, group, next);
										} else {
											next();
										}
									}, next);
								});
							}, next);
						}

						async.waterfall([
							async.apply(db.getSetMembers, 'groups'),
							function(groups, next) {
								async.filter(groups, function(group, next) {
									db.getObjectField('group:' + group, 'hidden', function(err, hidden) {
										next(!parseInt(hidden, 10));
									}, next);
								}, function(groups) {
									next(null, groups);
								});
							}
						], function(err, groups) {
							if (err) {
								return next(err);
							}

							groups.push('administrators', 'registered-users');

							async.eachLimit(cids, 50, function(cid, next) {
								upgradePrivileges(cid, groups, next);
							}, next);
						});
					}
				], function(err) {
					if (err) {
						winston.error('[2014/11/11] Error encountered while upgrading permissions');
						return next(err);
					}
					winston.info('[2014/11/11] Upgrading permissions done');
					Upgrade.update(thisSchemaDate, next);
				});
			} else {
				winston.info('[2014/11/11] Upgrading permissions skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2014, 10, 17, 13);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2014/11/17] Updating user email digest settings');

				async.waterfall([
					async.apply(db.getSortedSetRange, 'users:joindate', 0, -1),
					function(uids, next) {
						async.filter(uids, function(uid, next) {
							db.getObjectField('user:' + uid + ':settings', 'dailyDigestFreq', function(err, freq) {
								next(freq === 'daily');
							});
						}, function(uids) {
							next(null, uids);
						});
					},
					function(uids, next) {
						async.each(uids, function(uid, next) {
							db.setObjectField('user:' + uid + ':settings', 'dailyDigestFreq', 'day', next);
						}, next);
					}
				], function(err) {
					if (err) {
						winston.error('[2014/11/17] Error encountered while updating user email digest settings');
						return next(err);
					}
					winston.info('[2014/11/17] Updating user email digest settings done');
					Upgrade.update(thisSchemaDate, next);
				});
			} else {
				winston.info('[2014/11/17] Updating user email digest settings skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2014, 10, 29, 22);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2014/11/29] Updating config.json to new format');
				var configPath = path.join(__dirname, '../config.json');

				async.waterfall([
					async.apply(fs.readFile, configPath, { encoding: 'utf-8' }),
					function(config, next) {
						try {
							config = JSON.parse(config);

							// If the config contains "url", it has already been updated, abort.
							if (config.hasOwnProperty('url')) {
								return next();
							}

							config.url = config.base_url + (config.use_port ? ':' + config.port : '') + config.relative_path;
							if (config.port == '4567') {
								delete config.port;
							}
							if (config.bcrypt_rounds == 12) {
								delete config.bcrypt_rounds;
							}
							if (config.upload_path === '/public/uploads') {
								delete config.upload_path;
							}
							if (config.bind_address === '0.0.0.0') {
								delete config.bind_address;
							}
							delete config.base_url;
							delete config.use_port;
							delete config.relative_path;

							fs.writeFile(configPath, JSON.stringify(config, null, 4), next);
						} catch (err) {
							return next(err);
						}
					}
				], function(err) {
					if (err) {
						winston.error('[2014/11/29] Error encountered while updating config.json to new format');
						return next(err);
					}
					winston.info('[2014/11/29] Updating config.json to new format done');
					Upgrade.update(thisSchemaDate, next);
				});
			} else {
				winston.info('[2014/11/29] Updating config.json to new format skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2014, 11, 2);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2014/12/2] Removing register user fields');

				db.getSortedSetRange('users:joindate', 0, -1, function(err, uids) {
					if (err) {
						return next(err);
					}
					var fieldsToDelete = [
						'password-confirm',
						'recaptcha_challenge_field',
						'_csrf',
						'recaptcha_response_field',
						'referrer'
					];

					async.eachLimit(uids, 50, function(uid, next) {
						async.each(fieldsToDelete, function(field, next) {
							db.deleteObjectField('user:' + uid, field, next);
						}, next);
					}, function(err) {
						if (err) {
							winston.error('[2014/12/2] Error encountered while deleting user fields');
							return next(err);
						}
						winston.info('[2014/12/2] Removing register user fields done');
						Upgrade.update(thisSchemaDate, next);
					});
				});
			} else {
				winston.info('[2014/12/2] Removing register user fields skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2014, 11, 12);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2014/12/12] Updating teasers');

				db.getSortedSetRange('topics:tid', 0, -1, function(err, tids) {
					if (err) {
						return next(err);
					}

					async.eachLimit(tids, 50, function(tid, next) {
						Topics.updateTeaser(tid, next);
					}, function(err) {
						if (err) {
							winston.error('[2014/12/12] Error encountered while updating teasers');
							return next(err);
						}
						winston.info('[2014/12/12] Updating teasers done');
						Upgrade.update(thisSchemaDate, next);
					});
				});
			} else {
				winston.info('[2014/12/12] Updating teasers skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2014, 11, 20);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2014/12/20] Updating digest settings');

				async.waterfall([
					async.apply(db.getSortedSetRange, 'users:joindate', 0, -1),
					async.apply(User.getMultipleUserSettings)
				], function(err, userSettings) {
					if (err) {
						winston.error('[2014/12/20] Error encountered while updating digest settings');
						return next(err);
					}

					var now = Date.now();

					async.eachLimit(userSettings, 50, function(setting, next) {
						if (setting.dailyDigestFreq !== 'off') {
							db.sortedSetAdd('digest:' + setting.dailyDigestFreq + ':uids', now, setting.uid, next);
						} else {
							setImmediate(next);
						}
					}, function(err) {
						if (err) {
							winston.error('[2014/12/20] Error encountered while updating digest settings');
							return next(err);
						}
						winston.info('[2014/12/20] Updating digest settings done');
						Upgrade.update(thisSchemaDate, next);
					});
				});
			} else {
				winston.info('[2014/12/20] Updating digest settings skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2015, 0, 8);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2015/01/08] Updating category topics sorted sets');

				db.getSortedSetRange('topics:tid', 0, -1, function(err, tids) {
					if (err) {
						winston.error('[2015/01/08] Error encountered while Updating category topics sorted sets');
						return next(err);
					}

					var now = Date.now();

					async.eachLimit(tids, 50, function(tid, next) {
						db.getObjectFields('topic:' + tid, ['cid', 'postcount'], function(err, topicData) {
							if (err) {
								return next(err);
							}

							if (Utils.isNumber(topicData.postcount) && topicData.cid) {
								db.sortedSetAdd('cid:' + topicData.cid + ':tids:posts', topicData.postcount, tid, next);
							} else {
								next();
							}
						});
					}, function(err) {
						if (err) {
							winston.error('[2015/01/08] Error encountered while Updating category topics sorted sets');
							return next(err);
						}
						winston.info('[2015/01/08] Updating category topics sorted sets done');
						Upgrade.update(thisSchemaDate, next);
					});
				});
			} else {
				winston.info('[2015/01/08] Updating category topics sorted sets skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2015, 0, 9);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2015/01/09] Creating fullname:uid hash');

				db.getSortedSetRange('users:joindate', 0, -1, function(err, uids) {
					if (err) {
						winston.error('[2014/01/09] Error encountered while Creating fullname:uid hash');
						return next(err);
					}

					var now = Date.now();

					async.eachLimit(uids, 50, function(uid, next) {
						db.getObjectFields('user:' + uid, ['fullname'], function(err, userData) {
							if (err || !userData || !userData.fullname) {
								return next(err);
							}

							db.setObjectField('fullname:uid', userData.fullname, uid, next);
						});
					}, function(err) {
						if (err) {
							winston.error('[2015/01/09] Error encountered while Creating fullname:uid hash');
							return next(err);
						}
						winston.info('[2015/01/09] Creating fullname:uid hash done');
						Upgrade.update(thisSchemaDate, next);
					});
				});
			} else {
				winston.info('[2015/01/09] Creating fullname:uid hash skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2015, 0, 13);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2015/01/13] Creating uid:followed_tids sorted set');

				db.getSortedSetRange('topics:tid', 0, -1, function(err, tids) {
					if (err) {
						winston.error('[2014/01/13] Error encountered while Creating uid:followed_tids sorted set');
						return next(err);
					}

					var now = Date.now();

					async.eachLimit(tids, 50, function(tid, next) {
						db.getSetMembers('tid:' + tid + ':followers', function(err, uids) {
							if (err) {
								return next(err);
							}

							async.eachLimit(uids, 50, function(uid, next) {
								if (parseInt(uid, 10)) {
									db.sortedSetAdd('uid:' + uid + ':followed_tids', now, tid, next);
								} else {
									next();
								}
							}, next);
						});
					}, function(err) {
						if (err) {
							winston.error('[2015/01/13] Error encountered while Creating uid:followed_tids sorted set');
							return next(err);
						}
						winston.info('[2015/01/13] Creating uid:followed_tids sorted set done');
						Upgrade.update(thisSchemaDate, next);
					});
				});
			} else {
				winston.info('[2015/01/13] Creating uid:followed_tids sorted set skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2015, 0, 14);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2015/01/14] Upgrading follow sets to sorted sets');

				db.getSortedSetRange('users:joindate', 0, -1, function(err, uids) {
					if (err) {
						winston.error('[2014/01/14] Error encountered while Upgrading follow sets to sorted sets');
						return next(err);
					}

					var now = Date.now();

					async.eachLimit(uids, 50, function(uid, next) {
						async.parallel({
							following: function(next) {
								db.getSetMembers('following:' + uid, next);
							},
							followers: function(next) {
								db.getSetMembers('followers:' + uid, next);
							}
						}, function(err, results) {
							function updateToSortedSet(set, uids, callback) {
								async.eachLimit(uids, 50, function(uid, next) {
									if (parseInt(uid, 10)) {
										db.sortedSetAdd(set, now, uid, next);
									} else {
										next();
									}
								}, callback);
							}
							if (err) {
								return next(err);
							}

							async.parallel([
								async.apply(db.delete, 'following:' + uid),
								async.apply(db.delete, 'followers:' + uid)
							], function(err) {
								if (err) {
									return next(err);
								}
								async.parallel([
									async.apply(updateToSortedSet, 'following:' + uid, results.following),
									async.apply(updateToSortedSet, 'followers:' + uid, results.followers),
									async.apply(db.setObjectField, 'user:' + uid, 'followingCount', results.following.length),
									async.apply(db.setObjectField, 'user:' + uid, 'followerCount', results.followers.length),
								], next);
							});
						});
					}, function(err) {
						if (err) {
							winston.error('[2015/01/14] Error encountered while Upgrading follow sets to sorted sets');
							return next(err);
						}
						winston.info('[2015/01/14] Upgrading follow sets to sorted sets done');
						Upgrade.update(thisSchemaDate, next);
					});
				});
			} else {
				winston.info('[2015/01/14] Upgrading follow sets to sorted sets skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2015, 0, 15);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2015/01/15] Creating topiccount for users');

				db.getSortedSetRange('users:joindate', 0, -1, function(err, uids) {
					if (err) {
						winston.error('[2015/01/15] Error encountered while Creating topiccount for users');
						return next(err);
					}

					async.eachLimit(uids, 50, function(uid, next) {
						db.sortedSetCard('uid:' + uid + ':topics', function(err, count) {
							if (err) {
								return next(err);
							}

							if (parseInt(count, 10)) {
								db.setObjectField('user:' + uid, 'topiccount', count, next);
							} else {
								next();
							}
						});
					}, function(err) {
						if (err) {
							winston.error('[2015/01/15] Error encountered while Creating topiccount for users');
							return next(err);
						}
						winston.info('[2015/01/15] Creating topiccount for users done');
						Upgrade.update(thisSchemaDate, next);
					});
				});
			} else {
				winston.info('[2015/01/15] Creating topiccount for users skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2015, 0, 19);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2015/01/19] Generating group slugs');

				async.waterfall([
					async.apply(db.getSetMembers, 'groups'),
					function(groups, next) {
						async.filter(groups, function(groupName, next) {
							db.getObjectField('group:' + groupName, 'hidden', function(err, hidden) {
								next((err || parseInt(hidden, 10)) ? false : true);
							});
						}, function(groups) {
							next(null, groups);
						});
					}
				], function(err, groups) {
					var tasks = [];
					groups.forEach(function(groupName) {
						tasks.push(async.apply(db.setObjectField, 'group:' + groupName, 'slug', Utils.slugify(groupName)));
						tasks.push(async.apply(db.setObjectField, 'groupslug:groupname', Utils.slugify(groupName), groupName));
					});

					// Administrator group
					tasks.push(async.apply(db.setObjectField, 'group:administrators', 'slug', 'administrators'));
					tasks.push(async.apply(db.setObjectField, 'groupslug:groupname', 'administrators', 'administrators'));

					async.parallel(tasks, function(err) {
						if (err) {
							winston.error('[2015/01/19] Error encountered while Generating group slugs');
							return next(err);
						}

						winston.info('[2015/01/19] Generating group slugs done');
						Upgrade.update(thisSchemaDate, next);
					});
				});
			} else {
				winston.info('[2015/01/19] Generating group slugs skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2015, 0, 21);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2015/01/21] Upgrading groups to sorted set');

				db.getSetMembers('groups', function(err, groupNames) {
					if (err) {
						return next(err);
					}

					var now = Date.now();
					async.each(groupNames, function(groupName, next) {
						db.getSetMembers('group:' + groupName + ':members', function(err, members) {
							if (err) {
								return next(err);
							}

							async.series([
								function(next) {
									if (members && members.length) {
										db.delete('group:' + groupName + ':members', function(err) {
											if (err) {
												return next(err);
											}
											var scores = members.map(function() {
												return now;
											});
											db.sortedSetAdd('group:' + groupName + ':members', scores, members, next);
										});
									} else {
										next();
									}
								},
								async.apply(db.sortedSetAdd, 'groups:createtime', now, groupName),
								async.apply(db.setObjectField, 'group:' + groupName, 'createtime', now)
							], next);
						});

					}, function(err) {
						winston.info('[2015/01/21] Upgrading groups to sorted set done');
						Upgrade.update(thisSchemaDate, next);
					});
				});
			} else {
				winston.info('[2015/01/21] Upgrading groups to sorted set skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2015, 0, 30);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2015/01/30] Adding group member counts');

				db.getSortedSetRange('groups:createtime', 0, -1, function(err, groupNames) {
					if (err) {
						return next(err);
					}

					var now = Date.now();
					async.each(groupNames, function(groupName, next) {
						db.sortedSetCard('group:' + groupName + ':members', function(err, memberCount) {
							if (err) {
								return next(err);
							}

							if (parseInt(memberCount, 10)) {
								db.setObjectField('group:' + groupName, 'memberCount', memberCount, next);
							} else {
								next();
							}
						});
					}, function(err) {
						winston.info('[2015/01/30] Adding group member counts done');
						Upgrade.update(thisSchemaDate, next);
					});
				});
			} else {
				winston.info('[2015/01/30] Adding group member counts skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2015, 1, 8);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2015/02/08] Clearing reset tokens');

				db.deleteAll(['reset:expiry', 'reset:uid'], function(err) {
					if (err) {
						winston.error('[2015/02/08] Error encountered while Clearing reset tokens');
						return next(err);
					}

					winston.info('[2015/02/08] Clearing reset tokens done');
					Upgrade.update(thisSchemaDate, next);
				});
			} else {
				winston.info('[2015/02/08] Clearing reset tokens skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2015, 1, 17);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2015/02/17] renaming home.tpl to categories.tpl');

				db.rename('widgets:home.tpl', 'widgets:categories.tpl', function(err) {
					if (err) {
						return next(err);
					}

					winston.info('[2015/02/17] renaming home.tpl to categories.tpl done');
					Upgrade.update(thisSchemaDate, next);
				});
			} else {
				winston.info('[2015/02/17] renaming home.tpl to categories.tpl skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2015, 1, 23);
			if (schemaDate < thisSchemaDate) {
				db.setAdd('plugins:active', 'nodebb-rewards-essentials', function(err) {
					winston.info('[2015/2/23] Activating NodeBB Essential Rewards');
					Plugins.reload(function() {
						if (err) {
							next(err);
						} else {
							Upgrade.update(thisSchemaDate, next);
						}
					});
				});
			} else {
				winston.info('[2015/2/23] Activating NodeBB Essential Rewards - skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2015, 1, 24);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2015/02/24] Upgrading plugins:active to sorted set');

				db.getSetMembers('plugins:active', function(err, activePlugins) {
					if (err) {
						return next(err);
					}
					if (!Array.isArray(activePlugins) || !activePlugins.length) {
						winston.info('[2015/02/24] Upgrading plugins:active to sorted set done');
						Upgrade.update(thisSchemaDate, next);
					}

					db.delete('plugins:active', function(err) {
						if (err) {
							return next(err);
						}
						var order = -1;
						async.eachSeries(activePlugins, function(plugin, next) {
							++order;
							db.sortedSetAdd('plugins:active', order, plugin, next);
						}, function(err) {
							if (err) {
								return next(err);
							}
							winston.info('[2015/02/24] Upgrading plugins:active to sorted set done');
							Upgrade.update(thisSchemaDate, next);
						});
					});
				});
			} else {
				winston.info('[2015/02/24] Upgrading plugins:active to sorted set skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2015, 1, 24, 1);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2015/02/24] Upgrading privilege groups to system groups');

				var isPrivilegeGroup = /^cid:\d+:privileges:[\w:]+$/;
				db.getSortedSetRange('groups:createtime', 0, -1, function (err, groupNames) {
					groupNames = groupNames.filter(function(name) {
						return isPrivilegeGroup.test(name);
					});

					async.eachLimit(groupNames, 5, function(groupName, next) {
						db.setObjectField('group:' + groupName, 'system', '1', next);
					}, function(err) {
						if (err) {
							return next(err);
						}
						winston.info('[2015/02/24] Upgrading privilege groups to system groups done');
						Upgrade.update(thisSchemaDate, next);
					});
				});
			} else {
				winston.info('[2015/02/24] Upgrading privilege groups to system groups skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2015, 1, 25, 6);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2015/02/25] Upgrading menu items to dynamic navigation system');

				require('./navigation/admin').save(require('../install/data/navigation.json'), function(err) {
					if (err) {
						return next(err);
					}

					winston.info('[2015/02/25] Upgrading menu items to dynamic navigation system done');
					Upgrade.update(thisSchemaDate, next);
				});
			} else {
				winston.info('[2015/02/25] Upgrading menu items to dynamic navigation system skipped');
				next();
			}
		},
		function(next) {
			function upgradeHashToSortedSet(hash, callback) {
				db.getObject(hash, function(err, oldHash) {
					if (err || !oldHash) {
						return callback(err);
					}

					db.rename(hash, hash + '_old', function(err) {
						if (err) {
							return callback(err);
						}
						var keys = Object.keys(oldHash);
						if (!keys.length) {
							return callback();
						}
						async.each(keys, function(key, next) {
							db.sortedSetAdd(hash, oldHash[key], key, next);
						}, callback);
					});
				});
			}

			thisSchemaDate = Date.UTC(2015, 4, 7);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2015/05/07] Upgrading uid mappings to sorted set');

				async.series([
					async.apply(upgradeHashToSortedSet, 'email:uid'),
					async.apply(upgradeHashToSortedSet, 'fullname:uid'),
					async.apply(upgradeHashToSortedSet, 'username:uid'),
					async.apply(upgradeHashToSortedSet, 'userslug:uid'),
				], function(err) {
					if (err) {
						return next(err);
					}

					winston.info('[2015/05/07] Upgrading uid mappings to sorted set done');
					Upgrade.update(thisSchemaDate, next);
				});

			} else {
				winston.info('[2015/05/07] Upgrading uid mappings to sorted set skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2015, 4, 8);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2015/05/08] Fixing emails');

				db.getSortedSetRangeWithScores('email:uid', 0, -1, function(err, users) {
					if (err) {
						return next(err);
					}

					async.eachLimit(users, 100, function(user, next) {
						var newEmail = user.value.replace(/\uff0E/g, '.');
						if (newEmail === user.value) {
							return next();
						}
						async.series([
							async.apply(db.sortedSetRemove, 'email:uid', user.value),
							async.apply(db.sortedSetAdd, 'email:uid', user.score, newEmail)
						], next);

					}, function(err) {
						if (err) {
							return next(err);
						}
						winston.info('[2015/05/08] Fixing emails done');
						Upgrade.update(thisSchemaDate, next);
					});
				});

			} else {
				winston.info('[2015/05/08] Fixing emails skipped');
				next();
			}
		},
		function(next) {
			thisSchemaDate = Date.UTC(2015, 4, 11);
			if (schemaDate < thisSchemaDate) {
				updatesMade = true;
				winston.info('[2015/05/11] Updating widgets to tjs 0.2x');

				require('./widgets/admin').get(function(err, data) {					
					async.each(data.areas, function(area, next) {
						require('./widgets').getArea(area.template, area.location, function(err, widgets) {
							if (err) {
								return next(err);
							}
							
							for (var w in widgets) {
								if (widgets.hasOwnProperty(w)) {
									widgets[w].data.container = widgets[w].data.container
										.replace(/\{\{([\s\S]*?)\}\}/g, '{$1}')
										.replace(/\{([\s\S]*?)\}/g, '{{$1}}');
								}
							}

							require('./widgets').setArea({
								template: area.template,
								location: area.location,
								widgets: widgets
							}, next);
						});
					}, function(err) {
						if (err) {
							return next(err);
						}

						winston.info('[2015/05/11] Updating widgets to tjs 0.2x done');
						Upgrade.update(thisSchemaDate, next);
					});
				});
			} else {
				winston.info('[2015/05/11] Updating widgets to tjs 0.2x skipped');
				next();
			}
		}

		// Add new schema updates here
		// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 24!!!
	], function(err) {
		if (!err) {
			if(updatesMade) {
				winston.info('[upgrade] Schema update complete!');
			} else {
				winston.info('[upgrade] Schema already up to date!');
			}
		} else {
			switch(err.message) {
			case 'upgrade-not-possible':
				winston.error('[upgrade] NodeBB upgrade could not complete, as your database schema is too far out of date.');
				winston.error('[upgrade]   Please ensure that you did not skip any minor version upgrades.');
				winston.error('[upgrade]   (e.g. v0.1.x directly to v0.3.x)');
				break;

			default:
				winston.error('[upgrade] Errors were encountered while updating the NodeBB schema: ' + err.message);
				break;
			}
		}

		if (typeof callback === 'function') {
			callback(err);
		} else {
			process.exit();
		}
	});
};

module.exports = Upgrade;