var _ = require('underscore');
var fs = require('fs');
var keystone = require('../');
var mandrillapi = require('mandrill-api');
var moment = require('moment');
var path = require('path');
var util = require('util');
var utils = require('keystone-utils');
var nodemailer = require('nodemailer');
var juice = require('juice');

var templateCache = {};

var defaultConfig = {
	templateExt: 'jade',
	templateEngine: require('jade'),
	templateBasePath: path.normalize(path.join(__dirname, '..', 'templates', 'helpers', 'emails')),
	mandrill: {
		track_opens: true,
		track_clicks: true,
		preserve_recipients: false,
		inline_css: true
	},
	// Mandrill template
	templateMandrillName: null,
	templateForceHtml: false // Force html render
};

/** Custom Errors */

// TOTO: Uses arguments.callee for the stack trace, which prevents optimisation.
// I've marked these to be ignored by jshint but we should find a better way.

var ErrorNoEmailTemplateName = function() {
	Error.apply(this, arguments);
	Error.captureStackTrace(this, arguments.callee);
	this.message = 'No email templateName specified.';
	this.name = 'ErrorNoEmailTemplateName';
};
util.inherits(ErrorNoEmailTemplateName, Error);

var ErrorEmailsPathNotSet = function() {
	Error.apply(this, arguments);
	Error.captureStackTrace(this, arguments.callee);
	this.message = 'Keystone has not been configured for email support. Set the `emails` option in your configuration.';
	this.name = 'ErrorEmailsPathNotSet';
};
util.inherits(ErrorEmailsPathNotSet, Error);

var ErrorEmailOptionsRequired = function() {
	Error.apply(this, arguments);
	Error.captureStackTrace(this, arguments.callee);
	this.message = 'The keystone.Email class requires a templateName or options argument to be provided.';
	this.name = 'ErrorEmailOptionsRequired';
};
util.inherits(ErrorEmailsPathNotSet, Error);

/** Helper methods */

var getEmailsPath = function() {
	var path = keystone.getPath('emails');
	if (path) {
		return path;
	}
	throw new ErrorEmailsPathNotSet();
};

/**
 * Flatten an object
 * @param  {Object} obj   Source object
 * @param  {String} delim Delimeter
 * @return {Object}       Flat object
 */
var flattenObject = function(obj, delim) {
	delim = delim || '_';
	var _ret = {};

	for (var key in obj) {
		if (!obj.hasOwnProperty(key)) continue;

		if ((typeof obj[key]) === 'object' && obj[key] !== null) {
			var _flat = flattenObject(obj[key], delim);
			for (var next in _flat) {
				if (!_flat.hasOwnProperty(next)) continue;

				_ret[key + delim + next] = _flat[next];
			}
		} else {
			_ret[key] = obj[key];
		}
	}
	return _ret;
};

/**
 * Converts javascript Objects into
 * Mandrill's vars objects.
 *
 * @param  {Mixed}  _var Object to parse
 * @return {Array}       An array of vars
 */
var objToMandrillVars = function (_var) {
	var _new = [];
	if ('object' === typeof _var) {
		var _flat = flattenObject(_var, '_');
		_.each(_flat, function(value, key) {
			_new.push({ name: key, content: value });
		});
		return _new;
	}
	return _var;
};

/** CSS Helper methods */

var templateCSSMethods = {
	shadeColor: function(color, percent) {
		/* eslint-disable space-infix-ops */
		var num = parseInt(color.slice(1), 16), amt = Math.round(2.55 * percent), R = (num >> 16) + amt, G = (num >> 8 & 0x00FF) + amt, B = (num & 0x0000FF) + amt;
		return '#' + (0x1000000 + (R<255?R<1?0:R:255)*0x10000 + (G<255?G<1?0:G:255)*0x100 + (B<255?B<1?0:B:255)).toString(16).slice(1);
		/* eslint-enable */
	}
};



