const CLogger = function(serviceName) {
	this.serviceName = serviceName;
	this.INFO = 4
	this.WARN = 3
	this.ERROR = 2
	this.log = function(level, msg) {
		Zabbix.log(level, '['+this.serviceName+'] ' + msg);
	}
}

const CWebhook = function(value) {
	try {
		params = JSON.parse(value);

		if (['0', '1', '2', '3', '4'].indexOf(params.event_source) === -1) {
			throw 'Incorrect "event_source" parameter given: ' + params.event_source + '.\nMust be 0-4.';
		}

		if (['0', '3', '4'].indexOf(params.event_source) !== -1 && ['0', '1'].indexOf(params.event_value) === -1) {
			throw 'Incorrect "event_value" parameter given: ' + params.event_value + '.\nMust be 0 or 1.';
		}

		if (['0', '3', '4'].indexOf(params.event_source) !== -1) {
			if (params.event_source === '1' && ['0', '1', '2', '3'].indexOf(params.event_value) === -1) {
				throw 'Incorrect "event_value" parameter given: ' + params.event_value + '.\nMust be 0-3.';
			}

			if (params.event_source === '0' && ['0', '1'].indexOf(params.event_update_status) === -1) {
				throw 'Incorrect "event_update_status" parameter given: ' + params.event_update_status + '.\nMust be 0 or 1.';
			}

			if (params.event_source === '4') {
				if (['0', '1', '2', '3', '4', '5'].indexOf(params.event_update_nseverity) !== -1 && params.event_update_nseverity != params.event_nseverity) {
					params.event_nseverity = params.event_update_nseverity;
					params.event_severity = params.event_update_severity;
					params.event_update_status = '1';
				}
			}
		}

		this.runCallback = function(name, params) {
			if (typeof this[name] === 'function') {
				return this[name].apply(this, [params]);
			}
		}

		this.handleEvent = function(source, event) {
			return [
				this.runCallback('on' + source + event, {}),
				this.runCallback('on' + event, {source: source}),
				this.runCallback('onEvent', {source: source, event: event})
			];
		}

		this.handleEventless = function(source) {
			return [
				this.runCallback('on' + source, {}),
				this.runCallback('onEvent', {source: source, event: null})
			];
		}

		this.run = function() {
			var results = [];
			if (typeof this.httpProxy === 'string' && this.httpProxy.trim() !== '') {
				this.request.setProxy(this.httpProxy);
			}
			const types = {'0': 'Trigger', '1': 'Discovery', '2': 'Autoreg', '3': 'Internal', '4': 'Service'};

			if (['0', '3', '4'].indexOf(this.params.event_source) !== -1) {
				var event = (this.params.event_update_status === '1')
					? 'Update'
					: ((this.params.event_value === '1') ? 'Problem' : 'Resolve');

				results = this.handleEvent(types[this.params.event_source], event);
			}
			else if (typeof types[this.params.event_source] !== 'undefined') {
				results = this.handleEventless(types[this.params.event_source]);
			}
			else {
				throw 'Unexpected "event_source": ' + this.params.event_source;
			}

			for (idx in results) {
				if (typeof results[idx] !== 'undefined') {
					return JSON.stringify(results[idx]);
				}
			}
		}
		this.httpProxy = params.http_proxy;
		this.params = params;
		this.runCallback('onCheckParams', {});
	} catch (error) {
		throw 'Webhook processing failed: ' + error;
	}
}

