(function (root, factory) {
	'use strict';

	var api = factory();
	root.SubinConditionalLogic = api;
	if (typeof module === 'object' && module.exports) {
		module.exports = api;
	}
}(typeof window !== 'undefined' ? window : globalThis, function () {
	'use strict';

	var VERSION = 1;
	var MAX_RULES = 200;
	var MAX_GROUPS_PER_RULE = 20;
	var MAX_CONDITIONS_PER_GROUP = 50;
	var MAX_TOTAL_CONDITIONS = 1000;
	var MAX_VALUES = 100;
	var MAX_IDENTIFIER_LENGTH = 128;
	var MAX_VALUE_LENGTH = 2048;
	var ACTIONS = ['show', 'hide'];
	var TARGET_TYPES = ['field', 'row', 'block'];
	var OPERATORS = [
		'is',
		'is_not',
		'contains',
		'not_contains',
		'empty',
		'not_empty',
		'before',
		'after'
	];

	function hasOwn(object, key) {
		return Object.prototype.hasOwnProperty.call(object, key);
	}

	function isObject(value) {
		return value !== null && typeof value === 'object' && !Array.isArray(value);
	}

	function scalarString(value) {
		if (value === true) {
			return '1';
		}
		if (value === false || value === null || value === undefined) {
			return '';
		}
		if (typeof value === 'string' || typeof value === 'number') {
			return String(value);
		}
		return '';
	}

	function lowerToken(value) {
		return scalarString(value).trim().toLowerCase();
	}

	function asciiLower(value) {
		return String(value).replace(/[A-Z]/g, function (character) {
			return character.toLowerCase();
		});
	}

	function sanitizeIdentifier(value) {
		return scalarString(value)
			.trim()
			.replace(/[^A-Za-z0-9_.:-]/g, '')
			.slice(0, MAX_IDENTIFIER_LENGTH);
	}

	function normalizeBoolean(value) {
		if (value === true || value === 1) {
			return true;
		}
		if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
			return false;
		}
		return ['1', 'true', 'yes', 'on'].indexOf(String(value).trim().toLowerCase()) !== -1;
	}

	function truncateValue(value) {
		return Array.from(value).slice(0, MAX_VALUE_LENGTH).join('');
	}

	function normalizeValue(value) {
		var output = [];

		function append(item, depth) {
			var keys;
			var index;

			if (output.length >= MAX_VALUES || depth > 10) {
				return;
			}

			if (Array.isArray(item)) {
				for (index = 0; index < item.length && output.length < MAX_VALUES; index += 1) {
					append(item[index], depth + 1);
				}
				return;
			}

			// PHP receives decoded JSON objects as arrays, so mirror that behavior.
			if (isObject(item)) {
				keys = Object.keys(item);
				for (index = 0; index < keys.length && output.length < MAX_VALUES; index += 1) {
					append(item[keys[index]], depth + 1);
				}
				return;
			}

			if (item === null || item === undefined || typeof item === 'function' || typeof item === 'symbol') {
				return;
			}
			if (typeof item === 'number' && !Number.isFinite(item)) {
				return;
			}

			item = scalarString(item).trim();
			if (item === '') {
				return;
			}
			output.push(truncateValue(item));
		}

		append(value, 0);

		return output.filter(function (item, index) {
			return output.indexOf(item) === index;
		});
	}

	function sanitizeCondition(condition) {
		var field;
		var operator;
		var normalized;
		var collectionValue;

		if (!isObject(condition)) {
			return null;
		}
		field = sanitizeIdentifier(condition.field);
		operator = lowerToken(condition.operator);
		if (!field || OPERATORS.indexOf(operator) === -1) {
			return null;
		}

		normalized = [];
		collectionValue = Array.isArray(condition.value) || isObject(condition.value);
		if (operator !== 'empty' && operator !== 'not_empty') {
			normalized = normalizeValue(condition.value);
		}

		return {
			field: field,
			operator: operator,
			value: operator === 'empty' || operator === 'not_empty'
				? ''
				: (collectionValue ? normalized : (normalized[0] || ''))
		};
	}

	function sanitizeGroup(group) {
		var match;
		var conditions;
		var clean;

		if (!isObject(group)) {
			return null;
		}
		match = lowerToken(group.match);
		if (match !== 'all' && match !== 'any') {
			return null;
		}

		clean = { match: match, conditions: [] };
		conditions = Array.isArray(group.conditions) ? group.conditions : [];
		conditions.slice(0, MAX_CONDITIONS_PER_GROUP).forEach(function (condition) {
			condition = sanitizeCondition(condition);
			if (condition) {
				clean.conditions.push(condition);
			}
		});
		return clean;
	}

	function sanitizeRule(rule) {
		var id;
		var action;
		var target;
		var type;
		var targetId;
		var groups;
		var clean;

		if (!isObject(rule)) {
			return null;
		}
		if (exceedsTotalConditionBudget([rule])) {
			return null;
		}
		id = sanitizeIdentifier(rule.id);
		action = lowerToken(rule.action);
		target = isObject(rule.target) ? rule.target : {};
		type = lowerToken(target.type);
		targetId = sanitizeIdentifier(target.id);
		if (!id || ACTIONS.indexOf(action) === -1 || TARGET_TYPES.indexOf(type) === -1 || !targetId) {
			return null;
		}

		clean = {
			id: id,
			enabled: normalizeBoolean(hasOwn(rule, 'enabled') ? rule.enabled : true),
			action: action,
			target: { type: type, id: targetId },
			groups: []
		};
		groups = Array.isArray(rule.groups) ? rule.groups : [];
		groups.slice(0, MAX_GROUPS_PER_RULE).forEach(function (group) {
			group = sanitizeGroup(group);
			if (group) {
				clean.groups.push(group);
			}
		});
		return clean;
	}

	function exceedsTotalConditionBudget(rules) {
		var total = 0;
		var ruleIndex;
		var groupIndex;
		var groups;
		var conditions;

		for (ruleIndex = 0; ruleIndex < rules.length; ruleIndex += 1) {
			if (!isObject(rules[ruleIndex])) {
				continue;
			}
			groups = Array.isArray(rules[ruleIndex].groups) ? rules[ruleIndex].groups : [];
			for (groupIndex = 0; groupIndex < groups.length; groupIndex += 1) {
				if (!isObject(groups[groupIndex])) {
					continue;
				}
				conditions = Array.isArray(groups[groupIndex].conditions) ? groups[groupIndex].conditions : [];
				total += conditions.length;
				if (total > MAX_TOTAL_CONDITIONS) {
					return true;
				}
			}
		}

		return false;
	}

	function sanitize(logic) {
		var clean = { version: VERSION, rules: [] };
		var rules = isObject(logic) && Array.isArray(logic.rules) ? logic.rules : [];

		if (exceedsTotalConditionBudget(rules)) {
			return clean;
		}
		rules.slice(0, MAX_RULES).forEach(function (rule) {
			rule = sanitizeRule(rule);
			if (rule) {
				clean.rules.push(rule);
			}
		});
		return clean;
	}

	function addError(errors, path, code, message) {
		errors.push({ path: path, code: code, message: message });
	}

	function knownIdentifierSet(identifiers) {
		var set = Object.create(null);
		(Array.isArray(identifiers) ? identifiers : []).forEach(function (identifier) {
			identifier = sanitizeIdentifier(identifier);
			if (identifier) {
				set[identifier] = true;
			}
		});
		return set;
	}

	function knownTargetSet(knownTargets, type) {
		if (!isObject(knownTargets) || !hasOwn(knownTargets, type) || !Array.isArray(knownTargets[type])) {
			return null;
		}
		return knownIdentifierSet(knownTargets[type]);
	}

	function daysInMonth(year, month) {
		if (month === 2) {
			return (year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)) ? 29 : 28;
		}
		return [4, 6, 9, 11].indexOf(month) !== -1 ? 30 : 31;
	}

	function normalizeDate(value) {
		var match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value));
		var year;
		var month;
		var day;
		if (!match) {
			return null;
		}
		year = Number(match[1]);
		month = Number(match[2]);
		day = Number(match[3]);
		if (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) {
			return null;
		}
		return match[1] + '-' + match[2] + '-' + match[3];
	}

	function validateRule(rule, knownFields, knownTargets, path) {
		var errors = [];
		var id;
		var action;
		var target;
		var type;
		var targetId;
		var targetSet;
		var fieldSet = knownIdentifierSet(knownFields);

		path = path || 'rule';
		if (!isObject(rule)) {
			addError(errors, path, 'invalid_rule', 'Each rule must be an object.');
			return errors;
		}

		id = sanitizeIdentifier(rule.id);
		if (!id || id !== scalarString(rule.id).trim()) {
			addError(errors, path + '.id', 'invalid_rule_id', 'Rule identifier is missing or invalid.');
		}
		action = lowerToken(rule.action);
		if (ACTIONS.indexOf(action) === -1) {
			addError(errors, path + '.action', 'invalid_action', 'Rule action must be show or hide.');
		}

		target = isObject(rule.target) ? rule.target : {};
		type = lowerToken(target.type);
		targetId = sanitizeIdentifier(target.id);
		if (TARGET_TYPES.indexOf(type) === -1) {
			addError(errors, path + '.target.type', 'invalid_target_type', 'Target type must be field, row, or block.');
		}
		if (!targetId || targetId !== scalarString(target.id).trim()) {
			addError(errors, path + '.target.id', 'invalid_target_id', 'Target identifier is missing or invalid.');
		}
		targetSet = knownTargetSet(knownTargets, type);
		if (targetSet !== null && targetId && !targetSet[targetId]) {
			addError(errors, path + '.target.id', 'unknown_target', 'Rule target does not exist.');
		}

		if (!Array.isArray(rule.groups) || rule.groups.length === 0) {
			addError(errors, path + '.groups', 'missing_groups', 'A rule must contain at least one condition group.');
			return errors;
		}
		if (rule.groups.length > MAX_GROUPS_PER_RULE) {
			addError(errors, path + '.groups', 'too_many_groups', 'Rule contains too many groups.');
		}

		rule.groups.forEach(function (group, groupIndex) {
			var groupPath = path + '.groups.' + groupIndex;
			var match;
			if (!isObject(group)) {
				addError(errors, groupPath, 'invalid_group', 'Each condition group must be an object.');
				return;
			}
			match = lowerToken(group.match);
			if (match !== 'all' && match !== 'any') {
				addError(errors, groupPath + '.match', 'invalid_group_match', 'Group match must be all or any.');
			}
			if (!Array.isArray(group.conditions) || group.conditions.length === 0) {
				addError(errors, groupPath + '.conditions', 'missing_conditions', 'A group must contain at least one condition.');
				return;
			}
			if (group.conditions.length > MAX_CONDITIONS_PER_GROUP) {
				addError(errors, groupPath + '.conditions', 'too_many_conditions', 'Group contains too many conditions.');
			}

			group.conditions.forEach(function (condition, conditionIndex) {
				var conditionPath = groupPath + '.conditions.' + conditionIndex;
				var field;
				var operator;
				var expected;
				if (!isObject(condition)) {
					addError(errors, conditionPath, 'invalid_condition', 'Each condition must be an object.');
					return;
				}
				field = sanitizeIdentifier(condition.field);
				if (!field || field !== scalarString(condition.field).trim()) {
					addError(errors, conditionPath + '.field', 'invalid_field', 'Condition field is missing or invalid.');
				} else if (Array.isArray(knownFields) && knownFields.length > 0 && !fieldSet[field]) {
					addError(errors, conditionPath + '.field', 'unknown_field', 'Condition field does not exist.');
				}

				operator = lowerToken(condition.operator);
				if (OPERATORS.indexOf(operator) === -1) {
					addError(errors, conditionPath + '.operator', 'invalid_operator', 'Condition operator is not supported.');
					return;
				}
				if (operator !== 'empty' && operator !== 'not_empty') {
					expected = normalizeValue(condition.value);
					if (expected.length === 0) {
						addError(errors, conditionPath + '.value', 'missing_value', 'Condition value is required for this operator.');
					} else if ((operator === 'before' || operator === 'after') && expected.some(function (value) {
						return normalizeDate(value) === null;
					})) {
						addError(errors, conditionPath + '.value', 'invalid_date', 'Date comparisons require a valid YYYY-MM-DD value.');
					}
				}
			});
		});

		return errors;
	}

	function validate(logic, knownFields, knownTargets) {
		var errors = [];
		var seen = Object.create(null);
		var rules;

		logic = isObject(logic) ? logic : {};
		knownFields = Array.isArray(knownFields) ? knownFields : [];
		knownTargets = isObject(knownTargets) ? knownTargets : {};
		if (Number(logic.version || 0) !== VERSION) {
			addError(errors, 'version', 'unsupported_version', 'Conditional logic version must be 1.');
		}
		if (!Array.isArray(logic.rules)) {
			addError(errors, 'rules', 'invalid_rules', 'Rules must be an array.');
			return errors;
		}
		rules = logic.rules;
		if (rules.length > MAX_RULES) {
			addError(errors, 'rules', 'too_many_rules', 'Conditional logic contains too many rules.');
		}
		if (exceedsTotalConditionBudget(rules)) {
			addError(errors, 'rules', 'too_many_total_conditions', 'Conditional logic contains too many conditions in total.');
			return errors;
		}
		rules.forEach(function (rule, index) {
			var path = 'rules.' + index;
			var id;
			if (!isObject(rule)) {
				addError(errors, path, 'invalid_rule', 'Each rule must be an object.');
				return;
			}
			errors = errors.concat(validateRule(rule, knownFields, knownTargets, path));
			id = sanitizeIdentifier(rule.id);
			if (id) {
				if (seen[id]) {
					addError(errors, path + '.id', 'duplicate_rule_id', 'Rule identifiers must be unique.');
				}
				seen[id] = true;
			}
		});
		return errors;
	}

	function anyEqual(actual, expected) {
		return actual.some(function (actualValue) {
			actualValue = asciiLower(actualValue);
			return expected.some(function (expectedValue) {
				return actualValue === asciiLower(expectedValue);
			});
		});
	}

	function anyContains(actual, expected) {
		return actual.some(function (actualValue) {
			actualValue = asciiLower(actualValue);
			return expected.some(function (expectedValue) {
				expectedValue = asciiLower(expectedValue);
				return expectedValue !== '' && actualValue.indexOf(expectedValue) !== -1;
			});
		});
	}

	function compareCondition(actual, operator, expected) {
		var actualValues = normalizeValue(actual);
		var expectedValues;
		var matched;

		operator = lowerToken(operator);
		if (operator === 'empty') {
			return actualValues.length === 0;
		}
		if (operator === 'not_empty') {
			return actualValues.length > 0;
		}
		if (OPERATORS.indexOf(operator) === -1) {
			return false;
		}

		expectedValues = normalizeValue(expected);
		if (operator === 'is' || operator === 'is_not') {
			matched = anyEqual(actualValues, expectedValues);
			return operator === 'is' ? matched : !matched;
		}
		if (operator === 'contains' || operator === 'not_contains') {
			matched = anyContains(actualValues, expectedValues);
			return operator === 'contains' ? matched : !matched;
		}

		return actualValues.some(function (actualValue) {
			var actualDate = normalizeDate(actualValue);
			if (actualDate === null) {
				return false;
			}
			return expectedValues.some(function (expectedValue) {
				var expectedDate = normalizeDate(expectedValue);
				if (expectedDate === null) {
					return false;
				}
				return operator === 'before' ? actualDate < expectedDate : actualDate > expectedDate;
			});
		});
	}

	function fieldValue(values, field) {
		if (values && typeof values === 'object' && hasOwn(values, field)) {
			return values[field];
		}
		return null;
	}

	function evaluateCondition(condition, values) {
		return compareCondition(fieldValue(values, condition.field), condition.operator, condition.value);
	}

	function evaluateGroup(group, values) {
		if (!group.conditions.length) {
			return false;
		}
		if (group.match === 'any') {
			return group.conditions.some(function (condition) {
				return evaluateCondition(condition, values);
			});
		}
		return group.conditions.every(function (condition) {
			return evaluateCondition(condition, values);
		});
	}

	function evaluateRule(rule, values) {
		rule = sanitizeRule(rule);
		if (!rule || !rule.enabled || !rule.groups.length) {
			return false;
		}
		return rule.groups.some(function (group) {
			return evaluateGroup(group, values || {});
		});
	}

	function evaluate(logic, values) {
		var states = Object.create(null);
		var visibility = {};

		sanitize(logic).rules.forEach(function (rule) {
			var type;
			var id;
			var key;
			var matched;
			if (!rule.enabled) {
				return;
			}
			type = rule.target.type;
			id = rule.target.id;
			key = type + ':' + id;
			if (!states[key]) {
				states[key] = {
					type: type,
					id: id,
					hasShow: false,
					showMatch: false,
					hideMatch: false
				};
			}
			matched = evaluateRule(rule, values || {});
			if (rule.action === 'show') {
				states[key].hasShow = true;
				states[key].showMatch = states[key].showMatch || matched;
			} else {
				states[key].hideMatch = states[key].hideMatch || matched;
			}
		});

		Object.keys(states).forEach(function (key) {
			var state = states[key];
			if (!visibility[state.type]) {
				visibility[state.type] = Object.create(null);
			}
			visibility[state.type][state.id] = !state.hideMatch && (!state.hasShow || state.showMatch);
		});
		return visibility;
	}

	function isTargetVisible(targetType, targetId, logic, values) {
		var type = lowerToken(targetType);
		var id = sanitizeIdentifier(targetId);
		var visibility;
		if (TARGET_TYPES.indexOf(type) === -1 || !id) {
			return true;
		}
		visibility = evaluate(logic, values || {});
		return !visibility[type] || !hasOwn(visibility[type], id) ? true : visibility[type][id];
	}

	function isFieldVisible(fieldId, logic, values) {
		return isTargetVisible('field', fieldId, logic, values);
	}

	function getVisibleFieldIds(fieldIds, logic, values) {
		var visibility = evaluate(logic, values || {});
		return (Array.isArray(fieldIds) ? fieldIds : []).filter(function (fieldId) {
			var id = sanitizeIdentifier(fieldId);
			return id && (!visibility.field || !hasOwn(visibility.field, id) || visibility.field[id]);
		});
	}

	return {
		VERSION: VERSION,
		MAX_TOTAL_CONDITIONS: MAX_TOTAL_CONDITIONS,
		ACTIONS: ACTIONS.slice(),
		TARGET_TYPES: TARGET_TYPES.slice(),
		OPERATORS: OPERATORS.slice(),
		sanitize: sanitize,
		sanitizeLogic: sanitize,
		sanitizeRule: sanitizeRule,
		validate: validate,
		validateRule: validateRule,
		evaluate: evaluate,
		evaluateRule: evaluateRule,
		isTargetVisible: isTargetVisible,
		isFieldVisible: isFieldVisible,
		getVisibleFieldIds: getVisibleFieldIds,
		normalizeValue: normalizeValue,
		compareCondition: compareCondition
	};
}));