/**
 * Email Class
 * ===========
 *
 * Helper class for sending emails with Mandrill.
 *
 * New instances take a `templatePath` string which must be a folder in the
 * emails path, and must contain either `templateName/email.templateExt` or `templateName.templateExt`
 * which is used as the template for the HTML part of the email.
 *
 * Once created, emails can be rendered or sent.
 *
 * Requires the `emails` path option and the `mandrill api key` option to be
 * set on Keystone.
 *
 * @api public
 */

var Email = function(options) {

	// Passing a string will use Email.defaults for everything but template name
	if ('string' === typeof options) {
		options = {
			templateName: options
		};
	} else if (!utils.isObject(options)) {
		throw new ErrorEmailOptionsRequired();
	}

	this.templateMandrillName = options.templateMandrillName;
	this.templateMandrillContent = _.isArray(options.templateMandrillContent) ? options.templateMandrillContent : [];
	
	this.templateName = options.templateName || this.templateMandrillName;
	this.templateExt = options.templateExt || Email.defaults.templateExt;
	this.templateEngine = options.templateEngine || Email.defaults.templateEngine;
	this.templateBasePath = options.templateBasePath || Email.defaults.templateBasePath;

	if (!this.templateName) {
		throw new ErrorNoEmailTemplateName();
	}

	return this;

};

/**
 * Renders the email and passes it to the callback. Used by `email.send()` but
 * can also be called directly to generate a preview.
 *
 * @param {Object} locals - object of local variables provided to the template
 * @param {Function} callback(err, email)
 *
 * @api public
 */

Email.prototype.render = function(locals, callback) {

	if ('function' === typeof locals && !callback) {
		callback = locals;
		locals = {};
	}

	locals = ('object' === typeof locals) ? locals : {};
	callback = ('function' === typeof callback) ? callback : function() {};

	if (keystone.get('email locals')) {
		_.defaults(locals, keystone.get('email locals'));
	}

	_.defaults(locals, {
		pretty: true,
		_: _,
		moment: moment,
		utils: utils,
		subject: '(no subject)',
		brand: keystone.get('brand'),
		theme: {},
		css: templateCSSMethods
	});

	if (!locals.theme.buttons) {
		locals.theme.buttons = {};
	}

	this.compileTemplate(function(err) {

		if (err) {
			return callback(err);
		}

		var html = templateCache[this.templateName](locals);

		// ensure extended characters are replaced
		html = html.replace(/[\u007f-\uffff]/g, function(c) {
			return '&#x' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4) + ';';
		});

		// process email rules
		var rules = keystone.get('email rules');

		if (rules) {

			if (!Array.isArray(rules)) {
				rules = [rules];
			}

			_.each(rules, function(rule) {
				if (rule.find && rule.replace) {

					var find = rule.find,
						replace = rule.replace;

					if ('string' === typeof find) {
						find = new RegExp(find, 'gi');
					}

					html = html.replace(find, replace);
				}
			});

		}

		callback(null, {
			subject: locals.subject,
			html: html
		});

	}.bind(this));

};


/**
 * Loads the template. Looks for `templateName.templateExt`, followed by `templateName/email.templateExt`
 *
 * @param {Function} callback(err)
 *
 * @api private
 */

Email.prototype.loadTemplate = function(callback) {

	callback = ('function' === typeof callback) ? callback : function() {};

	var fsTemplatePath = path.join(Email.getEmailsPath(), this.templateName + '.' + this.templateExt);

	fs.readFile(fsTemplatePath, function(err, contents) {

		if (err) {
			if (err.code === 'ENOENT') {

				fsTemplatePath = path.join(Email.getEmailsPath(), this.templateName, 'email.' + this.templateExt);

				fs.readFile(fsTemplatePath, function(err, contents) {
					callback(err, fsTemplatePath, contents);
				});

			} else {
				return callback(err);
			}
		} else {
			return callback(err, fsTemplatePath, contents);
		}

	}.bind(this));

};

/**
 * Ensures the template for the email has been compiled
 *
 * @param {Function} callback(err)
 *
 * @api private
 */