const CParamValidator = {

	isType: function(value, type) {
		return ((type === 'array') ? Array.isArray(value) : (typeof value === type));
	},

	isDefined: function(value) {
		return !CParamValidator.isType(value, 'undefined');
	},

	isEmpty: function(value) {
		if (!CParamValidator.isType(value, 'string')) {
			throw 'Value "' + value + '" must be a string to be checked for emptiness.';
		}

		return (value.trim() === '');
	},

	isMacroSet: function(value, macro) {
		if (CParamValidator.isDefined(macro)) {
			return !(CParamValidator.ifMatch(value, '^\{' + macro + '\}$'))
		}

		return !(CParamValidator.ifMatch(value, '^\{[$#]{0,1}[A-Z_\.]+[\:]{0,1}["]{0,1}.*["]{0,1}\}$') || value === '*UNKNOWN*')
	},

	withinRange: function(value, min, max) {
		if (!CParamValidator.isType(value, 'number')) {
			throw 'Value "' + value + '" must be a number to be checked for range.';
		}
		if (value < ((CParamValidator.isDefined(min)) ? min : value)
			|| value > ((CParamValidator.isDefined(max)) ? max : value)) {
			return false;
		}

		return true;
	},

	inArray: function(value, array) {
		if (!CParamValidator.isType(array, 'array')) {
			throw 'The array must be an array to check the value for existing in it.';
		}
	
		return (array.indexOf((typeof value === 'string') ? value.toLowerCase() : value) !== -1);
	},

	ifMatch: function(value, regex) {
		return Boolean(CParamValidator.match(value, regex));
	},

	match: function(value, regex) {
		if (!CParamValidator.isType(value, 'string')) {
			throw 'Value "' + value + '" must be a string to be matched with the regular expression.';
		}

		return value.match(new RegExp(regex));
	},

	checkURL: function (value) {
		if (CParamValidator.isEmpty(value)) {
			throw 'URL value "' + value + '" must be a non-empty string.';
		}
		if (!CParamValidator.ifMatch(value, '^(http|https):\/\/.+')) {
			throw 'URL value "' + value + '" must contain a schema.';
		}

		return value.endsWith('/') ? value.slice(0, -1) : value;
	},

	check: function(key, rule, params) {
		if (!CParamValidator.isDefined(rule.type)) {
			throw 'Mandatory attribute "type" has not been defined for parameter "' + key + '".';
		}
		if (!CParamValidator.isDefined(params[key])) {
			throw 'Checked parameter "'+key+'" was not found in the list of input parameters.';
		}
		var value = params[key],
			error_message = null;
		switch(rule.type) {
			case 'string':
				if (!CParamValidator.isType(value, 'string')) {
					throw 'Value "' + key + '" must be a string.';
				}
				if (CParamValidator.isEmpty(value)) {
					error_message = 'Value "' + key + '" must be a non-empty string';
				}
				else if (CParamValidator.isDefined(rule.len) && value.length < rule.len) {
					error_message = 'Value "' + key + '" must be a string with a length > ' + rule.len;
				}
				if (CParamValidator.isDefined(rule.regex) && !CParamValidator.ifMatch(value, rule.regex)) {
					error_message = 'Value "' + key + '" must match the regular expression "' + rule.regex + '"';
				}
				if (CParamValidator.isDefined(rule.url) && rule.url === true) {
					value = CParamValidator.checkURL(value);
				}
				break;
			case 'number':
				value = parseInt(value);
				if (isNaN(value)) {
					error_message = 'Value "' + key + '" must be a number';
				}
				else if ((CParamValidator.isDefined(rule.min) || CParamValidator.isDefined(rule.max))
					&& !CParamValidator.withinRange(value, rule.min, rule.max)) {
					error_message = 'Value "' + key + '" must be a number ' + ((CParamValidator.isDefined(rule.min) && CParamValidator.isDefined(rule.max))
						? (rule.min + '..' + rule.max) : ((CParamValidator.isDefined(rule.min)) ? '>' + rule.min : '<' + rule.max));
				}
				break;
			case 'boolean':
				if (CParamValidator.inArray(value, ['1', 'true', 'yes', 'on'])) {
					value = true;
				}
				else if (CParamValidator.inArray(value, ['0', 'false', 'no', 'off'])) {
					value = false;
				}
				else {
					error_message = 'Value "' + key + '" must be a boolean-like.';
				}
				break;
			case 'array':
				value = JSON.parse(value);
				if (!CParamValidator.isType(value, 'array')) {
					error_message = 'Value "' + key + '" must be an array.';
				}
				if (CParamValidator.isDefined(rule.tags) && rule.tags === true) {
					value = value.reduce(function(acc, obj) {
						var key = Object.keys(obj)[0];
						acc[key] = obj[key];
						return acc;
					}, {});
				}
				break;
			case 'object':
				value = JSON.parse(value);
				if (!CParamValidator.isType(value, 'object')) {
					error_message = 'Value "' + key + '" must be an object.';
				}
				break;
			default:
				throw 'Unexpected attribute type "' + rule.type + '" for value "' + key + '". Available: ' + ['number', 'string', 'boolean'].join(', ');
		}
		params[key] = value;
		if (!CParamValidator.isMacroSet(value.toString(), rule.macro)) {
			error_message = 'The macro ' + ((CParamValidator.isDefined(rule.macro)) ? '{' + rule.macro + '} ' : ' ' ) + 'is not set';
		}
		if (CParamValidator.isDefined(rule.array) && !CParamValidator.inArray(value, rule.array)) {
			error_message = 'Value "' + key + '" must be in the array ' + JSON.stringify(rule.array);
		}
		if (error_message !== null) {
			if (CParamValidator.isType(rule.default, rule.type)) {
				params[key] = rule.default;
			}
			else {
				Zabbix.log(4, 'Default value for "' + key + '" must be a ' + rule.type + '. Skipped.');
				throw 'Incorrect value for variable "' + key + '". ' + error_message;
			}
		}

		return this;
	},

	validate: function(rules, params) {
		if (!CParamValidator.isType(params, 'object') || CParamValidator.isType(params, 'array')) {
			throw 'Incorrect parameters value. The value must be an object.';
		}
		for (var key in rules) {
			CParamValidator.check(key, rules[key], params);
		}
	}
}

