/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 * @NModuleScope SameAccount
 *
 * Script Name: PP_Order_Change.js
 * Description: Sends Sales Order change payloads to Hub orderupdate endpoint.
 */

define(['N/https', 'N/log', 'N/record', 'N/runtime'], function (https, log, record, runtime) {
	var DEFAULT_ENDPOINT_URL = 'https://hub.portalpeople.co.uk/api/netsuite/orderupdate';
	var ORDER_RECORD_TYPES = {
		salesorder: true
	};
	var EXTERNAL_REF_FIELDS = [
		'custbody_external_ref',
		'custbody_external_reference',
		'custbody_order_ref',
		'custbody_order_reference',
		'otherrefnum',
		'externalid'
	];
	var CORE_BODY_FIELDS = [
		'tranid',
		'entity',
		'otherrefnum',
		'externalid',
		'orderstatus',
		'status',
		'trandate',
		'total',
		'subtotal',
		'taxtotal',
		'shippingcost',
		'shipmethod',
		'terms',
		'memo',
		'currency',
		'location',
		'department',
		'class'
	];
	var ITEM_FIELDS = [
		'line',
		'lineuniquekey',
		'item',
		'quantity',
		'rate',
		'amount',
		'taxcode',
		'description',
		'isclosed'
	];

	function afterSubmit(context) {
		try {
			if (!shouldProcessEvent(context.type, context.UserEventType)) {
				return;
			}

			var sourceRecord = getSourceRecord(context);
			if (!sourceRecord || !isOrderTransaction(sourceRecord.type)) {
				return;
			}

			var oldRecordObj = context.oldRecord || null;
			var newRecordObj = null;

			if (context.type !== context.UserEventType.DELETE) {
				newRecordObj = loadCurrentRecord(sourceRecord);
				if (!newRecordObj) {
					return;
				}
			}

			var externalRef = getExternalRef(newRecordObj || oldRecordObj);
			if (!externalRef) {
				log.debug({
					title: 'Skipped order update: missing external reference',
					details: 'Record Type: ' + sourceRecord.type + ', ID: ' + sourceRecord.id
				});
				return;
			}

			var fieldsToCompare = unique(CORE_BODY_FIELDS.concat(EXTERNAL_REF_FIELDS).concat(getFieldsSafe(oldRecordObj)).concat(getFieldsSafe(newRecordObj)));
			var oldSnapshot = oldRecordObj ? buildSnapshot(oldRecordObj, fieldsToCompare) : null;
			var newSnapshot = newRecordObj ? buildSnapshot(newRecordObj, fieldsToCompare) : null;

			var bodyChanges = diffBody(
				oldSnapshot ? oldSnapshot.body : null,
				newSnapshot ? newSnapshot.body : null
			);
			var itemChanges = diffItems(
				oldSnapshot ? oldSnapshot.items : [],
				newSnapshot ? newSnapshot.items : []
			);

			if (context.type === context.UserEventType.EDIT || context.type === context.UserEventType.XEDIT) {
				if (!hasAnyChanges(bodyChanges, itemChanges)) {
					return;
				}
			}

			var payload = {
				source: 'netsuite',
				eventType: context.type,
				executionContext: runtime.executionContext,
				timestamp: new Date().toISOString(),
				endpointVersion: '1.0',
				record: {
					type: sourceRecord.type,
					id: sourceRecord.id,
					tranid: firstValue([
						getBodyValue(newSnapshot, 'tranid'),
						getBodyValue(oldSnapshot, 'tranid')
					]),
					externalRef: externalRef
				},
				changes: {
					body: bodyChanges,
					items: itemChanges
				},
				previous: oldSnapshot,
				current: newSnapshot
			};

			sendPayload(payload);
		} catch (e) {
			log.error({
				title: 'PP_Order_Change afterSubmit error',
				details: stringifyError(e)
			});
		}
	}

	function shouldProcessEvent(eventType, userEventType) {
		return eventType === userEventType.CREATE ||
			eventType === userEventType.EDIT ||
			eventType === userEventType.XEDIT ||
			eventType === userEventType.DELETE;
	}

	function getSourceRecord(context) {
		if (context.type === context.UserEventType.DELETE) {
			return context.oldRecord || null;
		}

		return context.newRecord || null;
	}

	function isOrderTransaction(recordType) {
		return !!ORDER_RECORD_TYPES[(recordType || '').toLowerCase()];
	}

	function loadCurrentRecord(sourceRecord) {
		try {
			return record.load({
				type: sourceRecord.type,
				id: sourceRecord.id,
				isDynamic: false
			});
		} catch (e) {
			log.error({
				title: 'Unable to load current record',
				details: 'Type: ' + sourceRecord.type + ', ID: ' + sourceRecord.id + ', Error: ' + stringifyError(e)
			});
			return null;
		}
	}

	function getExternalRef(recordObj) {
		var value = firstValueFromFields(recordObj, EXTERNAL_REF_FIELDS);
		if (!isBlank(value)) {
			return String(value);
		}

		return null;
	}

	function buildSnapshot(recordObj, fieldIds) {
		return {
			body: buildBodySnapshot(recordObj, fieldIds),
			items: buildItemSnapshot(recordObj)
		};
	}

	function buildBodySnapshot(recordObj, fieldIds) {
		var body = {};

		for (var i = 0; i < fieldIds.length; i++) {
			var fieldId = fieldIds[i];
			if (!fieldId) {
				continue;
			}

			var value = safeGetValue(recordObj, fieldId);
			var text = safeGetText(recordObj, fieldId);

			if (value !== null || text !== null) {
				body[fieldId] = {
					value: normalizeValue(value),
					text: normalizeValue(text)
				};
			}
		}

		return body;
	}

	function buildItemSnapshot(recordObj) {
		var lines = [];
		var lineCount = 0;

		try {
			lineCount = recordObj.getLineCount({ sublistId: 'item' }) || 0;
		} catch (e) {
			return lines;
		}

		for (var i = 0; i < lineCount; i++) {
			var lineObj = {};
			var lineKey = null;

			for (var j = 0; j < ITEM_FIELDS.length; j++) {
				var fieldId = ITEM_FIELDS[j];
				var value = safeGetSublistValue(recordObj, 'item', fieldId, i);
				var text = safeGetSublistText(recordObj, 'item', fieldId, i);

				if (value !== null || text !== null) {
					lineObj[fieldId] = {
						value: normalizeValue(value),
						text: normalizeValue(text)
					};
				}

				if (fieldId === 'lineuniquekey' && !isBlank(value)) {
					lineKey = String(value);
				}
			}

			if (!lineKey) {
				var lineNumber = safeGetSublistValue(recordObj, 'item', 'line', i);
				lineKey = !isBlank(lineNumber) ? String(lineNumber) : String(i + 1);
			}

			lineObj._key = lineKey;
			lines.push(lineObj);
		}

		return lines;
	}

	function diffBody(oldBody, newBody) {
		var changes = {};
		var fields = {};
		var key;

		oldBody = oldBody || {};
		newBody = newBody || {};

		for (key in oldBody) {
			if (Object.prototype.hasOwnProperty.call(oldBody, key)) {
				fields[key] = true;
			}
		}

		for (key in newBody) {
			if (Object.prototype.hasOwnProperty.call(newBody, key)) {
				fields[key] = true;
			}
		}

		for (key in fields) {
			if (!Object.prototype.hasOwnProperty.call(fields, key)) {
				continue;
			}

			var oldVal = oldBody[key] || null;
			var newVal = newBody[key] || null;

			if (!isSameJson(oldVal, newVal)) {
				changes[key] = {
					old: oldVal,
					new: newVal
				};
			}
		}

		return changes;
	}

	function diffItems(oldItems, newItems) {
		var oldMap = mapByKey(oldItems || []);
		var newMap = mapByKey(newItems || []);
		var changes = {
			added: [],
			removed: [],
			updated: []
		};
		var key;

		for (key in oldMap) {
			if (!Object.prototype.hasOwnProperty.call(oldMap, key)) {
				continue;
			}

			if (!Object.prototype.hasOwnProperty.call(newMap, key)) {
				changes.removed.push(oldMap[key]);
				continue;
			}

			var oldLine = oldMap[key];
			var newLine = newMap[key];

			if (!isSameJson(oldLine, newLine)) {
				changes.updated.push({
					key: key,
					old: oldLine,
					new: newLine
				});
			}
		}

		for (key in newMap) {
			if (!Object.prototype.hasOwnProperty.call(newMap, key)) {
				continue;
			}

			if (!Object.prototype.hasOwnProperty.call(oldMap, key)) {
				changes.added.push(newMap[key]);
			}
		}

		return changes;
	}

	function hasAnyChanges(bodyChanges, itemChanges) {
		return Object.keys(bodyChanges || {}).length > 0 ||
			(itemChanges && (
				(itemChanges.added && itemChanges.added.length > 0) ||
				(itemChanges.removed && itemChanges.removed.length > 0) ||
				(itemChanges.updated && itemChanges.updated.length > 0)
			));
	}

	function sendPayload(payload) {
		var endpointUrl = getScriptParameter('custscript_pp_orderupdate_url') || DEFAULT_ENDPOINT_URL;
		var authHeader = getScriptParameter('custscript_pp_orderupdate_auth');

		var headers = {
			'Content-Type': 'application/json',
			'Accept': 'application/json'
		};

		if (!isBlank(authHeader)) {
			headers.Authorization = authHeader;
		}

		var response = https.post({
			url: endpointUrl,
			headers: headers,
			body: JSON.stringify(payload)
		});

		var code = Number(response.code || 0);
		var summary = 'HTTP ' + code + ' | recordId=' + payload.record.id + ' | externalRef=' + payload.record.externalRef;

		if (code >= 200 && code < 300) {
			log.audit({
				title: 'Order change payload sent',
				details: summary
			});
			return;
		}

		log.error({
			title: 'Order change payload failed',
			details: summary + ' | body=' + truncate(response.body, 1500)
		});
	}

	function getScriptParameter(paramName) {
		try {
			return runtime.getCurrentScript().getParameter({ name: paramName });
		} catch (e) {
			return null;
		}
	}

	function getFieldsSafe(recordObj) {
		if (!recordObj || typeof recordObj.getFields !== 'function') {
			return [];
		}

		try {
			return recordObj.getFields() || [];
		} catch (e) {
			return [];
		}
	}

	function safeGetValue(recordObj, fieldId) {
		if (!recordObj) {
			return null;
		}

		try {
			var value = recordObj.getValue({ fieldId: fieldId });
			return value === undefined ? null : value;
		} catch (e) {
			return null;
		}
	}

	function safeGetText(recordObj, fieldId) {
		if (!recordObj) {
			return null;
		}

		try {
			var value = recordObj.getText({ fieldId: fieldId });
			return value === undefined ? null : value;
		} catch (e) {
			return null;
		}
	}

	function safeGetSublistValue(recordObj, sublistId, fieldId, line) {
		if (!recordObj) {
			return null;
		}

		try {
			var value = recordObj.getSublistValue({
				sublistId: sublistId,
				fieldId: fieldId,
				line: line
			});
			return value === undefined ? null : value;
		} catch (e) {
			return null;
		}
	}

	function safeGetSublistText(recordObj, sublistId, fieldId, line) {
		if (!recordObj) {
			return null;
		}

		try {
			var value = recordObj.getSublistText({
				sublistId: sublistId,
				fieldId: fieldId,
				line: line
			});
			return value === undefined ? null : value;
		} catch (e) {
			return null;
		}
	}

	function normalizeValue(value) {
		if (value === undefined || value === null) {
			return null;
		}

		if (Object.prototype.toString.call(value) === '[object Date]') {
			return value.toISOString();
		}

		if (Array.isArray(value)) {
			var arr = [];
			for (var i = 0; i < value.length; i++) {
				arr.push(normalizeValue(value[i]));
			}
			return arr;
		}

		if (typeof value === 'object') {
			try {
				return JSON.parse(JSON.stringify(value));
			} catch (e) {
				return String(value);
			}
		}

		return value;
	}

	function firstValueFromFields(recordObj, fieldIds) {
		if (!recordObj || !fieldIds || fieldIds.length === 0) {
			return null;
		}

		for (var i = 0; i < fieldIds.length; i++) {
			var value = safeGetValue(recordObj, fieldIds[i]);
			if (!isBlank(value)) {
				return value;
			}
		}

		return null;
	}

	function firstValue(values) {
		for (var i = 0; i < values.length; i++) {
			if (!isBlank(values[i])) {
				return values[i];
			}
		}

		return null;
	}

	function getBodyValue(snapshot, fieldId) {
		if (!snapshot || !snapshot.body || !snapshot.body[fieldId]) {
			return null;
		}

		return snapshot.body[fieldId].value;
	}

	function unique(arr) {
		var out = [];
		var map = {};

		for (var i = 0; i < arr.length; i++) {
			var item = arr[i];
			if (!item || map[item]) {
				continue;
			}
			map[item] = true;
			out.push(item);
		}

		return out;
	}

	function mapByKey(lines) {
		var mapped = {};

		for (var i = 0; i < lines.length; i++) {
			var line = lines[i];
			if (!line || !line._key) {
				continue;
			}

			mapped[line._key] = line;
		}

		return mapped;
	}

	function isBlank(value) {
		return value === null || value === undefined || value === '';
	}

	function isSameJson(a, b) {
		return JSON.stringify(a) === JSON.stringify(b);
	}

	function truncate(value, maxLength) {
		var str = String(value || '');
		if (str.length <= maxLength) {
			return str;
		}

		return str.substring(0, maxLength) + '...';
	}

	function stringifyError(e) {
		if (!e) {
			return 'Unknown error';
		}

		return (e.name || 'Error') + ': ' + (e.message || String(e)) + (e.stack ? '\n' + e.stack : '');
	}

	return {
		afterSubmit: afterSubmit
	};
});
