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) {
			const alert = { source: source, event: event };
			return [
				this.runCallback('on' + source + event, alert),
				this.runCallback('on' + event, alert),
				this.runCallback('onEvent', alert)
			];
		}

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

		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) {
		if (type === 'array') {
			return Array.isArray(value);
		}
		if (type === 'integer') {
			return CParamValidator.isInteger(value);
		}
		if (type === 'float') {
			return CParamValidator.isFloat(value);
		}

		return (typeof value === type);
	},

	isInteger: function(value) {
		if (!CParamValidator.ifMatch(value, /^-?\d+$/)) {
			return false;
		}

		return !isNaN(parseInt(value));
	},

	isFloat: function(value) {
		if (!CParamValidator.ifMatch(value, /^-?\d+\.\d+$/)) {
			return false;
		}

		return !isNaN(parseFloat(value));
	},

	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 (new RegExp(regex)).test(value);
	},

	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';
					break;
				}
				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 'integer':
				if (!CParamValidator.isInteger(value)) {
					error_message = 'Value "' + key + '" must be an integer';
					break;
				}
				value = parseInt(value);
				break;
			case 'float':
				if (!CParamValidator.isFloat(value)) {
					error_message = 'Value "' + key + '" must be a floating-point number';
					break;
				}
				value = parseFloat(value);
				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':
				try {
					value = JSON.parse(value);
				} catch (error) {
					throw 'Value "' + key + '" contains invalid JSON.';
				}
				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) {
						acc[obj.tag] = obj.value || null;
						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: ' +
				['integer', 'float', 'string', 'boolean', 'array', 'object'].join(', ');
		}
		params[key] = value;
		if (CParamValidator.inArray(rule.type, ['integer', 'float']) && error_message === null && (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));
		}
		else if (CParamValidator.isDefined(rule.array) && !CParamValidator.inArray(value, rule.array)) {
			error_message = 'Value "' + key + '" must be in the array ' + JSON.stringify(rule.array);
		}
		else if (CParamValidator.isDefined(rule.macro) && !CParamValidator.isMacroSet(value.toString(), rule.macro)) {
			error_message = 'The macro ' + ((CParamValidator.isDefined(rule.macro)) ? '{' + rule.macro + '} ' : ' ') + 'is not set';
		}
		if (error_message !== null) {
			if (CParamValidator.isDefined(rule.default) && 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;
		} else if (event_source === '4') {
			return zabbix_url + '/zabbix.php?action=service.list';
		}

		return zabbix_url;
	},

};

var ZABBIX_SEVERITY_MAP = ["not_classified", "information", "warning", "average", "high", "disaster"],
	serviceLogName = 'Zammad Webhook',
	Logger = new CLogger(serviceLogName),
	Zammad = CWebhook;

Zammad.prototype.onCheckParams = function () {
	CParamValidator.validate({
		alert_message: {type: 'string'},
		alert_subject: {type: 'string'},
		zammad_url: {type: 'string', url: true},
		zammad_customer: {type: 'string'},
		zammad_access_token: {type: 'string'},
		zammad_group: {type: 'string'},
		zammad_enable_tags: {type: 'boolean', default: false},
		event_nseverity: {type: 'integer', default: -1},
		zabbix_url: {type: 'string', url: true}
	}, this.params);

	if (CParamValidator.inArray(this.params.event_source, ['0', '3', '4'])) {
		CParamValidator.validate({
			event_tags: {type: 'array', macro: 'EVENT.TAGSJSON', tags: true, default: {}}
		}, this.params);
	}

	var priority;

	if (this.params.event_nseverity >= 0 && this.params.event_nseverity < ZABBIX_SEVERITY_MAP.length) {
		priority = this.params['severity_' + ZABBIX_SEVERITY_MAP[this.params.event_nseverity]];
	}
	this.priority = (CParamValidator.isDefined(priority)) ? priority.trim() : null;

	this.result = {tags: {}};
};