Email.prototype.compileTemplate = function(callback) {

	callback = ('function' === typeof callback) ? callback : function() {};

	if (keystone.get('env') === 'production' && templateCache[this.templateName]) {
		return process.nextTick(callback);
	}

	this.loadTemplate(function(err, filename, contents) {

		if (err) return callback(err);

		var template = this.templateEngine.compile(contents.toString(), Email.defaults.compilerOptions || { filename: fs.realpathSync(filename), basedir: this.templateBasePath });

		templateCache[this.templateName] = template;

		callback();

	}.bind(this));

};

/**
 * Prepares the email, with or without render
 *
 * @param  {Object}   options  options to render the email
 * @param  {Function} callback(err, message)
 *
 * @api private
 */
Email.prototype.prepare = function(options, callback) {

	var locals = options;

	if (arguments.length === 3 || !utils.isFunction(callback)) {
		callback = arguments[2];
		options = arguments[1] || arguments[0];
	}

	callback = ('function' === typeof callback) ? callback : function() {};

	// Renders an email via Keystone (default behavior)
	if (!this.templateMandrillName || this.templateForceHtml) {

		this.render(locals, function(err, email){

			_.extend(options, email);
			this.buildOptions.call(this, err, options, callback);

		}.bind(this));
	}
	// Uses a Mandrill template directly
	else {
		this.buildOptions.call(this, null, options, callback);
	}
};

/**
 * Build an options object
 *
 * @param  {Object}   options  Input options
 * @param  {Function} callback(err, callback)
 *
 * @api private
 */