const CHttpRequest = function(logger) {
	this.request = new HttpRequest();
	if (typeof logger !== 'object' || logger === null) {
		this.logger = Zabbix;
	}
	else {
		this.logger = logger;
	}

	this.clearHeader = function() {
		this.request.clearHeader();
	}

	this.addHeaders = function(value) {
		var headers = [];

		if (typeof value === 'object' && value !== null) {
			if (!Array.isArray(value)) {
				Object.keys(value).forEach(function (key) {
					headers.push(key + ': ' + value[key]);
				});
			}
			else {
				headers = value;
			}
		}
		else if (typeof value === 'string') {
			value.split('\r\n').forEach(function (header) {
				headers.push(header);
			});
		}

		for (var idx in headers) {
			this.request.addHeader(headers[idx]);
		}
	}

	this.setProxy = function(proxy) {
		this.request.setProxy(proxy);
	}

	this.plainRequest = function(method, url, data) {
		var resp = null;
		method = method.toLowerCase();
		this.logger.log(4, 'Sending ' + method + ' request:' + JSON.stringify(data));
		if (['get', 'post', 'put', 'patch', 'delete', 'trace'].indexOf(method) !== -1) {
			resp = this.request[method](url, data);
		}
		else if (['connect', 'head', 'options'].indexOf(method) !== -1) {
			resp = this.request[method](url);
		}
		else {
			throw 'Unexpected method. Method ' + method + ' is not supported.';
		}
		this.logger.log(4, 'Response has been received: ' + resp);

		return resp;
	}

	this.jsonRequest = function(method, url, data) {
		this.addHeaders('Content-Type: application/json');
		var resp = this.plainRequest(method, url, JSON.stringify(data));
		try {
			resp = JSON.parse(resp);
		}
		catch (error) {
			throw 'Failed to parse response: not well-formed JSON was received';
		}

		return resp;
	}

	this.getStatus = function() {
		return this.request.getStatus();
	}
}

const CWebhookHelper = {

	createProblemURL: function (event_source, zabbix_url, trigger_id, event_id) {
		if (event_source === '0') {
			return zabbix_url + '/tr_events.php?triggerid=' + trigger_id + '&eventid=' + event_id;
		}

		return zabbix_url;
	},

}


var serviceLogName = 'MS Teams Webhook',
	Logger = new CLogger(serviceLogName),
	MSTeams = CWebhook;

MSTeams.prototype.onCheckParams = function () {
	CParamValidator.validate({
		alert_subject: { type: 'string' }, alert_message: { type: 'string' },
		zabbix_url: { type: 'string', url: true }, teams_endpoint: { type: 'string', url: true }
	}, this.params);

	if (this.params.event_source === '0') {
		CParamValidator.validate({ event_id: { type: 'number' }, trigger_id: { type: 'number' } }, this.params);
	}

	this.body = {
		type: "message",
		attachments: [
			{
				contentType: "application/vnd.microsoft.card.adaptive",
				contentUrl: null,
				content: {
					$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
					type: "AdaptiveCard",
					version: "1.4",
					body: [
						{
							type: "Container",
							items: [
								{
									type: "TextBlock",
									size: "Medium",
									wrap: "true",
									weight: "Bolder",
									text: this.params.alert_subject
								}
							],
							style: "",
							bleed: true
						}
					],
					actions: [
						{
							type: "Action.OpenUrl",
							title: "Event info",
							url: CWebhookHelper.createProblemURL(this.params.event_source, this.params.zabbix_url, this.params.trigger_id, this.params.event_id)
						}
					]
				}
			}
		]
	};

	this.params.alert_message = this.params.alert_message.split('\n');

	for (line in this.params.alert_message) {
		this.body.attachments[0].content.body.push({
			type: "TextBlock",
			wrap: "true",
			text: this.params.alert_message[line]
		});
	}

};

MSTeams.prototype.sendRequest = function (color) {
	this.body.attachments[0].content.body[0].style = color;

	this.request.addHeaders({ "Content-Type": "application/json" });
	var response = this.request.plainRequest('POST', this.params.teams_endpoint, JSON.stringify(this.body));

	if (this.request.getStatus() !== 202) {
		Logger.log(Logger.INFO, 'HTTP code: ' + this.request.getStatus() + '. Endpoint response:' + response);
		throw 'HTTP code: ' + this.request.getStatus() + '. Endpoint response:' + response;
	}

	return 'OK';
};

MSTeams.prototype.onProblem = function () {
	return this.sendRequest("attention");
};

MSTeams.prototype.onResolve = function () {
	return this.sendRequest("good");
};

MSTeams.prototype.onUpdate = function () {
	return this.sendRequest("emphasis");
};

MSTeams.prototype.onDiscovery = function () {
	return this.sendRequest("emphasis");
};

MSTeams.prototype.onAutoreg = function () {
	return this.sendRequest("emphasis");
};

try {
	var hook = new MSTeams(value);
	hook.request = new CHttpRequest(Logger);
	return hook.run();
}
catch (error) {
	Logger.log(Logger.WARN, 'notification failed: ' + error);
	throw 'Sending failed: ' + error;
};