Zammad.prototype.onProblem = function (alert) {
	if (CParamValidator.isDefined(this.params.event_tags['__zbx_zammad_ticket_id'])) {
		return this.onUpdate(alert);
	}

	Logger.log(Logger.INFO, 'Source: ' + alert.source + '; Event: ' + alert.event);

	this.request.addHeaders('Authorization: Token token=' + this.params.zammad_access_token);

	var payload_data = {
		title: this.params.alert_subject,
		group: this.params.zammad_group,
		article: {
			subject: this.params.alert_subject,
			body: this.params.alert_message + '\n' + CWebhookHelper.createProblemURL(this.params.event_source, this.params.zabbix_url, this.params.trigger_id, this.params.event_id),
			type: 'note',
			internal: false
		},
		customer: this.params.zammad_customer
	};

	if (this.priority) {
		payload_data.priority_id = this.priority;
	}

	const response = this.request.jsonRequest('POST', this.params.zammad_url + '/api/v1/tickets', payload_data);

	if (this.request.getStatus() != 201 || !CParamValidator.isDefined(response.id)) {
		var message = 'Cannot create Zammad ticket. Request failed with status code ' + this.request.getStatus();

		if (CParamValidator.isDefined(response.error) && Object.keys(response.error).length > 0) {
			message += ': ' + response.error;
		}

		throw message + ' Check debug log for more information.';
	}

	this.result.tags = {
		__zbx_zammad_ticket_id: response.id,
		__zbx_zammad_ticketlink: this.params.zammad_url + '/#ticket/zoom/' + response.id
	};

	if (this.params.zammad_enable_tags && Object.keys(this.params.event_tags).length > 0 && CParamValidator.inArray(this.params.event_source, ['0', '3', '4'])) {
		this.request.clearHeader();
		this.request.addHeaders({
			"Content-Type": "application/json",
			"Authorization": "Token token=" + this.params.zammad_access_token
		});

		payload_data = {
			item: '',
			object: 'Ticket',
			o_id: response.id
		};

		try {
			for (var tag in this.params.event_tags) {
				payload_data.item = tag;

				if (this.params.event_tags[tag]) {
					payload_data.item += ": " + this.params.event_tags[tag];
				}

				this.request.plainRequest('POST', this.params.zammad_url + '/api/v1/tags/add', JSON.stringify(payload_data));

				if (this.request.getStatus() != 201) {
					Logger.log(Logger.INFO, 'Failed to set tag: ' + tag);
				}
			}
		}
		catch (error) {
			Logger.log(Logger.INFO, 'Failed to add ticket tags: ' + error);
		}
	}

	return this.result;
}

Zammad.prototype.onUpdate = function (alert) {
	Logger.log(Logger.INFO, 'Source: ' + alert.source + '; Event: ' + alert.event);

	if (!CParamValidator.isDefined(this.params.event_tags['__zbx_zammad_ticket_id'])) {
		throw "Failed to update the existing ticket: no ticket ID was received."
	}


	this.request.addHeaders('Authorization: Token token=' + this.params.zammad_access_token);

	const payload_data = {
		ticket_id: this.params.event_tags['__zbx_zammad_ticket_id'],
		subject: this.params.alert_subject,
		body: this.params.alert_message,
		type: 'note',
		internal: false
	};

	const response = this.request.jsonRequest('POST', this.params.zammad_url + '/api/v1/ticket_articles', payload_data);

	if (this.request.getStatus() != 201 || !CParamValidator.isDefined(response.id)) {
		var message = 'Cannot update Zammad ticket. Request failed with status code ' + this.request.getStatus();

		if (CParamValidator.isDefined(response.error) && Object.keys(response.error).length > 0) {
			message += ': ' + response.error;
		}

		throw message + ' Check debug log for more information.';
	}

	return this.result;
}

Zammad.prototype.onResolve = function (alert) {
	return this.onUpdate(alert);
}

Zammad.prototype.onDiscovery = function (alert) {
	return this.onProblem(alert);
}

Zammad.prototype.onAutoreg = function (alert) {
	return this.onProblem(alert);
}

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