Email.prototype.buildOptions = function(err, options, callback) {

	callback = ('function' === typeof callback) ? callback : function() {};

	if (err) {
		return callback(err);
	}

	if (!utils.isObject(options)) {
		return callback({
			from: 'Email.send',
			key: 'invalid options',
			message: 'options object is required'
		});
	}

	if ('string' === typeof options.from) {
		options.fromName = options.from;
		options.fromEmail = options.from;
	} else if (utils.isObject(options.from)) {
		options.fromName = utils.isObject(options.from.name) ? options.from.name.full : options.from.name;
		options.fromEmail = options.from.email;
	}

	if (!options.fromName || !options.fromEmail) {
		return callback({
			from: 'Email.send',
			key: 'invalid options',
			message: 'options.fromName and options.fromEmail are required'
		});
	}

	if(!keystone.get('emails transport')) {
		return callback({
			from: 'Email.send',
			key: 'missing email transport type',
			message: 'You must either provide a email transport type: mandrill or smtp.'
		});
	} else {
		options.transportType = keystone.get('emails transport');

		if(options.transportType == 'mandrill') {
			if (!options.mandrill) {
				if (!keystone.get('mandrill api key')) {
					return callback({
						from: 'Email.send',
						key: 'missing api key',
						message: 'You must either provide a Mandrill API Instance or set the mandrill api key before sending email.'
					});
				}

				options.mandrill = new mandrillapi.Mandrill(keystone.get('mandrill api key'));
			}
		} else {
			var smtpConfig = {
				host: keystone.get('emails smtp host'),
				port: keystone.get('emails smtp port'),
				secure: keystone.get('emails smtp secure'),
				auth: {
					user: keystone.get('emails smtp user'),
					pass: keystone.get('emails smtp password')
				}
			};

			if(keystone.get('emails smtp ignoretsl') === false) {
				smtpConfig.tsl = {
					rejectUnauthorized: false
				};
			}

			if(smtpConfig.host == null || smtpConfig.port == null || smtpConfig.secure == null || smtpConfig.auth.user == null || smtpConfig.auth.pass == null) {
				return callback({
					from: 'Email.send',
					key: 'missing smtp config',
					message: 'You must either provide a valid SMTP configuration for "emails smtp [host|port|secure|user|password]".'
				});
			}

			var transporter = nodemailer.createTransport(smtpConfig);

			options.smtp = {
				config: smtpConfig,
				transporter: transporter
			}
		}		
	}

	options.tags = utils.isArray(options.tags) ? options.tags : [];
	options.tags.push('sent:' + moment().format('YYYY-MM-DD'));
	options.tags.push(this.templateName);

	if (keystone.get('env') === 'development') {
		options.tags.push('development');
	}

	/**
	 * Convert and concat globalMergeVars
	 */
	if (options.globalMergeVars) {
		options.global_merge_vars = (options.global_merge_vars || []).concat(objToMandrillVars(options.globalMergeVars));
	}

	var recipients = [];
	var mergeVars = [];

	options.to = Array.isArray(options.to) ? options.to : [options.to];

	for (var i = 0; i < options.to.length; i++) {

		if ('string' === typeof options.to[i]) {
			options.to[i] = { email: options.to[i] };
		} else if ('object' === typeof options.to[i] && options.to[i] !== null) {

			if (!options.to[i].email) {
				return callback({
					from: 'Email.send',
					key: 'invalid recipient',
					message: 'Recipient ' + (i + 1) + ' does not have a valid email address.'
				});
			}

		} else {

			return callback({
				from: 'Email.send',
				key: 'invalid recipient',
				message: 'Recipient ' + (i + 1) + ' is not a string or an object.'
			});

		}

		var recipient = { email: options.to[i].email },
			vars = [{ name: 'email', content: recipient.email }];

		if ('string' === typeof options.to[i].name) {
			recipient.name = options.to[i].name;
			vars.push({ name: 'name', content: options.to[i].name });
		} else if ('object' === typeof options.to[i].name) {
			recipient.name = options.to[i].name.full || '';
			vars.push({ name: 'name', content: options.to[i].name.full || '' });
			vars.push({ name: 'first_name', content: options.to[i].name.first || '' });
			vars.push({ name: 'last_name', content: options.to[i].name.last || '' });
		}

		// Mandrill template
		if (recipient.vars) {
			vars.concat(objToMandrillVars(recipient.vars));
		}

		recipients.push(recipient);

		mergeVars.push({
			rcpt: recipient.email,
			vars: vars
		});
	}

	var message = {
		from_name: options.fromName,
		from_email: options.fromEmail,
		tags: options.tags,
		attachments: options.attachments,
		to: recipients,
		global_merge_vars: options.global_merge_vars,
		merge_vars: mergeVars,
		async: true
	};

	if (options.subject) {
		message.subject = options.subject;
	}

	if (options.html) {
		message.html = options.html;
	}

	_.defaults(message, options.mandrillOptions);
	_.defaults(message, Email.defaults.mandrill);

	var toSend = {
		message: message
	};

	if (this.templateMandrillName) {
		toSend.template_name = this.templateMandrillName;
		toSend.template_content = this.templateMandrillContent;
		if (_.isArray(options.templateMandrillContent)) {
			toSend.template_content = toSend.template_content.concat(options.templateMandrillContent);
		}
	}

	callback(null, toSend);
};

/**
 * Sends the email
 *
 * Options:
 *
 * - mandrill
 *   Initialised Mandrill API instance
 *
 * - tags
 *   Array of tags to send to Mandrill
 *
 * - to
 *   Object / String or Array of Objects / Strings to send to, e.g.
 *   ['jed@team9.com.au', { email: 'jed.watson@gmail.com' }]
 *   { email: 'jed@team9.com.au' }
 *   'jed@team9.com.au'
 *
 * - fromName
 *   Name to send from
 *
 * - fromEmail
 *   Email address to send from
 *
 * For compatibility with older implementations, send supports providing
 * locals and options objects as the first and second arguments, and the
 * callback as the third.
 *
 * @param {Object} options (passed to `email.render()`)
 * @param {Function} callback(err, info)
 *
 * @api private
 */ 

Email.prototype.send = function(options, callback) {

	var locals = options;
	var prepareOptions = [locals];

	if (arguments.length === 3 ) {
		// we expect locals, options, callback
		if (_.isObject(arguments[1])) {
			prepareOptions.push(arguments[1]);
		}
		callback = arguments[2];
		
	} else if (arguments.length === 2 && !utils.isFunction(callback)) {
		// no callback so we expect locals, options
		if (_.isObject(arguments[1])) {
			prepareOptions.push(arguments[1]);
		}
		callback = function(err, info) {// eslint-disable-line no-unused-vars
			if (err) console.log(err);
		};
		
	} else if (arguments.length === 1) {
		// we expect options here and it is pushed already
		callback = function(err, info){// eslint-disable-line no-unused-vars
			if (err) console.log(err);
		};
	}  

	prepareOptions.push(function(err, toSend) {
		if (err) {
			return callback(err, null);
		}

		var onSuccess = function(info) {
			callback(null, info);
		};

		var onFail = function(info) {
			callback({
				from: 'Email.send',
				key: 'send error',
				message: 'Mandrill encountered an error and did not send the emails.',
				info: info
			});
		};


		if(options.transportType == 'mandrill') {
			if (this.templateMandrillName) {
				options.mandrill.messages.sendTemplate(toSend, onSuccess, onFail);
			} else {
				options.mandrill.messages.send(toSend, onSuccess, onFail);
			}
		} else {
			return process.nextTick(function() {
				var emailData = {
					//from: (toSend.message.from_name) ? '"' + toSend.message.from_name + '" <' + toSend.message.from_email + '>' : toSend.message.from_email,
					from: toSend.message.from_email,
					to: _.map(toSend.message.to, function(email) { return email.email; }).join(','),
					subject: toSend.message.subject,
					attachments: _.map(toSend.message.attachments, function(attach) {
						return {
							filename: attach.name,
							content: attach.content,
							contentType: attach.type,
							encoding: 'base64'
						};
					}),
					html: juice(toSend.message.html)
				}

				options.smtp.transporter.sendMail(emailData, function(error, info) {
					if(error){
						onFail();
					} else {
						onSuccess();
					}
				});
			});
		}
	}.bind(this));

	this.prepare.apply(this, prepareOptions);
};

/**
 * Renders a Mandrill template
 * @param  {Object}   options  Same options that sendTemplate
 * @param  {Function} callback(err, info)
 */
Email.prototype.renderMandrill = function(options, callback) {

	var locals = options;
	var prepareOptions = [locals];

	if (arguments.length === 3 || !utils.isFunction(callback)) {
		callback = arguments[2];
		options = arguments[1] || arguments[0];
		prepareOptions.push(options);
	}

	callback = ('function' === typeof callback) ? callback : function() {};

	prepareOptions.push(function(err, toSend) {

		if (err) {
			return callback(err, null);
		}

		/**
		 * The object sent to render is not the same as for the sendTemplate
		 * method, so it needs to be changed accordingly.
		 *
		 * https://mandrillapp.com/api/docs/templates.JSON.html#method=render
		 * https://mandrillapp.com/api/docs/messages.JSON.html#method=send-template
		 */

		if (toSend.message.merge_vars && toSend.message.merge_vars.length) {
			toSend.merge_vars = toSend.message.merge_vars[0].vars;
		}

		if (toSend.message.global_merge_vars) {
			toSend.global_merge_vars = toSend.message.global_merge_vars;
		}

		var onSuccess = function(info) {
			callback(null, info);
		};

		var onFail = function(info) {
			callback({
				from: 'Email.renderMandrill',
				key: 'render error',
				message: 'Mandrill encountered an error rendering the email.',
				info: info
			});
		};

		options.mandrill.templates.render(toSend, onSuccess, onFail);
	});

	this.prepare.apply(this, prepareOptions);
};

Email.getEmailsPath = getEmailsPath;
Email.templateCache = templateCache;
Email.templateCSSMethods = templateCSSMethods;
Email.defaults = defaultConfig;

exports = module.exports = Email;
